Creating a Streamable HTTP MCP server with Micronaut

In previous articles, I explored how to create an MCP server with Micronaut by vibe-coding one, following the Model Context Protocol specification (which was a great way to better understand the underpinnings) and how to create an MCP server with Quarkus.
Micronaut lacked a dedicated module for creating MCP servers, but fortunately, recently Micronaut added official support for MCP, so I was eager to try it out!
Note: For the impatient, you can checkout the code we’ll be covering in this article on GitHub.
What to build?
Like in my previous article with Quarkus, I decided to build another version of my 🌔 moon phases MCP server. This is interesting to be able to contrast Quarkus and Micronaut’s approaches.
I reused my code for calculating the moon phases.
My MoonPhasesService
is fairly simple (as long as you don’t look at the exact math calculation)
and consists in two methods:
currentMoonPhase()
— to know the phase at this point in time,moonPhaseAtUnixTimestamp(long timeSeconds)
— to know the phase at a specific point in time.
The contract is as follows, nothing specific to MCP for now:
@Singleton
public class MoonPhasesService {
// ...
public MoonPhase currentMoonPhase() { /*...*/ }
public MoonPhase moonPhaseAtUnixTimestamp(long timeSeconds) { /*...*/ }
}
Compared to my Quarkus version, the service returns MoonPhase
record
s instead of enum
values,
as it seems Micronaut is unhappy with returning my enum
.
So I changed MoonPhase
to look like this:
@JsonSchema
@Introspected
public record MoonPhase(
@NotBlank String phase,
@NotBlank String emoji
) { }
What’s interesting here is the @JsonSchema
annotation which comes from the Micronaut JSON Schema module,
which provides very rich support for all the subtleties of the JSON Schema specification.
The @Instrospected
annotation is here to help with annotation processing and Ahead-of-Time compilation.
Let’s look at the MoonPhasesMcpServer
now:
@Singleton
public class MoonPhasesMcpServer {
@Inject
private MoonPhasesService moonPhasesService;
@Tool(name = "current-moon-phase",
description = "Provides the current moon phase")
public MoonPhase currentMoonPhase() {
return moonPhasesService.currentMoonPhase();
}
@Tool(name = "moon-phase-at-date",
description = "Provides the moon phase at a certain date (with a format of yyyy-MM-dd)")
public MoonPhase moonPhaseAtDate(
@ToolArg(name = "localDate")
@NotBlank @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}")
String localDate
) {
LocalDate parsedLocalDate = LocalDate.parse(localDate);
return moonPhasesService.moonPhaseAtUnixTimestamp(parsedLocalDate.toEpochDay() * 86400);
}
}
You’ll find the same couple annotations as in Quarkus: @Tool
and @ToolArg
.
In Micronaut, @ToolArg
is missing a description
field, but it should be added soon.
What’s more powerful here in Micronaut is the use of Micronaut Validation annotations:
notice the @NotBlank
and even better, the @Pattern
annotation!
With Micronaut, I don’t have to handle the mal-formed inputs, as they are caught by validation much earlier. If the input is incorrect, Micronaut will handle the situation on its own, and your method won’t even be called. So no need to handle the bad values.
Testing the MCP server with the MCP Inspector
When using MCP Inspector to test my server manually, if I pass a blank value to the moon-phase-at-date
method,
I’ll see validation kicking in:
MCP error -32603: moonPhaseAtDate.localDate: must not be blank,
moonPhaseAtDate.localDate: must match "\d{4}-\d{2}-\d{2}"
Extra bonus point: Micronaut MCP will create (at compile time) the JSON schemas for the various @JsonSchema
annotated beans,
adding more fine-grained information about the manipulated input / output structures.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "http://localhost:8080/schemas/moonPhase.schema.json",
"title": "Phase of the moon",
"description": "The phase of the moon is composed of the name of the phase and an emoji representing it",
"type": "object",
"properties": {
"emoji": {
"type": "string",
"minLength": 1
},
"phase": {
"type": "string",
"minLength": 1
}
}
}
For those schemas to be served as static assets, application.properties
must configure the static resources:
# specify the HTTP Streamable transport
micronaut.mcp.server.transport=HTTP
micronaut.mcp.server.info.name=moon-phases
micronaut.mcp.server.info.version=1.0.0
# Specify how & where the schemas should be exposed
micronaut.router.static-resources.jsonschema.paths=classpath:META-INF/schemas
micronaut.router.static-resources.jsonschema.mapping=/schemas/**
# Potentially define a specific base URL, otherwise it's infered
# micronaut.jsonschema.validation.baseUri=https://example.com/schemas
Quick look at the dependencies
You can checkout the code and read the README, but I’d like to mention how I scaffolded the project in the first place, and which dependencies (and tweaks) were needed.
Creating the Micronaut application
This project was bootstrapped with the mn
Micronaut command-line tool,
which can be installed via SDKman.
mn create-app --build=gradle --jdk=21 --lang=java --test=junit \
--features=jackson-databind,json-schema,validation,json-schema-validation mn.mcp.server.mn-mcp-server
As the MCP support is based on the official MCP SDK, which is currently tied to Jackson, you must use the Jackson data binding
(not Micronaut’s built-in serialization). You need to add json-schema
, validation
, and json-schema-validation
features.
But you’ll have to make some tweaks to the dependencies.
Custom dependency tweaks
The following dependencies were added to build.gradle
, or updated, to support the MCP server and enhance JSON Schema generation:
dependencies {
// Existing dependencies
// ...
// The Micronaut MCP support
implementation("io.micronaut.mcp:micronaut-mcp-server-java-sdk:0.0.3")
// For rich JSON schema handling
annotationProcessor("io.micronaut.jsonschema:micronaut-json-schema-processor:1.7.0")
implementation("io.micronaut.jsonschema:micronaut-json-schema-annotations:1.7.0")
}
First of all, the MCP module is not yet part of the features you can select from the mn
command,
or from the Micronaut launch site,
but once the MCP support stabilizes, it’ll be available.
I had to update the dependency version of the JSON Schema support (instead of using the default version from the BOM), but this new version will be available soon in the Micronaut BOM.
So maybe when you read this, you’ll just add the mcp
feature to the list of features, and have everything configured properly.
But those tweaks are just because I’m living on the bleeding edge right now!
Invoking the server via Gemini CLI
For the fun, I decided to add this MCP server to my Gemini CLI installation.
Before launching the CLI, I installed the MCP server as follows:
gemini mcp add moonPhases --transport http http://localhost:8080/mcp
Then when I launch gemini
and list the MCP servers, I can see the moon phase server:
I ask what was the phase of the moon when mankind first landed on the moon (and Gemini figures out the correct date format, although I gave it in plain English). Gemini CLI asks for my acknowledgement to execute the MCP server tool:
Finally, Gemini CLI responds with a proper English response:
Going further
I hope you enjoyed the ride so far, but what are the next steps?
- Check out the source code of this simple HTTP Streamable MCP server.
- Read about the MCP support in Micronaut.
- Have a look at Sergio Delamo’s sample MCP project.
Java developers have some great options nowadays for developing their MCP servers, including Quarkus and Micronaut. Be sure to evaluate those options for your next projects! For enterprise deployments, nothing beats Java! 😉 And Micronaut offers a pretty elegant handling of structured inputs and outputs thanks to its rich JSON Schema support.
Generating videos in Java with Veo 3