Vibe coding an MCP server with Micronaut, LangChain4j, and Gemini

Unlike Quarkus and Spring Boot, Micronaut doesn’t (yet?) provide a module to facilitate the implementation of MCP servers (Model Context Protocol). But being my favorite framework, I decided to see what it takes to build a quick implementation, by vibe coding it, with the help of Gemini!
In a recent article, I explored how to use the MCP reference implementation for Java to implement an MCP server, served as a servlet via Jetty, and to call that server from LangChain4j’s great MCP support. One approach with Micronaut may have been to somehow integrate the servlet I had built via Micronaut’s servlet support, but that didn’t really feel like a genuine and native way to implement a server, so I decided to do it from scratch.
Vibe coding with Gemini
The concept of vibe coding came from a tweet from Andrej Karpathy who defined the concept as interacting with an LLM to build a new prototype or weekend project, and iterating with the LLM till it works, but without looking at or touching the code yourself. It’s quite a bit different than using AI assistance to build a production-ready code base. And Simon Willison’s just written a good piece on what is and what is not vibe coding.
I started throwing Gemini 2.5 Pro some simple prompts for creating an MCP server with Micronaut, with Java 21, but it would not generate something really usable, at least not in one-shot! For example, it would not use Server-Sent Events, or it hadn’t figured out how that the protocol is using JSON-RPC, etc. So instead of steering the LLM in the right direction via multiple prompts, I reused my tacticts of feeding as much information as needed into the prompt. This is the approach I took to grok one’s own content with LLMs.txt, or when I wrote about the power of large context windows for your documentation efforts.
So what was the successful prompt?
Let’s implement a Model Context Protocol (MCP) using the Micronaut framework.
We will use Micronaut 4.8 and Java 21. You can find Micronaut’s documentation here: https://docs.micronaut.io/4.8.11/guide/
The details of the Model Context Protocol (MCP) can be found here: https://modelcontextprotocol.io/llms-full.txt
For the client, we will use LangChain4j as shown in this article: https://glaforge.dev/posts/2025/04/04/mcp-client-and-server-with-java-mcp-sdk-and-langchain4j/
You can find all the code of the LangChain4j MCP client support in the attached file.
📎 [gitingest of the LangChain4j MCP client code]
You can reuse the Java classes of the LangChain4j MCP client to implement the MCP server support with Micronaut.
Implement a simple MCP server to let MCP clients request the weather forecast. Return fake data like
{"forecast": "sunny"}
The trick here was to feed the whole MCP specification thanks to the llms-full.txt
file,
the whole Micronaut single-page documentation, and also the LangChain4j MCP client source code in attachment (via gitingest).
Did it work in one shot? Actually, no. Because my client wouldn’t connect to it somehow, I figured that it wasn’t actually using Server-Sent Events. So I sent a follow-up prompt:
The Micronaut controller must use HTTP Server Sent Events, as this is what the MCP protocol mandates for MCP remote servers. Please update the controller to use SSE.
Then I had a running server.
Gemini created an SseBroadcaster
class which handles the Server-Sent Event handling,
thanks to Reactor’s Publisher
, Flux
, and Sinks
, and Micronaut’s JsonMapper
and SSE support.
It handles the various JSON-RPC operations (initialize
, notifications/initialized
, tools/list
, tools/call
, and ping
)
in the PostController
:
private McpResponse processRequest(McpRequest request) {
// --- Same logic as before to generate the McpResponse object ---
switch (request.method()) {
case "initialize":
log.info("Handling initialize request");
InitializeResult initResult = new InitializeResult(new ServerCapabilities());
return new McpResponse(request.id(), initResult);
case "notifications/initialized":
log.info("Received initialized notification");
// This is a notification FROM the client. MCP spec says notifications
// don't have responses. So we return null here, and the POST handler
// will just return HTTP OK.
return null;
case "tools/list":
log.info("Handling tools/list request");
ToolSpecificationData weatherTool = new ToolSpecificationData(
WEATHER_TOOL_NAME,
"Gets the current weather forecast.",
new InputSchema(
"object",
Map.of("location", Map.of(
"type", "string",
"description", "Location to get the weather for")
),
List.of("location"),
false)
);
ListToolsResult listResult = new ListToolsResult(List.of(weatherTool));
return new McpResponse(request.id(), listResult);
case "tools/call":
log.info("Handling tools/call request");
if (request.params() != null && request.params().has("name")) {
String toolName = request.params().get("name").asText();
if (WEATHER_TOOL_NAME.equals(toolName)) {
log.info("Executing tool: {}", toolName);
TextContentData textContent = new TextContentData(FAKE_WEATHER_JSON);
CallToolResult callResult = new CallToolResult(List.of(textContent));
return new McpResponse(request.id(), callResult);
} else {
log.warn("Unknown tool requested: {}", toolName);
return new McpResponse(request.id(), new McpError(-32601, "Method not found: " + toolName));
}
} else {
log.error("Invalid tools/call request: Missing 'name' in params");
return new McpResponse(request.id(), new McpError(-32602, "Invalid params for tools/call"));
}
case "ping":
log.info("Handling ping request");
return new McpResponse(request.id(), Collections.emptyMap());
default:
log.warn("Unsupported MCP method: {}", request.method());
return new McpResponse(request.id(), new McpError(-32601, "Method not found: " + request.method()));
}
}
From vibe coding, to a more classical AI-assisted approach
The vibe coding part ended somewhere here, as I then went on to make a few tweaks here and there to the code base. When you’re a developer, you can’t resist tweaking a few things here and there, right?
But I continued the journey also with the help of Gemini, but via Gemini Code Assist within IntelliJ IDEA.
I reused my MCP client from my recent MCP article, but I asked Gemini Code Assist to transform the Java class into a proper JUnit test. Since the Micronaut documentation is still part of the context of the conversation, thanks to Gemini’s huge context window, it did again a great job at converting my code into a proper Micronaut running unit test, launching an embedded server.
I like that Gemini created three test methods: to test that the server can reply to some simple greeting message, then can list and the available MCP tools at the disposal of the clients:
@Test
void testListTools() {
log.info("Testing listTools...");
assertDoesNotThrow(() -> {
List<dev.langchain4j.agent.tool.ToolSpecification> tools = mcpClient.listTools();
assertNotNull(tools, "Tool list should not be null");
assertFalse(tools.isEmpty(), "Tool list should not be empty");
// Add more specific assertions if needed, e.g., check tool names
assertTrue(tools.stream().anyMatch(t -> "getWeatherForecast".equals(t.name())),
"Should find the 'getWeatherForecast' tool");
log.info("listTools returned: {}", tools);
}, "Listing tools should not throw an exception");
}
And finally is able to reply to a weather request by returning some dummy weather data:
@Test
void testWeatherRequest() {
log.info("Testing weather request...");
String question = "What's the weather like in Paris today?";
String response = assertDoesNotThrow(() -> weatherAssistant.request(question),
"Weather request should not throw an exception");
log.info("Question: {}", question);
log.info("Response: {}", response);
assertNotNull(response, "Response should not be null");
assertFalse(response.isBlank(), "Response should not be blank");
// Check if the response likely contains the mocked forecast
assertTrue(response.toLowerCase().contains("sunny"),
"Response should contain the weather information (sunny)");
}
Now, the code!
As you made it till the end, you’ll get a reward: I pushed the code in this Github repository! The project doesn’t implement all the bells and whistles of the MCP specification (like prompts, resources, sampling, etc.) but it’s certainly a starting point if you want to write your own MCP server with Micronaut. Since LangChain4j released some new versions today, I updated the dependencies to use the latest and greatest LangChain4j.
Going further, I’d love to see Micronaut offer a dedicated MCP server module, to make it easier to implement MCP servers, with some clever annotations, to streamline the whole process. Fingers crossed 🤞
And of course, once you’re happy with your MCP server implementation, the extra step is to deploy the MCP server to Google’s Cloud Run, like I explained in this article about the various ways to deploy Micronaut apps to Cloud Run.