MCP
To use it, add the following dependency:
implementation ("io.micronaut.mcp:micronaut-mcp-server-java-sdk" )<dependency >
<groupId >io.micronaut.mcp</groupId >
<artifactId >micronaut-mcp-server-java-sdk</artifactId >
</dependency >
Via configuration, you should define your mcp server name and version:
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.
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.
Warning
Server to Client communication via an SSE stream is not yet supported. The server responses are currently only of Content-Type: application/json.
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.0micronaut :
mcp :
server :
transport : STDIO
info :
name : 'pgn-resources'
version : '1.0.0' [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.0micronaut :
mcp :
server :
transport : HTTP
info :
name : 'pgn-resources'
version : '1.0.0' [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"
}
}
}
}
}
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
McpSyncServer
McpServer.SyncSpecification
STDIO
true
McpAsyncServer
McpServer.AsyncSpecification
HTTP
false
McpStatelessAsyncServer
McpServer.StatelessAsyncSpecification
HTTP
true
McpStatelessSyncServer
McpServer.StatelessSyncSpecification
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
McpServerFeatures.SyncPromptSpecification
McpServerFeatures.SyncToolSpecification
McpServerFeatures.SyncResourceSpecification
McpSchema.ResourceTemplate
STDIO
true
McpServerFeatures.AsyncPromptSpecification
McpServerFeatures.AsyncToolSpecification
McpServerFeatures.AsyncResourceSpecification
McpSchema.ResourceTemplate
HTTP
false
McpStatelessServerFeatures.SyncPromptSpecification
McpStatelessServerFeatures.SyncToolSpecification
McpServerFeatures.SyncResourceSpecification
McpSchema.ResourceTemplate
HTTP
true
McpStatelessServerFeatures.AsyncPromptSpecification
McpStatelessServerFeatures.AsyncToolSpecification
McpStatelessServerFeatures.AsyncResourceSpecification
McpSchema.ResourceTemplate
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.
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.5 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.
3.6 MCP Transport Context
Micronaut MCP ships MicronautMcpTransportContext , an extension to io.modelcontextprotocol.common.McpTransportContext, which allows you to access concepts such as the authenticated user, locale, host, etc.
Tools: Executable functions that allow models to perform actions or retrieve information
If you need to expose a Search Tool in your MCP Server, you can do it easily defining a bean of type SearchTool
@Singleton
class MicronautModulesSearch implements SearchTool {
@Override
public SearchResponse search (SearchRequest request , McpTransportContext transportContext ) {
return new SearchResponse (List.of (SearchResult.builder ()
.id ("micronaut-security" )
.title ("Micronaut Security" )
.url ("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.build ()));
}
} @Singleton
class MicronautModulesSearch (SearchTool ):
def search (self, request: SearchRequest, transport_context: McpTransportContext) -> SearchResponse:
return SearchResponse(List.of(SearchResult.builder()
.id("micronaut-security" )
.title("Micronaut Security" )
.url("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.build())) @Singleton
class MicronautModulesSearch : SearchTool {
override fun search (request: SearchRequest , transportContext: McpTransportContext ?): SearchResponse {
return SearchResponse (listOf (SearchResult.builder ()
.id ("micronaut-security" )
.title ("Micronaut Security" )
.url ("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.build ()))
}
} @Singleton
class MicronautModulesSearch implements SearchTool {
@Override
SearchResponse search (SearchRequest request , McpTransportContext transportContext ) {
new SearchResponse ([SearchResult. builder()
.id("micronaut-security" )
.title("Micronaut Security" )
.url("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.build()])
}
}
If you need to expose a Fetch Tool in your MCP Server, you can do it easily defining a bean of type FetchTool
@Singleton
class MicronautModulesFetch implements FetchTool {
@Override
public Optional<FetchResponse > fetch (FetchRequest request , McpTransportContext transportContext ) {
return Optional.of (FetchResponse.builder ()
.id ("micronaut-security" )
.title ("Micronaut Security" )
.url ("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.text ("Built-in security features. Authentication providers and strategies, Token Propagation." )
.build ());
}
} @Singleton
class MicronautModulesFetch (FetchTool ):
def fetch (self, request: FetchRequest, transport_context: McpTransportContext) -> Optional[FetchResponse]:
return Optional.of(FetchResponse.builder()
.id("micronaut-security" )
.title("Micronaut Security" )
.url("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.text("Built-in security features. Authentication providers and strategies, Token Propagation." )
.build()) @Singleton
class MicronautModulesFetch : FetchTool {
override fun fetch (request: FetchRequest , transportContext: McpTransportContext ?): Optional <FetchResponse > {
return Optional.of (FetchResponse.builder ()
.id ("micronaut-security" )
.title ("Micronaut Security" )
.url ("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.text ("Built-in security features. Authentication providers and strategies, Token Propagation." )
.build ())
}
} @Singleton
class MicronautModulesFetch implements FetchTool {
@Override
Optional<FetchResponse> fetch (FetchRequest request , McpTransportContext transportContext ) {
Optional. of(FetchResponse. builder()
.id("micronaut-security" )
.title("Micronaut Security" )
.url("https://micronaut-projects.github.io/micronaut-security/latest/guide" )
.text("Built-in security features. Authentication providers and strategies, Token Propagation." )
.build())
}
}
The preferred way to declare a tool is using a method annotated with Tool in a @Singleton bean.
imports package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import io.micronaut.mcp.server.context.MicronautMcpTransportContext;
import jakarta.inject.Singleton;@Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
String fenEvaluation (String fen ,
MicronautMcpTransportContext ctx ) {
if (fen.equals ("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" )) {
return "+0.12" ;
}
return "+0.0" ;
}
} imports from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.mcp.annotations import Tool
from micronaut.mcp.server.context import MicronautMcpTransportContext@Singleton
class Tools :
@Tool (description = "Evaluate a chess position using a FEN string." )
def fen_evaluation (self, fen: str , ctx: MicronautMcpTransportContext) -> str :
if fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" :
return "+0.12"
return "+0.0" imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.server.context.MicronautMcpTransportContext
import jakarta.inject.Singleton @Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
fun fenEvaluation (fen: String ,
ctx: MicronautMcpTransportContext ): String {
if (fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
} imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.server.context.MicronautMcpTransportContext
import jakarta.inject.Singleton@Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
String fenEvaluation (String fen ,
MicronautMcpTransportContext ctx ) {
if (fen == "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.
Note
The Python method name is used verbatim, so the tool of the previous example is named fen_evaluation. Set the name attribute of the @Tool annotation to expose a different name.
You can use annotation hints to inform the client that a tool is read-only.
ChatGPT, for example, informs users whether a tool performs write operations.
@Singleton
class HelloWorldTool {
@Tool (title = "Hello World" ,
annotations = @Tool.ToolAnnotations (readOnlyHint = true ,
title = "Hello World" ,
destructiveHint = false ,
idempotentHint = true ,
openWorldHint = false ,
returnDirect = true ))
String helloWorld () {
return "Hello, World!" ;
}
} @Singleton
class HelloWorldTool :
@Tool (title = "Hello World" ,
annotations = Tool.ToolAnnotations(readOnlyHint = True ,
title = "Hello World" ,
destructiveHint = False ,
idempotentHint = True ,
openWorldHint = False ,
returnDirect = True ))
def hello_world (self) -> str :
return "Hello, World!" @Singleton
class HelloWorldTool {
@Tool (title = "Hello World" ,
annotations = Tool.ToolAnnotations (readOnlyHint = true ,
title = "Hello World" ,
destructiveHint = false ,
idempotentHint = true ,
openWorldHint = false ,
returnDirect = true ))
fun helloWorld (): String {
return "Hello, World!"
}
} @Singleton
class HelloWorldTool {
@Tool (title = "Hello World" ,
annotations = @Tool.ToolAnnotations (readOnlyHint = true ,
title = "Hello World" ,
destructiveHint = false ,
idempotentHint = true ,
openWorldHint = false ,
returnDirect = true ))
String helloWorld () {
return "Hello, World!"
}
}
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.
imports package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import io.micronaut.mcp.annotations.ToolArg;
import io.modelcontextprotocol.common.McpTransportContext;
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 ,
McpTransportContext ctx ) {
if (forsythEdwardsNotation.equals ("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" )) {
return "+0.12" ;
}
return "+0.0" ;
}
} imports from typing import Annotated
from io.modelcontextprotocol.common import McpTransportContext
from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.mcp.annotations import Tool, ToolArg@Singleton
class Tools :
@Tool (name = "fenEvaluation" , description = "Evaluate a chess position using a FEN string." )
def forsyth_edwards_notation_evaluation (self,
forsyth_edwards_notation: Annotated[str , ToolArg(name = "fen" )],
ctx: McpTransportContext) -> str :
if forsyth_edwards_notation == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" :
return "+0.12"
return "+0.0" imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.annotations.ToolArg
import io.modelcontextprotocol.common.McpTransportContext
import jakarta.inject.Singleton @Singleton
class Tools {
@Tool (name = "fenEvaluation" , description = "Evaluate a chess position using a FEN string." )
fun forsythEdwardsNotationEvaluation (@ToolArg (name = "fen" ) forsythEdwardsNotation: String ,
ctx: McpTransportContext ): String {
if (forsythEdwardsNotation == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
} imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.annotations.ToolArg
import io.modelcontextprotocol.common.McpTransportContext
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 ,
McpTransportContext ctx ) {
if (forsythEdwardsNotation == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
}
In a method annotated with @Tool you can bind parameters with types:
In a method annotated with @Tool, you can use as a return type:
io.modelcontextprotocol.spec.McpSchema.CallToolResult
String
A class or Java record which will be serialized to JSON. If the return type is annotated with @JsonSchema and it is defined as the tool output it will be used as structured content.
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:
For example, you can use a dataclass to define your tool input (the attribute docstrings become the property descriptions of the JSON schema):
imports import io.micronaut.jsonschema.JsonSchema;
import io.micronaut.serde.annotation.Serdeable;/**
*
* @param fen A Chess position in Forsyth–Edwards Notation
*/
@JsonSchema
@Serdeable
public record FenEvaluationRequest (String fen) {
} imports from dataclasses import dataclass
from micronaut.jsonschema import JsonSchema
from micronaut.serde.annotation import Serdeable@JsonSchema
@Serdeable
@dataclass
class FenEvaluationRequest :
fen: str
"""A Chess position in Forsyth–Edwards Notation""" imports import io.micronaut.jsonschema.JsonSchema
import io.micronaut.serde.annotation.Serdeable @JsonSchema
@Serdeable
data class FenEvaluationRequest (
/** A Chess position in Forsyth–Edwards Notation */
val fen: String
) imports import io.micronaut.jsonschema.JsonSchema
import io.micronaut.serde.annotation.Serdeable/**
*
* @param fen A Chess position in Forsyth–Edwards Notation
*/
@JsonSchema
@Serdeable
class FenEvaluationRequest {
/**
* A Chess position in Forsyth–Edwards Notation
*/
final String fen
FenEvaluationRequest (String fen ) {
this . fen = fen
}
}
Then, just use the Java record as the method parameter:
Then, just use the dataclass as the method parameter:
imports package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import io.micronaut.mcp.server.context.MicronautMcpTransportContext;
import jakarta.inject.Singleton;@Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
String fenEvaluation (FenEvaluationRequest req ,
MicronautMcpTransportContext ctx ) {
if (req.fen ().equals ("r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" )) {
return "+0.12" ;
}
return "+0.0" ;
}
} imports from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.mcp.annotations import Tool
from micronaut.mcp.server.context import MicronautMcpTransportContext
from .FenEvaluationRequest import FenEvaluationRequest@Singleton
class Tools :
@Tool (description = "Evaluate a chess position using a FEN string." )
def fen_evaluation (self, req: FenEvaluationRequest, ctx: MicronautMcpTransportContext) -> str :
if req.fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" :
return "+0.12"
return "+0.0" imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.server.context.MicronautMcpTransportContext
import jakarta.inject.Singleton @Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
fun fenEvaluation (req: FenEvaluationRequest ,
ctx: MicronautMcpTransportContext ): String {
if (req.fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
} imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.server.context.MicronautMcpTransportContext
import jakarta.inject.Singleton@Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
String fenEvaluation (FenEvaluationRequest req ,
MicronautMcpTransportContext ctx ) {
if (req. fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
}
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:
For example, you can use a dataclass to define your tool output:
imports import io.micronaut.jsonschema.JsonSchema;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import org.jspecify.annotations.NonNull;@Serdeable
@JsonSchema
public record FenEvaluationResponse (
@NonNull @NotBlank String fen,
@NonNull @NotBlank String evaluation
) {
} imports from dataclasses import dataclass
from typing import Annotated
from jakarta.validation.constraints import NotBlank
from micronaut.jsonschema import JsonSchema
from micronaut.serde.annotation import Serdeable
from org.jspecify.annotations import NonNull@Serdeable
@JsonSchema
@dataclass
class FenEvaluationResponse :
fen: Annotated[str , NonNull, NotBlank]
evaluation: Annotated[str , NonNull, NotBlank] imports import io.micronaut.jsonschema.JsonSchema
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank @Serdeable
@JsonSchema
data class FenEvaluationResponse (
@field :NotBlank val fen : String ,
@field :NotBlank val evaluation : String
) imports import io.micronaut.jsonschema.JsonSchema
import io.micronaut.serde.annotation.Serdeable
import jakarta.validation.constraints.NotBlank
import org.jspecify.annotations.NonNull@Serdeable
@JsonSchema
class FenEvaluationResponse {
@NonNull
@NotBlank
final String fen
@NonNull
@NotBlank
final String evaluation
FenEvaluationResponse (String fen , String evaluation ) {
this . fen = fen
this . evaluation = evaluation
}
}
Then, use the Java record as the method return type:
Then, use the dataclass as the method return type:
imports package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.mcp.annotations.Tool;
import io.micronaut.mcp.docs.tools.jsonschema.FenEvaluationRequest;
import io.micronaut.mcp.server.context.MicronautMcpTransportContext;
import jakarta.inject.Singleton;@Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
FenEvaluationResponse fenEvaluation (FenEvaluationRequest req ,
MicronautMcpTransportContext ctx ) {
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" );
}
} imports from jakarta.inject import Singleton
from micronaut.context.annotation import Requires
from micronaut.mcp.annotations import Tool
from micronaut.mcp.docs.tools.jsonschema.FenEvaluationRequest import FenEvaluationRequest
from micronaut.mcp.server.context import MicronautMcpTransportContext
from .FenEvaluationResponse import FenEvaluationResponse@Singleton
class Tools :
@Tool (description = "Evaluate a chess position using a FEN string." )
def fen_evaluation (self, req: FenEvaluationRequest, ctx: MicronautMcpTransportContext) -> FenEvaluationResponse:
fen = req.fen
if fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" :
return FenEvaluationResponse(fen, "+0.12" )
return FenEvaluationResponse(fen, "+0.0" )imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.docs.tools.jsonschema.FenEvaluationRequest
import io.micronaut.mcp.server.context.MicronautMcpTransportContext
import jakarta.inject.Singleton @Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
fun fenEvaluation (req: FenEvaluationRequest ,
ctx: MicronautMcpTransportContext ): FenEvaluationResponse {
val fen = req.fen
if (fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return FenEvaluationResponse (fen, "+0.12" )
}
return FenEvaluationResponse (fen, "+0.0" )
}
} imports package example.micronaut
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.annotations.Tool
import io.micronaut.mcp.docs.tools.jsonschema.FenEvaluationRequest
import io.micronaut.mcp.server.context.MicronautMcpTransportContext
import jakarta.inject.Singleton@Singleton
class Tools {
@Tool (description = "Evaluate a chess position using a FEN string." )
FenEvaluationResponse fenEvaluation (FenEvaluationRequest req ,
MicronautMcpTransportContext ctx ) {
String fen = req. fen
if (fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return new FenEvaluationResponse (fen, "+0.12" )
}
return new FenEvaluationResponse (fen, "+0.0" )
}
}
Alternatively, you can define tools by registering beans (typically in a bean factory ).
imports package example.micronaut;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.common.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 fenEvaluationTool () {
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 (ctx, req) -> {
String content = evaluation (req.arguments ().get ("fen" ).toString ());
return McpSchema.CallToolResult.builder ().addTextContent (content).isError (false ).build ();
};
}
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" ;
}
} imports from io.modelcontextprotocol.server import McpStatelessServerFeatures
from io.modelcontextprotocol.spec import McpSchema
from jakarta.inject import Singleton
from java.util import List, Map
from micronaut.context.annotation import Factory, Requires@Factory
class ToolsFactory :
@Singleton
def fen_evaluation_tool (self) -> McpStatelessServerFeatures.SyncToolSpecification:
return (McpStatelessServerFeatures.SyncToolSpecification.builder()
.tool(self .tool())
.callHandler(lambda ctx, req: self .call_tool(req))
.build())
def tool (self) -> McpSchema.Tool:
return (McpSchema.Tool.builder()
.name("fenEvaluation" )
.description("Evaluate a chess position using a FEN string." )
.inputSchema(self .input_schema())
.build())
def input_schema (self) -> McpSchema.JsonSchema:
fen_schema = McpSchema.JsonSchema("string" , None , None , None , None , None )
return McpSchema.JsonSchema("object" , Map.of("fen" , fen_schema), List.of("fen" ), None , None , None )
def call_tool (self, req: McpSchema.CallToolRequest) -> McpSchema.CallToolResult:
content = self .evaluation(str (req.arguments().get("fen" )))
return McpSchema.CallToolResult.builder().addTextContent(content).isError(False ).build()
def evaluation (self, fen: str ) -> str :
if fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" :
return "+0.12"
return "+0.0" imports package example.micronaut
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.common.McpTransportContext
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton
import java.util.function.BiFunction @Factory
class ToolsFactory {
@Singleton
fun fenEvaluationTool (): McpStatelessServerFeatures .SyncToolSpecification {
return McpStatelessServerFeatures.SyncToolSpecification.builder ()
.tool (tool ())
.callHandler (callHandler ())
.build ()
}
private fun tool (): McpSchema .Tool {
return McpSchema.Tool.builder ()
.name ("fenEvaluation" )
.description ("Evaluate a chess position using a FEN string." )
.inputSchema (inputSchema ())
.build ()
}
private fun inputSchema (): McpSchema .JsonSchema {
val fenSchema = McpSchema.JsonSchema ("string" , null , null , null , null , null )
return McpSchema.JsonSchema ("object" , mapOf ("fen" to fenSchema), listOf ("fen" ), null , null , null )
}
private fun callHandler (): BiFunction <McpTransportContext , McpSchema .CallToolRequest , McpSchema .CallToolResult > {
return BiFunction { _, req ->
val content = evaluation (req.arguments ()["fen" ].toString ())
McpSchema.CallToolResult.builder ().addTextContent (content).isError (false ).build ()
}
}
private fun evaluation (fen: String ): String {
if (fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
} imports package example.micronaut
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.common.McpTransportContext
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton
import java.util.function.BiFunction@Factory
class ToolsFactory {
@Singleton
McpStatelessServerFeatures.SyncToolSpecification fenEvaluationTool () {
McpStatelessServerFeatures.SyncToolSpecification. builder()
.tool(tool())
.callHandler(callHandler())
.build()
}
private McpSchema.Tool tool () {
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 )
new McpSchema.JsonSchema ("object" , [fen : fenSchema], ["fen" ], null , null , null )
}
private BiFunction<McpTransportContext, McpSchema.CallToolRequest, McpSchema.CallToolResult> callHandler () {
return { McpTransportContext ctx , McpSchema.CallToolRequest req ->
String content = evaluation(req. arguments(). get("fen" ). toString())
McpSchema.CallToolResult. builder(). addTextContent(content). isError(false ). build()
} as BiFunction<McpTransportContext, McpSchema.CallToolRequest, McpSchema.CallToolResult>
}
private String evaluation (String fen ) {
if (fen == "r1bqk2r/ppp2ppp/2n5/1BbpP3/3Nn3/8/PPP2PPP/RNBQK2R w KQkq - 1 8" ) {
return "+0.12"
}
return "+0.0"
}
}
Prompts: Pre-defined templates or instructions that guide language model interactions
3.7.2.1 Classpath Prompts
It is possible to define prompts by placing a text file in the classpath and defining the prompt attributes and path via configuration:
micronaut.mcp.classpath-prompts.introspection-testing.name =introspection-testing
micronaut.mcp.classpath-prompts.introspection-testing.title =Introspection-Testing
micronaut.mcp.classpath-prompts.introspection-testing.description =Test whether a class is introspected in a Micronaut application
micronaut.mcp.classpath-prompts.introspection-testing.path =prompts/introspection-testing.md
micronaut.mcp.classpath-prompts.introspection-testing.arguments[0].name =className
micronaut.mcp.classpath-prompts.introspection-testing.arguments[0].description =The class for which you want to test introspection
micronaut.mcp.classpath-prompts.dev-default-environment.name =dev-default-environment
micronaut.mcp.classpath-prompts.dev-default-environment.title =Development-Default-Environment
micronaut.mcp.classpath-prompts.dev-default-environment.description =Modify a Micronaut application to set dev as the default environment
micronaut.mcp.classpath-prompts.dev-default-environment.path =prompts/dev-default-environment.md
You can define the prompt content with argument interpolation in a text file.
src/main/resources/prompts/
Please, write a test to verify introspection for ${className}
The following tests shows how to test if a class is introspected. The following test verifies if the `CreateGame` class is annotated with `@Introspected` .
```java
@Test
void isAnnotatedWithIntrospected () {
assertDoesNotThrow (() -> BeanIntrospection.getIntrospection (CreateGame.class));
}
```
3.7.2.2 Prompts Configuration
3.7.2.3 Prompts with Annotations
The preferred way to define prompts is to annotate a method with Prompt in a @Singleton bean.
imports 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);
}
} imports from typing import Annotated
from jakarta.inject import Singleton
from micronaut.mcp.annotations import Prompt, PromptArg@Singleton
class Prompts :
@Prompt (name = "chess-statistics" , description = "Displays statistics for chess games" )
def prompt (self, name: Annotated[str , PromptArg(description = "Player Name" )]) -> str :
"""
:return: Chess statistics
"""
return f "You generate chess statistics for { name} ...." imports 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" )
fun prompt (@PromptArg (description = "Player Name" ) name: String ): String {
return String.format ("You generate chess statistics for %s ...." , name)
}
} imports 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 ) {
String. format("You generate chess statistics for %s ...." , name)
}
}
3.7.2.3.1 Prompt Annotation Method Parameters
In a method annotated with @Prompt you can bind parameters with types:
3.7.2.3.2 Prompt Annotation Method Return Type
In a method annotated with @Prompt, you can use as a return type:
3.7.2.4 Prompts with a Factory
Alternatively, you define prompts registering beans (typically in a bean factory ) using the low-level MCP SDK API.
imports 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 ))), (ctx, 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 );
});
}
} imports from io.modelcontextprotocol.server import McpStatelessServerFeatures
from io.modelcontextprotocol.spec import McpSchema
from jakarta.inject import Singleton
from java.util import List
from micronaut.context.annotation import Factory, Requires@Factory
class PromptsFactory :
@Singleton
def prompt (self) -> McpStatelessServerFeatures.SyncPromptSpecification:
return McpStatelessServerFeatures.SyncPromptSpecification(
McpSchema.Prompt("chess-statistics" , "Displays statistics for chess games" ,
List.of(McpSchema.PromptArgument("name" , "Player Name" , True ))),
lambda ctx, req: self .get_prompt_result(req))
def get_prompt_result (self, req: McpSchema.GetPromptRequest) -> McpSchema.GetPromptResult:
player_name_obj = req.arguments().get("name" )
player_name = str (player_name_obj) if player_name_obj is not None else ""
assistant_content = McpSchema.TextContent(f "You generate chess statistics for { player_name} ...." )
assistant_message = McpSchema.PromptMessage(McpSchema.Role.ASSISTANT , assistant_content)
return McpSchema.GetPromptResult("Chess statistics" , List.of(assistant_message), None )imports 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 @Factory
class PromptsFactory {
@Singleton
fun prompt (): McpStatelessServerFeatures .SyncPromptSpecification {
return McpStatelessServerFeatures.SyncPromptSpecification (
McpSchema.Prompt ("chess-statistics" , "Displays statistics for chess games" ,
listOf (McpSchema.PromptArgument ("name" , "Player Name" , true )))) { _, req ->
val playerName = req.arguments ()["name" ]?.toString () ?: ""
val assistantContent = McpSchema.TextContent ("You generate chess statistics for $playerName ...." )
val assistantMessage = McpSchema.PromptMessage (McpSchema.Role.ASSISTANT, assistantContent)
McpSchema.GetPromptResult ("Chess statistics" , listOf (assistantMessage), null )
}
}
} imports package example.micronaut
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.modelcontextprotocol.common.McpTransportContext
import io.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton@Factory
class PromptsFactory {
@Singleton
McpStatelessServerFeatures.SyncPromptSpecification prompt () {
new McpStatelessServerFeatures.SyncPromptSpecification (
new McpSchema.Prompt ("chess-statistics" , "Displays statistics for chess games" ,
[new McpSchema.PromptArgument ("name" , "Player Name" , true )]), { McpTransportContext ctx , McpSchema.GetPromptRequest req ->
Object playerNameObj = req. arguments(). get("name" )
String playerName = playerNameObj != null ? playerNameObj. toString() : ""
McpSchema.TextContent assistantContent = new McpSchema.TextContent ("You generate chess statistics for ${ playerName } ...." . toString())
McpSchema.PromptMessage assistantMessage = new McpSchema.PromptMessage (McpSchema.Role. ASSISTANT , assistantContent)
new McpSchema.GetPromptResult ("Chess statistics" , [assistantMessage], null )
})
}
}
Resources: Structured data or content that provides additional context to the model
3.7.3.1 Resources Configuration
3.7.3.2 Resources with Annotations
The preferred way to declare a resource is using a method annotated with Resource in a @Singleton bean.
imports 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" ;
}
} imports from jakarta.inject import Singleton
from micronaut.mcp.annotations import Resource@Singleton
class Resources :
@Resource (
uri = "example://hello" ,
name = "hello" ,
title = "Hello" ,
description = "Hello text" ,
mimeType = "text/plain"
)
def hello (self) -> str :
return "Hello World" imports 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"
)
fun hello (): String {
return "Hello World"
}
} imports 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 () {
"Hello World"
}
}
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 the name attribute of the @Resource annotation.
You can optionally provide title, description and mimeType (defaults to text/plain).
3.7.3.2.1 Resource Annotation Method Parameters
In a method annotated with @Resource you can bind parameters with types:
3.7.3.2.2 Resource Annotation Method Return Type
In a method annotated with @Resource, you can use as a return type:
io.modelcontextprotocol.spec.McpSchema.ReadResourceResult
String. If the method returns a String, the server responds with a single text resource content using the provided mimeType.
3.7.3.3 Resources with a Factory
Alternatively, you define resources registering beans (typically in a bean factory ).
imports package example.micronaut;
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.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;
private final ResourceLoader resourceLoader;
ResourcesFactory (ResourceLoader resourceLoader , PgnLoader pgnLoader ) {
this .resourceLoader = resourceLoader;
this .pgnLoader = pgnLoader;
}
@EachBean (PgnFile.class)
@Singleton
McpStatelessServerFeatures.SyncResourceSpecification createPgnSyncResourceSpecification (PgnFile pgnFile ) throws IOException {
McpSchema. Resource resource = getResource (pgnFile);
return new McpStatelessServerFeatures.SyncResourceSpecification (resource,
(mcpTransportContext, readResourceRequest) -> readResourceResult (readResourceRequest.uri (), pgnLoader));
}
private static Integer round (String uri ) {
int lastSlash = uri.lastIndexOf ('/' );
String roundStr = uri.substring (lastSlash + 1 );
return Integer.parseInt (roundStr);
}
static McpSchema.ReadResourceResult readResourceResult (String uri , PgnLoader pgnLoader ) {
Integer round = round (uri);
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);
}
private McpSchema.Resource getResource (PgnFile pgnFile ) throws IOException {
return size (pgnFile.getPath ())
.map (size -> {
Integer round = pgnFile.getRound ();
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" ;
return new McpSchema.Resource (uri, name, title, description, PGN_MIME_TYPE, size, null , null );
}).orElseThrow (() -> new ConfigurationException ("unable find resource for path " + pgnFile.getPath ()));
}
private Optional<Long > size (String path ) throws IOException {
Optional< InputStream > inputStreamOptional = resourceLoader.getResourceAsStream (path);
if (inputStreamOptional.isPresent ()) {
try (InputStream inputStream = inputStreamOptional.get ()) {
return Optional.of ((long ) inputStream.readAllBytes ().length);
}
}
return Optional.empty ();
}
} imports from io.modelcontextprotocol.server import McpStatelessServerFeatures
from io.modelcontextprotocol.spec import McpSchema
from jakarta.inject import Singleton
from java.util import ArrayList
from micronaut.context.annotation import Context, EachBean, Factory, Requires
from micronaut.context.exceptions import ConfigurationException
from micronaut.core.io import ResourceLoader
from .PgnFile import PgnFile
from .PgnLoader import PgnLoaderPGN_MIME_TYPE = "application/x-chess-pgn"
def round_of (uri: str ) -> int :
last_slash = uri.rfind("/" )
return int (uri[last_slash + 1 :])
def read_resource_result (uri: str , pgn_loader: PgnLoader) -> McpSchema.ReadResourceResult:
round = round_of(uri)
contents = ArrayList()
text = pgn_loader.load_pgn(round )
if text is not None :
contents.add(McpSchema.TextResourceContents(uri, PGN_MIME_TYPE , text))
return McpSchema.ReadResourceResult(contents)
@Context
@Factory
class ResourcesFactory :
def __init__ (self, resource_loader: ResourceLoader, pgn_loader: PgnLoader):
self .resource_loader = resource_loader
self .pgn_loader = pgn_loader
@EachBean (PgnFile)
@Singleton
def create_pgn_sync_resource_specification (self, pgn_file: PgnFile) -> McpStatelessServerFeatures.SyncResourceSpecification:
resource = self .get_resource(pgn_file)
return McpStatelessServerFeatures.SyncResourceSpecification(
resource,
lambda ctx, read_resource_request: read_resource_result(read_resource_request.uri(), self .pgn_loader))
def get_resource (self, pgn_file: PgnFile) -> McpSchema.Resource:
size = self .size(pgn_file.path)
if size is None :
raise ConfigurationException("unable find resource for path " + pgn_file.path)
round = pgn_file.round
uri = f "pgn://round/ { round } "
name = f "round { round } PgnFideWCC2024"
title = f "PGN of the Round { round } game of the World Chess Championship"
description = f " { title} between Ding Liren and Gukesh Dommaraju"
return McpSchema.Resource(uri, name, title, description, PGN_MIME_TYPE , size, None , None )
def size (self, path: str ) -> int | None :
input_stream_optional = self .resource_loader.getResourceAsStream(path)
if input_stream_optional.isPresent():
input_stream = input_stream_optional.get()
try :
return len (input_stream.readAllBytes())
finally :
input_stream.close()
return None imports package example.micronaut
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.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton
import java.util.Optional @Context
@Factory
class ResourcesFactory (private val resourceLoader: ResourceLoader , private val pgnLoader: PgnLoader ) {
@EachBean (PgnFile::class )
@Singleton
fun createPgnSyncResourceSpecification (pgnFile: PgnFile ): McpStatelessServerFeatures .SyncResourceSpecification {
val resource = getResource (pgnFile)
return McpStatelessServerFeatures.SyncResourceSpecification (resource) { _, readResourceRequest ->
readResourceResult (readResourceRequest.uri (), pgnLoader)
}
}
private fun getResource (pgnFile: PgnFile ): McpSchema .Resource {
return size (pgnFile.path!! )
.map { size ->
val round = pgnFile.round
val uri = "pgn://round/ $round "
val name = "round${round}PgnFideWCC2024"
val title = "PGN of the Round $round game of the World Chess Championship"
val description = " $title between Ding Liren and Gukesh Dommaraju"
McpSchema.Resource (uri, name, title, description, PGN_MIME_TYPE, size, null , null )
}.orElseThrow { ConfigurationException ("unable find resource for path " + pgnFile.path) }
}
private fun size (path: String ): Optional <Long > {
val inputStreamOptional = resourceLoader.getResourceAsStream (path)
if (inputStreamOptional.isPresent) {
inputStreamOptional.get ().use { inputStream ->
return Optional.of (inputStream.readAllBytes ().size.toLong ())
}
}
return Optional.empty ()
}
companion object {
const val PGN_MIME_TYPE = "application/x-chess-pgn"
private fun round (uri: String ): Int {
val lastSlash = uri.lastIndexOf ('/' )
return uri.substring (lastSlash + 1 ).toInt ()
}
fun readResourceResult (uri: String , pgnLoader: PgnLoader ): McpSchema .ReadResourceResult {
val round = round (uri)
val contents = ArrayList <McpSchema .ResourceContents >()
pgnLoader.loadPgn (round).ifPresent { text ->
contents.add (McpSchema.TextResourceContents (uri, PGN_MIME_TYPE, text))
}
return McpSchema.ReadResourceResult (contents)
}
}
} imports package example.micronaut
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.modelcontextprotocol.common.McpTransportContext
import io.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton@Context
@Factory
class ResourcesFactory {
public static final String PGN_MIME_TYPE = "application/x-chess-pgn"
private final PgnLoader pgnLoader
private final ResourceLoader resourceLoader
ResourcesFactory (ResourceLoader resourceLoader , PgnLoader pgnLoader ) {
this . resourceLoader = resourceLoader
this . pgnLoader = pgnLoader
}
@EachBean (PgnFile )
@Singleton
McpStatelessServerFeatures.SyncResourceSpecification createPgnSyncResourceSpecification (PgnFile pgnFile ) throws IOException {
McpSchema.Resource resource = getResource(pgnFile)
new McpStatelessServerFeatures.SyncResourceSpecification (resource,
{ McpTransportContext ctx , McpSchema.ReadResourceRequest readResourceRequest -> readResourceResult(readResourceRequest. uri(), pgnLoader) })
}
private static Integer round (String uri ) {
int lastSlash = uri. lastIndexOf('/' )
String roundStr = uri. substring(lastSlash + 1 )
Integer. parseInt(roundStr)
}
static McpSchema.ReadResourceResult readResourceResult (String uri , PgnLoader pgnLoader ) {
Integer round = round(uri)
List<McpSchema.ResourceContents> contents = []
pgnLoader. loadPgn(round). ifPresent { String text ->
contents. add(new McpSchema.TextResourceContents (uri, PGN_MIME_TYPE , text))
}
new McpSchema.ReadResourceResult (contents)
}
private McpSchema.Resource getResource (PgnFile pgnFile ) throws IOException {
size(pgnFile. path)
.map { Long size ->
Integer round = pgnFile. round
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"
new McpSchema.Resource (uri, name, title, description, PGN_MIME_TYPE , size, null , null )
}. orElseThrow { new ConfigurationException ("unable find resource for path " + pgnFile. path) }
}
private Optional<Long> size (String path ) throws IOException {
Optional<InputStream> inputStreamOptional = resourceLoader. getResourceAsStream(path)
if (inputStreamOptional. isPresent()) {
return inputStreamOptional. get(). withCloseable { InputStream inputStream ->
Optional. of((long ) inputStream. readAllBytes(). length)
}
}
Optional. empty()
}
}
3.7.3.4 Resources Templates
Resource templates allow servers to expose parameterized resources using URI templates
3.7.3.4.1 Resource Template with Annotations
The preferred way to declare a resource is using a method annotated with ResourceTemplate in a @Singleton bean.
@Singleton
class MyResourcesTemplates {
private static final String PGN_MIME_TYPE = "application/x-chess-pgn" ;
private final PgnLoader pgnLoader;
MyResourcesTemplates (PgnLoader pgnLoader ) {
this .pgnLoader = pgnLoader;
}
@ResourceTemplate (uriTemplate = "pgn://round/{round}" ,
mimeType = PGN_MIME_TYPE,
name = "2024ChessChampionshipRoundPgn" ,
title = "PGN of a round World Chess Championship 2024" ,
description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju" )
String pgn (Integer round ) {
return pgnLoader.loadPgn (round)
.orElseThrow (() -> new McpError (new McpSchema.JSONRPCResponse.JSONRPCError (McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "resource for round not found" , null )));
}
} PGN_MIME_TYPE = "application/x-chess-pgn"
@Singleton
class MyResourcesTemplates :
def __init__ (self, pgn_loader: PgnLoader):
self .pgn_loader = pgn_loader
@ResourceTemplate (uriTemplate = "pgn://round/ {round} " ,
mimeType = PGN_MIME_TYPE ,
name = "2024ChessChampionshipRoundPgn" ,
title = "PGN of a round World Chess Championship 2024" ,
description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju" )
def pgn (self, round: int ) -> str :
pgn = self .pgn_loader.load_pgn(round )
if pgn is None :
raise McpError(McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND , "resource for round not found" , None ))
return pgn@Singleton
class MyResourcesTemplates (private val pgnLoader: PgnLoader ) {
@ResourceTemplate (uriTemplate = "pgn://round/{round}" ,
mimeType = PGN_MIME_TYPE,
name = "2024ChessChampionshipRoundPgn" ,
title = "PGN of a round World Chess Championship 2024" ,
description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju" )
fun pgn (round: Int ): String {
return pgnLoader.loadPgn (round)
.orElseThrow { McpError (McpSchema.JSONRPCResponse.JSONRPCError (McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "resource for round not found" , null )) }
}
companion object {
private const val PGN_MIME_TYPE = "application/x-chess-pgn"
}
} @Singleton
class MyResourcesTemplates {
private static final String PGN_MIME_TYPE = "application/x-chess-pgn"
private final PgnLoader pgnLoader
MyResourcesTemplates (PgnLoader pgnLoader ) {
this . pgnLoader = pgnLoader
}
@ResourceTemplate (uriTemplate = "pgn://round/{round}" ,
mimeType = PGN_MIME_TYPE ,
name = "2024ChessChampionshipRoundPgn" ,
title = "PGN of a round World Chess Championship 2024" ,
description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju" )
String pgn (Integer round ) {
pgnLoader. loadPgn(round)
.orElseThrow { new McpError (new McpSchema.JSONRPCResponse.JSONRPCError (McpSchema.ErrorCodes. RESOURCE_NOT_FOUND , "resource for round not found" , null )) }
}
}
3.7.3.4.1.1 Resource Annotation Method Parameters
In a method annotated with @ResourceTemplate you can bind parameters with types:
Additionally, you can add method parameter matching the uri template variable as show in the previous example.
3.7.3.4.1.2 Resource Annotation Method Return Type
In a method annotated with @ResourceTemplate, you can use as a return type:
io.modelcontextprotocol.spec.McpSchema.ReadResourceResult
String. If the method returns a String, the server responds with a single text resource content using the provided mimeType.
3.7.3.4.2 Resource Templates with a Factory
Alternatively, you define resources registering beans (typically in a bean factory ).
imports 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 static io.micronaut.mcp.docs.resources.ResourcesFactory.PGN_MIME_TYPE;
import static io.micronaut.mcp.docs.resources.ResourcesFactory.readResourceResult;@Factory
class ResourcesTemplatesFactory {
private final PgnLoader pgnLoader;
ResourcesTemplatesFactory (PgnLoader pgnLoader ) {
this .pgnLoader = pgnLoader;
}
@Singleton
McpStatelessServerFeatures.SyncResourceTemplateSpecification pgnResourceTemplateSpecification () {
McpSchema. ResourceTemplate resourceTemplate = createPgnResourceTemplate ();
return new McpStatelessServerFeatures.SyncResourceTemplateSpecification (resourceTemplate,
(mcpTransportContext, readResourceRequest) -> readResourceResult (readResourceRequest.uri (), pgnLoader));
}
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 );
}
} imports from .PgnLoader import PgnLoader
from .ResourcesFactory import PGN_MIME_TYPE , read_resource_result
from io.modelcontextprotocol.server import McpStatelessServerFeatures
from io.modelcontextprotocol.spec import McpSchema
from jakarta.inject import Singleton
from micronaut.context.annotation import Factory, Requires@Factory
class ResourcesTemplatesFactory :
def __init__ (self, pgn_loader: PgnLoader):
self .pgn_loader = pgn_loader
@Singleton
def pgn_resource_template_specification (self) -> McpStatelessServerFeatures.SyncResourceTemplateSpecification:
resource_template = self .create_pgn_resource_template()
return McpStatelessServerFeatures.SyncResourceTemplateSpecification(
resource_template,
lambda ctx, read_resource_request: read_resource_result(read_resource_request.uri(), self .pgn_loader))
def create_pgn_resource_template (self) -> McpSchema.ResourceTemplate:
uri_template = "pgn://round/ {round} "
name = "2024ChessChampionshipRoundPgn"
title = "PGN of a round World Chess Championship 2024"
description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju"
return McpSchema.ResourceTemplate(uri_template, name, title, description, PGN_MIME_TYPE , None , None )imports package example.micronaut
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.micronaut.mcp.docs.resources.ResourcesFactory.Companion.PGN_MIME_TYPE
import io.micronaut.mcp.docs.resources.ResourcesFactory.Companion.readResourceResult
import io.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton @Factory
class ResourcesTemplatesFactory (private val pgnLoader: PgnLoader ) {
@Singleton
fun pgnResourceTemplateSpecification (): McpStatelessServerFeatures .SyncResourceTemplateSpecification {
val resourceTemplate = createPgnResourceTemplate ()
return McpStatelessServerFeatures.SyncResourceTemplateSpecification (resourceTemplate) { _, readResourceRequest ->
readResourceResult (readResourceRequest.uri (), pgnLoader)
}
}
fun createPgnResourceTemplate (): McpSchema .ResourceTemplate {
val uriTemplate = "pgn://round/{round}"
val name = "2024ChessChampionshipRoundPgn"
val title = "PGN of a round World Chess Championship 2024"
val description = "Given a round, it returns a PGN of the World Chess Championship 2024 between Ding Liren and Gukesh Dommaraju"
return McpSchema.ResourceTemplate (uriTemplate, name, title, description, PGN_MIME_TYPE, null , null )
}
} imports package example.micronaut
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.modelcontextprotocol.common.McpTransportContext
import io.modelcontextprotocol.server.McpStatelessServerFeatures
import io.modelcontextprotocol.spec.McpSchema
import jakarta.inject.Singleton
import static io.micronaut.mcp.docs.resources.ResourcesFactory.PGN_MIME_TYPE
import static io.micronaut.mcp.docs.resources.ResourcesFactory.readResourceResult@Factory
class ResourcesTemplatesFactory {
private final PgnLoader pgnLoader
ResourcesTemplatesFactory (PgnLoader pgnLoader ) {
this . pgnLoader = pgnLoader
}
@Singleton
McpStatelessServerFeatures.SyncResourceTemplateSpecification pgnResourceTemplateSpecification () {
McpSchema.ResourceTemplate resourceTemplate = createPgnResourceTemplate()
new McpStatelessServerFeatures.SyncResourceTemplateSpecification (resourceTemplate,
{ McpTransportContext ctx , McpSchema.ReadResourceRequest readResourceRequest -> readResourceResult(readResourceRequest. uri(), pgnLoader) })
}
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"
new McpSchema.ResourceTemplate (uriTemplate, name, title, description, PGN_MIME_TYPE , null , null )
}
}
The Model Context Protocol (MCP) provides a standardized way for servers to offer argument autocompletion suggestions for prompts and resource URIs.
3.7.4.1 Prompt Completions with Annotations
The preferred way to declare a prompt completion is using a method annotated with PromptCompletion in a @Singleton bean.
@Singleton
class MyPromptsCompletions {
@PromptCompletion (name = "code_review" )
List<String > languages (String language ) {
if (language != null && language.startsWith ("py" )) {
return List.of ("python" , "pytorch" , "pyside" );
}
return Collections.emptyList ();
}
} @Singleton
class MyPromptsCompletions :
@PromptCompletion (name = "code_review" )
def languages (self, language: str | None ) -> list[str ]:
if language is not None and language.startswith("py" ):
return ["python" , "pytorch" , "pyside" ]
return []@Singleton
class MyPromptsCompletions {
@PromptCompletion (name = "code_review" )
fun languages (language: String ?): List <String > {
if (language != null && language.startsWith ("py" )) {
return listOf ("python" , "pytorch" , "pyside" )
}
return emptyList ()
}
} @Singleton
class MyPromptsCompletions {
@PromptCompletion (name = "code_review" )
List<String> languages (String language ) {
if (language != null && language. startsWith("py" )) {
return ["python" , "pytorch" , "pyside" ]
}
Collections. emptyList()
}
}
Note
You need a prompt with the same name as the completion prompt. Moreover, the name of the prompt completion argument must match the name of the prompt argument.
3.7.4.1.1 PromptCompletion Annotation Method Parameters
In a method annotated with @PromptCompletion you can bind parameters with types:
MicronautMcpTransportContext
io.modelcontextprotocol.spec.McpSchema.CompleteRequest
io.modelcontextprotocol.spec.McpSchema.CompleteRequest.CompleteArgument
You can also bind the completion argument’s name as a method parameter, as illustrated in the code listing above.
3.7.4.1.2 PromptCompletion Annotation Method Return Type
In a method annotated with @PromptCompletion, you can use as a return type:
3.7.4.2 Resource Completions with Annotations
The preferred way to declare a resource completion is using a method annotated with ResourceCompletion in a @Singleton bean.
@Singleton
class MyResourceCompletions {
@ResourceCompletion (uri = "file:///home/user/documents/{fileName}" )
List<String > resourcesCompletions (String fileName ) {
return List.of (
"report.pdf" ,
"data.csv" ,
"notes.txt"
).stream ()
.filter (name -> name.startsWith (fileName))
.toList ();
}
} @Singleton
class MyResourceCompletions :
@ResourceCompletion (uri = "file:///home/user/documents/ {fileName} " )
def resources_completions (self, fileName: str ) -> list[str ]:
return [name for name in ["report.pdf" , "data.csv" , "notes.txt" ] if name.startswith(fileName)]@Singleton
class MyResourceCompletions {
@ResourceCompletion (uri = "file:///home/user/documents/{fileName}" )
fun resourcesCompletions (fileName: String ): List <String > {
return listOf (
"report.pdf" ,
"data.csv" ,
"notes.txt"
)
.filter { name -> name.startsWith (fileName) }
}
} @Singleton
class MyResourceCompletions {
@ResourceCompletion (uri = "file:///home/user/documents/{fileName}" )
List<String> resourcesCompletions (String fileName ) {
[
"report.pdf" ,
"data.csv" ,
"notes.txt"
]. findAll { String name -> name. startsWith(fileName) }
}
}
You can use resource completions, for example, in combination with Resource templates (see the uri for both the resource template and resource completion match):
@Singleton
class MyResourceTemplates {
@ResourceTemplate (
uriTemplate = "file:///home/user/documents/{fileName}" ,
name = "userDocument" ,
title = "User Document"
)
String ref (String fileName ) {
if (fileName.equals ("report.pdf" )) {
return "Report PDF" ;
} else if (fileName.equals ("data.csv" )) {
return "Data CSV" ;
} else if (fileName.equals ("notes.txt" )) {
return "Notes TXT" ;
}
return "" ;
}
} @Singleton
class MyResourceTemplates :
@ResourceTemplate (
uriTemplate = "file:///home/user/documents/ {fileName} " ,
name = "userDocument" ,
title = "User Document"
)
def ref (self, fileName: str ) -> str :
if fileName == "report.pdf" :
return "Report PDF"
elif fileName == "data.csv" :
return "Data CSV"
elif fileName == "notes.txt" :
return "Notes TXT"
return "" @Singleton
class MyResourceTemplates {
@ResourceTemplate (
uriTemplate = "file:///home/user/documents/{fileName}" ,
name = "userDocument" ,
title = "User Document"
)
fun ref (fileName: String ): String {
return when (fileName) {
"report.pdf" -> "Report PDF"
"data.csv" -> "Data CSV"
"notes.txt" -> "Notes TXT"
else -> ""
}
}
} @Singleton
class MyResourceTemplates {
@ResourceTemplate (
uriTemplate = "file:///home/user/documents/{fileName}" ,
name = "userDocument" ,
title = "User Document"
)
String ref (String fileName ) {
if (fileName == "report.pdf" ) {
return "Report PDF"
} else if (fileName == "data.csv" ) {
return "Data CSV"
} else if (fileName == "notes.txt" ) {
return "Notes TXT"
}
""
}
}
3.7.4.2.1 ResourceCompletion Annotation Method Parameters
In a method annotated with @ResourceCompletion you can bind parameters with types:
MicronautMcpTransportContext
io.modelcontextprotocol.spec.McpSchema.CompleteRequest
io.modelcontextprotocol.spec.McpSchema.CompleteRequest.CompleteArgument
You can also bind the completion argument’s name as a method parameter, as illustrated in the code listing above.
3.7.4.2.2 ResourceCompletion Annotation Method Return Type
In a method annotated with @ResourceCompletion, you can use as a return type:
You can configure multiple clients via configuration:
To use MCP Clients powered by the MCP Java SDK , use the following dependency:
implementation ("io.micronaut.mcp:micronaut-mcp-client-java-sdk" )<dependency >
<groupId >io.micronaut.mcp</groupId >
<artifactId >micronaut-mcp-client-java-sdk</artifactId >
</dependency >
Additionally, if you are building an MCP Server , you can test it by injecting a bean of type io.modelcontextprotocol.client.McpSyncClient or io.modelcontextprotocol.client.McpAsyncClient in your tests. The MCP Client pointing to your MCP Server has a name qualifier with value embeddedServer.
To customize client creation for your application’s needs, define
BeanCreatedEventListener s
for HttpClientStreamableHttpTransport.Builder, McpClient.SyncSpec, or McpClient.AsyncSpec.
4.2 Langchain4j MCP Client
implementation ("io.micronaut.mcp:micronaut-mcp-client-langchain4j" )<dependency >
<groupId >io.micronaut.mcp</groupId >
<artifactId >micronaut-mcp-client-langchain4j</artifactId >
</dependency >
Additionally, if you are building an MCP Server, you can test it by injecting a bean of type dev.langchain4j.mcp.client.McpClient in your tests.
The MCP Client pointing to your MCP Server has a name qualifier with value embeddedServer.
To customize client creation for your application’s needs, define a
BeanCreatedEventListener
for StreamableHttpMcpTransport.Builder.
You can find the source code of this project in this repository: