Skip to content

kliushnichenko/jooby-mcp

Repository files navigation

Maven Central Javadoc Github

jooby-mcp

This module integrates the official Java MCP SDK with Jooby’s routing, server features, and dependency injection.

This module enables declarative (annotation-based) registration of tools, prompts, and resources. Annotations are discovered at build time using APT, eliminating the need for runtime reflection. To use it, add the annotation processor alongside the module dependency.

Compatibility:

Jooby Version Jooby MCP Version
3.x 1.x

Features:

  • SSE, Streamable-HTTP and Stateless Streamable-HTTP transport
  • Multiple servers support
  • Tools
  • Prompts
  • Resources
  • Resource Templates
  • Prompt Completions
  • Resource Template Completions
  • Required input arguments validation in tools
  • Build time method signature and return type validation
  • Elicitation, Sampling, Progress support via exchange object
  • MCP Inspector integration module

Table of Contents:

Quick Start

  1. Add the dependency to your pom.xml:

    <dependency>
        <groupId>io.github.kliushnichenko</groupId>
        <artifactId>jooby-mcp</artifactId>
        <version>${jooby.mcp.version}</version>
    </dependency>
  2. Add annotation processor

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <annotationProcessorPaths>
                <path>
                    <groupId>io.github.kliushnichenko</groupId>
                    <artifactId>jooby-mcp-apt</artifactId>
                    <version>${jooby.mcp.version}</version>
                </path>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
  3. Add configuration to application.conf
    ⚠️ Since version 1.4.0 default transport was changed from SSE to Streamable HTTP

    mcp.default {                       # `default` is the server key, can be customized over compiler arguments
      name: "my-awesome-mcp-server"     # Required
      version: "0.1.0"                  # Required
      transport: "sse"                  # Optional (default: streamable-http)
      sseEndpoint: "/mcp/sse"           # Optional (default: /mcp/sse), applicable only to SSE transport
      messageEndpoint: "/mcp/message"   # Optional (default: /mcp/message), applicable only to SSE transport
    }
    

    Full config for Streamable HTTP transport:

     mcp.default {
       name: "my-awesome-mcp-server"    # Required
       version: "0.1.0"                 # Required
       transport: "streamable-http"     # Optional (default: streamable-http)
       mcpEndpoint: "/mcp/streamable"   # Optional (default: /mcp), applicable only to Streamable HTTP transport
       disallowDelete: true             # Optional (default: false)
       keepAliveInterval: 45            # Optional (default: N/A), in seconds
     }
    

    Full config for Stateless Streamable HTTP transport:

     mcp.default {
       name: "my-awesome-mcp-server"    
       version: "0.1.0"                 
       transport: "stateless-streamable-http"    
       mcpEndpoint: "/mcp/stateless-streamable"   # Optional (default: /mcp)
     }
    

    *keepAliveInterval - enables sending periodic keep-alive messages to the client.
    Disabled by default to avoid excessive network overhead. Set to a positive integer value (in seconds) to enable.

  4. Implement your features (tools, prompts, resources, etc.), see examples below or in the example-project

  5. Install the module. After compilation, you can observe generated DefaultMcpServer class. Now register its instance in the module:

    {
       install(new JacksonModule());                    // a JSON encode/decoder is required for JSONRPC
       install(AvajeInjectModule.of());                 // or other DI module of your choice
       install(new McpModule(new DefaultMcpServer());   // register MCP server
    }

Tools & Prompts Example

import io.github.kliushnichenko.jooby.mcp.annotation.Tool;
import io.github.kliushnichenko.jooby.mcp.annotation.ToolArg;
import io.github.kliushnichenko.jooby.mcp.annotation.Prompt;

@Singleton
public class ToolsAndPromptsExample {

    @Tool(name = "add", description = "Adds two numbers together")
    public String add(
            @ToolArg(name = "first", description = "First number to add") int a,
            @ToolArg(name = "second", description = "Second number to add") int b
    ) {
        int result = a + b;
        return String.valueOf(result);
    }

    @Tool
    public String subtract(int a, int b) {
        int result = a - b;
        return String.valueOf(result);
    }

    @Prompt(name = "summarizeText", description = "Summarizes the provided text into a specified number of sentences")
    public String summarizeText(@PromptArg(name = "text") String text, String maxSentences) {
        return String.format("""
                Please provide a clear and concise summary of the following text in no more than %s sentences:
                %s
                """, maxSentences, text);
    }
} 

Tools Output Schema

The output schema is automatically generated based on the return type of the method. For example, for the find_pet tool below

@Tool(name = "find_pet", description = "Finds a pet by its ID")
public Pet findPet(String petId) {
    ...
}

the generated output schema will reflect the Pet class structure.

If the return type is one of the reserved types (String, McpSchema.CallToolResult, McpSchema.Content) auto-generation step is omitted. Use @OutputSchema annotation to explicitly define the output schema in such cases:

  • @OutputSchema.From(Pet.class) - to use schema generated from a specific class
  • @OutputSchema.ArrayOf(Pet.class) - to use array of specific class as output schema
  • @OutputSchema.MapOf(Pet.class) - to use map of specific class as output schema

Example:

@Tool(name = "find_pet", description = "Finds a pet by its ID")
@OutputSchema.From(Pet.class)
public McpSchema.CallToolResult findPet(String petId) {
    ...
    
    return new McpSchema.CallToolResult(pet, false);
}

Tip: You can use @OutputSchema.Suppressed to skip output schema generation from return type.

How to enrich json schema?

Whether the schema is generated from method arguments or return type, you can enrich it using OpenAPI annotations. At the moment, generator will respect description and required attributes from @Schema annotation. Also, if @JsonProperty is present on a field, its value will be used as the property name in the generated schema.

import io.swagger.v3.oas.annotations.media.Schema;  
class User {
    @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The user's middle name")
    @JsonProperty("middle-name")
    private String middleName;
}

Resource Example

import io.github.kliushnichenko.jooby.mcp.annotation.Resource;

@Singleton
public class ResourceExamples {

    @Resource(uri = "file:///project/README.md", name = "README.md", title = "README", mimeType = "text/markdown")
    public McpSchema.TextResourceContents textResource() {
        String content = """
                # Project Title

                This is an example README file for the project.

                ## Features

                - Feature 1
                - Feature 2
                - Feature 3

                """;
        return new McpSchema.TextResourceContents("file:///project/README.md", "text/markdown", content);
    }
}

Find more examples in the example-project

Resource Template Example

import io.github.kliushnichenko.jooby.mcp.annotation.ResourceTemplate;
import io.github.kliushnichenko.jooby.mcp.annotation.CompleteResourceTemplate;

@Singleton
public class ResourceTemplateExamples {

    private static final Map<String, String> PROJECTS = Map.of(
            "project-alpha", "This is Project Alpha.",
            "project-beta", "This is Project Beta.",
            "project-gamma", "This is Project Gamma."
    );

    @ResourceTemplate(name = "get_project", uriTemplate = "file:///project/{name}")
    public McpSchema.TextResourceContents getProject(String name, ResourceUri resourceUri) {
        String content = PROJECTS.getOrDefault(name, "<Project not found>");
        return new McpSchema.TextResourceContents(resourceUri.uri(), "text/markdown", content);
    }

    @CompleteResourceTemplate("get_project")
    public List<String> projectNameCompletion(@CompleteArg(name = "name") String partialInput) {
        return PROJECTS.keySet()
                .stream()
                .filter(name -> name.contains(partialInput))
                .toList();
    }
}

Prompt Completion Example

import io.github.kliushnichenko.jooby.mcp.annotation.CompleteArg;
import io.github.kliushnichenko.jooby.mcp.annotation.CompletePrompt;
import io.github.kliushnichenko.jooby.mcp.annotation.Prompt;

@Singleton
public class PromptCompletionsExample {

    private static final List<String> SUPPORTED_LANGUAGES = List.of("Java", "Python", "JavaScript", "Go", "TypeScript");

    @Prompt(name = "code_review", description = "Code Review Prompt")
    public String codeReviewPrompt(String codeSnippet, String language) {
        return """
                You are a senior software engineer tasked with reviewing the following %s code snippet:
                   
                %s
                   
                Please provide feedback on:
                1. Code readability and maintainability.
                2. Potential bugs or issues.
                3. Suggestions for improvement.
                 """.formatted(language, codeSnippet);
    }

    @CompletePrompt("code_review")
    public List<String> completeCodeReviewLang(@CompleteArg(name = "language") String partialInput) {
        return SUPPORTED_LANGUAGES.stream()
                .filter(lang -> lang.toLowerCase().contains(partialInput.toLowerCase()))
                .toList();
    }
}

Exchange Object

An exchange object can be used to access request metadata, and support Elicitation, Sampling or Progress features.
Add McpSyncServerExchange argument to your tool method:

public class ElicitationExample {
    
    @Tool(name = "elicitation_example")
    public String elicitationExample(McpSyncServerExchange exchange) {
        ...
        exchange.createElicitation(request);
        ...
    }
} 

See full example at example-project

In the similar manner you can support Sampling, Progress and other features by using McpSyncServerExchange object.
Explore SDK documentation for more details: https://modelcontextprotocol.io/sdk/java/mcp-server#using-sampling-from-a-server

Multiple Servers Support

Use @McpServer annotation to assign a tool or prompt to a specific server. Annotation can be applied at the class or method level.

import io.github.kliushnichenko.jooby.mcp.annotation.McpServer;

@Singleton
@McpServer("weather")
public class WeatherService {

 public record Coordinates(double latitude, double longitude) {
 }

 @Tool(name = "get_weather")
 public String getWeather(Coordinates coordinates) {
         ...
 }
}

As a result, additional WeatherMcpServer class will be generated. Register it in the module:

{
     install(new McpModule(new DefaultMcpServer(),new WeatherMcpServer()));
}

The weather MCP server should have its own configuration section in application.conf:

mcp.weather {
  name: "weather-mcp-server"
  version: "0.1.0"
  mcpEndpoint: "/mcp/weather"
}

Customizing Default Server Name and Package

You can customize the default server name and the package where the generated server classes will be placed by providing compiler arguments. For example, to set the default server name to CalculatorMcpServer and the package to com.acme.corp.mcp, you can add the following configuration to your pom.xml:

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <configuration>
     <annotationProcessorPaths>
         <path>
             <groupId>io.github.kliushnichenko</groupId>
             <artifactId>jooby-mcp-apt</artifactId>
             <version>${version}</version>
         </path>
     </annotationProcessorPaths>
     <compilerArgs>
         <arg>-Amcp.default.server.key=calculator</arg>
         <arg>-Amcp.target.package=com.acme.corp.mcp</arg>
     </compilerArgs>
 </configuration>
</plugin>

Mind, that mcp.default.server.key should match the configuration section in application.conf:

mcp.calculator {
  name: "calculator-mcp-server"
  version: "0.1.0"
  mcpEndpoint: "/mcp/calculator"
}

MCP Inspector Module

You can use the McpInspectorModule to spin up mcp-inspector-ui right on jooby server and speed up your development process. Mind, that it works in Direct connection mode only, aimed solely to test your local MCP server during development and requires McpModule to be installed. To enable it, just install the module alongside McpModule:

{
    install(new McpInspectorModule());
}

By default, inspector will be available at /mcp-inspector path. And it will try to auto-connect to MCP server upon loading.
You can customize both options over corresponding mutators:

{
    install(new McpInspectorModule()
                .path("/custom-inspector-path")
                .autoConnect(false);
    );
}:

Dependency:

 <dependency>
    <groupId>io.github.kliushnichenko</groupId>
    <artifactId>jooby-mcp-inspector</artifactId>
    <version>${jooby.mcp.version}</version>
</dependency>

Appendix: Supported Return Types

Supported return types in Tools
  • String
  • McpSchema.CallToolResult
  • McpSchema.Content
  • McpSchema.TextContent
  • POJO (will be serialized to JSON)
Supported return types in Prompts
  • McpSchema.GetPromptResult
  • McpSchema.PromptMessage
  • List<McpSchema.PromptMessage>
  • McpSchema.Content
  • String
  • POJO (toString() method will be invoked to get the string representation)
Supported return types in Resources and Resource Templates
  • McpSchema.ReadResourceResult
  • McpSchema.ResourceContents
  • List<McpSchema.ResourceContents>
  • McpSchema.TextResourceContents
  • McpSchema.BlobResourceContents
  • POJO (will be serialized to JSON)
Supported return types in Completions
  • McpSchema.CompleteResult
  • McpSchema.CompleteResult.CompleteCompletion
  • List<String>
  • String

About

Jooby module that lets you run a Model Context Protocol (MCP) server

Topics

Resources

License

Stars

Watchers

Forks

Languages