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
- Tools & Prompts Example
- Resource Example
- Resource Template Example
- Prompt Completion Example
- Exchange Object
- Multiple Servers Support
- Customizing Default Server Name and Package
- MCP Inspector Module
- Appendix: Supported Return Types
-
Add the dependency to your
pom.xml:<dependency> <groupId>io.github.kliushnichenko</groupId> <artifactId>jooby-mcp</artifactId> <version>${jooby.mcp.version}</version> </dependency>
-
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>
-
Add configuration to
application.conf
⚠️ Since version1.4.0default transport was changed fromSSEtoStreamable HTTPmcp.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 HTTPtransport: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 HTTPtransport: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. -
Implement your features (tools, prompts, resources, etc.), see examples below or in the example-project
-
Install the module. After compilation, you can observe generated
DefaultMcpServerclass. 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 }
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);
}
} 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.
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;
}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
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();
}
}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();
}
}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
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"
}
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"
}
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>StringMcpSchema.CallToolResultMcpSchema.ContentMcpSchema.TextContent- POJO (will be serialized to JSON)
McpSchema.GetPromptResultMcpSchema.PromptMessageList<McpSchema.PromptMessage>McpSchema.ContentString- POJO (
toString()method will be invoked to get the string representation)
McpSchema.ReadResourceResultMcpSchema.ResourceContentsList<McpSchema.ResourceContents>McpSchema.TextResourceContentsMcpSchema.BlobResourceContents- POJO (will be serialized to JSON)
McpSchema.CompleteResultMcpSchema.CompleteResult.CompleteCompletionList<String>String