Skip to content

Speak MCP 2026-07-28 without breaking initialize-era clients - #4

Merged
molty3000 merged 3 commits into
mainfrom
cursor/mcp-protocol-upgrade-d808
Sep 5, 2026
Merged

Speak MCP 2026-07-28 without breaking initialize-era clients#4
molty3000 merged 3 commits into
mainfrom
cursor/mcp-protocol-upgrade-d808

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

This PR upgrades go-mcp to the current MCP specification (2026-07-28) while keeping every existing client path working.

Protocol

  • Default version is now 2026-07-28.
  • New required RPC: server/discover (supported versions, capabilities, identity, cache hints).
  • Per-request _meta version negotiation. Unknown _meta versions return -32022 (UnsupportedProtocolVersionError) with supported / requested.
  • resultType, ttlMs, cacheScope, and result _meta are emitted only when the client declares 2026-07-28. Legacy initialize / tools/list responses keep the pre-2026 JSON shape.
  • initialize still works and echoes 2024-11-05, 2025-03-26, or 2025-11-25 when the client asks for them. An unknown initialize.protocolVersion is negotiated, not rejected.
  • ping, notifications/initialized, and resources/templates/list (empty catalog) remain available so older and probing clients do not get -32601.

Security

  • Registry maps are mutex-protected; AddTool / AddResource / AddPrompt are safe during Run.
  • Handler panics are recovered and answered in-band; the panic value is logged to stderr, not sent to the client.
  • Optional Server.HandlerTimeout (cooperative — handlers that ignore ctx are not preempted).
  • JSON-RPC requests without jsonrpc: "2.0" or a method are rejected as -32600.
  • gomcp.SafeJoin rejects .. traversal, adjacent-prefix escapes, absolute paths outside root, and symlink escapes. Containment uses filepath.Rel so SafeJoin("/", ...) works. The fs-navigator example uses it and caps file reads.

Extensions

  • Optional title, annotations, outputSchema on tools; title on resources and prompts.
  • SetInstructions for initialize / discover guidance.
  • Deterministic list order. Optional cursor pagination via Server.ListPageSize.
  • RunContext / RunWithIOContext so callers can cancel in-flight handlers.

Compatibility contract

Existing Tool.Handler signatures, error codes for unknown tools/resources/prompts, in-band isError for tool failures, and the initialize handshake are unchanged. Extra JSON fields appear only for 2026-07-28 clients.

Tests

go test ./gomcp/ -race covers discover, version negotiation, modern vs legacy wire shapes, pagination (including MaxInt page size), panic recovery without value leak, timeouts, and SafeJoin attack cases including filesystem-root and adjacent-prefix.

Adversarial review

Two independent reviews (security + correctness) were run against this branch. Findings and disposition:

Fixed in this PR

  • SafeJoin("/", "etc/passwd") was rejected because root+separator became //. Now uses filepath.Rel.
  • Panic values (panic("secret"), type-assertion dumps) were returned on the MCP wire. They now go to stderr only.
  • paginate with pageSize == math.MaxInt could overflow start+pageSize and panic. Compare against remaining length.
  • Modern result fields were keyed off any protocolVersion field. Only _meta.io.modelcontextprotocol/protocolVersion flips the 2026 wire shape.

Accepted (documented, not changed)

  • HandlerTimeout is cooperative. A handler that ignores ctx still stalls the sequential stdio loop. Preemption would require abandoning goroutines or a different architecture.
  • notifications/cancelled is a no-op under sequential dispatch: by the time the line is read, the named request has already finished. Acknowledging it avoids hanging a client that attached an id.
  • SafeJoin is a check, not an open. A concurrent writer can still swap a path component before os.Open. Callers on hostile trees should open with O_NOFOLLOW / openat.
  • Default ListPageSize == 0 still returns the full catalog (historical behavior). Set ListPageSize to page.
  • Unknown tools still return in-band isError rather than -32602, matching existing clients.

Not in scope for this stdio framework

  • Streamable HTTP headers (Mcp-Method, Mcp-Name), MRTR input_required, subscriptions/listen, and OAuth/CIMD. Those belong to a remote transport, which this library does not provide.
Open in Web Open in Cursor 

cursoragent and others added 3 commits September 5, 2026 10:21
Upgrade the default protocol to 2026-07-28 (server/discover, per-request
_meta, cache hints, resultType) without breaking initialize-era clients.
Legacy responses keep the pre-2026 JSON shape; modern fields are emitted
only when the client declares 2026-07-28.

Also harden the dispatch loop: mutex-protected registries, handler panic
recovery and optional timeouts, JSON-RPC request validation, deterministic
list order with optional cursor pagination, and SafeJoin for model-supplied
filesystem paths.

Co-authored-by: Rolando Santamaria Maso <jkyberneees@users.noreply.github.com>
A ListPageSize of math.MaxInt could wrap start+pageSize and panic on
slice bounds. Compare against the remaining length instead.

Also gate modern result fields on params._meta.protocolVersion so a
stray top-level protocolVersion on tools/list cannot change the wire
shape for legacy clients.

Co-authored-by: Rolando Santamaria Maso <jkyberneees@users.noreply.github.com>
underRoot used root+separator, so SafeJoin("/", "etc/passwd") became a
prefix of "//" and rejected every legitimate path. Compare via
filepath.Rel instead.

Handler panics are still recovered so the loop stays up, but the panic
value is written to stderr only — the client sees a generic
"handler panicked" string.

Co-authored-by: Rolando Santamaria Maso <jkyberneees@users.noreply.github.com>
@jkyberneees
jkyberneees marked this pull request as ready for review September 5, 2026 10:29
@molty3000
molty3000 merged commit ecc5e36 into main Sep 5, 2026
6 checks passed
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.

3 participants