fix(docs): use canonical wire path capabilities.experimental.padCmdhelp (#342)

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.
This commit is contained in:
xarmian
2026-05-01 11:15:21 -04:00
committed by GitHub
parent 0cd22f7bad
commit e05ea07d62
7 changed files with 29 additions and 26 deletions
+1 -1
View File
@@ -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`.
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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 {
+13 -10
View File
@@ -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
// }
// },
// ...
// },
// ...
// }
+3 -4
View File
@@ -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}
+3 -3
View File
@@ -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 {
+6 -5
View File
@@ -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"