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