mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
df8a3631e7
* feat(mcp): v0.2 catalog scaffold + ToolSurfaceVersion + pad_meta tool (TASK-979) First commit of TASK-970's 3-stage rollout (PLAN-969). Introduces the hand-curated v0.2 catalog types (ToolDef, ActionFn, ActionEnv) and ships one tool — pad_meta — end-to-end. v0.1 cmdhelp-walk surface stays live alongside; subsequent commits (TASK-980, TASK-981) migrate the rest and flip v0.1 off. Architecture record: DOC-978. The fan-out registry sits ABOVE the dispatcher boundary — Dispatcher / route table are unchanged, so both ExecDispatcher (stdio) and HTTPHandlerDispatcher (HTTP) inherit the new shape for free. Changes: - internal/mcp/catalog.go (new) — ToolDef, ActionFn, ActionEnv, passThrough helper, RegisterCatalog, makeFanOutHandler, structured error helpers. - internal/mcp/catalog_meta.go (new) — pad_meta tool with three inline actions: server-info, version, tool-surface (full catalog dump for PLAN-943 docs generation). - internal/mcp/version.go — add ToolSurfaceVersion = "0.2" + matching experimentalToolSurfaceKey. Independent of CmdhelpVersion (cmdhelp owns CLI help-tree contract; ToolSurfaceVersion owns MCP catalog). - internal/mcp/meta.go — extend MetaPayload with ToolSurfaceVersion; experimentalCapabilities advertises both padCmdhelp + padToolSurface. - cmd/pad/mcp.go — call RegisterCatalog alongside Register so v0.2 surface is live. - Tests: catalog_test.go + catalog_meta_test.go (new); meta_test.go + server_test.go updated to assert the new field/capability. Parent: TASK-970 → PLAN-969. * fix(mcp): keep ToolSurfaceVersion at "0.1" until catalog is complete per Codex review (round 1) Codex P1: advertising tool_surface_version=0.2 while the user-visible surface is still predominantly v0.1 (cmdhelp walker active alongside, only pad_meta in the catalog) misleads consumers that pin against the handshake or pad://_meta/version. The padToolSurface namespace would suggest the full resource/action shape is available when in reality only pad_meta uses it. Delay the 0.1 → 0.2 bump to TASK-981 — the commit that retires the cmdhelp walker and ships the complete catalog. The constant stays declared so the surface contract is wired through the handshake + meta resource + pad_meta.tool-surface, the version string just truthfully reflects "still v0.1" until the catalog is complete. No test changes needed: every assertion uses the constant, not a literal "0.2". Parent: TASK-979 → TASK-970 → PLAN-969. * fix(mcp): scope pad_meta.tool-surface to v0.2 catalog only per Codex review (round 2) Codex P1: pad_meta.tool-surface description claimed "Full catalog dump: every tool" but during PLAN-969's parallel rollout, tools/list contains both the catalog (currently just pad_meta) AND the cmdhelp walker's ~85 verb tools. Calling the catalog dump "every tool" misleads consumers who expect a complete enumeration. Same spirit as round 1's fix: stop claiming what isn't true. The catalog dump is the v0.2 catalog by design — consumers wanting the complete advertised surface should read tools/list directly. Hand-mapping the walker output into the catalog dump would cost duplication for a surface that's about to disappear in TASK-981. Wire-level changes: - Tighten the action description in padMetaToolDescription to say "v0.2 catalog dump: every tool managed by the hand-curated catalog" and explicitly note tools/list is the source for the complete surface. - Add rollout_status field to the response payload: "in-progress" while ToolSurfaceVersion stays at "0.1", "complete" once TASK-981 bumps it. Lets consumers detect the rollout state programmatically. - Test asserts the new field tracks ToolSurfaceVersion. Parent: TASK-979 → TASK-970 → PLAN-969. * fix(mcp): include params in pad_meta.tool-surface dump per Codex review (round 3) Codex P1: tool description claimed the dump includes each tool's "input schema" but the payload only emitted name/description/workspace/ actions[]. Misleading for docs generators (TASK-957) that would build getpad.dev/docs/mcp from this canonical source. Going with the substantive fix rather than just trimming the description: include a synthesized params[] per tool entry. Mirrors what consumers see in tools/list — `action` (always required, enum of declared action names), `workspace` (when ToolDef.Schema.Workspace=true), and per-tool ParamDefs. Synthesizing `action` and `workspace` rather than copying them from ToolDef makes the dump self-contained: a docs generator doesn't need to reproduce buildToolFromDef's implicit-param logic separately. Test asserts each catalog entry has params[] starting with `action` (enum length matches action handler count) and the right total length based on Schema.Workspace + Schema.Params. Parent: TASK-979 → TASK-970 → PLAN-969.
143 lines
5.0 KiB
Go
143 lines
5.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
// MetaPayload is the JSON shape returned by pad://_meta/version.
|
|
//
|
|
// The fields are explicit (no embedded structs, no omitempty) so
|
|
// downstream consumers can pin against a known schema across pad
|
|
// releases. Adding fields is safe; renaming or removing them is a
|
|
// CmdhelpVersion bump.
|
|
type MetaPayload struct {
|
|
// PadVersion is the runtime version of the pad binary (e.g. "0.1.5").
|
|
PadVersion string `json:"pad_version"`
|
|
|
|
// CmdhelpVersion is the CLI help-tree stability tier. See the
|
|
// CmdhelpVersion constant for the bump policy.
|
|
CmdhelpVersion string `json:"cmdhelp_version"`
|
|
|
|
// ToolSurfaceVersion is the MCP tool catalog stability tier. See
|
|
// the ToolSurfaceVersion constant for the bump policy. Independent
|
|
// of CmdhelpVersion: the catalog can rev without changing the CLI.
|
|
ToolSurfaceVersion string `json:"tool_surface_version"`
|
|
|
|
// ToolSurfaceStable signals that the tool surface has shipped its
|
|
// stability contract. False during pre-release iteration.
|
|
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).
|
|
MCPProtocolVersion string `json:"mcp_protocol_version"`
|
|
}
|
|
|
|
// BuildMetaPayload returns the meta payload for the given pad runtime
|
|
// version. An empty padVersion falls back to FallbackVersion for the
|
|
// 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
|
|
// upper bound, not any specific session.
|
|
func BuildMetaPayload(padVersion string) MetaPayload {
|
|
if padVersion == "" {
|
|
padVersion = FallbackVersion
|
|
}
|
|
return MetaPayload{
|
|
PadVersion: padVersion,
|
|
CmdhelpVersion: CmdhelpVersion,
|
|
ToolSurfaceVersion: ToolSurfaceVersion,
|
|
ToolSurfaceStable: true,
|
|
MCPProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
|
|
}
|
|
}
|
|
|
|
// RegisterMeta installs the pad://_meta/version static resource on srv
|
|
// so MCP clients can discover pad's tool-surface stability tier
|
|
// without parsing the (free-form) handshake instructions field.
|
|
//
|
|
// padVersion is typically the same string passed to NewServer's
|
|
// Options.Version (the runtime fullVersion()).
|
|
func RegisterMeta(srv *server.MCPServer, padVersion string) {
|
|
payload := BuildMetaPayload(padVersion)
|
|
resource := mcp.NewResource(
|
|
MetaVersionURI,
|
|
"pad meta version",
|
|
mcp.WithResourceDescription(
|
|
"Tool-surface stability metadata for this pad MCP server. "+
|
|
"Returns pad runtime version, cmdhelp surface version "+
|
|
"(the contract external agents depend on), and the MCP "+
|
|
"protocol revision the server pins against.",
|
|
),
|
|
mcp.WithMIMEType(jsonMIMEType),
|
|
)
|
|
srv.AddResource(resource, func(_ context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
// json.Marshal failing on a struct with only string + bool
|
|
// fields is so unlikely it would indicate a runtime bug;
|
|
// surface as an error rather than panicking the handler.
|
|
return nil, fmt.Errorf("marshal meta payload: %w", err)
|
|
}
|
|
return []mcp.ResourceContents{
|
|
mcp.TextResourceContents{
|
|
URI: req.Params.URI,
|
|
MIMEType: jsonMIMEType,
|
|
Text: string(body),
|
|
},
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
// experimentalCapabilities returns the map advertised at
|
|
// capabilities.experimental in the initialize handshake's result
|
|
// envelope. Lets clients discover the cmdhelp + tool-surface tiers in
|
|
// one round-trip without reading the meta resource.
|
|
//
|
|
// Wire shape:
|
|
//
|
|
// "result": {
|
|
// "capabilities": {
|
|
// "experimental": {
|
|
// "padCmdhelp": {
|
|
// "version": "0.1",
|
|
// "tool_surface_stable": true
|
|
// },
|
|
// "padToolSurface": {
|
|
// "version": "0.2",
|
|
// "tool_surface_stable": true
|
|
// }
|
|
// },
|
|
// ...
|
|
// },
|
|
// ...
|
|
// }
|
|
//
|
|
// Two independent contracts: padCmdhelp tracks CLI help-tree shape,
|
|
// padToolSurface tracks the MCP tool catalog. They version separately
|
|
// so a catalog reshape doesn't force a cmdhelp bump and vice versa.
|
|
func experimentalCapabilities() map[string]any {
|
|
return map[string]any{
|
|
experimentalCapabilityKey: map[string]any{
|
|
"version": CmdhelpVersion,
|
|
"tool_surface_stable": true,
|
|
},
|
|
experimentalToolSurfaceKey: map[string]any{
|
|
"version": ToolSurfaceVersion,
|
|
"tool_surface_stable": true,
|
|
},
|
|
}
|
|
}
|