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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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)
[](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());
+ }
+}
+```