Files

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
```