MCP Tools Reference
Implementation detail for S-03: Agent surfaces, the decision that makes MCP one of the two ways an assistant reaches kapi.
The tool list is generated. Tool names, descriptions, input schemas, the surface each belongs to, and the resource addresses the server answers reads at are generated from a running server onto the MCP reference page, with a CI drift gate, so no hand-maintained copy can drift from the server. This note covers what a generated table cannot: where the handlers live and how a tool gets registered.
How a tool reaches the server
kapi mcp (cli/mcp.go) starts an mcp.Server over stdio and calls
host.ApplyMCPToolFactories. Every factory registered through
host.RegisterMCPToolFactory (host/mcpregistry.go) is invoked with the
server and the *host.App, and adds its tools. Registration happens in
init(), so linking a package is what exposes its tools, which is why the
kapi porcelain lives in an importable package rather than in package main.
App.MCPSurface is set from the --tools, --all-tools, --all-flows and
--all flags and never from the environment, because the surface is a property
of how the server was started. The factories read AllTools and AllFlows to
decide what to add. Sets is the tool-set selection: once every factory has
run, ApplyMCPToolFactories removes each tool and resource template whose set
is not selected. The sets are one table, mcpToolSets in host/mcp_sets.go,
and a tool on no list (a widened registry tool, a flow verb, a plugin's tool)
is served whatever sets are named. ParseMCPToolSets reads the flag, serving
writing when it is absent, and MCPInstructionsFor sends the server's
instructions only when the writing set is served, because they name its tools.
The surface snapshot test (kapi/cmd/kapi/mcp_snapshot_test.go) fails when a
tool on the default server belongs to no set.
App.ResolveMCPProject accepts -p / --project through the CLI's project
flag convention and otherwise runs the shared upward walk. KAPI_NO_PROJECT=1
disables implicit discovery; an explicit recipe still wins. The resolver retains
the exact recipe path and source language before serving tools. Invalid project
loading returns an error. check_file and a scoped check_text thread that path
into their synthetic host commands, so profile and terms resolution uses the
bound project even when implicit discovery is disabled. Custom recipe filenames
remain intact.
The context:// resources, context_read and context_search also use the server's bound
recipe. Resource locations resolve relative to that recipe's root, including
when the server runs from a subdirectory or outside the project. Explicit
standalone store inputs on context_search retain their override semantics.
check_text.context_path names a project-relative destination, including a file
that has not been written. It resolves the destination's voice channel and terms
without extracting a file. The path requires a bound project and cannot be
combined with profile_file or profile_pack. Unscoped snippets retain those
explicit profile options. Invalid paths and context-resolution errors fail the
operation instead of dropping the requested scope.
A scoped draft report keeps target.kind: "text" and records the destination in
target.context_path. Its findings and analyzer entries have no file location:
they describe the supplied snippet. check_file remains the post-save check for
document extraction, structure and block locations.
check_file with diff (unified diff text) or diff_against (a git revision)
runs the diff-scoped check kapi check --diff-file and --diff-against run,
through the same runDiffCheck in host/check_diff.go: file becomes
optional and narrows the scope, and the report carries scope. Voice resolves
per changed file unless the call names a profile. target and validate are
refused with a diff.
For project-scoped check_file, omit profile_file and profile_pack so the
file's voice channel resolves from the project. Either explicit option replaces
that voice selection; loading the project's profile YAML directly does not
select the file's channel. Project terms still resolve for the file. Record the
arguments and execution.contexts when assessing check coverage. Context entries
record the effective voice selection, loaded profile source, project profile
and channel, plus whether terms were supplied. A file-scoped voice resolution
returns the composed profile and its metadata together; the check uses that
same profile. Explicit overrides are marked override and carry no inferred
project channel. Terms resolve independently of a voice override. A tool name
alone does not establish which voice governed the check.
The canonical CLI/MCP check report includes optional execution.analyzers and
phase timings. An absent inventory means unreported coverage. Applicable
semantic guidance has an explicit unsupported entry in a deterministic run;
its presence cannot be inferred from the score. Requested analysis or context
that fails returns the existing operation-error transport. See the
checks documentation
for analyzer scope and timing boundaries.
| Handlers | Where |
|---|---|
context_search, context_read, the context:// resources | host/mcp_context.go |
context_note, context_session_summary | host/mcp_grow.go (each wraps host calls in host/contextops.go) |
up, up_plan | host/mcp_up.go |
check_text, check_file | host/mcp_check.go |
read_blocks, apply_edits, describe_format | host/mcp_edit.go, over the change service App.ChangeService builds (host/changes.go) |
stats | host/mcp_stats.go |
voice_check, voice_rewrite | host/mcp_voice.go |
Curated framework tools (translate, term-check, redact) | host/mcp_tools.go |
detect_format, the listing and flow verbs | kapi/mcptools/tools.go |
review_queue, review_block | kapi/mcptools/review.go, with the read in host/reviewblock.go |
Curation
host/mcp_tools.go projects registry tools into MCP tools by rendering their
ComponentSchema as the input schema, plus a required text and an optional
target_lang. Only the tools in agentFacingTools are offered by default.
Most of the unfiltered surface was never authored for an assistant: it arrived
because someone added a pipeline step. Anything with a porcelain equivalent is
deliberately absent too, since two names for one job means the caller picks
wrong half the time, and recycle and diff-leverage are what up does for
you.
neverAgentFacing is stricter still: external-command and script execute
caller-supplied commands and JavaScript, and are withheld even under
--all-tools. "Show me every tool" and "let a caller run arbitrary code" are
different decisions, and bundling them would make the first silently grant the
second. host/mcp_tools_curation_test.go asserts this.
Resources as well as tools
registerContextResources (host/mcp_context.go) registers the two
context:// addresses as resource templates, both with one handler: the URI
itself says which address form was asked for, so dispatch never depends on which
template the SDK matched; both templates match context://profile/x, which
would otherwise be a coin toss.
A client lists no resources from a template, so an agent working from its tool
list never finds these addresses. context_read is the same read as a tool: its
handler builds the context://<path> URI from path, format and project
and returns exactly the text the resource returns.
The URI is split by hand rather than through url.Parse. Under context:// the
first path segment would be read as an authority and lowercased, silently
renaming a location on a case-sensitive filesystem.
Surface widening does not apply here: --all-tools and --all-flows govern
which tools a caller may run, not what it may read. The tool sets do: the
two templates belong to the writing set, so a server that does not serve it
answers no context:// read.
Result shapes
Handlers return typed Go structs, which the SDK serializes into the result's
structuredContent and mirrors as JSON text content. The struct is the schema:
read it rather than a prose copy.
| Result | Type |
|---|---|
read_blocks | change.Page |
apply_edits | change.Result, a kapi.change-result/v1 document |
describe_format | change.Description |
detect_format | mcptools.DetectFormatOutput |
run_flow, pseudo_translate | mcptools.RunFlowOutput |
list_formats, list_flows, list_tools | mcptools.ListFormatsOutput, ListFlowsOutput, ListToolsOutput |
| Review tools | mcptools.ReviewQueueOutput (each row with the ref review_block takes), mcptools.ReviewBlockOutput (the ref and rev of the edition under review and, under block, the host.ReviewUnitInfo with its review context) |
check_text, check_file | a kapi.check/v2 Report; see the JSON contract |
context_note | host.contextRecordOutput: the operation id, its kind (observe, correct or withdraw) and status (suggested or contested, with contested_by naming the other side), the session, what was recorded, the command that reviews it, and what happens to it next |
context_session_summary | host.contextSessionOutput: the counts observed, corrected, suggested, contested, established, withdrawn, dropped and reset; the Stop hook's outcomes checked, failing, caught and files, read through host.SessionOutcomes and present at zero; the sentence to end a report on; and the kapi context review --session <id> command a person reviews the session's suggestions with. With no session, host.App.LatestContextSession picks the session with the project's latest check run (contextop.KindCheck), falling back to the session that recorded last in any operation, and chosen names it, the rule that applied and when it last recorded; a log with none answers with report alone |
stats | the same document kapi stats --json emits |
| A curated framework tool | host.frameworkToolOutput: target translations, rewritten source, properties, overlays, and annotations for the one processed block |
read_blocks, apply_edits and describe_format build their results
themselves rather than through the SDK's typed path: each result is encoded once
with HTML escaping off and set as both the structured content and the text
content, so the <x id="…"/> placeholders a block's text holds reach the client
as written. read_blocks and describe_format declare their output schemas
as envelopes (readBlocksOutputSchema, describeFormatOutputSchema), because
the run model nests and the tool descriptions name a page's fields;
apply_edits declares the generated result schema,
changeschema.ResultSchema().
apply_edits registers through Server.AddTool and reads the raw arguments.
Its input schema is changeschema.Schema() with project added; the handler
removes project and decodes the rest with change.Decode, so a change set
that does not decode is refused with the contract's invalid code and the JSON
pointer of what was wrong rather than with the SDK's validation message. The
actor is the calling agent: kind agent, the client's initialize name, and
MCPSessionID(). A refused or partial change set is an error result carrying
the kapi.change-result/v1 result. A refusal with no operation to attach it to
(a change set that does not decode, a read the service refuses) is an error
result carrying change.ErrorResult: the same result with status: "refused",
record: null, empty docs and ops, and error saying why. After a change set
lands, the handler collects the wording each applied set_content and
replace_text wrote into a document's own edition, under the canonical
reference the result names, and passes it to noteAgentEdits
(host/contextusage.go). A translation's wording is left out, because a
suggestion's preferred form is source-language wording.
Each call builds the change service for its project (mcpChangeService): the
recipe the project argument resolves, else the one the server started with,
else none, and then the documents are those under the server's working
directory. The service takes the call's source language from
mcpCallSourceLocale, which loads the recipe and ranks it against the language
named when the server started, so App.SourceLang, which every host function
that resolves a project rewrites, plays no part. Building the service reads the
recipe and writes nothing: a reference to a source file resolves through
ProjectContext.ResolvePaths alone, the content patterns are expanded only for
a reference that names no source file (the file of a translation), and the
project's state directory is created at the first commit. review_block reads
the block through the same service, in the project's own source language, to
report the canonical reference and the revision of the edition under review,
then answers with ReviewUnitWithContext, addressing the block by the file the
recipe writes that edition to.
An advise lands through RecordAIReviews, under agent/<client>. It needs a
score, and one the review queue has no row for is refused as not_found once
the content beside it was written, which makes the change set partial.
The tool sets are mcpToolSets in host/mcp_sets.go. pruneMCPToolSets
removes each tool that no selected set lists, so apply_edits, which the
writing and review sets both list, is served when either is.
The surface is a contract
kapi/cmd/kapi/mcp_snapshot_test.go snapshots every tool name and input schema
to testdata/mcp_tools.golden.json, and every resource address and mime type to
testdata/mcp_resources.golden.json. Descriptions may evolve freely; names,
input schemas and addresses may only be extended. Renaming a tool, removing one,
changing a field's type, or moving an address breaks agent integrations already
in the field and needs an explicit decision: regenerate with
KAPI_UPDATE_GOLDEN=1 and record it in
the CLI contract. A second test,
TestMCPToolSurfaceExtendsFrozen, holds the tools to
testdata/mcp_tools.frozen.json, frozen with kapi.change/v1 for a release and
never regenerated: a tool may be added and an input schema extended, and a
removed or renamed tool or field, a changed type or a newly required field
fails.