Add mcp-advanced module: resources, prompts, sampling and elicitation on Spring AI 2.0.2 and MCP Java SDK 2.1.0
An incident-desk MCP server with a fixed resource, two resource templates, a prompt, a tool that borrows the client's model (sampling) and a tool that asks the operator to approve a restart (elicitation). Driven by a plain SDK client in JUnit and by raw curl, so the JSON-RPC for both server-to-client requests is committed. Also captured: a client that declared neither capability (a gated tool degrades, an ungated one returns an isError result), a slow client hitting the server request timeout, and protocol=STATELESS silently dropping every tool that takes McpSyncRequestContext (one WARN line, an empty tools/list). Negotiated protocol version is 2025-11-25; the 2026-07-28 spec is not implemented by SDK 2.1.0. No docs/ folder. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b5jAFLJrmUXmbo5EuV
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# initialize handshake (client declares sampling + elicitation)
|
||||
server.name: incident-desk
|
||||
negotiated protocolVersion: 2025-11-25
|
||||
server capability: tools: true
|
||||
server capability: resources: true
|
||||
server capability: prompts: true
|
||||
server capability: logging: true
|
||||
client declared: sampling: true
|
||||
client declared: elicitation: true
|
||||
@@ -0,0 +1,13 @@
|
||||
# resources/list, resources/templates/list, resources/read
|
||||
resource incident://index name=incident-index mime=text/plain
|
||||
template incident://{id}/log name=incident-log
|
||||
template runbook://{service} name=runbook
|
||||
|
||||
read incident://index ->
|
||||
INC-1042
|
||||
INC-1043
|
||||
read incident://INC-1043/log ->
|
||||
11:02:44 search-api WARN index segment merge running
|
||||
11:02:51 search-api INFO p99 latency 840ms (SLO 500ms)
|
||||
|
||||
read incident://INC-9999/log -> McpError: Error invoking resource method: log in com.ankurm.mcpadvanced.IncidentResources. /nCause: No such incident: INC-9999
|
||||
@@ -0,0 +1,13 @@
|
||||
# prompts/list and prompts/get
|
||||
prompt postmortem -- Draft a blameless postmortem for an incident
|
||||
arg incidentId required=true
|
||||
arg audience required=false
|
||||
|
||||
get postmortem(INC-1043, on-call manager) ->
|
||||
[USER] Write a blameless postmortem for the on-call manager. Sections: impact, timeline, root cause, follow-ups. Use only the log below.
|
||||
|
||||
11:02:44 search-api WARN index segment merge running
|
||||
11:02:51 search-api INFO p99 latency 840ms (SLO 500ms)
|
||||
|
||||
|
||||
get postmortem(INC-9999) -> McpError: Error invoking prompt method: postmortem in com.ankurm.mcpadvanced.IncidentPrompts./nCause: No such incident: INC-9999
|
||||
@@ -0,0 +1,13 @@
|
||||
# tools/call summarize_incident -> server sends sampling/createMessage to the client
|
||||
what the CLIENT received from the server:
|
||||
systemPrompt: You are an SRE. Answer in two sentences, no preamble.
|
||||
messages: 1
|
||||
messages[0].role: USER
|
||||
messages[0] starts with: Summarize this incident log:
|
||||
maxTokens: 120
|
||||
temperature: 0.2
|
||||
includeContext: NONE
|
||||
|
||||
tool result returned to the caller:
|
||||
Connection pool on payments-api was exhausted by a 14.8s ledger query, so 212 clients got HTTP 503. Fix the query or raise the pool.
|
||||
isError: false
|
||||
@@ -0,0 +1,8 @@
|
||||
# tools/call restart_service -> server sends elicitation/create to the client
|
||||
message shown to the human: Restart payments-api? This drops in-flight requests.
|
||||
requestedSchema sent by the server: {type=object, properties={confirm={type=boolean, description=Tick to approve the restart}, ticket={type=string, description=Change ticket number, e.g. CHG-2291}}, required=[confirm, ticket]}
|
||||
|
||||
ACCEPT confirm=true ticket=CHG-2291: RESTARTED payments-api under CHG-2291
|
||||
ACCEPT confirm=false: NOT RESTARTED -- form submitted with the box unticked
|
||||
DECLINE: NOT RESTARTED -- operator declined
|
||||
CANCEL : NOT RESTARTED -- operator dismissed the dialog
|
||||
@@ -0,0 +1,14 @@
|
||||
# a client that declared neither sampling nor elicitation
|
||||
summarize_incident (gated with sampleEnabled()):
|
||||
SAMPLING_UNAVAILABLE -- client has no model to lend. Raw log:
|
||||
11:02:44 search-api WARN index segment merge running
|
||||
11:02:51 search-api INFO p99 latency 840ms (SLO 500ms)
|
||||
|
||||
|
||||
restart_service (gated with elicitEnabled()):
|
||||
ELICITATION_UNAVAILABLE -- refusing to restart payments-api without an approval channel
|
||||
|
||||
summarize_incident_ungated (calls sample() with no check):
|
||||
result: Sampling not supported by the client: Implementation[name=Java SDK MCP Client, title=null, version=0.15.0, description=null, icons=null, websiteUrl=null]
|
||||
Sampling not supported by the client: Implementation[name=Java SDK MCP Client, title=null, version=0.15.0, description=null, icons=null, websiteUrl=null]
|
||||
isError: true
|
||||
@@ -0,0 +1,5 @@
|
||||
# client model takes 5s, server spring.ai.mcp.server.request-timeout=3s
|
||||
outcome: result isError=true text=java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 3000ms in 'source(MonoCreate)' (and no fallback has been configured)
|
||||
Did not observe any item or terminal signal within 3000ms in 'source(MonoCreate)' (and no fallback has been configured)
|
||||
returned in about (seconds, rounded): 3
|
||||
(timing is indicative, not a result)
|
||||
@@ -0,0 +1,5 @@
|
||||
# spring.ai.mcp.server.protocol=STATELESS, client declares sampling + elicitation
|
||||
tools listed: []
|
||||
summarize_incident: exception McpError: Unknown tool: invalid_tool_name
|
||||
restart_service: exception McpError: Unknown tool: invalid_tool_name
|
||||
times the client's handlers were called: 0
|
||||
@@ -0,0 +1,4 @@
|
||||
# log line from the STATELESS test run (spring.ai.mcp.server.protocol=STATELESS)
|
||||
McpPredicates : Stateless servers doesn't support bidirectional parameters. Skipping method public java.lang.String com.ankurm.mcpadvanced.IncidentTools.restart(java.lang.String,org.springframework.ai.mcp.annotation.context.McpSyncRequestContext) with bidirectional parameters
|
||||
McpPredicates : Stateless servers doesn't support bidirectional parameters. Skipping method public java.lang.String com.ankurm.mcpadvanced.IncidentTools.summarize(java.lang.String,org.springframework.ai.mcp.annotation.context.McpSyncRequestContext) with bidirectional parameters
|
||||
McpPredicates : Stateless servers doesn't support bidirectional parameters. Skipping method public java.lang.String com.ankurm.mcpadvanced.IncidentTools.summarizeUngated(java.lang.String,org.springframework.ai.mcp.annotation.context.McpSyncRequestContext) with bidirectional parameters
|
||||
@@ -0,0 +1,56 @@
|
||||
# sampling/createMessage on the wire
|
||||
|
||||
## 1. client -> server (POST, answers arrive later on the same session)
|
||||
tools/call summarize_incident {"incidentId":"INC-1043"}
|
||||
|
||||
## 2. server -> client, on the SSE stream of that same POST
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "sampling/createMessage",
|
||||
"id": "7899c995-7e19-45a9-8b27-a043acabe802-0",
|
||||
"params": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Summarize this incident log:\n11:02:44 search-api WARN index segment merge running\n11:02:51 search-api INFO p99 latency 840ms (SLO 500ms)\n"
|
||||
}
|
||||
}
|
||||
],
|
||||
"systemPrompt": "You are an SRE. Answer in two sentences, no preamble.",
|
||||
"includeContext": "none",
|
||||
"temperature": 0.2,
|
||||
"maxTokens": 120
|
||||
}
|
||||
}
|
||||
|
||||
## 3. client -> server, a NEW POST carrying the answer (same id as the server's request)
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7899c995-7e19-45a9-8b27-a043acabe802-0",
|
||||
"result": {
|
||||
"role": "assistant",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "A segment merge pushed p99 to 840ms against a 500ms SLO."
|
||||
},
|
||||
"model": "curl-model",
|
||||
"stopReason": "endTurn"
|
||||
}
|
||||
}
|
||||
|
||||
## 4. server -> client, the original tools/call finally completes
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "A segment merge pushed p99 to 840ms against a 500ms SLO."
|
||||
}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# elicitation/create on the wire
|
||||
|
||||
## 1. client -> server (POST, answers arrive later on the same session)
|
||||
tools/call restart_service {"service":"payments-api"}
|
||||
|
||||
## 2. server -> client, on the SSE stream of that same POST
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "elicitation/create",
|
||||
"id": "7899c995-7e19-45a9-8b27-a043acabe802-1",
|
||||
"params": {
|
||||
"message": "Restart payments-api? This drops in-flight requests.",
|
||||
"requestedSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirm": {
|
||||
"type": "boolean",
|
||||
"description": "Tick to approve the restart"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"description": "Change ticket number, e.g. CHG-2291"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"confirm",
|
||||
"ticket"
|
||||
]
|
||||
},
|
||||
"_meta": {},
|
||||
"mode": "form"
|
||||
}
|
||||
}
|
||||
|
||||
## 3. client -> server, a NEW POST carrying the answer (same id as the server's request)
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7899c995-7e19-45a9-8b27-a043acabe802-1",
|
||||
"result": {
|
||||
"action": "accept",
|
||||
"content": {
|
||||
"confirm": true,
|
||||
"ticket": "CHG-2291"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## 4. server -> client, the original tools/call finally completes
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "RESTARTED payments-api under CHG-2291"
|
||||
}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user