mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-14 07:44:43 +00:00
- 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.
16 KiB
16 KiB
description, applyTo
| description | applyTo |
|---|---|
| Instructions for building Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x | **/*.java, **/pom.xml, **/build.gradle, **/build.gradle.kts |
Java MCP Server Development
These instructions apply to the MCP Java SDK 2.x. SDK 2.0.x supports the MCP specification revision 2025-11-25 and the earlier revisions. SDK 3.x will add the revision 2026-07-28.
Instructions
- Import the BOM
io.modelcontextprotocol.sdk:mcp-bom:2.0.1and add theio.modelcontextprotocol.sdk:mcpartifact. - In Gradle, import the BOM with
implementation(platform("io.modelcontextprotocol.sdk:mcp-bom:2.0.1")). Also addtestRuntimeOnly("org.junit.platform:junit-platform-launcher"). Without it, thetesttask fails. - The
mcpartifact contains the SDK core and the Jackson 3 JSON mapper. For Jackson 2, usemcp-coreandmcp-json-jackson2. - Do not use code from SDK 0.x or 1.x. Read the migration guide when you update a server.
- The packages are
io.modelcontextprotocol.server,io.modelcontextprotocol.spec, andio.modelcontextprotocol.json. The protocol types are nested inMcpSchema. - Make a server with
McpServer.sync(transport)orMcpServer.async(transport). SetserverInfo(name, version)andcapabilities(...), and then callbuild(). - 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)andprompts(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 aMapfromrequest.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), andReadResourceResult.builder(contents). The builders without arguments are deprecated. - A required field must not be
null. Anullvalue causes anIllegalArgumentException. - Give
inputSchemaandoutputSchemaasMap<String, Object>values. TheJsonSchemarecord is deprecated. - The server checks each schema against JSON Schema 2020-12 when you call
build()oraddTool(). An incorrect schema causes anIllegalArgumentException. - The SDK validates the arguments against
inputSchemabefore it calls the handler. It also validatesstructuredContentagainstoutputSchema. If the validation fails, the client gets a tool result withisErrorset totrue. - If a tool has an
outputSchema, returnstructuredContentin each successful result. Without it, the SDK returns a tool result withisErrorset totrue. - For machine-readable output, set
outputSchemaon the tool and returnCallToolResult.builder().structuredContent(map). Also add the same data as text content for older clients. - Set tool hints with
ToolAnnotations.builder().readOnlyHint(true).build(). UsedestructiveHint(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
McpErrorfor 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 withSyncResourceTemplateSpecification, prompts withSyncPromptSpecification, and completions withSyncCompletionSpecification. - Define resource templates with RFC 6570 URI templates, for example
users://{id}. - Report progress with
exchange.progressNotification(...)only whenrequest.progressToken()is notnull. - Get user input with
exchange.createElicitation(ElicitFormRequest.builder(message, requestedSchema).build()). First, make sure thatexchange.getClientCapabilities().elicitation()is notnull. - 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(...)andserver.removeTool(name). Then callserver.notifyToolsListChanged(). - In a stdio server, stdout is the protocol channel. Do not call
System.out.println(). Useslf4j-simple(it writes to stderr), or set the target of the Logback console appender toSystem.err. - For a remote server, use Streamable HTTP. The SSE transports are deprecated.
- For a servlet container, use
HttpServletStreamableServerTransportProvider. For a server without sessions, useHttpServletStatelessServerTransport. - The HTTP transports do not check the
HostandOriginheaders by default. Always setsecurityValidator(DefaultServerTransportSecurityValidator.builder()...build()). This also applies to the Spring AI starters. - For Spring Boot, use the MCP starters of Spring AI 2.0 or later, for example
spring-ai-starter-mcp-server-webmvc. Do not use theio.modelcontextprotocol.sdk:mcp-spring-*artifacts. - In Spring AI, write tools as bean methods with
@McpTooland@McpToolParam(fromorg.springframework.ai.mcp.annotation). - For browser clients, configure CORS. Allow the
Mcp-*request headers. Expose theMcp-Session-Idresponse header. - Package a stdio server as one executable JAR with the
maven-shade-plugin. In Gradle, use theapplicationplugin and the start script frominstallDist. Do not use./gradlew runas 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
- Give each tool one responsibility.
- Write clear tool descriptions. The description tells the model when to use the tool.
- Add a
descriptionto each property ininputSchema. - 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)
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
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
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
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
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<String> 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
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<Void> 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)
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
@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
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());
}
}