mcp: align with the 2025-11-25 spec#27133
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cfe9cf8eca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if last_event_id_text != '' { | ||
| events = s.replay_events_after(session_id, last_event_id) | ||
| } else { | ||
| events = s.drain_to_event_log(session_id) |
There was a problem hiding this comment.
Include queued events when resuming SSE with Last-Event-ID
When Last-Event-ID is present, handle_http_get replays only event_log and skips draining the live notification_queue. Any notifications or server-initiated requests generated after the disconnect but before the resume GET stay in the queue and are not sent on that resumed stream, so clients can miss messages exactly in the recovery path this header is meant to protect.
Useful? React with 👍 / 👎.
| mut session := s.state.sessions[session_id] | ||
| session.pending_responses[response.id] = response | ||
| s.state.sessions[session_id] = session | ||
| signal = s.state.response_signals[key] or { unsafe { nil } } |
There was a problem hiding this comment.
Discard late timed-out replies to prevent pending response buildup
deliver_response always inserts incoming replies into session.pending_responses, even if the waiter already timed out and removed its semaphore entry. In that timeout-then-late-reply scenario, nothing drains these stale map entries (request IDs are unique and never awaited again), so long-lived sessions can accumulate unbounded pending_responses memory.
Useful? React with 👍 / 👎.
cfe9cf8 to
974d0a9
Compare
This PR brings
vlib/mcpto full conformance with the Model Context Protocol 2025-11-25 specification, validated end-to-end against the official JSON Schema. It fixes wire shapes that diverged from the spec, adds every server feature the spec describes, and ships a showcase example that doubles as the fixture for the schema validator.Why
The module was on an older draft and shipped with stdio framing and a few JSON shapes that did not match what current MCP clients (Claude Desktop, Cursor, MCP Inspector) expect. After this PR, any spec-conformant client interoperates with a V MCP server out of the box.
What's in the box
Transports
Content-Length). Reads bypass libcfreadso each message arrives as soon as it lands behind a pipe; stdout is flushed after every frame.Origin(403, with loopback fallback),Accept(406), andMCP-Protocol-Version(400). GET requires an activeMCP-Session-Idand returns an SSE stream with per-event ids andLast-Event-IDresumption.Server primitives
readOnlyHint,destructiveHint,idempotentHint,openWorldHint,title),icons, andexecution.taskSupport.icons,size, the fullAnnotationsobject (audience,priority,lastModified), plusresources/subscribe/unsubscribeandnotifications/resources/updated.titleandicons.completion/completeclampsvaluesto the spec-mandated 100-item maximum and flipshasMoretotrueon truncation.Lifecycle and capabilities
Implementationcarries the 2025-11-25 metadata fields (title,description,websiteUrl,icons).tools.listChanged,resources.{listChanged, subscribe},prompts.listChanged,logging,completions) are advertised only when matching primitives are registered.ping(and theinitializehandshake itself) is gated with-32002 server_not_initializeduntil the client sendsnotifications/initialized.notifications/<x>/list_changedbroadcast.Server-initiated requests
roots/list,sampling/createMessage,elicitation/createare exposed as blocking helpers onServer. The wait is backed by async.Semaphoreper pending request, so the server never burns CPU polling for a response.tools,toolChoiceandincludeContext.-32042URLElicitationRequiredErrorcode is exposed andnotify_elicitation_complete()emits the matching notification.Logging, progress, cancellation
notify_logemitsnotifications/messagefiltered per session by RFC 5424 level (logging/setLevelconfigurable).Context.notify_progress(progress, total, message)enforces strict monotonic increases perprogressToken(the spec's MUST).notifications/cancelledpropagates throughContext.is_cancelled()so long-running handlers can cooperate.Wire-shape correctness
Response.encode()emitserror.dataas raw JSON (V'sjson.encodeignores@[raw]on encode, so a hand-rolled writer is required).CallToolResult.contentis always emitted (REQUIRED by the schema, even when empty).resources/readagainst an unknown URI returns-32002with the missing URI indata(was-32602).text,image,audio, embedded text/blob, resource link) ship with_with_annotationsvariants.Showcase example
examples/mcp/server.vexercises every shipped capability — three tools (annotations, icons, progress, cancellation, destructive hint), a concrete resource (size + annotations), a resource template, a prompt, two completions, RFC 5424 logging, and both stdio and HTTP transports. It is the same binary the schema validator drives, and works out of the box with MCP Inspector both in stdio mode and HTTP mode (./server --http :8080).Tests
vlib/mcp/spec_compliance_test.vis new and cross-checks the JSON shape of every refactored field against the published schema, with regression cases for monotonic progress, the 100-item completion cap, URL elicitation encoding, sampling tools, content annotations, rawerror.dataencoding, and the-32002resource-not-found path. The existing client and server tests are preserved and pass alongside the new file (3 test files, 55 test functions, 230+ assertions, all green).In addition, the showcase server was driven over stdio by an Ajv 2020 runner that loads the official
schema.jsonand validates every payload. The twelve result envelopes (InitializeResult,ListToolsResult,CallToolResult,ListResourcesResult,ListResourceTemplatesResult,ReadResourceResult,ListPromptsResult,GetPromptResult,CompleteResult,EmptyResult, plusJSONRPCErrorResponsefor unknown tool and unknown resource) and the fiveProgressNotificationevents emitted bycount_toall validate cleanly against their schema definitions.How to test
Showcase with MCP Inspector — stdio:
Showcase over Streamable HTTP:
Then point MCP Inspector at
http://127.0.0.1:8080/mcpin Streamable HTTP mode.Deferred (documented in
vlib/mcp/README.md)tasks/*) is experimental in the 2025-11-25 spec; the matching capability is intentionally not advertised.SHOULDrather thanMUSTand is left for a follow-up.