Add mcp-server module: order-lookup service exposed via @McpTool/@McpResource/@McpPrompt over Streamable HTTP

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
This commit is contained in:
Claude
2026-09-23 11:56:37 +00:00
parent c1fc41e0de
commit 9919da58a7
21 changed files with 704 additions and 0 deletions
@@ -0,0 +1,21 @@
# POST /mcp -- initialize, then tools/list
Request (initialize):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"transcript-capture","version":"1.0"}}}
Response (200):
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"order-lookup-server","version":"1.0.0"}}}
Request (tools/list):
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
Response (200):
id:59682b84-e49c-42d2-908e-a8105aa31514
event:message
data:{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"lookup_order","title":"lookup_order","description":"Look up a single order by its ID and return its customer, status, items, and total.","inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"orderId":{"type":"string","description":"The order ID, e.g. ORD-1001"}},"required":["orderId"]},"annotations":{"title":"","readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true}}]}}
@@ -0,0 +1,13 @@
# POST /mcp -- tools/call lookup_order, a real order
Request:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"lookup_order","arguments":{"orderId":"ORD-1001"}}}
Response (200):
id:59682b84-e49c-42d2-908e-a8105aa31514
event:message
data:{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"id\":\"ORD-1001\",\"customer\":\"Priya Nair\",\"status\":\"SHIPPED\",\"items\":[\"Mechanical keyboard\",\"USB-C cable\"],\"total\":4899.00}"}],"isError":false}}
@@ -0,0 +1,13 @@
# POST /mcp -- resources/read orders://catalog
Request:
{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"orders://catalog"}}
Response (200):
id:59682b84-e49c-42d2-908e-a8105aa31514
event:message
data:{"jsonrpc":"2.0","id":4,"result":{"contents":[{"uri":"orders://catalog","mimeType":"text/plain","text":"ORD-1001\nORD-1002\nORD-1003\nORD-1004"}]}}
@@ -0,0 +1,13 @@
# POST /mcp -- prompts/get summarize_order
Request:
{"jsonrpc":"2.0","id":5,"method":"prompts/get","params":{"name":"summarize_order","arguments":{"orderId":"ORD-1002"}}}
Response (200):
id:59682b84-e49c-42d2-908e-a8105aa31514
event:message
data:{"jsonrpc":"2.0","id":5,"result":{"messages":[{"role":"assistant","content":{"type":"text","text":"Call the lookup_order tool for order ORD-1002, then write a single short paragraph a customer support agent could paste into a reply: state the order status in plain language, list the items, and mention the total. Do not invent any detail the tool did not return."}}]}}
@@ -0,0 +1,13 @@
# POST /mcp -- tools/call lookup_order, an order ID that does not exist
Request:
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"lookup_order","arguments":{"orderId":"ORD-9999"}}}
Response (200), note isError:true inside a 200 -- HTTP itself never fails for a tool error:
id:59682b84-e49c-42d2-908e-a8105aa31514
event:message
data:{"jsonrpc":"2.0","id":6,"result":{"content":[{"type":"text","text":"No order with ID ORD-9999\nNo order with ID ORD-9999"}],"isError":true}}
@@ -0,0 +1,35 @@
# The 404 that shows up with no spring.ai.mcp.server.protocol set, and why
Captured manually against a standalone run of this module with application.yml's
`spring.ai.mcp.server.protocol: streamable` line removed -- everything else identical.
Reproduced with: `mvn -o spring-boot:run --server.port=18099 --debug`, then:
$ curl -X POST http://localhost:18099/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":"test","version":"1.0"}}}'
{"timestamp":"2026-09-23T11:44:01.196Z","status":404,"error":"Not Found","path":"/mcp"}
The real reason, from Boot's own --debug condition-evaluation report (trimmed to the one line
that matters):
McpServerStreamableHttpWebMvcAutoConfiguration:
Did not match:
- AllNestedConditions 1 matched 1 did not; NestedCondition on
McpServerAutoConfiguration.EnabledStreamableServerCondition.StreamableEnabledCondition
@ConditionalOnProperty (spring.ai.mcp.server.protocol=STREAMABLE) did not find
property 'protocol'; NestedCondition on
McpServerAutoConfiguration.EnabledStreamableServerCondition.McpServerEnabledCondition
@ConditionalOnProperty (spring.ai.mcp.server.enabled=true) matched
The jar's spring-configuration-metadata.json lists "streamable" as spring.ai.mcp.server.protocol's
default value -- but that default is only ever applied when something actually binds the
@ConfigurationProperties object (McpServerProperties). The @ConditionalOnProperty guarding
McpServerStreamableHttpWebMvcAutoConfiguration runs before any binding happens, checking the
Spring Environment directly for a literal "spring.ai.mcp.server.protocol" entry. No entry, no
match, no RouterFunction registered for /mcp -- the endpoint simply does not exist, and every
request to it 404s exactly like a request to a path nobody ever mapped, which is exactly what it
is. Setting spring.ai.mcp.server.protocol: streamable explicitly in application.yml fixes it, and
that same run then returns a normal 200 with a real MCP initialize response, e.g.:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"order-lookup-server","version":"1.0.0"}}}