# MCP Java SDK The MCP Java SDK (`io.modelcontextprotocol.sdk`) is an official Java implementation of the [Model Context Protocol](https://modelcontextprotocol.io/), a standardized interface that enables Java applications to integrate AI models with external tools, resources, and prompt templates. It supports both client and server roles, allowing developers to either consume MCP-compliant servers or expose their own tools and data to AI models. The SDK targets Java 17+, uses Project Reactor for non-blocking operations, and ships with built-in transport implementations for STDIO, SSE (Server-Sent Events), and Streamable HTTP—all without requiring external web frameworks. The library is organized into focused Maven modules: `mcp-core` provides the reference implementation (transports, client, server, JSON abstraction); `mcp-json-jackson2` and `mcp-json-jackson3` are pluggable JSON backends; the `mcp` convenience bundle ships `mcp-core` together with Jackson 3; and `mcp-test` provides shared testing utilities. Spring Framework integrations (`mcp-spring-webflux`, `mcp-spring-webmvc`) are available separately as part of Spring AI 2.0+ (`org.springframework.ai`). Both synchronous (blocking) and asynchronous (reactive `Mono`/`Flux`) APIs are offered for every feature, and all operations follow a fluent builder pattern. --- ## Dependencies / BOM Add the BOM and the convenience `mcp` bundle to your project; it pulls in `mcp-core` together with Jackson 3 JSON support. ```xml io.modelcontextprotocol.sdk mcp-bom 1.0.0 pom import io.modelcontextprotocol.sdk mcp ``` ```groovy // Gradle dependencies { implementation platform("io.modelcontextprotocol.sdk:mcp-bom:1.0.0") implementation "io.modelcontextprotocol.sdk:mcp" } ``` --- ## McpClient.sync() — Build a Synchronous MCP Client `McpClient.sync(transport)` creates a blocking `McpSyncClient` that waits for each operation to complete before returning. Use it when a simple request/response model is sufficient. ```java import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.spec.McpSchema.*; import java.time.Duration; import java.util.Map; // 1. Build a Streamable HTTP transport pointing at a running MCP server McpClientTransport transport = HttpClientStreamableHttpTransport .builder("http://localhost:8080") .endpoint("/mcp") .build(); // 2. Configure and build the sync client McpSyncClient client = McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(15)) .capabilities(ClientCapabilities.builder() .roots(true) // expose filesystem roots .sampling() // allow server to request LLM sampling .elicitation() // allow server to request user input .build()) .sampling(req -> new CreateMessageResult(/* call your LLM here */ null, null, Role.ASSISTANT, null)) .loggingConsumer(notification -> System.out.printf("[%s] %s%n", notification.level(), notification.data())) .progressConsumer(p -> System.out.printf("Progress: %.0f / %.0f%n", p.progress(), p.total())) .build(); // 3. Perform the MCP initialization handshake client.initialize(); // 4. Discover and call a tool ListToolsResult tools = client.listTools(); tools.tools().forEach(t -> System.out.println("Tool: " + t.name())); CallToolResult result = client.callTool(new CallToolRequest( "calculator", Map.of("operation", "add", "a", 5, "b", 3) )); result.content().forEach(c -> System.out.println(((TextContent) c).text())); // "Result: 8" // 5. Read a resource ReadResourceResult res = client.readResource(new ReadResourceRequest("file:///workspace/config.yaml")); // 6. Render a prompt GetPromptResult prompt = client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "Alice"))); prompt.messages().forEach(m -> System.out.println(m.content())); // 7. Graceful shutdown client.closeGracefully(); ``` --- ## McpClient.async() — Build an Asynchronous MCP Client `McpClient.async(transport)` returns a reactive `McpAsyncClient` that exposes `Mono`-based operations and supports real-time change consumers. ```java import reactor.core.publisher.Mono; McpAsyncClient asyncClient = McpClient.async(transport) .requestTimeout(Duration.ofSeconds(10)) .capabilities(ClientCapabilities.builder() .roots(true) .sampling() .build()) .sampling(req -> Mono.fromCallable(() -> callLlm(req))) .toolsChangeConsumer(tools -> Mono.fromRunnable( () -> System.out.println("Tools updated: " + tools.size()))) .resourcesChangeConsumer(resources -> Mono.fromRunnable( () -> System.out.println("Resources updated: " + resources.size()))) .promptsChangeConsumer(prompts -> Mono.fromRunnable( () -> System.out.println("Prompts updated: " + prompts.size()))) .loggingConsumer(n -> Mono.fromRunnable( () -> System.out.println("Log: " + n.data()))) .build(); // Chain operations reactively asyncClient.initialize() .flatMap(init -> asyncClient.listTools()) .flatMap(tools -> asyncClient.callTool(new CallToolRequest( "weather", Map.of("city", "Paris")))) .flatMap(result -> { System.out.println("Weather: " + ((TextContent) result.content().get(0)).text()); return asyncClient.listResources(); }) .flatMap(resources -> asyncClient.readResource( new ReadResourceRequest(resources.resources().get(0).uri()))) .doFinally(sig -> asyncClient.closeGracefully().subscribe()) .subscribe(); ``` --- ## Client Transport — STDIO `StdioClientTransport` launches a subprocess and communicates via its standard input/output streams, the typical approach for local MCP servers distributed as npm packages or executables. ```java import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; // Launch the reference "everything" MCP server via npx ServerParameters params = ServerParameters.builder("npx") .args("-y", "@modelcontextprotocol/server-everything", "/tmp/workspace") .env(Map.of("LOG_LEVEL", "debug")) .build(); McpTransport transport = new StdioClientTransport(params); McpSyncClient client = McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(30)) .build(); client.initialize(); // List and invoke a tool from the local server ListToolsResult tools = client.listTools(); System.out.println("Available tools: " + tools.tools().stream() .map(Tool::name).toList()); // Expected: [echo, add, longRunningOperation, printEnv, ...] CallToolResult echo = client.callTool( new CallToolRequest("echo", Map.of("message", "Hello from Java!"))); System.out.println(((TextContent) echo.content().get(0)).text()); // Expected: "Hello from Java!" client.closeGracefully(); ``` --- ## Client Transport — Streamable HTTP `HttpClientStreamableHttpTransport` uses the JDK `HttpClient` to connect to an HTTP MCP server using the Streamable HTTP protocol (the recommended modern transport). ```java import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; McpTransport transport = HttpClientStreamableHttpTransport .builder("https://api.example.com") .endpoint("/mcp") .build(); McpSyncClient client = McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(20)) .build(); client.initialize(); CallToolResult result = client.callTool( new CallToolRequest("search", Map.of("query", "MCP protocol"))); System.out.println(((TextContent) result.content().get(0)).text()); client.closeGracefully(); ``` --- ## Client Transport — SSE HTTP (Legacy) `HttpClientSseClientTransport` connects to a legacy SSE-based MCP server (the older HTTP transport using a `/sse` endpoint plus a separate message endpoint). ```java import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; McpTransport transport = HttpClientSseClientTransport .builder("http://localhost:8080") .build(); McpSyncClient client = McpClient.sync(transport).build(); client.initialize(); ListToolsResult tools = client.listTools(); System.out.println("Tools: " + tools.tools().stream().map(Tool::name).toList()); client.closeGracefully(); ``` --- ## McpServer.sync() — Build a Synchronous MCP Server `McpServer.sync(transportProvider)` creates a blocking `McpSyncServer` that registers tools, resources, and prompts for AI clients to discover and invoke. ```java import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpServerFeatures.*; import io.modelcontextprotocol.spec.McpSchema.*; var inputSchema = McpSchema.parseInputSchema(""" { "type": "object", "properties": { "operation": { "type": "string", "enum": ["add","sub","mul","div"] }, "a": { "type": "number" }, "b": { "type": "number" } }, "required": ["operation","a","b"] } """); McpSyncServer server = McpServer.sync(new StdioServerTransportProvider()) .serverInfo("my-calculator-server", "1.0.0") .capabilities(ServerCapabilities.builder() .tools(true) .resources(false, true) .prompts(true) .logging() .build()) // Register a tool inline using the convenience method .toolCall( Tool.builder() .name("calculator") .description("Performs basic arithmetic operations") .inputSchema(inputSchema) .build(), (exchange, request) -> { String op = (String) request.arguments().get("operation"); double a = ((Number) request.arguments().get("a")).doubleValue(); double b = ((Number) request.arguments().get("b")).doubleValue(); double result = switch (op) { case "add" -> a + b; case "sub" -> a - b; case "mul" -> a * b; case "div" -> { if (b == 0) { // Return a tool-level (LLM-recoverable) error yield null; // handled below } yield a / b; } default -> throw new IllegalArgumentException("Unknown op: " + op); }; if ("div".equals(op) && b == 0) { return CallToolResult.builder() .content(List.of(new TextContent("Error: division by zero"))) .isError(true) .build(); } // Log via exchange exchange.loggingNotification(LoggingMessageNotification.builder() .level(LoggingLevel.DEBUG) .logger("calculator") .data("Computed " + a + " " + op + " " + b + " = " + result) .build()); return CallToolResult.builder() .content(List.of(new TextContent("Result: " + result))) .build(); }) // Register a resource .resources(new SyncResourceSpecification( Resource.builder() .uri("config://app/settings") .name("App Settings") .description("Current application configuration") .mimeType("application/json") .build(), (exchange, req) -> new ReadResourceResult(List.of( new TextResourceContents(req.uri(), "application/json", "{\"version\":\"1.0\",\"debug\":false}"))))) // Register a prompt .prompts(new SyncPromptSpecification( new Prompt("summarize", "Summarize text", List.of( new PromptArgument("text", "Text to summarize", true), new PromptArgument("length", "Max words", false))), (exchange, req) -> { String text = req.arguments().getOrDefault("text", ""); String length = req.arguments().getOrDefault("length", "100"); return new GetPromptResult("Summarize the following text in at most " + length + " words.", List.of(new PromptMessage(Role.USER, new TextContent(text)))); })) .build(); // Server is now accepting connections; block until shutdown Runtime.getRuntime().addShutdownHook(new Thread(server::close)); ``` --- ## McpServer.async() — Build an Asynchronous MCP Server `McpServer.async(transportProvider)` creates a non-blocking `McpAsyncServer` using Project Reactor. Prefer this for Servlet or WebFlux deployments to avoid blocking the event loop. ```java import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpServerFeatures.*; import reactor.core.publisher.Mono; McpAsyncServer asyncServer = McpServer.async(transportProvider) .serverInfo("async-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) // Register a tool via AsyncToolSpecification builder .tools(AsyncToolSpecification.builder() .tool(Tool.builder() .name("weather") .description("Fetch current weather for a city") .inputSchema(weatherSchema) .build()) .callHandler((exchange, request) -> { String city = (String) request.arguments().get("city"); return Mono.fromCallable(() -> fetchWeatherApi(city)) .map(weatherJson -> CallToolResult.builder() .content(List.of(new TextContent(weatherJson))) .build()) .onErrorResume(e -> Mono.just(CallToolResult.builder() .content(List.of(new TextContent("Error: " + e.getMessage()))) .isError(true) .build())); }) .build()) // React to client roots changes .rootsChangeHandler((exchange, roots) -> Mono.fromRunnable(() -> System.out.println("Client roots: " + roots))) .build(); // Register additional tools dynamically after construction asyncServer.addTool(AsyncToolSpecification.builder() .tool(Tool.builder().name("ping").description("Connectivity check").inputSchema(emptySchema).build()) .callHandler((exchange, req) -> Mono.just( CallToolResult.builder().content(List.of(new TextContent("pong"))).build())) .build()) .doOnSuccess(v -> System.out.println("ping tool registered")) .subscribe(); ``` --- ## Server Transport — STDIO `StdioServerTransportProvider` exposes an MCP server over the current process's stdin/stdout, which is used when the server is invoked as a subprocess by an MCP host (e.g., Claude Desktop). ```java import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import com.fasterxml.jackson.databind.ObjectMapper; StdioServerTransportProvider transportProvider = new StdioServerTransportProvider(new ObjectMapper()); McpSyncServer server = McpServer.sync(transportProvider) .serverInfo("stdio-server", "1.0.0") .toolCall(echoTool, (exchange, req) -> CallToolResult.builder() .content(List.of(new TextContent(req.arguments().get("message").toString()))) .build()) .build(); // The process now communicates with the host via stdin/stdout ``` --- ## Server Transport — Streamable HTTP (Servlet) `HttpServletStreamableServerTransportProvider` serves both stateful and stateless MCP sessions over standard Jakarta Servlet containers (Tomcat, Jetty, etc.), registering a single endpoint for bidirectional Streamable HTTP. ```java import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class McpServerConfig { @Bean public HttpServletStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) { return HttpServletStreamableServerTransportProvider.builder() .jsonMapper(jsonMapper) .mcpEndpoint("/mcp") .build(); } @Bean public ServletRegistrationBean mcpServlet( HttpServletStreamableServerTransportProvider transport) { return new ServletRegistrationBean<>(transport); } @Bean public McpSyncServer mcpServer(HttpServletStreamableServerTransportProvider transport) { return McpServer.sync(transport) .serverInfo("http-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .toolCall( Tool.builder().name("greet").description("Greets a user").inputSchema(schema).build(), (exchange, req) -> CallToolResult.builder() .content(List.of(new TextContent("Hello, " + req.arguments().get("name") + "!"))) .build()) .build(); } } ``` --- ## Server Transport — SSE (Servlet, Legacy) `HttpServletSseServerTransportProvider` implements the older SSE-based HTTP transport using a `/sse` endpoint for server-to-client events and a separate configurable message endpoint for client-to-server requests. ```java import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.fasterxml.jackson.databind.ObjectMapper; @Configuration @EnableWebMvc public class SseServerConfig { @Bean public HttpServletSseServerTransportProvider sseTransport() { return new HttpServletSseServerTransportProvider( new ObjectMapper(), "/mcp/message"); } @Bean public ServletRegistrationBean sseServlet( HttpServletSseServerTransportProvider transport) { return new ServletRegistrationBean<>(transport); } @Bean public McpSyncServer server(HttpServletSseServerTransportProvider transport) { return McpServer.sync(transport) .serverInfo("sse-server", "1.0.0") .toolCall(myTool, myHandler) .build(); } } ``` --- ## Tool Registration — SyncToolSpecification / AsyncToolSpecification Tool specifications bundle a `Tool` schema definition with a call handler. Use the builder for full control, or the convenience `.toolCall()` method on the server builder. ```java import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; // --- Sync tool using the explicit builder --- var syncSpec = SyncToolSpecification.builder() .tool(Tool.builder() .name("file-reader") .description("Reads a file from the filesystem") .inputSchema(McpSchema.parseInputSchema(""" {"type":"object","properties":{"path":{"type":"string"}},"required":["path"]} """)) .build()) .callHandler((exchange, request) -> { String path = (String) request.arguments().get("path"); try { String content = Files.readString(Path.of(path)); return CallToolResult.builder() .content(List.of(new TextContent(content))) .build(); } catch (IOException e) { return CallToolResult.builder() .content(List.of(new TextContent("Cannot read file: " + e.getMessage()))) .isError(true) .build(); } }) .build(); // --- Async tool --- var asyncSpec = AsyncToolSpecification.builder() .tool(Tool.builder() .name("http-fetch") .description("Fetches content from a URL") .inputSchema(urlSchema) .build()) .callHandler((exchange, request) -> { String url = (String) request.arguments().get("url"); return webClient.get().uri(url).retrieve().bodyToMono(String.class) .map(body -> CallToolResult.builder() .content(List.of(new TextContent(body))) .build()) .onErrorResume(e -> Mono.just(CallToolResult.builder() .content(List.of(new TextContent("Fetch failed: " + e.getMessage()))) .isError(true).build())); }) .build(); McpSyncServer server = McpServer.sync(transport) .serverInfo("demo", "1.0.0") .tools(syncSpec) .build(); ``` --- ## Resource Registration — SyncResourceSpecification / AsyncResourceSpecification Resources expose server-side data sources (files, databases, API responses) via a URI that clients can read. ```java import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceSpecification; // Static resource var configResource = new SyncResourceSpecification( Resource.builder() .uri("config://app/database") .name("Database Config") .description("Current database connection settings") .mimeType("application/json") .build(), (exchange, request) -> new ReadResourceResult(List.of( new TextResourceContents(request.uri(), "application/json", """ {"host":"db.example.com","port":5432,"name":"mydb"} """)))); // Dynamic resource (async) var logsResource = new AsyncResourceSpecification( Resource.builder() .uri("logs://app/latest") .name("Application Logs") .description("Last 100 log lines") .mimeType("text/plain") .build(), (exchange, request) -> logService.fetchLast100Lines() .map(lines -> new ReadResourceResult(List.of( new TextResourceContents(request.uri(), "text/plain", lines))))); McpSyncServer server = McpServer.sync(transport) .serverInfo("data-server", "1.0.0") .capabilities(ServerCapabilities.builder().resources(true, true).build()) .resources(configResource) .build(); // Push update notification to subscribed clients when the resource changes server.notifyResourcesUpdated(new McpSchema.ResourcesUpdatedNotification("config://app/database")); ``` --- ## Resource Template Registration Resource templates expose parameterized URI patterns, letting clients access dynamic resources by substituting variables into a URI template. ```java import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; var fileTemplate = new SyncResourceTemplateSpecification( ResourceTemplate.builder() .uriTemplate("file://{path}") .name("Filesystem Access") .description("Read any file by path") .mimeType("application/octet-stream") .build(), (exchange, request) -> { // The SDK resolves the URI template and passes the full URI String filePath = request.uri().replace("file://", ""); try { byte[] bytes = Files.readAllBytes(Path.of(filePath)); return new ReadResourceResult(List.of( new BlobResourceContents(request.uri(), "application/octet-stream", Base64.getEncoder().encodeToString(bytes)))); } catch (IOException e) { throw new RuntimeException("File not found: " + filePath, e); } }); McpSyncServer server = McpServer.sync(transport) .serverInfo("fs-server", "1.0.0") .capabilities(ServerCapabilities.builder().resources(false, true).build()) .resourceTemplates(fileTemplate) .build(); ``` --- ## Prompt Registration — SyncPromptSpecification / AsyncPromptSpecification Prompt specifications expose reusable, parameterized prompt templates that clients can render by passing argument values. ```java import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; var codeReviewPrompt = new SyncPromptSpecification( new Prompt("code-review", "Review submitted code for correctness, style, and potential issues", List.of( new PromptArgument("code", "Source code to review", true), new PromptArgument("language", "Programming language", true), new PromptArgument("context", "Additional context", false))), (exchange, request) -> { String code = request.arguments().getOrDefault("code", ""); String lang = request.arguments().getOrDefault("language", "unknown"); String context = request.arguments().getOrDefault("context", ""); String systemMsg = "You are an expert " + lang + " code reviewer. " + "Provide concise, actionable feedback on correctness, style, and security."; String userMsg = "Please review the following " + lang + " code:\n\n```" + lang + "\n" + code + "\n```" + (context.isEmpty() ? "" : "\n\nContext: " + context); return new GetPromptResult( "Code review prompt for " + lang, List.of( new PromptMessage(Role.SYSTEM, new TextContent(systemMsg)), new PromptMessage(Role.USER, new TextContent(userMsg)))); }); McpSyncServer server = McpServer.sync(transport) .serverInfo("prompt-server", "1.0.0") .capabilities(ServerCapabilities.builder().prompts(true).build()) .prompts(codeReviewPrompt) .build(); ``` --- ## Completion Registration — SyncCompletionSpecification / AsyncCompletionSpecification Completion specifications provide argument autocompletion suggestions for prompt arguments and resource template variables. ```java import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; // Autocomplete the "language" argument of the "code-review" prompt var languageCompletion = new SyncCompletionSpecification( new McpSchema.PromptReference("code-review"), (exchange, request) -> { if (!"language".equals(request.argument().name())) { return new McpSchema.CompleteResult( new McpSchema.CompleteResult.CompleteCompletion(List.of(), 0, false)); } String partial = request.argument().value().toLowerCase(); List all = List.of("java", "kotlin", "python", "typescript", "go", "rust", "c++", "c#"); List matches = all.stream() .filter(l -> l.startsWith(partial)) .toList(); return new McpSchema.CompleteResult( new McpSchema.CompleteResult.CompleteCompletion(matches, matches.size(), false)); }); McpSyncServer server = McpServer.sync(transport) .serverInfo("complete-server", "1.0.0") .capabilities(ServerCapabilities.builder().completions().prompts(false).build()) .completions(languageCompletion) .build(); ``` --- ## Client — Tool Execution with Schema Validation and Caching Clients can optionally validate tool call results against JSON schemas and cache schemas to avoid repeated `listTools` calls. ```java import io.modelcontextprotocol.json.schema.JsonSchemaValidator; McpSyncClient client = McpClient.sync(transport) .jsonSchemaValidator(JsonSchemaValidator.defaultValidator()) // validate output schemas .enableCallToolSchemaCaching(true) // cache schemas from listTools .build(); client.initialize(); // First callTool automatically fetches and caches all tool schemas CallToolResult result = client.callTool(new CallToolRequest( "structured-output-tool", Map.of("input", "some data"))); // Subsequent calls use the cached schemas — no extra listTools round-trip CallToolResult result2 = client.callTool(new CallToolRequest( "structured-output-tool", Map.of("input", "other data"))); result.content().forEach(c -> System.out.println(((TextContent) c).text())); ``` --- ## Client — Resource Subscriptions Clients can subscribe to individual resources and receive live updates whenever the server pushes a `notifications/resources/updated` notification. ```java McpSyncClient client = McpClient.sync(transport) .resourcesUpdateConsumer(updatedContents -> { updatedContents.forEach(c -> { if (c instanceof TextResourceContents t) { System.out.println("Resource updated [" + t.uri() + "]: " + t.text()); } }); }) .build(); client.initialize(); // Subscribe to a specific URI client.subscribeResource(new McpSchema.SubscribeRequest("config://app/settings")); // ... do work; consumer is called each time the server notifies an update ... // Unsubscribe when no longer needed client.unsubscribeResource(new McpSchema.UnsubscribeRequest("config://app/settings")); client.closeGracefully(); ``` --- ## Client — Sampling Support Sampling lets MCP servers delegate LLM inference back to the client, which retains control over model selection and API keys. ```java import io.modelcontextprotocol.spec.McpSchema.*; McpSyncClient client = McpClient.sync(transport) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingRequest -> { // Extract the messages the server wants the LLM to process List messages = samplingRequest.messages(); String systemPrompt = samplingRequest.systemPrompt(); ModelPreferences prefs = samplingRequest.modelPreferences(); // The client forwards to its own LLM backend String llmResponse = myLlmClient.complete(systemPrompt, messages, prefs); return new CreateMessageResult( /* model */ prefs.hints().isEmpty() ? "gpt-4o" : prefs.hints().get(0).name(), /* stopReason */ StopReason.END_TURN, Role.ASSISTANT, new TextContent(llmResponse)); }) .build(); client.initialize(); // Now when the server calls exchange.createMessage(...), the above handler is invoked CallToolResult result = client.callTool( new CallToolRequest("ai-calculator", Map.of("expression", "sqrt(144) + 5"))); System.out.println(((TextContent) result.content().get(0)).text()); // "17" ``` --- ## Client — Elicitation Support Elicitation allows servers to interrupt a tool execution and request structured user input via the client. ```java McpSyncClient client = McpClient.sync(transport) .capabilities(ClientCapabilities.builder().elicitation().build()) .elicitation(elicitRequest -> { // elicitRequest.message() describes what's needed // elicitRequest.requestedSchema() describes the expected JSON shape System.out.println("Server asks: " + elicitRequest.message()); // In a real application, show a dialog/form to the user boolean confirmed = showConfirmDialog(elicitRequest.message()); if (confirmed) { return new ElicitResult( ElicitResult.Action.ACCEPT, Map.of("confirmed", true)); } else { return new ElicitResult(ElicitResult.Action.DECLINE, null); } }) .build(); client.initialize(); // The server can now call exchange.elicit(...) inside tool handlers CallToolResult result = client.callTool( new CallToolRequest("delete-file", Map.of("path", "/tmp/important.txt"))); System.out.println(((TextContent) result.content().get(0)).text()); ``` --- ## Server — Sampling from Within a Tool Handler A server tool can request the connected client to run an LLM inference on its behalf using `exchange.createMessage()`. ```java McpSyncServer server = McpServer.sync(transportProvider) .serverInfo("ai-server", "1.0.0") .toolCall( Tool.builder() .name("explain-code") .description("Explains what a code snippet does using AI") .inputSchema(codeSchema) .build(), (exchange, request) -> { // Guard: verify the connected client supports sampling if (exchange.getClientCapabilities().sampling() == null) { return CallToolResult.builder() .content(List.of(new TextContent("Client does not support AI sampling"))) .isError(true).build(); } String code = (String) request.arguments().get("code"); CreateMessageRequest samplingRequest = CreateMessageRequest.builder() .messages(List.of(new SamplingMessage(Role.USER, new TextContent("Explain this code concisely:\n\n```\n" + code + "\n```")))) .modelPreferences(ModelPreferences.builder() .hints(List.of(ModelHint.of("claude-3-5-sonnet"), ModelHint.of("gpt-4o"))) .intelligencePriority(0.8) .speedPriority(0.5) .build()) .systemPrompt("You are a helpful programming tutor. Be brief and clear.") .maxTokens(300) .build(); CreateMessageResult aiResult = exchange.createMessage(samplingRequest); String explanation = ((TextContent) aiResult.content()).text(); return CallToolResult.builder() .content(List.of(new TextContent(explanation))) .build(); }) .build(); ``` --- ## Server — Elicitation from Within a Tool Handler A server tool can pause and request structured user input from the client using `exchange.elicit()`. ```java McpSyncServer server = McpServer.sync(transportProvider) .serverInfo("interactive-server", "1.0.0") .toolCall( Tool.builder() .name("send-email") .description("Sends an email after user confirmation") .inputSchema(emailSchema) .build(), (exchange, request) -> { String to = (String) request.arguments().get("to"); String subject = (String) request.arguments().get("subject"); if (exchange.getClientCapabilities().elicitation() == null) { return CallToolResult.builder() .content(List.of(new TextContent("Client does not support elicitation"))) .isError(true).build(); } ElicitRequest confirm = ElicitRequest.builder() .message("Send email to " + to + " with subject '" + subject + "'?") .requestedSchema(Map.of( "type", "object", "properties", Map.of("confirmed", Map.of("type", "boolean")))) .build(); ElicitResult result = exchange.elicit(confirm); return switch (result.action()) { case ACCEPT -> { emailService.send(to, subject, (String) request.arguments().get("body")); yield CallToolResult.builder() .content(List.of(new TextContent("Email sent to " + to))) .build(); } case DECLINE -> CallToolResult.builder() .content(List.of(new TextContent("Email cancelled by user"))) .build(); case CANCEL -> CallToolResult.builder() .content(List.of(new TextContent("Operation cancelled"))) .isError(true).build(); }; }) .build(); ``` --- ## Server — Structured Logging Servers emit structured log messages to connected clients via `exchange.loggingNotification()`. Clients filter messages by severity level. ```java // Server side: emit logs from inside a tool handler var loggingTool = AsyncToolSpecification.builder() .tool(Tool.builder() .name("process-data") .description("Processes a data file with detailed logging") .inputSchema(fileSchema) .build()) .callHandler((exchange, request) -> { String file = (String) request.arguments().get("file"); return Mono.fromCallable(() -> processFile(file)) .flatMap(lineCount -> { // Emit an INFO log to the client return exchange.loggingNotification( LoggingMessageNotification.builder() .level(LoggingLevel.INFO) .logger("process-data") .data("Processed " + lineCount + " lines from " + file) .build()) .thenReturn(CallToolResult.builder() .content(List.of(new TextContent("Done. Lines: " + lineCount))) .build()); }); }) .build(); McpAsyncServer server = McpServer.async(transportProvider) .serverInfo("logging-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).logging().build()) .tools(loggingTool) .build(); // Client side: receive and filter log messages McpSyncClient client = McpClient.sync(transport) .loggingConsumer(n -> System.out.printf("[%s][%s] %s%n", n.level(), n.logger(), n.data())) .build(); client.initialize(); client.setLoggingLevel(McpSchema.LoggingLevel.INFO); // filter out DEBUG client.callTool(new CallToolRequest("process-data", Map.of("file", "data.csv"))); ``` --- ## Server — Roots Change Handler Servers can register handlers that are invoked whenever the client updates its list of accessible filesystem roots. ```java McpSyncServer server = McpServer.sync(transportProvider) .serverInfo("roots-aware-server", "1.0.0") .capabilities(ServerCapabilities.builder() .resources(false, true) .build()) .rootsChangeHandler((exchange, roots) -> { System.out.println("Client roots changed:"); roots.forEach(root -> System.out.println(" " + root.uri() + " - " + root.name())); // Dynamically adjust which file resources are exposed based on new roots }) .build(); // On the client: update roots dynamically McpSyncClient client = McpClient.sync(transport).build(); client.initialize(); client.addRoot(new Root("file:///home/user/project", "My Project")); client.rootsListChangedNotification(); // triggers the server's rootsChangeHandler client.removeRoot("file:///home/user/project"); ``` --- ## Stateless Server (McpStatelessAsyncServer / McpStatelessSyncServer) Stateless servers handle each request independently with no persistent session state, suitable for serverless deployments. ```java import io.modelcontextprotocol.spec.McpStatelessServerTransport; // Stateless async server — no session tracking McpStatelessAsyncServer statelessServer = McpServer.async(statelessTransport) .serverInfo("stateless-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .toolCall( Tool.builder() .name("hash") .description("Computes SHA-256 of input text") .inputSchema(textSchema) .build(), (context, request) -> { String text = (String) request.arguments().get("text"); String hash = DigestUtils.sha256Hex(text); return Mono.just(CallToolResult.builder() .content(List.of(new TextContent(hash))) .build()); }) .build(); // Stateless sync server McpStatelessSyncServer syncStateless = McpServer.sync(statelessTransport) .serverInfo("stateless-sync", "1.0.0") .toolCall(echoTool, (ctx, req) -> CallToolResult.builder() .content(List.of(new TextContent(req.arguments().get("msg").toString()))) .build()) .build(); ``` --- ## Summary The MCP Java SDK enables two primary integration patterns. The **server pattern** is used when building a Java application that exposes tools, data resources, or prompt templates to an AI host (such as Claude Desktop, an LLM agent framework, or a custom AI orchestrator). Developers register tool handlers, resource providers, and prompt templates on an `McpSyncServer` or `McpAsyncServer`, choose a transport (STDIO for desktop/CLI tools, Streamable HTTP Servlet for web services), and optionally implement sampling delegation or user elicitation to give the AI model controlled access back to external systems. This pattern is the foundation for building AI tool servers, data connectors, and agentic backends in Java. The **client pattern** is used when a Java application wants to consume an existing MCP server — for example, a Spring Boot API gateway aggregating multiple AI tools, an IDE plugin discovering available models, or an automation pipeline that dynamically calls LLM tools. Using `McpClient.sync()` or `McpClient.async()`, developers connect to any MCP-compliant server over STDIO, SSE, or Streamable HTTP, discover its capabilities, and invoke tools, read resources, or render prompts programmatically. The reactive async API integrates naturally with Spring WebFlux, Project Reactor pipelines, and other non-blocking Java stacks, while the sync API provides a clean blocking interface for straightforward scripting or batch workloads.