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:

implementation("io.micronaut.mcp:micronaut-mcp-server-java-sdk")
<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:

🔗
Table 1. Configuration Properties for McpServerInfoConfigurationProperties
Property Type Description

micronaut.mcp.server.info.name

java.lang.String

micronaut.mcp.server.info.version

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.

🔗
Table 1. Configuration Properties for McpServerConfigurationProperties
Property Type Description

micronaut.mcp.server.endpoint

java.lang.String

The MCP Server endpoint. It applies to MCP Servers using HTTP transport. It defaults to /mcp.

micronaut.mcp.server.transport

Transport

Set the MCP Transport. It defaults to HTTP.

micronaut.mcp.server.reactive

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

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.

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

🔗
Table 1. Configuration Properties for ToolsConfigurationProperties
Property Type Description

micronaut.mcp.server.tools.list-changed

boolean

whether the server will emit notifications when the list of available tools changes. Default value false.

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

🔗
Table 1. Configuration Properties for PromptsConfigurationProperties
Property Type Description

micronaut.mcp.server.prompts.list-changed

boolean

whether the server will emit notifications when the list of available prompts changes. Default value false.

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 the name attribute of the @Resource annotation.

  • You can optionally provide title, description and mimeType (defaults to text/plain).

  • If the method returns a String, the server responds with a single text resource content using the provided mimeType.

  • Supported method signatures:

    • String myResource() or ReadResourceResult myResource()

    • String myResource(String uri) or ReadResourceResult 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 single TextResourceContents with the configured mimeType

    • io.modelcontextprotocol.spec.McpSchema.ReadResourceResult — for advanced responses (multiple contents, binary, sizes, etc.)

3.4.3.2 Resources Configuration

🔗
Table 1. Configuration Properties for ResourcesConfigurationProperties
Property Type Description

micronaut.mcp.server.resources.subscribe

boolean

whether the client can subscribe to be notified of changes to individual resources. Default value false.

micronaut.mcp.server.resources.list-changed

boolean

whether the server will emit notifications when the list of available resources changes. Default value false.

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: