From e05ea07d62810e151d498241e9c799ab342af005 Mon Sep 17 00:00:00 2001 From: xarmian Date: Fri, 1 May 2026 11:15:21 -0400 Subject: [PATCH] fix(docs): use canonical wire path capabilities.experimental.padCmdhelp (#342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex caught the same accuracy issue on pad-web that exists in five spots in this repo: prose described the handshake location as "serverCapabilities.experimental.padCmdhelp", but per the MCP spec the InitializeResult shape is { result: { capabilities: { experimental: { ... } } } } There's no `serverCapabilities` field on the wire — `ServerCapabilities` is the Go-side struct type name in mcp-go; the JSON tag is `capabilities`. Anyone copying the path out of our docs to navigate a real JSON-RPC envelope was getting the wrong key. Updated to `capabilities.experimental.padCmdhelp` (or the fully qualified `result.capabilities.experimental.padCmdhelp` where the JSON-RPC envelope context wasn't otherwise obvious) in: - README.md — public-facing prose - CLAUDE.md — agent-facing prose - internal/mcp/version.go — discovery-surfaces doc comment + the experimentalCapabilityKey doc comment - internal/mcp/server.go — comment near WithExperimental - internal/mcp/meta.go — experimentalCapabilities() doc + the wire shape example (now wrapped under `result` for accuracy) - internal/mcp/server_test.go — test docstring + failure message - cmd/pad/mcp.go — comment near RegisterMeta The Go type `serverCapabilities` in `internal/server/handlers_capabilities.go` is unrelated (it's the response shape for `GET /api/v1/server/capabilities`) and stays as-is. No code/behaviour changes; pure prose accuracy fix. `make check` clean. Companion fix to pad-web PR #42, which Codex flagged the same issue on. --- CLAUDE.md | 2 +- README.md | 2 +- cmd/pad/mcp.go | 4 ++-- internal/mcp/meta.go | 23 +++++++++++++---------- internal/mcp/server.go | 7 +++---- internal/mcp/server_test.go | 6 +++--- internal/mcp/version.go | 11 ++++++----- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 69fc8b35..bb774a02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -191,7 +191,7 @@ Surface: - **Resources:** `pad://workspace/{ws}/items/{ref}`, `pad://workspace/{ws}/items`, `pad://workspace/{ws}/dashboard`, `pad://workspace/{ws}/collections`, plus the server-wide `pad://_meta/version`. - **Prompts:** `pad_plan`, `pad_ideate`, `pad_retro`, `pad_onboard` — multi-step workflows lifted from `skills/pad/SKILL.md`. -**Stability contract.** The cmdhelp tool surface is versioned via `internal/mcp.CmdhelpVersion` (currently `"0.1"`). Bump the major when tool names, argument shapes, or resource URIs change incompatibly — external agents (Cursor, Claude Desktop, the future Pad Cloud remote MCP) pin against it. The version is advertised on the wire two ways: under `serverCapabilities.experimental.padCmdhelp` in the initialize handshake, and as a JSON document at `pad://_meta/version`. +**Stability contract.** The cmdhelp tool surface is versioned via `internal/mcp.CmdhelpVersion` (currently `"0.1"`). Bump the major when tool names, argument shapes, or resource URIs change incompatibly — external agents (Cursor, Claude Desktop, the future Pad Cloud remote MCP) pin against it. The version is advertised on the wire two ways: at `capabilities.experimental.padCmdhelp` in the initialize handshake, and as a JSON document at `pad://_meta/version`. Code lives in `internal/mcp/` (built on `github.com/mark3labs/mcp-go`). Public docs at `getpad.dev/mcp/local`. diff --git a/README.md b/README.md index 98454b72..f51971de 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ pad mcp install claude-desktop # or: cursor, windsurf, --all ``` The server advertises a tool-surface stability tier (`cmdhelp_version`) -in the initialize handshake under `serverCapabilities.experimental.padCmdhelp`, +in the initialize handshake at `capabilities.experimental.padCmdhelp`, and as a queryable JSON document at `pad://_meta/version`. External agents can pin against this so a future tool rename doesn't break them silently. diff --git a/cmd/pad/mcp.go b/cmd/pad/mcp.go index c0e3f9cb..b9caeed8 100644 --- a/cmd/pad/mcp.go +++ b/cmd/pad/mcp.go @@ -300,8 +300,8 @@ Shuts down cleanly on EOF, SIGINT, or SIGTERM.`, // Tool-surface stability metadata (TASK-963). Static // resource at pad://_meta/version. Complements the - // handshake's serverCapabilities.experimental.padCmdhelp - // for clients that prefer reading a JSON document. + // handshake's capabilities.experimental.padCmdhelp for + // clients that prefer reading a JSON document. mcpserver.RegisterMeta(srv.MCP(), fullVersion()) if err := srv.Run(cmd.Context()); err != nil { diff --git a/internal/mcp/meta.go b/internal/mcp/meta.go index 679a10c5..dce76639 100644 --- a/internal/mcp/meta.go +++ b/internal/mcp/meta.go @@ -95,19 +95,22 @@ func RegisterMeta(srv *server.MCPServer, padVersion string) { }) } -// experimentalCapabilities returns the map advertised in the -// initialize handshake's serverCapabilities.experimental field. Lets -// clients discover the cmdhelp tier in one round-trip without reading -// the meta resource. +// experimentalCapabilities returns the map advertised at +// capabilities.experimental in the initialize handshake's result +// envelope. Lets clients discover the cmdhelp tier in one round-trip +// without reading the meta resource. // // Wire shape: // -// "capabilities": { -// "experimental": { -// "padCmdhelp": { -// "version": "0.1", -// "tool_surface_stable": true -// } +// "result": { +// "capabilities": { +// "experimental": { +// "padCmdhelp": { +// "version": "0.1", +// "tool_surface_stable": true +// } +// }, +// ... // }, // ... // } diff --git a/internal/mcp/server.go b/internal/mcp/server.go index d27d3d6f..7ac87006 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -59,10 +59,9 @@ func NewServer(opts Options) *Server { // subprocesses with arbitrary client-supplied args. server.WithRecovery(), // Tool-surface stability tier (TASK-963). Surfaces the cmdhelp - // contract directly in the handshake under - // serverCapabilities.experimental.padCmdhelp, so external - // agents can detect compatibility without reading - // pad://_meta/version first. + // contract directly in the handshake at + // capabilities.experimental.padCmdhelp, so external agents can + // detect compatibility without reading pad://_meta/version first. server.WithExperimental(experimentalCapabilities()), ) return &Server{mcp: mcp, debug: opts.Debug} diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 873e6069..7ab23189 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -39,8 +39,8 @@ func TestNewServer_Construction(t *testing.T) { // TestServer_InitializeHandshake is the contract test: a real // initialize round-trip through the stdio pipeline must return // serverInfo with our canonical name + the version we passed in, -// AND must advertise the cmdhelp tool-surface tier under -// serverCapabilities.experimental.padCmdhelp. +// AND must advertise the cmdhelp tool-surface tier at +// capabilities.experimental.padCmdhelp in the initialize result. // // This is what every MCP client (Claude Desktop, Cursor, Windsurf, // mcp-inspector) sees on connect — if it regresses, downstream @@ -64,7 +64,7 @@ func TestServer_InitializeHandshake(t *testing.T) { // directly from the handshake. exp := res.Capabilities.Experimental if exp == nil { - t.Fatalf("serverCapabilities.experimental missing — cmdhelp tier not advertised") + t.Fatalf("capabilities.experimental missing — cmdhelp tier not advertised") } rawNS, ok := exp[experimentalCapabilityKey] if !ok { diff --git a/internal/mcp/version.go b/internal/mcp/version.go index 5589eaf5..e9d411a4 100644 --- a/internal/mcp/version.go +++ b/internal/mcp/version.go @@ -30,9 +30,9 @@ const FallbackVersion = "0.0.0-dev" // // - "0.1" — initial cmdhelp-derived surface from PLAN-942. // -// Discovery surfaces: +// Discovery surfaces (paths into the JSON-RPC envelope): // -// - serverCapabilities.experimental.padCmdhelp.version (handshake). +// - result.capabilities.experimental.padCmdhelp.version (handshake). // - pad://_meta/version resource (queryable JSON document). const CmdhelpVersion = "0.1" @@ -48,7 +48,8 @@ const MetaVersionURI = "pad://_meta/version" // reads it dynamically (see meta.go) so the value never drifts from // what the library actually advertises. -// experimentalCapabilityKey is the namespace under -// serverCapabilities.experimental that carries the cmdhelp tier. -// Namespaced so other servers' experimental capabilities don't collide. +// experimentalCapabilityKey is the JSON object key under +// capabilities.experimental that carries the cmdhelp tier in the +// initialize handshake. Namespaced so other servers' experimental +// capabilities don't collide. const experimentalCapabilityKey = "padCmdhelp"