diff --git a/go.mod b/go.mod index 5f934aaa..7acfb375 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.10.0 - github.com/mark3labs/mcp-go v0.58.0 + github.com/mark3labs/mcp-go v1.0.0 github.com/ory/fosite v0.49.0 github.com/pb33f/libopenapi v0.38.7 github.com/pquerna/otp v1.5.0 diff --git a/go.sum b/go.sum index 874f2519..62e1d0a6 100644 --- a/go.sum +++ b/go.sum @@ -339,8 +339,8 @@ github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mark3labs/mcp-go v0.58.0 h1:AWfBk8lgRR0KZYve7PaLbR2MIjpw1oK2eGpBApaNS+Q= -github.com/mark3labs/mcp-go v0.58.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas= +github.com/mark3labs/mcp-go v1.0.0 h1:CZqCzXwUiTOstkIdW1MyOZGuM+LaKMEJSu+ZYluN4DU= +github.com/mark3labs/mcp-go v1.0.0/go.mod h1:r2fW4o3wsoJ7IMsx1Wuq5xeP8PRGXPDfNveoGAYbb/s= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= diff --git a/internal/mcp/meta.go b/internal/mcp/meta.go index 6c0e94de..84b1b65a 100644 --- a/internal/mcp/meta.go +++ b/internal/mcp/meta.go @@ -33,11 +33,24 @@ type MetaPayload struct { ToolSurfaceStable bool `json:"tool_surface_stable"` // MCPProtocolVersion is the latest MCP wire protocol revision this - // server can negotiate. Sourced from the underlying mcp-go library's - // LATEST_PROTOCOL_VERSION constant so the value never drifts from - // what NewMCPServer actually advertises in the handshake. Surfaced - // so consumers can detect feature support (e.g. RFC 8707 Resource - // Indicators land in the 2025-11-25 revision). + // server can negotiate. Surfaced so consumers can detect feature + // support (e.g. RFC 8707 Resource Indicators land in the 2025-11-25 + // revision). + // + // It is a PAD-OWNED constant, not the library's LATEST_PROTOCOL_VERSION. + // It was the library's until mcp-go 1.0, on the reasoning that sourcing + // it there meant it "never drifts from what NewMCPServer actually + // advertises". 1.0 falsified that: LATEST_PROTOCOL_VERSION moved to + // 2026-07-28, while the handshake answers through + // mcp.NegotiateLegacyVersion, which returns at most + // LATEST_LEGACY_PROTOCOL_VERSION (2025-11-25) and cannot return the + // modern revision at all. Sourcing from LATEST would therefore have + // CAUSED the drift it was meant to prevent, and published a claim to + // negotiate a revision this server cannot negotiate. + // + // So the advertised revision is a claim pad makes deliberately. Moving + // it means reading the new revision's delta against this server's + // surface first; a library bump must not move it on its own. MCPProtocolVersion string `json:"mcp_protocol_version"` } @@ -46,11 +59,17 @@ type MetaPayload struct { // same reason serverInfo.version does — empty values confuse some // clients that display them in their UI. // -// MCPProtocolVersion is sourced from mcp.LATEST_PROTOCOL_VERSION, which -// is the maximum protocol revision the server can negotiate. If a -// client downgrades during initialize, the per-session negotiated -// version may be lower; the meta document reports the server's +// MCPProtocolVersion reports the maximum protocol revision this server can +// negotiate. If a client downgrades during initialize, the per-session +// negotiated version may be lower; the meta document reports the server's // upper bound, not any specific session. +// AdvertisedMCPProtocolVersion is the MCP wire protocol revision pad claims +// to negotiate. It is deliberately a literal rather than a library constant — +// see MetaPayload.MCPProtocolVersion — and TestAdvertisedProtocolVersion pins +// it to what the library's handshake actually answers, so a bump that moves +// one and not the other fails instead of shipping a false claim. +const AdvertisedMCPProtocolVersion = "2025-11-25" + func BuildMetaPayload(padVersion string) MetaPayload { if padVersion == "" { padVersion = FallbackVersion @@ -60,7 +79,7 @@ func BuildMetaPayload(padVersion string) MetaPayload { CmdhelpVersion: CmdhelpVersion, ToolSurfaceVersion: ToolSurfaceVersion, ToolSurfaceStable: true, - MCPProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, + MCPProtocolVersion: AdvertisedMCPProtocolVersion, } } diff --git a/internal/mcp/meta_test.go b/internal/mcp/meta_test.go index 28c120f1..22223fa6 100644 --- a/internal/mcp/meta_test.go +++ b/internal/mcp/meta_test.go @@ -27,13 +27,41 @@ func TestBuildMetaPayload_FallbackVersion(t *testing.T) { if !got.ToolSurfaceStable { t.Errorf("ToolSurfaceStable = false, want true") } - if got.MCPProtocolVersion != mcp.LATEST_PROTOCOL_VERSION { - t.Errorf("MCPProtocolVersion = %q, want library LATEST %q", - got.MCPProtocolVersion, mcp.LATEST_PROTOCOL_VERSION) + if got.MCPProtocolVersion != AdvertisedMCPProtocolVersion { + t.Errorf("MCPProtocolVersion = %q, want %q", + got.MCPProtocolVersion, AdvertisedMCPProtocolVersion) } - // Belt-and-braces: never empty, regardless of library state. - if got.MCPProtocolVersion == "" { - t.Errorf("MCPProtocolVersion is empty — library constant unset?") +} + +// TestAdvertisedProtocolVersion is what replaced a tautology. The old +// assertion compared the payload against mcp.LATEST_PROTOCOL_VERSION while +// the payload was BUILT from it, so it could not fail — and when mcp-go 1.0 +// moved that constant to 2026-07-28 it would have passed while pad published +// a claim to negotiate a revision it cannot negotiate. +// +// Two assertions, and each catches something the other cannot: +// +// - against the LITERAL string, so moving pad's claim is a deliberate edit +// that a reader sees in the diff rather than a consequence of a bump; +// - against what the library's handshake ACTUALLY answers, which is the +// property the old comment claimed and did not have. mcp-go answers +// initialize through NegotiateLegacyVersion, which returns at most +// LATEST_LEGACY_PROTOCOL_VERSION — so a client asking for the newest +// revision is told 2025-11-25, and that is the true upper bound the meta +// document is supposed to report. +func TestAdvertisedProtocolVersion(t *testing.T) { + if AdvertisedMCPProtocolVersion != "2025-11-25" { + t.Errorf("AdvertisedMCPProtocolVersion = %q, want the literal 2025-11-25 — "+ + "moving pad's advertised revision needs the new revision's delta read "+ + "against this server's surface first", AdvertisedMCPProtocolVersion) + } + + // What a client asking for the newest revision is actually told. + handshake := mcp.NegotiateLegacyVersion(mcp.LATEST_PROTOCOL_VERSION) + if handshake != AdvertisedMCPProtocolVersion { + t.Errorf("the handshake answers %q but the meta document advertises %q; "+ + "these must agree or the meta document is a false claim about what "+ + "this server can negotiate", handshake, AdvertisedMCPProtocolVersion) } }