Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
92 lines
4.2 KiB
Markdown
92 lines
4.2 KiB
Markdown
# 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](https://ankurm.com/spring-ai-2-0-mcp-server-streamable-http/) on [ankurm.com](https://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](https://github.com/modelcontextprotocol/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`](src/main/java/com/ankurm/mcpserver/tools/OrderTools.java),
|
|
which imports nothing MCP-specific except the three annotations -- the domain type
|
|
([`Order.java`](src/main/java/com/ankurm/mcpserver/domain/Order.java)) and the repository behind
|
|
it ([`OrderRepository.java`](src/main/java/com/ankurm/mcpserver/domain/OrderRepository.java))
|
|
would exist whether or not MCP was ever wired in.
|
|
|
|
## The one property that matters
|
|
|
|
```yaml
|
|
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`](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
|
|
```
|