Adding an MCP Tool
Audience: Pulse internals contributors adding a new tool to the
embedded MCP server. The MCP layer is split into the SDK-free core
(internal/mcp/), the go-sdk adapter (mcp/gosdk/, the only package importing
the MCP SDK), and the leaf metadata package (internal/mcp/toolmeta/).
Each MCP tool wraps one slice of pulse.Pulse and surfaces it over
stdio / Streamable HTTP transports. The catalog covers one tool per
facade method (pulse_inspect, pulse_predict, pulse_process,
pulse_compose, pulse_sample, pulse_facet, pulse_facet_schema,
pulse_manifest, pulse_errors_lookup, …) plus the skills / examples /
import / label tools, and the two resource schemes (pulse://*.pulse,
pulse-skill://*). Treat the manifest (pulse manifest) as the
source-of-truth count — never hardcode a number. Adding a new tool
means extending the core catalog, registering the metadata, optionally
binding a field-aware JSON Schema, and updating the MCP-integration
skill.
1. Implement the handler
Implement the new tool handler in the SDK-free core (internal/mcp/handlers.go).
The handler is a typed function over *pulse.Pulse: accept the typed
In struct, call the facade method, return the typed Out (coded
errors surface verbatim as {code, message, details}). Add the tool’s
ToolDescriptor to the core catalog (internal/mcp/tools.go) so Tools(cfg)
emits it; the go-sdk adapter (mcp/gosdk/) mounts whatever the catalog
returns via gosdk.Register.
2. Register tool metadata
Add the tool’s name + description in internal/mcp/toolmeta/meta.go.
The description opens with one CALL … sentence saying when to call the
tool, and that sentence is the first line of the tool skill’s
## When to use section, word for word
(TestToolDescriptionsLeadWithWhenToUse).
The internal/mcp/toolmeta package is imported by internal/descriptor/ (which assembles
the manifest) and by the core, so this is the leaf-metadata package that
lets the descriptor surface the tool without importing the MCP layer or
the SDK.
3. Field-name parameters (optional)
If the new tool has field-name parameters (e.g. a field: string
argument that takes a cohort field name), add a per-tool JSON Schema
builder in internal/mcp/bind.go + an entry in Bind. After pulse_inspect
succeeds against a cohort the adapter binds session-scoped variants of
every schema-aware tool whose JSON Schema constrains field-name
parameters to the inspected cohort’s actual fields. internal/mcp/bind.go is
pure (no MCP SDK); the per-session server mutation that consumes the
schemas lives in the adapter (mcp/gosdk/bind.go).
Schema-binding parity is enforced by:
TestMCPSchemaBinding_RemovesInvalidFieldsTestMCPSchemaBinding_AllFieldsInFiltererEnumTestMCPSchemaBinding_SampleAndFacetFieldEnumTestMCPSchemaBinding_DedupAndSortTestMCPSchemaBinding_NilSchemaTestBindForInstance_PayloadSchemaParity— every payload-schema key under a bound request root is in the bound schema (allowlisted omissions carry a reason), and no bound key is absent from the instance’s payload schema
The transport caveat: bind-on-inspect works on the single stdio session
(post-serve AddTool/RemoveTools auto-emits list_changed); there is
no per-session override for shared HTTP servers — a documented
limitation. See the MCP integration skill for the configuration recipe.
4. Update the session-bootstrap skill
Add a section to skills/session-bootstrap.md covering the new
tool’s purpose, request shape, response shape, and (if applicable)
the Schema-bound enums it exposes after pulse_inspect.
TestSkillsCoverAllMCPTools enforces presence by name.
5. Run the gates
go test ./internal/skills/ -run TestSkillsCoverAllMCPTools
go test ./internal/descriptor/ -run TestManifestMCPToolsComplete
go test ./internal/mcp/ -run TestMCPSchemaBinding
The Update Demand row for MCP tools covers all of these in one PR; see The Update Demand.