Speak MCP 2026-07-28 without breaking initialize-era clients - #4
Merged
Conversation
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
marked this pull request as ready for review
September 5, 2026 10:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR upgrades go-mcp to the current MCP specification (
2026-07-28) while keeping every existing client path working.Protocol
2026-07-28.server/discover(supported versions, capabilities, identity, cache hints)._metaversion negotiation. Unknown_metaversions return-32022(UnsupportedProtocolVersionError) withsupported/requested.resultType,ttlMs,cacheScope, and result_metaare emitted only when the client declares2026-07-28. Legacyinitialize/tools/listresponses keep the pre-2026 JSON shape.initializestill works and echoes2024-11-05,2025-03-26, or2025-11-25when the client asks for them. An unknowninitialize.protocolVersionis negotiated, not rejected.ping,notifications/initialized, andresources/templates/list(empty catalog) remain available so older and probing clients do not get-32601.Security
AddTool/AddResource/AddPromptare safe duringRun.Server.HandlerTimeout(cooperative — handlers that ignorectxare not preempted).jsonrpc: "2.0"or a method are rejected as-32600.gomcp.SafeJoinrejects..traversal, adjacent-prefix escapes, absolute paths outside root, and symlink escapes. Containment usesfilepath.RelsoSafeJoin("/", ...)works. The fs-navigator example uses it and caps file reads.Extensions
title,annotations,outputSchemaon tools;titleon resources and prompts.SetInstructionsfor initialize / discover guidance.Server.ListPageSize.RunContext/RunWithIOContextso callers can cancel in-flight handlers.Compatibility contract
Existing
Tool.Handlersignatures, error codes for unknown tools/resources/prompts, in-bandisErrorfor tool failures, and the initialize handshake are unchanged. Extra JSON fields appear only for 2026-07-28 clients.Tests
go test ./gomcp/ -racecovers discover, version negotiation, modern vs legacy wire shapes, pagination (includingMaxIntpage 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 becauseroot+separatorbecame//. Now usesfilepath.Rel.panic("secret"), type-assertion dumps) were returned on the MCP wire. They now go to stderr only.paginatewithpageSize == math.MaxIntcould overflowstart+pageSizeand panic. Compare against remaining length.protocolVersionfield. Only_meta.io.modelcontextprotocol/protocolVersionflips the 2026 wire shape.Accepted (documented, not changed)
HandlerTimeoutis cooperative. A handler that ignoresctxstill stalls the sequential stdio loop. Preemption would require abandoning goroutines or a different architecture.notifications/cancelledis 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 anid.SafeJoinis a check, not an open. A concurrent writer can still swap a path component beforeos.Open. Callers on hostile trees should open withO_NOFOLLOW/openat.ListPageSize == 0still returns the full catalog (historical behavior). SetListPageSizeto page.isErrorrather than-32602, matching existing clients.Not in scope for this stdio framework
Mcp-Method,Mcp-Name), MRTRinput_required,subscriptions/listen, and OAuth/CIMD. Those belong to a remote transport, which this library does not provide.