Table of Contents

Micronaut MCP

Integration with MCP (Model Context Protocol).

Version: 0.0.12

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>

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.enabled

boolean

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.

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.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 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.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.

3.7 Primitives

3.7.1 Tools

Tools: Executable functions that allow models to perform actions or retrieve information

3.7.1.1 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.

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()));
    }
}

3.7.1.3 Fetch Tool

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());
    }
}

3.7.1.4 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 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";
    }
}

By default, the method name is used as the tool name. You can override this by setting the name attribute of the @Tool annotation.

3.7.1.4.1 Tool Hints

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!";
    }
}

3.7.1.4.2 Tool Annotation Method Parameters

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 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";
    }
}

In a method annotated with @Tool you can bind parameters with types:

3.7.1.4.3 Tool Annotation Method Return Type

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.

3.7.1.5 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 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";
    }
}

3.7.1.6 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.context.MicronautMcpTransportContext;
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,
                                        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");
    }
}

3.7.1.7 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.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 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.7.2 Prompts

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

🔗
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.7.2.3 Prompts with Annotations

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);
    }
}

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:

  • io.modelcontextprotocol.spec.McpSchema.GetPromptResult

  • String

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.

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.7.3 Resources

Resources: Structured data or content that provides additional context to the model

3.7.3.1 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.7.3.2 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).

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).

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.micronaut.mcp.server.utils.ResourceLoaderUtils;
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,
            (mcpSyncServerExchange,  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 ResourceLoaderUtils.size(resourceLoader, 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()));
    }
}

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 MyResources {
    private static final String PGN_MIME_TYPE = "application/x-chess-pgn";
    private final PgnLoader pgnLoader;

    MyResources(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)));
    }
}

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).

The following example assumes you set SYNC as your server type. Because of that, the following example creates defining resources with the class McpStatelessServerFeatures.SyncResourceTemplateSpecification. 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.micronaut.mcp.server.utils.PgnLoader;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.inject.Singleton;

import static io.micronaut.mcp.server.stateless.sync.resources.ResourcesFactory.PGN_MIME_TYPE;
import static io.micronaut.mcp.server.stateless.sync.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);
    }
}

3.7.4 Completions

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();
    }
}
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:

  • io.modelcontextprotocol.spec.McpSchema.CompleteResult

  • List<String>

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();
    }
}

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 "";
    }
}

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:

  • io.modelcontextprotocol.spec.McpSchema.CompleteResult

  • List<String>

4 MCP Client

You can configure multiple clients via configuration:

🔗
Table 1. Configuration Properties for McpClientHttpConfigurationProperties
Property Type Description

micronaut.mcp.client.http.*.url

java.net.URI

The MCP Server URL

micronaut.mcp.client.http.*.timeout

java.time.Duration

The duration to wait for server responses before timing out requests.

micronaut.mcp.client.http.*.log-requests

boolean

Whether to log requests. Default value false.

micronaut.mcp.client.http.*.log-responses

boolean

Whether to log responses. Default value false.

4.1 Java SDK MCP Client

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.

4.2 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>

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.

5 Repository

You can find the source code of this project in this repository: