From 957edc6f329b1c5fc0c74addc5ed799ec17105a3 Mon Sep 17 00:00:00 2001 From: Burak Kaygusuz <17873574+burakkaygusuz@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:10:01 +0300 Subject: [PATCH] docs(java-mcp-server): update instructions for MCP Java SDK 2.x - Replace SDK 0.14.1 patterns with SDK 2.0.1 builders and Map schemas. - Add the BOM, Gradle, and packaging rules. - Document schema validation, isError results, and structuredContent. - Add transport security, CORS, and Spring AI 2.0 guidance. - Replace sampling, protocol logging, and roots with alternatives. - Rewrite the Common Patterns with code that compiles on SDK 2.0.1. - Update the description in docs/README.instructions.md. --- docs/README.instructions.md | 2 +- instructions/java-mcp-server.instructions.md | 853 +++++++------------ 2 files changed, 307 insertions(+), 548 deletions(-) diff --git a/docs/README.instructions.md b/docs/README.instructions.md index e3bf08fc8..f511bb0fc 100644 --- a/docs/README.instructions.md +++ b/docs/README.instructions.md @@ -127,7 +127,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on | [Java 11 to Java 17 Upgrade Guide](../instructions/java-11-to-java-17-upgrade.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-11-to-java-17-upgrade.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-11-to-java-17-upgrade.instructions.md) | Comprehensive best practices for adopting new Java 17 features since the release of Java 11. | | [Java 17 to Java 21 Upgrade Guide](../instructions/java-17-to-java-21-upgrade.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-17-to-java-21-upgrade.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-17-to-java-21-upgrade.instructions.md) | Comprehensive best practices for adopting new Java 21 features since the release of Java 17. | | [Java 21 to Java 25 Upgrade Guide](../instructions/java-21-to-java-25-upgrade.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-21-to-java-25-upgrade.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-21-to-java-25-upgrade.instructions.md) | Comprehensive best practices for adopting new Java 25 features since the release of Java 21. | -| [Java MCP Server Development Guidelines](../instructions/java-mcp-server.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md) | 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. | +| [Java MCP Server Development](../instructions/java-mcp-server.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md) | Instructions for building Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x | | [Joyride User Scripts Project Assistant](../instructions/joyride-user-project.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-user-project.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-user-project.instructions.md) | Expert assistance for Joyride User Script projects - REPL-driven ClojureScript and user space automation of VS Code | | [Joyride Workspace Automation Assistant](../instructions/joyride-workspace-automation.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-workspace-automation.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-workspace-automation.instructions.md) | Expert assistance for Joyride Workspace automation - REPL-driven and user space ClojureScript automation within specific VS Code workspaces | | [JUnit 5 Assertions Best Practices](../instructions/java-junit5-assertions.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-junit5-assertions.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-junit5-assertions.instructions.md) | Standardizes JUnit 5 (Jupiter) assertions with best practices for performance, readability, and modern features (5.8+). Covers Supplier messages, assertAll, assertThrowsExactly, and performance-critical timeouts. | diff --git a/instructions/java-mcp-server.instructions.md b/instructions/java-mcp-server.instructions.md index 39f3a53d2..93f7b6c78 100644 --- a/instructions/java-mcp-server.instructions.md +++ b/instructions/java-mcp-server.instructions.md @@ -1,553 +1,312 @@ --- -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" +description: 'Instructions for building Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x' +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)); -}); -``` +# Java MCP Server Development + +These instructions apply to the [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) 2.x. SDK 2.0.x supports the MCP specification revision [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) and the earlier revisions. SDK 3.x will add the revision [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28). + +## Instructions + +- Import the BOM `io.modelcontextprotocol.sdk:mcp-bom:2.0.1` and add the `io.modelcontextprotocol.sdk:mcp` artifact. +- In Gradle, import the BOM with `implementation(platform("io.modelcontextprotocol.sdk:mcp-bom:2.0.1"))`. Also add `testRuntimeOnly("org.junit.platform:junit-platform-launcher")`. Without it, the `test` task fails. +- The `mcp` artifact contains the SDK core and the Jackson 3 JSON mapper. For Jackson 2, use `mcp-core` and `mcp-json-jackson2`. +- Do not use code from SDK 0.x or 1.x. Read the [migration guide](https://github.com/modelcontextprotocol/java-sdk/blob/main/MIGRATION-2.0.md) when you update a server. +- The packages are `io.modelcontextprotocol.server`, `io.modelcontextprotocol.spec`, and `io.modelcontextprotocol.json`. The protocol types are nested in `McpSchema`. +- Make a server with `McpServer.sync(transport)` or `McpServer.async(transport)`. Set `serverInfo(name, version)` and `capabilities(...)`, and then call `build()`. +- Use the sync server for most servers. Use the async server with Project Reactor (`Mono`) only when the tool code does not block a thread. +- For stdio transport, use `new StdioServerTransportProvider(McpJsonDefaults.getMapper())`. +- Enable only the capabilities that the server uses: `ServerCapabilities.builder().tools(false).resources(false, true).prompts(true).completions().build()`. `tools(listChanged)` and `prompts(listChanged)` enable list change notifications. `resources(subscribe, listChanged)` also enables resource subscriptions. +- Register tools with `SyncToolSpecification.builder().tool(tool).callHandler((exchange, request) -> ...)`. The handler gets the arguments as a `Map` from `request.arguments()`. +- Use the builders that take the required fields first: `Tool.builder(name, inputSchema)`, `Resource.builder(uri, name)`, `ResourceTemplate.builder(uriTemplate, name)`, `Prompt.builder(name)`, `TextContent.builder(text)`, and `ReadResourceResult.builder(contents)`. The builders without arguments are deprecated. +- A required field must not be `null`. A `null` value causes an `IllegalArgumentException`. +- Give `inputSchema` and `outputSchema` as `Map` values. The `JsonSchema` record is deprecated. +- The server checks each schema against JSON Schema 2020-12 when you call `build()` or `addTool()`. An incorrect schema causes an `IllegalArgumentException`. +- The SDK validates the arguments against `inputSchema` before it calls the handler. It also validates `structuredContent` against `outputSchema`. If the validation fails, the client gets a tool result with `isError` set to `true`. +- If a tool has an `outputSchema`, return `structuredContent` in each successful result. Without it, the SDK returns a tool result with `isError` set to `true`. +- For machine-readable output, set `outputSchema` on the tool and return `CallToolResult.builder().structuredContent(map)`. Also add the same data as text content for older clients. +- Set tool hints with `ToolAnnotations.builder().readOnlyHint(true).build()`. Use `destructiveHint(true)` for a destructive tool. +- Return `CallToolResult.builder().addTextContent(message).isError(true).build()` for an error that the model must read. +- Do not throw an exception or an `McpError` for a tool failure. The client gets a JSON-RPC error, not a tool result. Many hosts do not show this error to the model. +- Use `McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS).message(...).build()` only for a protocol error, for example an unknown resource URI. +- Register resources with `SyncResourceSpecification`, resource templates with `SyncResourceTemplateSpecification`, prompts with `SyncPromptSpecification`, and completions with `SyncCompletionSpecification`. +- Define resource templates with RFC 6570 URI templates, for example `users://{id}`. +- Report progress with `exchange.progressNotification(...)` only when `request.progressToken()` is not `null`. +- Get user input with `exchange.createElicitation(ElicitFormRequest.builder(message, requestedSchema).build())`. First, make sure that `exchange.getClientCapabilities().elicitation()` is not `null`. +- Do not use sampling (`exchange.createMessage()`). The revision 2026-07-28 deprecates it. Call the LLM provider API directly. +- Do not use protocol logging (`exchange.loggingNotification()`). The revision 2026-07-28 deprecates it. Log with SLF4J. +- Do not use roots (`exchange.listRoots()`). The revision 2026-07-28 deprecates it. Get paths from tool arguments, resource URIs, or the server configuration. +- Add or remove tools at runtime with `server.addTool(...)` and `server.removeTool(name)`. Then call `server.notifyToolsListChanged()`. +- In a stdio server, stdout is the protocol channel. Do not call `System.out.println()`. Use `slf4j-simple` (it writes to stderr), or set the target of the Logback console appender to `System.err`. +- For a remote server, use Streamable HTTP. The SSE transports are deprecated. +- For a servlet container, use `HttpServletStreamableServerTransportProvider`. For a server without sessions, use `HttpServletStatelessServerTransport`. +- The HTTP transports do not check the `Host` and `Origin` headers by default. Always set `securityValidator(DefaultServerTransportSecurityValidator.builder()...build())`. This also applies to the Spring AI starters. +- For Spring Boot, use the MCP starters of [Spring AI](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) 2.0 or later, for example `spring-ai-starter-mcp-server-webmvc`. Do not use the `io.modelcontextprotocol.sdk:mcp-spring-*` artifacts. +- In Spring AI, write tools as bean methods with `@McpTool` and `@McpToolParam` (from `org.springframework.ai.mcp.annotation`). +- For browser clients, configure CORS. Allow the `Mcp-*` request headers. Expose the `Mcp-Session-Id` response header. +- Package a stdio server as one executable JAR with the `maven-shade-plugin`. In Gradle, use the `application` plugin and the start script from `installDist`. Do not use `./gradlew run` as the client command. +- Test a server with `McpClient.sync(new StdioClientTransport(params, McpJsonDefaults.getMapper()))`. The SDK has no in-memory transport. +- Test servers manually with the MCP Inspector. ## 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) +- Give each tool one responsibility. +- Write clear tool descriptions. The description tells the model when to use the tool. +- Add a `description` to each property in `inputSchema`. +- Put each tool specification in its own method. This keeps the tools easy to test. +- In the async server, do not block the event loop. Run code that blocks with `Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())`. +- Use environment variables for configuration. +- Be careful when a tool gives access to the file system or the network. + +## Common Patterns + +The snippets do not show all imports. The protocol types are nested in `io.modelcontextprotocol.spec.McpSchema`. `Mono` is in `reactor.core.publisher`, and `Schedulers` is in `reactor.core.scheduler`. + +### Basic Server Setup (stdio) + +```java +import java.util.List; +import java.util.Map; + +import io.modelcontextprotocol.json.McpJsonDefaults; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.spec.McpSchema.Tool; + +public final class McpServerApp { + + static SyncToolSpecification addTool() { + Tool tool = Tool.builder("add", Map.of( + "type", "object", + "properties", Map.of( + "a", Map.of("type", "integer", "description", "First number"), + "b", Map.of("type", "integer", "description", "Second number")), + "required", List.of("a", "b"))) + .description("Add two integers.") + .build(); + + return SyncToolSpecification.builder() + .tool(tool) + .callHandler((exchange, request) -> { + long a = ((Number) request.arguments().get("a")).longValue(); + long b = ((Number) request.arguments().get("b")).longValue(); + return CallToolResult.builder().addTextContent(String.valueOf(a + b)).build(); + }) + .build(); + } + + public static void main(String[] args) { + McpServer.sync(new StdioServerTransportProvider(McpJsonDefaults.getMapper())) + .serverInfo("my-server", "0.1.0") + .capabilities(ServerCapabilities.builder().tools(false).build()) + .tools(addTool()) + .build(); + } +} +``` + +### Tool with Structured Output + +```java +static SyncToolSpecification wordCountTool() { + Tool tool = Tool.builder("word_count", Map.of( + "type", "object", + "properties", Map.of("text", Map.of("type", "string")), + "required", List.of("text"))) + .description("Count the words in a text.") + .outputSchema(Map.of( + "type", "object", + "properties", Map.of("words", Map.of("type", "integer")), + "required", List.of("words"))) + .annotations(ToolAnnotations.builder().readOnlyHint(true).build()) + .build(); + + return SyncToolSpecification.builder() + .tool(tool) + .callHandler((exchange, request) -> { + String text = (String) request.arguments().get("text"); + int words = text.isBlank() ? 0 : text.trim().split("\\s+").length; + return CallToolResult.builder() + .addTextContent("{\"words\":" + words + "}") + .structuredContent(Map.of("words", words)) + .build(); + }) + .build(); +} +``` + +### Error Handling + +```java +static SyncToolSpecification divideTool() { + Tool tool = Tool.builder("divide", Map.of( + "type", "object", + "properties", Map.of("a", Map.of("type", "number"), "b", Map.of("type", "number")), + "required", List.of("a", "b"))) + .description("Divide a by b.") + .build(); + + return SyncToolSpecification.builder() + .tool(tool) + .callHandler((exchange, request) -> { + double a = ((Number) request.arguments().get("a")).doubleValue(); + double b = ((Number) request.arguments().get("b")).doubleValue(); + if (b == 0) { + return CallToolResult.builder().addTextContent("b must not be zero.").isError(true).build(); + } + return CallToolResult.builder().addTextContent(String.valueOf(a / b)).build(); + }) + .build(); +} +``` + +### Resources and Resource Templates + +```java +static SyncResourceSpecification configResource() { + return new SyncResourceSpecification( + Resource.builder("config://app", "app-config").mimeType("application/json").build(), + (exchange, request) -> ReadResourceResult.builder(List.of( + TextResourceContents.builder(request.uri(), "{\"debug\":false}") + .mimeType("application/json") + .build())) + .build()); +} + +static SyncResourceTemplateSpecification userResource() { + return new SyncResourceTemplateSpecification( + ResourceTemplate.builder("users://{id}", "user").build(), + (exchange, request) -> { + String id = request.uri().substring("users://".length()); + return ReadResourceResult.builder(List.of( + TextResourceContents.builder(request.uri(), "User " + id).build())) + .build(); + }); +} +``` + +Register them with `.resources(configResource())` and `.resourceTemplates(userResource())`. + +### Prompt with Completion + +```java +static SyncPromptSpecification reviewPrompt() { + Prompt prompt = Prompt.builder("code-review") + .description("Review code in a language.") + .arguments(List.of(PromptArgument.builder("language").required(true).build())) + .build(); + + return new SyncPromptSpecification(prompt, (exchange, request) -> GetPromptResult.builder(List.of( + PromptMessage.builder(Role.USER, + TextContent.builder("Review this " + request.arguments().get("language") + " code.").build()) + .build())) + .build()); +} + +static SyncCompletionSpecification languageCompletion() { + List languages = List.of("java", "kotlin", "scala"); + return new SyncCompletionSpecification(new PromptReference("code-review"), + (exchange, request) -> new CompleteResult(new CompleteResult.CompleteCompletion( + languages.stream().filter(l -> l.startsWith(request.argument().value())).toList(), null, false))); +} +``` + +Register them with `.prompts(reviewPrompt())` and `.completions(languageCompletion())`. + +### Async Tool with Progress + +```java +static AsyncToolSpecification lookupTool() { + Tool tool = Tool.builder("lookup", Map.of( + "type", "object", + "properties", Map.of("query", Map.of("type", "string")), + "required", List.of("query"))) + .description("Look up a query in a slow backend.") + .build(); + + return AsyncToolSpecification.builder() + .tool(tool) + .callHandler((exchange, request) -> { + Object token = request.progressToken(); + Mono progress = token == null ? Mono.empty() + : exchange.progressNotification( + ProgressNotification.builder(token, 0.5).total(1.0).message("Halfway").build()); + return progress.then(Mono.fromCallable(() -> slowLookup((String) request.arguments().get("query"))) + .subscribeOn(Schedulers.boundedElastic()) + .map(text -> CallToolResult.builder().addTextContent(text).build())); + }) + .build(); +} +``` + +Register it on `McpServer.async(transport)`. + +### Streamable HTTP Server (Servlet) + +```java +HttpServletStreamableServerTransportProvider transport = HttpServletStreamableServerTransportProvider.builder() + .jsonMapper(McpJsonDefaults.getMapper()) + .mcpEndpoint("/mcp") + .securityValidator(DefaultServerTransportSecurityValidator.builder() + .allowedHost("localhost:8080") + .allowedOrigin("http://localhost:*") + .build()) + .build(); + +McpServer.sync(transport) + .serverInfo("my-server", "0.1.0") + .capabilities(ServerCapabilities.builder().tools(false).build()) + .tools(addTool()) + .build(); +``` + +The transport is a Jakarta Servlet. Register it in a servlet container at `/mcp` with async support. + +### Spring AI Tool + +```java +@Component +class CalculatorTools { + + @McpTool(name = "add", description = "Add two integers") + public int add(@McpToolParam(description = "First number") int a, + @McpToolParam(description = "Second number") int b) { + return a + b; + } +} +``` + +### Integration Test + +```java +class McpServerAppTest { + + private static McpSyncClient client; + + @BeforeAll + static void startServer() { + String java = Path.of(System.getProperty("java.home"), "bin", "java").toString(); + ServerParameters server = ServerParameters.builder(java) + .args("-cp", System.getProperty("java.class.path"), McpServerApp.class.getName()) + .build(); + client = McpClient.sync(new StdioClientTransport(server, McpJsonDefaults.getMapper())).build(); + client.initialize(); + } + + @AfterAll + static void stopServer() { + client.close(); + } + + @Test + void add() { + CallToolResult result = client.callTool( + CallToolRequest.builder("add").arguments(Map.of("a", 2, "b", 3)).build()); + assertEquals("5", ((TextContent) result.content().get(0)).text()); + } +} +```