Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
mcp-server
An existing Spring service -- an order-lookup repository, nothing MCP-aware about it -- exposed
as MCP tools, resources, and a prompt with @McpTool/@McpResource/@McpPrompt, served over
Streamable HTTP: Spring AI 2.0's default MCP server transport.
Companion code for Build an MCP Server with Spring AI 2.0 on ankurm.com.
Versions
Spring Boot 4.1.1, Spring AI 2.0.1, Java 25 (LTS) -- same baseline as the rest of this repository.
Quickstart
./scripts/run.sh
Then, in another shell:
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
or point MCP Inspector or Claude Desktop's
MCP settings at http://localhost:8080/mcp.
What's exposed
| Kind | Name | What it does |
|---|---|---|
| Tool | lookup_order |
Looks up one order by ID; returns customer, status, items, total |
| Resource | orders://catalog |
Every order ID currently in the system, one per line |
| Prompt | summarize_order |
A ready-made prompt asking a model to summarize one order for support |
All three live in OrderTools.java,
which imports nothing MCP-specific except the three annotations -- the domain type
(Order.java) and the repository behind
it (OrderRepository.java)
would exist whether or not MCP was ever wired in.
The one property that matters
spring:
ai:
mcp:
server:
protocol: streamable
The jar's own spring-configuration-metadata.json lists streamable as this property's default
-- but leave it out of application.yml and the /mcp endpoint never gets registered at all
(a plain 404, not a clearer error). See
output/06-protocol-property-unset-404.txt for the
real condition-evaluation log line that explains why, and the article for the full story.
Tests and captured output
OrderMcpServerTest drives the real, running server over its real Streamable HTTP transport
using the official MCP Java SDK client (io.modelcontextprotocol.sdk:mcp, the same library MCP
Inspector and Claude Desktop are themselves built on) -- not a mock -- and asserts on the parsed,
typed results. OrderMcpTranscriptTest separately captures the literal JSON-RPC bytes that cross
the wire, with a plain synchronous java.net.http.HttpClient driving the protocol by hand
(initialize, read the Mcp-Session-Id header, notifications/initialized, then one request per
method): an earlier version tried to capture the SDK client's own traffic with a shared servlet
filter and that was intermittently flaky, because the Streamable HTTP transport writes responses
during an async servlet dispatch and can multiplex more than one JSON-RPC message onto one
long-lived HTTP exchange -- one request per HttpClient.send(), read back synchronously,
sidesteps that instead of fighting it. Either way, every request and response quoted in the
article is real, not reconstructed.
| Output file | What it captures |
|---|---|
output/01-initialize-and-list-tools.txt |
The MCP handshake, then tools/list |
output/02-call-tool-lookup-order.txt |
tools/call for a real order |
output/03-read-resource-catalog.txt |
resources/read for the order catalog |
output/04-get-prompt-summarize-order.txt |
prompts/get for the summarize prompt |
output/05-call-tool-lookup-order-not-found.txt |
tools/call for an order ID that doesn't exist -- a tool error, not a protocol error |
output/06-protocol-property-unset-404.txt |
Captured manually: the 404 you get without spring.ai.mcp.server.protocol set, and the real Boot condition-evaluation log line that explains it |
Run mvn -o test (or ./scripts/run-all.sh) to regenerate 01 through 05.
Regenerating output
./scripts/run-all.sh