Skip to content

mcp: align with the 2025-11-25 spec#27133

Open
davlgd wants to merge 3 commits into
vlang:masterfrom
davlgd:davlgd-enhance-mcp
Open

mcp: align with the 2025-11-25 spec#27133
davlgd wants to merge 3 commits into
vlang:masterfrom
davlgd:davlgd-enhance-mcp

Conversation

@davlgd
Copy link
Copy Markdown
Contributor

@davlgd davlgd commented May 11, 2026

This PR brings vlib/mcp to 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

  • stdio uses newline-delimited JSON framing per spec (was LSP-style Content-Length). Reads bypass libc fread so each message arrives as soon as it lands behind a pipe; stdout is flushed after every frame.
  • Streamable HTTP validates Origin (403, with loopback fallback), Accept (406), and MCP-Protocol-Version (400). GET requires an active MCP-Session-Id and returns an SSE stream with per-event ids and Last-Event-ID resumption.

Server primitives

  • Tools support annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title), icons, and execution.taskSupport.
  • Resources and resource templates support icons, size, the full Annotations object (audience, priority, lastModified), plus resources/subscribe / unsubscribe and notifications/resources/updated.
  • Prompts gain optional title and icons.
  • completion/complete clamps values to the spec-mandated 100-item maximum and flips hasMore to true on truncation.

Lifecycle and capabilities

  • Implementation carries the 2025-11-25 metadata fields (title, description, websiteUrl, icons).
  • Capabilities (tools.listChanged, resources.{listChanged, subscribe}, prompts.listChanged, logging, completions) are advertised only when matching primitives are registered.
  • Every client request other than ping (and the initialize handshake itself) is gated with -32002 server_not_initialized until the client sends notifications/initialized.
  • Adding a tool, resource, resource template or prompt automatically queues the matching notifications/<x>/list_changed broadcast.

Server-initiated requests

  • roots/list, sampling/createMessage, elicitation/create are exposed as blocking helpers on Server. The wait is backed by a sync.Semaphore per pending request, so the server never burns CPU polling for a response.
  • Sampling carries tools, toolChoice and includeContext.
  • Elicitation supports both form and URL modes; the -32042 URLElicitationRequiredError code is exposed and notify_elicitation_complete() emits the matching notification.

Logging, progress, cancellation

  • notify_log emits notifications/message filtered per session by RFC 5424 level (logging/setLevel configurable).
  • Context.notify_progress(progress, total, message) enforces strict monotonic increases per progressToken (the spec's MUST).
  • notifications/cancelled propagates through Context.is_cancelled() so long-running handlers can cooperate.

Wire-shape correctness

  • Response.encode() emits error.data as raw JSON (V's json.encode ignores @[raw] on encode, so a hand-rolled writer is required).
  • CallToolResult.content is always emitted (REQUIRED by the schema, even when empty).
  • resources/read against an unknown URI returns -32002 with the missing URI in data (was -32602).
  • Content-block helpers (text, image, audio, embedded text/blob, resource link) ship with _with_annotations variants.

Showcase example

examples/mcp/server.v exercises 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.v is 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, raw error.data encoding, and the -32002 resource-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.json and validates every payload. The twelve result envelopes (InitializeResult, ListToolsResult, CallToolResult, ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult, ListPromptsResult, GetPromptResult, CompleteResult, EmptyResult, plus JSONRPCErrorResponse for unknown tool and unknown resource) and the five ProgressNotification events emitted by count_to all validate cleanly against their schema definitions.

How to test

Showcase with MCP Inspector — stdio:

v -o /tmp/mcp_showcase examples/mcp/server.v
npx @modelcontextprotocol/inspector /tmp/mcp_showcase

Showcase over Streamable HTTP:

v run examples/mcp/server.v -- --http 127.0.0.1:8080

Then point MCP Inspector at http://127.0.0.1:8080/mcp in Streamable HTTP mode.

Deferred (documented in vlib/mcp/README.md)

  • The Tasks utility (tasks/*) is experimental in the 2025-11-25 spec; the matching capability is intentionally not advertised.
  • OAuth Authorization is SHOULD rather than MUST and is left for a follow-up.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread vlib/mcp/server.v
Comment on lines +2342 to +2345
if last_event_id_text != '' {
events = s.replay_events_after(session_id, last_event_id)
} else {
events = s.drain_to_event_log(session_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread vlib/mcp/server.v Outdated
Comment on lines +800 to +803
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 } }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@davlgd davlgd force-pushed the davlgd-enhance-mcp branch from cfe9cf8 to 974d0a9 Compare May 11, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant