---
description: 'Best practices and patterns for building Model Context Protocol (MCP) servers in Java using the official MCP Java SDK with reactive streams and Spring integration.'
applyTo: "**/*.java, **/pom.xml, **/build.gradle, **/build.gradle.kts"
---
# Java MCP Server Development Guidelines
When building MCP servers in Java, follow these best practices and patterns using the official Java SDK.
## Dependencies
Add the MCP Java SDK to your Maven project:
```xml
io.modelcontextprotocol.sdk
mcp
0.14.1
```
Or for Gradle:
```kotlin
dependencies {
implementation("io.modelcontextprotocol.sdk:mcp:0.14.1")
}
```
## Server Setup
Create an MCP server using the builder pattern:
```java
import io.mcp.server.McpServer;
import io.mcp.server.McpServerBuilder;
import io.mcp.server.transport.StdioServerTransport;
McpServer server = McpServerBuilder.builder()
.serverInfo("my-server", "1.0.0")
.capabilities(capabilities -> capabilities
.tools(true)
.resources(true)
.prompts(true))
.build();
// Start with stdio transport
StdioServerTransport transport = new StdioServerTransport();
server.start(transport).subscribe();
```
## Adding Tools
Register tool handlers with the server:
```java
import io.mcp.server.tool.Tool;
import io.mcp.server.tool.ToolHandler;
import reactor.core.publisher.Mono;
// Define a tool
Tool searchTool = Tool.builder()
.name("search")
.description("Search for information")
.inputSchema(JsonSchema.object()
.property("query", JsonSchema.string()
.description("Search query")
.required(true))
.property("limit", JsonSchema.integer()
.description("Maximum results")
.defaultValue(10)))
.build();
// Register tool handler
server.addToolHandler("search", (arguments) -> {
String query = arguments.get("query").asText();
int limit = arguments.has("limit")
? arguments.get("limit").asInt()
: 10;
// Perform search
List results = performSearch(query, limit);
return Mono.just(ToolResponse.success()
.addTextContent("Found " + results.size() + " results")
.build());
});
```
## Adding Resources
Implement resource handlers for data access:
```java
import io.mcp.server.resource.Resource;
import io.mcp.server.resource.ResourceHandler;
// Register resource list handler
server.addResourceListHandler(() -> {
List resources = List.of(
Resource.builder()
.name("Data File")
.uri("resource://data/example.txt")
.description("Example data file")
.mimeType("text/plain")
.build()
);
return Mono.just(resources);
});
// Register resource read handler
server.addResourceReadHandler((uri) -> {
if (uri.equals("resource://data/example.txt")) {
String content = loadResourceContent(uri);
return Mono.just(ResourceContent.text(content, uri));
}
throw new ResourceNotFoundException(uri);
});
// Register resource subscribe handler
server.addResourceSubscribeHandler((uri) -> {
subscriptions.add(uri);
log.info("Client subscribed to {}", uri);
return Mono.empty();
});
```
## Adding Prompts
Implement prompt handlers for templated conversations:
```java
import io.mcp.server.prompt.Prompt;
import io.mcp.server.prompt.PromptMessage;
import io.mcp.server.prompt.PromptArgument;
// Register prompt list handler
server.addPromptListHandler(() -> {
List prompts = List.of(
Prompt.builder()
.name("analyze")
.description("Analyze a topic")
.argument(PromptArgument.builder()
.name("topic")
.description("Topic to analyze")
.required(true)
.build())
.argument(PromptArgument.builder()
.name("depth")
.description("Analysis depth")
.required(false)
.build())
.build()
);
return Mono.just(prompts);
});
// Register prompt get handler
server.addPromptGetHandler((name, arguments) -> {
if (name.equals("analyze")) {
String topic = arguments.getOrDefault("topic", "general");
String depth = arguments.getOrDefault("depth", "basic");
List messages = List.of(
PromptMessage.user("Please analyze this topic: " + topic),
PromptMessage.assistant("I'll provide a " + depth + " analysis of " + topic)
);
return Mono.just(PromptResult.builder()
.description("Analysis of " + topic + " at " + depth + " level")
.messages(messages)
.build());
}
throw new PromptNotFoundException(name);
});
```
## Reactive Streams Pattern
The Java SDK uses Reactive Streams (Project Reactor) for asynchronous processing:
```java
// Return Mono for single results
server.addToolHandler("process", (args) -> {
return Mono.fromCallable(() -> {
String result = expensiveOperation(args);
return ToolResponse.success()
.addTextContent(result)
.build();
}).subscribeOn(Schedulers.boundedElastic());
});
// Return Flux for streaming results
server.addResourceListHandler(() -> {
return Flux.fromIterable(getResources())
.map(r -> Resource.builder()
.uri(r.getUri())
.name(r.getName())
.build())
.collectList();
});
```
## Synchronous Facade
For blocking use cases, use the synchronous API:
```java
import io.mcp.server.McpSyncServer;
McpSyncServer syncServer = server.toSyncServer();
// Blocking tool handler
syncServer.addToolHandler("greet", (args) -> {
String name = args.get("name").asText();
return ToolResponse.success()
.addTextContent("Hello, " + name + "!")
.build();
});
```
## Transport Configuration
### Stdio Transport
For local subprocess communication:
```java
import io.mcp.server.transport.StdioServerTransport;
StdioServerTransport transport = new StdioServerTransport();
server.start(transport).block();
```
### HTTP Transport (Servlet)
For HTTP-based servers:
```java
import io.mcp.server.transport.ServletServerTransport;
import jakarta.servlet.http.HttpServlet;
public class McpServlet extends HttpServlet {
private final McpServer server;
private final ServletServerTransport transport;
public McpServlet() {
this.server = createMcpServer();
this.transport = new ServletServerTransport();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
transport.handleRequest(server, req, resp).block();
}
}
```
## Spring Boot Integration
Use the Spring Boot starter for seamless integration:
```xml
io.modelcontextprotocol.sdk
mcp-spring-boot-starter
0.14.1
```
Configure the server with Spring:
```java
import org.springframework.context.annotation.Configuration;
import io.mcp.spring.McpServerConfigurer;
@Configuration
public class McpConfiguration {
@Bean
public McpServerConfigurer mcpServerConfigurer() {
return server -> server
.serverInfo("spring-server", "1.0.0")
.capabilities(cap -> cap
.tools(true)
.resources(true)
.prompts(true));
}
}
```
Register handlers as Spring beans:
```java
import org.springframework.stereotype.Component;
import io.mcp.spring.ToolHandler;
@Component
public class SearchToolHandler implements ToolHandler {
@Override
public String getName() {
return "search";
}
@Override
public Tool getTool() {
return Tool.builder()
.name("search")
.description("Search for information")
.inputSchema(JsonSchema.object()
.property("query", JsonSchema.string().required(true)))
.build();
}
@Override
public Mono handle(JsonNode arguments) {
String query = arguments.get("query").asText();
return Mono.just(ToolResponse.success()
.addTextContent("Search results for: " + query)
.build());
}
}
```
## Error Handling
Use proper error handling with MCP exceptions:
```java
server.addToolHandler("risky", (args) -> {
return Mono.fromCallable(() -> {
try {
String result = riskyOperation(args);
return ToolResponse.success()
.addTextContent(result)
.build();
} catch (ValidationException e) {
return ToolResponse.error()
.message("Invalid input: " + e.getMessage())
.build();
} catch (Exception e) {
log.error("Unexpected error", e);
return ToolResponse.error()
.message("Internal error occurred")
.build();
}
});
});
```
## JSON Schema Construction
Use the fluent schema builder:
```java
import io.mcp.json.JsonSchema;
JsonSchema schema = JsonSchema.object()
.property("name", JsonSchema.string()
.description("User's name")
.minLength(1)
.maxLength(100)
.required(true))
.property("age", JsonSchema.integer()
.description("User's age")
.minimum(0)
.maximum(150))
.property("email", JsonSchema.string()
.description("Email address")
.format("email")
.required(true))
.property("tags", JsonSchema.array()
.items(JsonSchema.string())
.uniqueItems(true))
.additionalProperties(false)
.build();
```
## Logging and Observability
Use SLF4J for logging:
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log = LoggerFactory.getLogger(MyMcpServer.class);
server.addToolHandler("process", (args) -> {
log.info("Tool called: process, args: {}", args);
return Mono.fromCallable(() -> {
String result = process(args);
log.debug("Processing completed successfully");
return ToolResponse.success()
.addTextContent(result)
.build();
}).doOnError(error -> {
log.error("Processing failed", error);
});
});
```
Propagate context with Reactor:
```java
import reactor.util.context.Context;
server.addToolHandler("traced", (args) -> {
return Mono.deferContextual(ctx -> {
String traceId = ctx.get("traceId");
log.info("Processing with traceId: {}", traceId);
return Mono.just(ToolResponse.success()
.addTextContent("Processed")
.build());
});
});
```
## Testing
Write tests using the synchronous API:
```java
import org.junit.jupiter.api.Test;
import static org.assertj.core.Assertions.assertThat;
class McpServerTest {
@Test
void testToolHandler() {
McpServer server = createTestServer();
McpSyncServer syncServer = server.toSyncServer();
JsonNode args = objectMapper.createObjectNode()
.put("query", "test");
ToolResponse response = syncServer.callTool("search", args);
assertThat(response.isError()).isFalse();
assertThat(response.getContent()).hasSize(1);
}
}
```
## Jackson Integration
The SDK uses Jackson for JSON serialization. Customize as needed:
```java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Use custom mapper with server
McpServer server = McpServerBuilder.builder()
.objectMapper(mapper)
.build();
```
## Content Types
Support multiple content types in responses:
```java
import io.mcp.server.content.Content;
server.addToolHandler("multi", (args) -> {
return Mono.just(ToolResponse.success()
.addTextContent("Plain text response")
.addImageContent(imageBytes, "image/png")
.addResourceContent("resource://data", "application/json", jsonData)
.build());
});
```
## Server Lifecycle
Properly manage server lifecycle:
```java
import reactor.core.Disposable;
Disposable serverDisposable = server.start(transport).subscribe();
// Graceful shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down MCP server");
serverDisposable.dispose();
server.stop().block();
}));
```
## Common Patterns
### Request Validation
```java
server.addToolHandler("validate", (args) -> {
if (!args.has("required_field")) {
return Mono.just(ToolResponse.error()
.message("Missing required_field")
.build());
}
return processRequest(args);
});
```
### Async Operations
```java
server.addToolHandler("async", (args) -> {
return Mono.fromCallable(() -> callExternalApi(args))
.timeout(Duration.ofSeconds(30))
.onErrorResume(TimeoutException.class, e ->
Mono.just(ToolResponse.error()
.message("Operation timed out")
.build()))
.subscribeOn(Schedulers.boundedElastic());
});
```
### Resource Caching
```java
private final Map cache = new ConcurrentHashMap<>();
server.addResourceReadHandler((uri) -> {
return Mono.fromCallable(() ->
cache.computeIfAbsent(uri, this::loadResource))
.map(content -> ResourceContent.text(content, uri));
});
```
## Best Practices
1. **Use Reactive Streams** for async operations and backpressure
2. **Leverage Spring Boot** starter for enterprise applications
3. **Implement proper error handling** with specific error messages
4. **Use SLF4J** for logging, not System.out
5. **Validate inputs** in tool and prompt handlers
6. **Support graceful shutdown** with proper resource cleanup
7. **Use bounded elastic scheduler** for blocking operations
8. **Propagate context** for observability in reactive chains
9. **Test with synchronous API** for simplicity
10. **Follow Java naming conventions** (camelCase for methods, PascalCase for classes)