implementation("io.micronaut.mcp:micronaut-mcp-server-java-sdk")
Table of Contents
Micronaut MCP
Integration with MCP (Model Context Protocol).
Version: 0.0.2
1 Introduction
Integration with MCP (Model Context Protocol).
2 Release History
For this project, you can find a list of releases (with release notes) here: https://github.com/micronaut-projects/micronaut-mcp/releases
3 MCP Server
3.1 MCP Server Java SDK
Micronaut MCP Server support is based on the MCP Java SDK.
To use it, add the following dependency:
<dependency>
<groupId>io.micronaut.mcp</groupId>
<artifactId>micronaut-mcp-server-java-sdk</artifactId>
</dependency>
The MCP Java SDK is coupled to Jackson Core. Thus, you will need to use Micronaut Jackson Databind instead of Micronaut Serialization. |
3.2 Server Information
Via configuration, you should define your mcp server name and version:
Property | Type | Description |
---|---|---|
|
java.lang.String |
|
|
java.lang.String |
3.3 Configuration
To create a server, you will need to define the property micronaut.mcp.server.transport
with one of the values STDIO
, HTTP
.
If you define the property micronaut.mcp.server.reactive
with the value true
, you can define primitives (tools, prompts or resources) with call handlers wrapped in a Project Reactor Mono
.
Property | Type | Description |
---|---|---|
|
java.lang.String |
The MCP Server endpoint. It applies to MCP Servers using HTTP transport. It defaults to |
|
Set the MCP Transport. It defaults to HTTP. |
|
|
boolean |
Whether you want to define MCP Primitive handlers using reactive code. Default value {@value #DEFAULT_REACTIVE} |
3.3.1 Transport
The protocol currently defines two standard transport mechanisms for client-server communication: stdio (communication over standard in and standard out) and Streamable HTTP
For stdio transport, define the property micronaut.mcp.server.transport
as STDIO
.
For HTTP transport, define the property micronaut.mcp.server.transport
as HTTP
.
3.3.1.1 Stdio Sample Configuration
For stdio transport, your configuration may look like
micronaut.mcp.server.transport=STDIO
micronaut.mcp.server.info.name=pgn-resources
micronaut.mcp.server.info.version=1.0.0
micronaut:
mcp:
server:
transport: STDIO
info:
name: 'pgn-resources'
version: '1.0.0'
[micronaut]
[micronaut.mcp]
[micronaut.mcp.server]
transport="STDIO"
[micronaut.mcp.server.info]
name="pgn-resources"
version="1.0.0"
micronaut {
mcp {
server {
transport = "STDIO"
info {
name = "pgn-resources"
version = "1.0.0"
}
}
}
}
{
micronaut {
mcp {
server {
transport = "STDIO"
info {
name = "pgn-resources"
version = "1.0.0"
}
}
}
}
}
{
"micronaut": {
"mcp": {
"server": {
"transport": "STDIO",
"info": {
"name": "pgn-resources",
"version": "1.0.0"
}
}
}
}
}
3.3.1.2 HTTP Sample Configuration
For HTTP transport, your configuration may look like
micronaut.mcp.server.transport=HTTP
micronaut.mcp.server.info.name=pgn-resources
micronaut.mcp.server.info.version=1.0.0
micronaut:
mcp:
server:
transport: HTTP
info:
name: 'pgn-resources'
version: '1.0.0'
[micronaut]
[micronaut.mcp]
[micronaut.mcp.server]
transport="HTTP"
[micronaut.mcp.server.info]
name="pgn-resources"
version="1.0.0"
micronaut {
mcp {
server {
transport = "HTTP"
info {
name = "pgn-resources"
version = "1.0.0"
}
}
}
}
{
micronaut {
mcp {
server {
transport = "HTTP"
info {
name = "pgn-resources"
version = "1.0.0"
}
}
}
}
}
{
"micronaut": {
"mcp": {
"server": {
"transport": "HTTP",
"info": {
"name": "pgn-resources",
"version": "1.0.0"
}
}
}
}
}
3.3.2 Server Instance
Based on the transport and reactive setting, an MCP Java SDK Server class instance is created with @Context
scope.
micronaut.mcp.server.transport |
micronaut.mcp.server.reactive |
Server | Server Specification |
---|---|---|---|
STDIO |
false |
|
|
STDIO |
true |
|
|
HTTP |
false |
|
|
HTTP |
true |
|
|
McpServer.SyncSpecification
, McpServer.AsyncSpecification
, McpServer.StatelessAsyncSpecification
, McpServer.StatelessSyncSpecification
are builder classes. Thus, you can create beans of type BeanCreatedEventListener
to customize the server specification and creation further.
3.3.3 Primitive types per Transport
To create primitives (tools, prompts or resources), you will create beans of a particular type (typically in a bean factory). The bean type depends on the server type selected:
micronaut.mcp.server.transport |
micronaut.mcp.server.reactive |
Prompt Bean Type | Tool Bean Type | Resource Bean Type | Resource Template Bean Type |
---|---|---|---|---|---|
STDIO |
false |
|
|
|
|
STDIO |
true |
|
|
|
|
HTTP |
false |
|
|
|
|
HTTP |
true |
|
|
|
|
3.3.4 Server Capabilities
Based on the primitive beans you defined, Micronaut instantiates a bean of type McpSchema.ServerCapabilities
.
Moreover, you can create a BeanCreatedEventListener
for McpSchema.ServerCapabilities.Builder
to further customize the definition of the capabilities.
3.4 Primitives
3.4.1 Tools
Tools: Executable functions that allow models to perform actions or retrieve information
3.4.1.1 Tools with Annotations
The preferred way to declare a tool is using a method annotated with Tool in a @Singleton
bean.
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import jakarta.inject.Singleton;
@Singleton
class Tools {
@Tool(description = "Evaluate a chess position using a FEN string.")
String fenEvaluation(String fen) {
if (fen.equals("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8")) {
return "+0.12";
}
return "+0.0";
}
}
By default, the method name is used as the tool name.
You can override this by setting the name
attribute of the @Tool
annotation.
The method parameter names will be automatically used as the tool argument names, unless you use the ToolArg annotation to differentiate the tool argument name from the method parameter name.
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import io.micronaut.mcp.annotations.ToolArg;
import jakarta.inject.Singleton;
@Singleton
class Tools {
@Tool(name = "fenEvaluation", description = "Evaluate a chess position using a FEN string.")
String forsythEdwardsNotationEvaluation(@ToolArg(name = "fen") String forsythEdwardsNotation) {
if (forsythEdwardsNotation.equals("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8")) {
return "+0.12";
}
return "+0.0";
}
}
3.4.1.2 Tools Input JSON Schema
A tool definition includes an input JSON Schema.
You can leverage Micronaut JSON Schema to generate a JSON Schema at compilation and use it as your tool input JSON Schema.
For example, you can use a Java record to define your tool input:
package io.micronaut.mcp.server.stateless.sync.tools.jsonschema;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.jsonschema.JsonSchema;
/**
*
* @param fen A Chess position in Forsyth–Edwards Notation
*/
@JsonSchema
@Introspected
public record FenEvaluationRequest(String fen) {
}
Then, just use the Java record as the method parameter:
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import jakarta.inject.Singleton;
@Singleton
class Tools {
@Tool(description = "Evaluate a chess position using a FEN string.")
String fenEvaluation(FenEvaluationRequest req) {
if (req.fen().equals("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8")) {
return "+0.12";
}
return "+0.0";
}
}
3.4.1.3 Tools Output JSON Schema
A tool definition includes an input JSON Schema.
You can leverage Micronaut JSON Schema to generate a JSON Schema at compilation and use it as your tool output JSON Schema.
For example, you can use a Java record to define your tool output:
package io.micronaut.mcp.server.stateless.sync.tools.jsonschema.output;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.jsonschema.JsonSchema;
import jakarta.validation.constraints.NotBlank;
@Introspected
@JsonSchema
public record FenEvaluationResponse(
@NonNull @NotBlank String fen,
@NonNull @NotBlank String evaluation
) {
}
Then, use the Java record as the method return type:
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import io.micronaut.mcp.server.stateless.sync.tools.jsonschema.FenEvaluationRequest;
import jakarta.inject.Singleton;
@Singleton
class Tools {
@Tool(description = "Evaluate a chess position using a FEN string.")
FenEvaluationResponse fenEvaluation(FenEvaluationRequest req) {
String fen = req.fen();
if (fen.equals("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8")) {
return new FenEvaluationResponse(fen, "+0.12");
}
return new FenEvaluationResponse(fen, "+0.0");
}
}
3.4.1.4 Tools with a Factory
Alternatively, you can define tools by registering beans (typically in a bean factory).
The following example assumes you set SYNC as your server type. Because of that, the following example creates defining tools with the class McpStatelessServerFeatures.SyncToolSpecification . If you used a different server type, you should use a different class.
|
package example.micronaut;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.server.McpTransportContext;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
@Factory
class ToolsFactory {
@Singleton
McpStatelessServerFeatures.SyncToolSpecification getAlertsTools() {
return McpStatelessServerFeatures.SyncToolSpecification.builder()
.tool(tool())
.callHandler(callHandler())
.build();
}
private McpSchema.Tool tool() {
return McpSchema.Tool.builder()
.name("fenEvaluation")
.description("Evaluate a chess position using a FEN string.")
.inputSchema(inputSchema())
.build();
}
private McpSchema.JsonSchema inputSchema() {
McpSchema.JsonSchema fenSchema = new McpSchema.JsonSchema("string", null,null, null, null, null);
return new McpSchema.JsonSchema("object", Map.of("fen", fenSchema), List.of("fen"), null, null, null);
}
private BiFunction<McpTransportContext, McpSchema.CallToolRequest, McpSchema.CallToolResult> callHandler() {
return (exchange, req) -> {
String content = evaluation(req.arguments().get("fen").toString());
return new McpSchema.CallToolResult(content, false);
};
}
private String evaluation(String fen) {
if (fen.equals("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8")) {
return "+0.12";
}
return "+0.0";
}
}
3.4.1.5 Tools Configuration
Property | Type | Description |
---|---|---|
|
boolean |
whether the server will emit notifications when the list of available tools changes. Default value |
3.4.2 Prompts
Prompts: Pre-defined templates or instructions that guide language model interactions
The preferred way to define prompts is to annotate a method with Prompt in a @Singleton
bean.
import io.micronaut.mcp.annotations.Prompt;
import io.micronaut.mcp.annotations.PromptArg;
import jakarta.inject.Singleton;
@Singleton
class Prompts {
/**
*
* @return Chess statistics
*/
@Prompt(name = "chess-statistics", description = "Displays statistics for chess games")
String prompt(@PromptArg(description = "Player Name") String name) {
return String.format("You generate chess statistics for %s ....", name);
}
}
Alternatively, you define prompts registering beans (typically in a bean factory) using the low-level MCP SDK API.
The following example assumes you set SYNC as your server type. Because of that, the following example creates defining Prompts with the class McpStatelessServerFeatures.SyncPromptSpecification . If you used a different server type, you should use a different class.
|
package example.micronaut;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.inject.Singleton;
import java.util.List;
@Factory
class PromptsFactory {
@Singleton
McpStatelessServerFeatures.SyncPromptSpecification prompt() {
return new McpStatelessServerFeatures.SyncPromptSpecification(
new McpSchema.Prompt("chess-statistics", "Displays statistics for chess games",
List.of(new McpSchema.PromptArgument("name", "Player Name", true))), (ex, req) -> {
Object playerNameObj = req.arguments().get("name");
String playerName = playerNameObj != null ? playerNameObj.toString() : "";
McpSchema.TextContent assistantContent = new McpSchema.TextContent(String.format("""
You generate chess statistics for %s ....""", playerName));
McpSchema.PromptMessage assistantMessage = new McpSchema.PromptMessage(McpSchema.Role.ASSISTANT, assistantContent);
return new McpSchema.GetPromptResult("Chess statistics", List.of(assistantMessage), null);
});
}
}
3.4.2.1 Tools Configuration
Property | Type | Description |
---|---|---|
|
boolean |
whether the server will emit notifications when the list of available prompts changes. Default value |
3.4.3 Resources
Resources: Structured data or content that provides additional context to the model
The preferred way to define resources is to annotate a method with Resource in a @Singleton
bean. See Resources with Annotations.
Alternatively, you define resources registering beans (typically in a bean factory).
The following example assumes you set SYNC as your server type. Because of that, the following example creates defining resources with the class McpStatelessServerFeatures.SyncResourceSpecification . If you used a different server type, you should use a different class.
|
package example.micronaut;
import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.Context;
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.io.ResourceLoader;
import io.micronaut.mcp.server.utils.PgnLoader;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Context
@Factory
class ResourcesFactory {
public static final String PGN_MIME_TYPE = "application/x-chess-pgn";
private final PgnLoader pgnLoader;
ResourcesFactory(BeanContext beanContext, ResourceLoader resourceLoader, PgnLoader pgnLoader) {
this.pgnLoader = pgnLoader;
for (int round = 1; round <= 14; round++) {
Optional<InputStream> roundPgnInputStreamOptional = resourceLoader.getResourceAsStream("classpath:fidewwc2024/round_" + round + ".pgn");
if (roundPgnInputStreamOptional.isPresent()) {
try {
Long size = Long.valueOf(roundPgnInputStreamOptional.get().readAllBytes().length);
String uri = "pgn://round/" + round;
String name = "round" + round + "PgnFideWCC2024";
String title = "PGN of the Round " + round + " game of the World Chess Championship";
String description = title + " between Ding Liren and Gukesh Dommaraju";
beanContext.registerSingleton(new McpSchema.Resource(uri, name, title, description, PGN_MIME_TYPE, size, null, null));
} catch (IOException e) {
throw new ConfigurationException("unable to calculate the size of the resource");
}
}
}
}
@EachBean(McpSchema.Resource.class)
@Singleton
McpStatelessServerFeatures.SyncResourceSpecification createPgnSyncResourceSpecification(McpSchema.Resource resource) {
return new McpStatelessServerFeatures.SyncResourceSpecification(resource, (mcpSyncServerExchange, readResourceRequest) -> {
String uri = readResourceRequest.uri();
int lastSlash = uri.lastIndexOf('/');
String roundStr = uri.substring(lastSlash + 1);
int round = Integer.parseInt(roundStr);
List<McpSchema.ResourceContents> contents = new ArrayList<>();
pgnLoader.loadPgn(round).ifPresent(text ->
contents.add(new McpSchema.TextResourceContents(uri, PGN_MIME_TYPE, text)));
return new McpSchema.ReadResourceResult(contents);
});
}
}
3.4.3.1 Resources with Annotations
The preferred way to declare a resource is using a method annotated with Resource in a @Singleton
bean.
package example.micronaut;
import io.micronaut.mcp.annotations.Resource;
import jakarta.inject.Singleton;
@Singleton
class Resources {
@Resource(
uri = "example://hello",
name = "hello",
title = "Hello",
description = "Hello text",
mimeType = "text/plain"
)
String hello() {
return "Hello World";
}
}
Notes:
-
The
uri
attribute is required and must be unique for the resource. -
By default, the method name is used as the resource
name
. You can override this by setting thename
attribute of the@Resource
annotation. -
You can optionally provide
title
,description
andmimeType
(defaults totext/plain
). -
If the method returns a
String
, the server responds with a single text resource content using the providedmimeType
. -
Supported method signatures:
-
String myResource()
orReadResourceResult myResource()
-
String myResource(String uri)
orReadResourceResult myResource(String uri)
— receive the requested URI -
ReadResourceResult myResource(io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest request)
— full access to request fields
-
-
Supported return types:
-
String
— serialized as a singleTextResourceContents
with the configuredmimeType
-
io.modelcontextprotocol.spec.McpSchema.ReadResourceResult
— for advanced responses (multiple contents, binary, sizes, etc.)
-
3.4.3.2 Resources Configuration
Property | Type | Description |
---|---|---|
|
boolean |
whether the client can subscribe to be notified of changes to individual resources. Default value |
|
boolean |
whether the server will emit notifications when the list of available resources changes. Default value |
3.4.3.3 Resources Templates
Resource templates allow servers to expose parameterized resources using URI templates
You define resources registering beans of type McpSchema.ResourceTemplate
(typically in a bean factory).
package example.micronaut;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.inject.Singleton;
import static io.micronaut.mcp.server.stateless.sync.resources.ResourcesFactory.PGN_MIME_TYPE;
@Factory
class ResourcesTemplatesFactory {
@Singleton
McpSchema.ResourceTemplate createPgnResourceTemplate() {
String uriTemplate = "pgn://round/{round}";
String name = "2024ChessChampionshipRoundPgn";
String title = "PGN of a round World Chess Championship 2024";
String description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju";
return new McpSchema.ResourceTemplate(uriTemplate, name, title, description, PGN_MIME_TYPE, null, null);
}
}
3.5 STDIO Transport
If you use Standard Input/Output (stdio) protocol, your server MUST NOT write anything to its stdout .
|
Disable the Micronaut banner and configure Logback not to log to stdout
.
For example, you can configure Logback to log to a file or to stderr
(see <target>System.err</target>
in the configuration file below):
<configuration>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDERR" />
</root>
</configuration>
3.6 Testing your MCP Server
To test the MCP Server, implementation you can use the MCP Inspector
The MCP inspector is a developer tool for testing and debugging MCP servers.
4 MCP Client
4.1 Langchain4j MCP Client
To use an MCP Client powered by LangChain4j, use the following dependency:
implementation("io.micronaut.mcp:micronaut-mcp-client-langchain4j")
<dependency>
<groupId>io.micronaut.mcp</groupId>
<artifactId>micronaut-mcp-client-langchain4j</artifactId>
</dependency>
5 Repository
You can find the source code of this project in this repository: