MCP
Model Context Protocol (MCP) module for Jooby.
The MCP module provides seamless integration with the Model Context Protocol, allowing your application to act as a standardized AI context server. It automatically bridges your Java/Kotlin methods with LLM clients by exposing them as Tools, Resources, and Prompts.
Usage
1) Add the dependencies (Jooby MCP, Jackson, and the APT processor):
<!-- Jackson Module-->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson3</artifactId>
<version>4.5.0</version>
</dependency>
<!-- MCP Jackson Module-->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-mcp-jackson3</artifactId>
<version>4.5.0</version>
</dependency>
<!-- MCP Module-->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-mcp</artifactId>
<version>4.5.0</version>
</dependency>
|
Note
|
You must configure the Jooby Annotation Processor (APT) in your build. The MCP module relies on APT to generate high-performance routing code with zero reflection overhead. |
2) Define a service and expose capabilities via annotations:
import io.jooby.annotation.mcp.McpTool;
public class CalculatorService {
@McpTool(description = "Adds two numbers together")
public int add(int a, int b) {
return a + b;
}
}
3) Install the module using Jackson and the generated service:
import io.jooby.jackson3.Jackson3Module;
import io.jooby.mcp.jackson3.McpJackson3Module;
import io.jooby.mcp.McpModule;
{
install(new Jackson3Module()); (1)
install(new McpJackson3Module()); (2)
install(new McpModule(new CalculatorServiceMcp_())); (3)
}
-
Install JSON support (Jackson is required for MCP JSON-RPC serialization). For Jackson 2, use
Jackson2Module()instead. -
Install MCP JSON support. For Jackson 2, use
McpJackson2Module()instead. -
Install the MCP module with the APT-generated
McpServicedispatcher. The generated class always ends with theMcp_suffix.
Core Capabilities
The module uses annotations to expose application logic to AI clients:
-
@McpTool: Exposes a method as an executable tool. The module automatically generates JSON schemas for the parameters. -
@McpPrompt: Exposes a method as a reusable prompt template. -
@McpResource/@McpResourceTemplate: Exposes static or dynamic data as an MCP resource (e.g.,file://config). -
@McpCompletion: Handles auto-completion logic for prompts and resources.
Schema Descriptions & Javadoc
Rich descriptions are highly recommended for LLM usage so the model understands exactly what a tool, prompt, or resource does. You can provide these descriptions directly in the MCP annotations (e.g., @McpTool(description = "…")).
However, if you omit them, the Jooby Annotation Processor will automatically extract descriptions from your standard Javadoc comments, including method descriptions and @param tags.
import io.jooby.annotation.mcp.McpTool;
public class WeatherService {
/**
* Retrieves the current weather forecast for a given location.
*
* @param location The city and state, e.g., "San Francisco, CA"
* @param units The temperature unit to use (celsius or fahrenheit)
*/
@McpTool
public WeatherForecast getWeather(String location, String units) {
// ...
}
}
In the example above, the LLM will automatically receive the method’s Javadoc summary as the tool description, and the @param comments as the descriptions for the location and units JSON schema properties.
Transports
By default, the MCP module starts a single server using the STREAMABLE_HTTP transport. You can easily switch to other supported transports such as SSE (Server-Sent Events), WEBSOCKET, or STATELESS_STREAMABLE_HTTP.
import io.jooby.mcp.McpModule.Transport;
{
install(new McpModule(new CalculatorServiceMcp_())
.transport(Transport.WEBSOCKET));
}
Output Schema Generation
By default, the framework does not generate JSON output schemas for tools in order to save LLM context window tokens. You can enable it globally on the module, or override it per-method using the @McpOutputSchema annotation.
{
// Enable output schema generation for all tools by default
install(new McpModule(new CalculatorServiceMcp_())
.generateOutputSchema(true));
}
Alternatively, you can control output schema generation using your application configuration properties. Configuration properties always take precedence over the programmatic setup, and allow you to configure behavior per-server.
# Global fallback for all servers
mcp.generateOutputSchema = true
# Per-server override (takes precedence over the global flag)
mcp.calculator.generateOutputSchema = false
You can explicitly override the global/config flag and bypass Java type erasure using nested @McpOutputSchema annotations:
import io.jooby.annotation.mcp.McpTool;
import io.jooby.annotation.mcp.McpOutputSchema;
public class UserService {
@McpTool
@McpOutputSchema.ArrayOf(User.class) (1)
public List<Object> findUsers(String query) {
// ...
}
@McpTool
@McpOutputSchema.Off (2)
public HugeDataset getBigData() {
// ...
}
}
-
Forces array schema generation for
User, overriding genericObjecterasure and the global/config flag. -
Explicitly disables schema generation for this specific tool.
Custom Invokers
You can inject custom logic (like SLF4J MDC context propagation, tracing, or custom error handling) around every tool, prompt, or resource execution by providing an McpInvoker.
|
Note
|
Invokers are chained. You can register multiple invokers and they will wrap the execution in the order they were added:
|
import io.jooby.mcp.McpInvoker;
import io.jooby.mcp.McpOperation;
import io.jooby.mcp.McpChain;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import org.jspecify.annotations.Nullable;
import org.slf4j.MDC;
public class MdcMcpInvoker implements McpInvoker {
@Override
public <R> R invoke(@Nullable McpSyncServerExchange exchange, McpTransportContext transportContext, McpOperation operation, McpChain chain) throws Exception {
try {
MDC.put("mcp.id", operation.id()); // (1)
MDC.put("mcp.class", operation.className());
MDC.put("mcp.method", operation.methodName());
return chain.proceed(exchange, transportContext, operation); // (2)
} finally {
MDC.remove("mcp.id");
MDC.remove("mcp.class");
MDC.remove("mcp.method");
}
}
}
{
install(new McpModule(new CalculatorServiceMcp_())
.invoker(new MdcMcpInvoker())); // (3)
}
-
Extract rich contextual data from the
McpOperationrecord. -
Proceed to the next interceptor in the chain or execute the final target handler.
-
Register the invoker. Jooby will safely map any business exceptions thrown by your chain into valid MCP JSON-RPC errors.
Context Augmentation
You can use an McpInvoker to resolve contextual data (such as an authenticated user, a tenant ID, etc.) and inject it directly into the McpOperation.
This allows your tool methods to simply declare the custom type in their method signature, keeping your business logic clean and completely decoupled from transport-layer extraction.
import io.jooby.mcp.McpInvoker;
import io.jooby.mcp.McpOperation;
import io.jooby.mcp.McpChain;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import org.jspecify.annotations.Nullable;
public class UserContextInvoker implements McpInvoker {
@Override
@SuppressWarnings("unchecked")
public <R> R invoke(@Nullable McpSyncServerExchange exchange, McpTransportContext transportContext, McpOperation operation, McpChain chain) throws Exception {
User currentUser = retrieveUser();
// 2. Augment the operation with the resolved user
operation.setArgument("user", currentUser);
// 3. Proceed with the augmented operation
return (R) chain.proceed(exchange, transportContext, augmentedOp);
}
}
Once the invoker is registered, you can seamlessly declare the augmented argument in your MCP controllers. The Jooby Annotation Processor will automatically map the injected argument to your method parameter.
import io.jooby.annotation.mcp.McpTool;
public class BillingService {
/**
* @param user The authenticated user (injected by UserContextInvoker).
* Note: Because it is a complex type not present in the JSON request,
* it is safely ignored by the JSON schema generator.
*/
@McpTool(description = "Retrieves the billing history for the current user")
public InvoiceHistory getMyInvoices(User user, int limit) {
return database.findInvoices(user.getId(), limit);
}
}
Multiple Servers
You can run multiple, completely isolated MCP server instances within the same Jooby application by utilizing the @McpServer("serverName") annotation on your service classes.
When bootstrapping multiple servers, you must provide configuration for each server in your application.conf.
mcp.default.name = "default-mcp-server"
mcp.default.version = "1.0.0"
mcp.calculator.name = "calculator-mcp-server"
mcp.calculator.version = "1.0.0"
mcp.calculator.transport = "sse"
mcp.calculator.mcpEndpoint = "/mcp/calculator/sse"
{
// Bootstraps services each on their corresponding service base based on the @McpServer mappings
install(new McpModule(
new DefaultServiceMcp_(),
new CalculatorServiceMcp_()
));
}
Debugging & Testing (MCP Inspector)
When building an MCP server, it is highly recommended to test your tools, prompts, and resources locally before connecting them to a real LLM client.
The McpInspectorModule provides a built-in, interactive web UI that acts as a dummy LLM client. It allows you to manually trigger tools, view generated output schemas, inspect resources, and debug JSON-RPC payloads in real-time.
1) Add the inspector dependency (typically as a test or development dependency):
<!-- MCP Inspector-->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-mcp-inspector</artifactId>
<version>4.5.0</version>
</dependency>
2) Install the module, ensuring it is only active during development:
import io.jooby.mcp.McpModule;
import io.jooby.mcp.inspector.McpInspectorModule;
{
install(new McpModule(
new DefaultServiceMcp_(),
new CalculatorServiceMcp_()
));
// Only enable the inspector UI in the 'dev' environment
if (getEnvironment().isActive("dev")) {
install(new McpInspectorModule());
}
}
Once the application starts, open your browser and navigate to the default inspector route: http://localhost:8080/mcp-inspector.
Inspector Configuration
You can customize the behavior and mounting point of the Inspector UI using its programmatic builder methods:
{
if (getEnvironment().isActive("dev")) {
install(new McpInspectorModule()
.path("/debug/mcp") (1)
.defaultServer("calculator-mcp-server") (2)
.autoConnect(true) (3)
);
}
}
-
Changes the base path for the Inspector UI (defaults to
/mcp-inspector). -
Automatically selects a specific named server in the UI dropdown when dealing with multiple MCP servers.
-
Automatically connects to the selected server as soon as the page loads.
Special Thanks
A special thanks to kliushnichenko. This MCP module was heavily inspired by and based upon their foundational work and contributions to the ecosystem.