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:
2026-11-10 18:47:09 +00:00
co-authored by Claude Sonnet 5.5
parent 68da642f9a
commit 0006167c24
27 changed files with 1033 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Needs JDK 25, Maven, curl, python3.
# Session ids and request ids in the wire transcripts differ on every run; everything else is stable
# except the one timing line in 07, which is indicative only.
set -eu
cd "$(dirname "$0")/.."
# 1-8: the JUnit tests write output/01..08 themselves (the assertions and the transcripts are the same code)
mvn -B -q test 2>&1 | tee /tmp/mcp-advanced-test.log | grep -E 'Tests run:|BUILD|ERROR' || true
# 9: the one WARN the stateless server logs while it silently drops the tools
{
echo "# log line from the STATELESS test run (spring.ai.mcp.server.protocol=STATELESS)"
grep 'Stateless servers' /tmp/mcp-advanced-test.log | sed -E 's/^.*(McpPredicates)/\1/'
} > output/09-stateless-warning.txt
# 10-11: raw JSON-RPC with curl as the client
mvn -B -q package -DskipTests
bash scripts/wire-demo.sh
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Shows the sampling and elicitation messages as raw JSON-RPC over Streamable HTTP, using curl as the
# "client". Needs: target/mcp-advanced-1.0.0.jar (mvn package -DskipTests), JDK 25 on PATH, curl, python3.
# Output goes to output/10-wire-sampling.txt and output/11-wire-elicitation.txt.
set -u
cd "$(dirname "$0")/.."
URL=http://localhost:8089/mcp
H=(-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream')
OUT=output
TMP=$(mktemp -d)
setsid nohup java -Dserver.port=8089 -jar target/mcp-advanced-1.0.0.jar > "$TMP/app.log" 2>&1 < /dev/null &
APP_PID=$!
for i in $(seq 1 60); do
curl -s --noproxy '*' -o /dev/null -X POST "$URL" "${H[@]}" -d '{}' && break
sleep 1
done
# --- initialize, declaring BOTH sampling and elicitation ---------------------------------------
curl -s --noproxy '*' -D "$TMP/init.h" -o "$TMP/init.b" -X POST "$URL" "${H[@]}" -d '{
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-11-25",
"capabilities":{"sampling":{},"elicitation":{}},
"clientInfo":{"name":"curl-client","version":"0"}}}'
SID=$(grep -i '^mcp-session-id:' "$TMP/init.h" | awk '{print $2}' | tr -d '\r')
curl -s --noproxy '*' -o /dev/null -X POST "$URL" "${H[@]}" -H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# Run one tool call, read the server's request out of the SSE stream, answer it, then show the final result.
roundtrip () { # $1 = tool name $2 = arguments json $3 = answer result json $4 = output file $5 = title
local call_id=7
curl -s --noproxy '*' -N -X POST "$URL" "${H[@]}" -H "Mcp-Session-Id: $SID" \
-d "{\"jsonrpc\":\"2.0\",\"id\":$call_id,\"method\":\"tools/call\",\"params\":{\"name\":\"$1\",\"arguments\":$2}}" \
> "$TMP/stream.txt" &
local cpid=$!
for i in $(seq 1 40); do grep -q '"method":"\(sampling/createMessage\|elicitation/create\)"' "$TMP/stream.txt" 2>/dev/null && break; sleep 0.25; done
local req_line req_id
req_line=$(grep '^data:' "$TMP/stream.txt" | grep -E 'sampling/createMessage|elicitation/create' | head -1 | sed 's/^data: *//')
req_id=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["id"])' "$req_line")
curl -s --noproxy '*' -o /dev/null -X POST "$URL" "${H[@]}" -H "Mcp-Session-Id: $SID" \
-d "{\"jsonrpc\":\"2.0\",\"id\":\"$req_id\",\"result\":$3}"
wait $cpid
{
echo "# $5"
echo
echo "## 1. client -> server (POST, answers arrive later on the same session)"
echo "tools/call $1 $2"
echo
echo "## 2. server -> client, on the SSE stream of that same POST"
python3 -c 'import json,sys; print(json.dumps(json.loads(sys.argv[1]),indent=2))' "$req_line"
echo
echo "## 3. client -> server, a NEW POST carrying the answer (same id as the server's request)"
python3 -c 'import json,sys; print(json.dumps({"jsonrpc":"2.0","id":sys.argv[1],"result":json.loads(sys.argv[2])},indent=2))' "$req_id" "$3"
echo
echo "## 4. server -> client, the original tools/call finally completes"
grep '^data:' "$TMP/stream.txt" | tail -1 | sed 's/^data: *//' | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin),indent=2))'
} > "$OUT/$4"
}
roundtrip summarize_incident '{"incidentId":"INC-1043"}' \
'{"role":"assistant","content":{"type":"text","text":"A segment merge pushed p99 to 840ms against a 500ms SLO."},"model":"curl-model","stopReason":"endTurn"}' \
10-wire-sampling.txt "sampling/createMessage on the wire"
roundtrip restart_service '{"service":"payments-api"}' \
'{"action":"accept","content":{"confirm":true,"ticket":"CHG-2291"}}' \
11-wire-elicitation.txt "elicitation/create on the wire"
kill "$APP_PID" 2>/dev/null
rm -rf "$TMP"