
This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.
You can build an MCP server in Java with the official Java SDK (Maven group io.modelcontextprotocol.sdk) or with Spring AI's MCP starters, which sit on top of it. The plain SDK gives you full control with a builder API. Spring AI turns annotated methods into tools with very little code.
This guide walks through both paths. You will install the SDK, build a small stdio server, add a tool, test it with MCP Inspector and connect it to Claude Desktop. It also covers the one thing that catches most Java teams: the stable SDK still speaks the 2025-11-25 protocol, and support for the 2026-07-28 spec is a preview.
Checked against: the Java SDK README, the Java SDK quickstart and server guide, and the official Build an MCP server tutorial, on 2 November 2026. Code follows io.modelcontextprotocol.sdk:mcp 2.1.0, the latest stable GitHub release. eSEOspace did not compile or run this Java code; no Java runtime was available.
Key Takeaways
- The official Java SDK is Tier 2 on the official SDK list as of November 2026, and needs Java 17 or later.
- The latest stable release is 2.1.0 (16 October 2026); Maven Central also has 2.2.0 milestones (2.2.0-M2 is the newest).
- The stable "classic" server implements the 2025-11-25 spec; a stateless 2026-07-28 server is a preview in 2.2.0.
- Spring integrations (WebFlux, WebMVC) now live in Spring AI 2.0+, not in the SDK.
- Never write to
System.outin a stdio server; route all logs to stderr or a file. - Test every tool with MCP Inspector before you connect a real client.
Prerequisites
- Java 17 or later. The SDK README badge says "Java 17+", and the official tutorial lists Java 17 for its Spring example.
- Maven or Gradle.
- Node.js, to run MCP Inspector with
npx. - A client to connect to, such as Claude Desktop.
If you are new to how servers, clients and hosts fit together, start with how to build your first MCP server, which uses Python and TypeScript, then come back.
Choose your path: plain SDK or Spring AI
| Question | Plain Java SDK | Spring AI MCP |
|---|---|---|
| Dependency | io.modelcontextprotocol.sdk:mcp | org.springframework.ai:spring-ai-starter-mcp-server (stdio) |
| How tools are defined | SyncToolSpecification builder with a JSON Schema map | Annotated methods (@McpTool or @Tool) |
| HTTP transports | Servlet-based, in the SDK core | WebMVC and WebFlux starters |
| Best for | Libraries, non-Spring apps, full control | Teams already on Spring Boot |
The SDK README says Spring AI MCP "extends the MCP Java SDK with Spring Boot integration." The Spring AI server starter docs list three starters: spring-ai-starter-mcp-server (stdio), spring-ai-starter-mcp-server-webmvc and spring-ai-starter-mcp-server-webflux. We cover Spring AI in more depth in our Spring AI MCP tutorial. This guide focuses on the plain SDK.
Step 1: Add the dependency
The quickstart recommends importing the BOM (bill of materials) so every SDK module gets a matching version. Then add the mcp module, which bundles mcp-core with Jackson 3 JSON support:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-bom</artifactId>
<version>2.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
</dependency>
</dependencies>
Gradle users write implementation platform("io.modelcontextprotocol.sdk:mcp-bom:2.1.0") and then implementation "io.modelcontextprotocol.sdk:mcp".
A note on versions. The quickstart page shows 2.2.0-M2 in its BOM snippet, because the docs track the newest build. Maven Central listed 2.2.0-M2 as its newest version on 2 November 2026, but GitHub marks the 2.2.0 line as a pre-release. For production, pin the stable 2.1.0 unless you need the 2026-07-28 preview.
The quickstart says the mcp module "includes default STDIO, SSE, and Streamable HTTP transport implementations without requiring external web frameworks." If your project must stay on Jackson 2, use mcp-core plus mcp-json-jackson2 instead.
Step 2: Write a minimal stdio server with one tool
Stdio means the client starts your server as a child process and talks to it over standard input and output. It is the simplest way to run a local server. Here is a small bookshop search server built from the patterns in the server guide:
package com.example;
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.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.Tool;
public class BookshopServer {
private static final Map<String, String> CATALOG = Map.of(
"Dune", "Frank Herbert",
"Neuromancer", "William Gibson");
public static void main(String[] args) {
var transport = new StdioServerTransportProvider(McpJsonDefaults.getMapper());
Map<String, Object> schema = Map.of(
"type", "object",
"properties", Map.of(
"query", Map.of(
"type", "string",
"description", "Part of a title or author name")),
"required", List.of("query"));
var searchBooks = SyncToolSpecification.builder()
.tool(Tool.builder("search_books", schema)
.description("Search the catalog by title or author. Returns matching titles.")
.build())
.callHandler((exchange, request) -> {
String query = ((String) request.arguments().get("query")).toLowerCase();
String hits = CATALOG.entrySet().stream()
.filter(e -> e.getKey().toLowerCase().contains(query)
|| e.getValue().toLowerCase().contains(query))
.map(Map.Entry::getKey)
.reduce((a, b) -> a + "\n" + b)
.orElse("No matches.");
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent(hits)))
.build();
})
.build();
McpSyncServer server = McpServer.sync(transport)
.serverInfo("bookshop", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(searchBooks)
.build();
}
}
What each part does:
StdioServerTransportProviderreads JSON-RPC messages from stdin and writes responses to stdout. The server guide shows it built withMcpJsonDefaults.getMapper().Tool.builder(name, schema)takes the tool name and a JSON Schema as aMap. Thedescriptionstrings are what the model reads, so write them for the model.callHandlerreceives an exchange (for talking back to the client) and aCallToolRequest. Arguments come fromrequest.arguments().McpServer.sync(...)builds a blocking server. UseMcpServer.async(...)andAsyncToolSpecificationif you prefer Project Reactor types.
The server guide says the server validates arguments against inputSchema by default. If validation fails, the client gets a CallToolResult with isError set, and your handler never runs. Malformed schemas are rejected at build() time with an IllegalArgumentException.
Step 3: Add more tools
Each new tool is another SyncToolSpecification passed to .tools(...), or added later with server.addTool(spec). For quick one-off tools, the server guide also shows a .toolCall(tool, handler) shortcut on the builder.
Keep tools narrow. One tool that does one job, with a clear name and argument descriptions, is easier for a model to use correctly than one large tool with a mode flag.
To return an error the model can read, set isError(true) on the CallToolResult builder and explain what went wrong in the text content. Throwing an exception produces a protocol error instead, which models handle less gracefully.
Step 4: Test with MCP Inspector
Package your server as a runnable JAR, then point the MCP Inspector at it. The Inspector's CLI mode is the fastest check:
npx @modelcontextprotocol/inspector --cli java -jar target/bookshop.jar \
--protocol-era legacy --method tools/list
npx @modelcontextprotocol/inspector --cli java -jar target/bookshop.jar \
--protocol-era legacy --method tools/call \
--tool-name search_books --tool-arg query=dune
The --protocol-era legacy flag makes the Inspector use the initialize-based protocol that the classic server speaks. Running npx @modelcontextprotocol/inspector with no flags opens the web UI instead. Our lesson on testing with MCP Inspector explains the screens.
If tools/list hangs or returns garbage, something is printing to stdout. That is the first thing to check.
Step 5: Connect it to Claude Desktop
The official tutorial's Java example adds the server under mcpServers in claude_desktop_config.json. On macOS the file is ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows it is %AppData%\Claude\claude_desktop_config.json.
{
"mcpServers": {
"bookshop": {
"command": "java",
"args": ["-jar", "/ABSOLUTE/PATH/TO/target/bookshop.jar"]
}
}
}
Use an absolute path, save the file and restart Claude Desktop. Our MCP config generator writes this block for other clients too.
The 2026-07-28 spec: classic vs modern server
This is the most important thing to understand before you ship. The server guide says the classic server "implements the 2025-11-25 specification and earlier revisions." The official tutorial adds that clients supporting only 2026-07-28 cannot connect to it.
For the new spec, the SDK has a separate modern server in the io.modelcontextprotocol.modern packages. Its docs say it "is a preview in 2.2.0," its API "may change in minor releases until 3.0," and "for production servers that talk to today's clients, use the classic server." It has no sessions and no initialize handshake, and by default it only accepts the 2026-07-28 protocol version.
The modern server's own source folder does not exist in the 2.1.0 tag on GitHub, so you need a 2.2.0 milestone to try it. The two servers share no types, so plan any later move as a port, not a version bump.
Transports and deployment notes
| Transport | Class (classic server) | Use it for |
|---|---|---|
| Stdio | StdioServerTransportProvider | Local servers started by a desktop client |
| Streamable HTTP | HttpServletStreamableServerTransportProvider | Remote servers in any Servlet container |
| Stateless HTTP | Stateless transport builders | Horizontal scaling without sessions |
| SSE (legacy) | HttpServletSseServerTransportProvider | Old clients only |
A few deployment points from the server guide:
- The Streamable HTTP servlet keeps sessions, sends keep-alive pings every 30 minutes by default and evicts idle sessions on the same interval.
- "HTTP transports do not validate the Origin or Host headers by default." Turn on
DefaultServerTransportSecurityValidatorwith your allowed origins and hosts before you expose a server. - The SDK ships no authorization system. The README calls it "pluggable authorization hooks," so you bring Spring Security or another library.
For a remote server, read our lesson on deploying a remote MCP server.
Common pitfalls
- Logging to stdout. The official tutorial says never use
System.out.println()in a stdio server. The SDK logs through SLF4J with no backend, so check what your chosen backend writes to. In Spring Boot, the tutorial setsspring.main.bannerMode=offand an emptylogging.pattern.console. - Old Spring transport artifacts. WebFlux and WebMVC transports moved to Spring AI 2.0+ under
org.springframework.ai. Old tutorials that import them from the SDK group will not resolve. - Copying the milestone version. Docs snippets show 2.2.0-M2. Pin 2.1.0 for production unless you need the preview.
- Expecting 2026-07-28 from the classic server. It speaks 2025-11-25. Check which spec your target clients need.
- Relative JAR paths in client config. Desktop clients start your process from their own working directory.
- Leaving HTTP header validation off. It is opt-in, so add it before you deploy.
Next step
Work through the full MCP course to add resources, prompts and OAuth. If you want a production Java server built and reviewed for you, see our MCP development service. Our overview of the official MCP SDKs compares Java with the other nine languages.
More in the MCP Guide
Free tools: server.json Validator · MCP Error Code Lookup
Latest research: State of MCP: October 2026
Frequently asked questions
Is there an official MCP SDK for Java?
Yes. The Java SDK lives in the modelcontextprotocol GitHub organization under an MIT license. As of November 2026 the official SDK page lists it as Tier 2. Its latest stable release is 2.1.0.
Does the Java MCP SDK support the 2026-07-28 spec?
Only as a preview. The stable classic server implements 2025-11-25 and earlier. A stateless 2026-07-28 server is a preview in the 2.2.0 line, and its docs say to use the classic server for production today.
Should I use Spring AI or the plain Java SDK?
Use Spring AI if your team already builds on Spring Boot; annotated tools and starters save a lot of wiring. Use the plain SDK for libraries, non-Spring apps or when you want full control over transports and threading.
What Java version do I need?
Java 17 or later, per the SDK README. Spring AI's examples in the official tutorial also assume Java 17 and Spring Boot 4.1.x or higher.
Why does my Java MCP server work in a terminal but not in Claude Desktop?
Usually something writes to stdout, such as a banner or a console logger, which corrupts the JSON-RPC stream. Other common causes are a relative JAR path or a different java on the client's PATH. Use an absolute path to both if needed.











