Gå til hovedinnhold

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.

HandlersWhere
context_search, context_read, the context:// resourceshost/mcp_context.go
context_note, context_session_summaryhost/mcp_grow.go (each wraps host calls in host/contextops.go)
up, up_planhost/mcp_up.go
check_text, check_filehost/mcp_check.go
read_blocks, apply_edits, describe_formathost/mcp_edit.go, over the change service App.ChangeService builds (host/changes.go)
statshost/mcp_stats.go
voice_check, voice_rewritehost/mcp_voice.go
Curated framework tools (translate, term-check, redact)host/mcp_tools.go
detect_format, the listing and flow verbskapi/mcptools/tools.go
review_queue, review_blockkapi/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.

ResultType
read_blockschange.Page
apply_editschange.Result, a kapi.change-result/v1 document
describe_formatchange.Description
detect_formatmcptools.DetectFormatOutput
run_flow, pseudo_translatemcptools.RunFlowOutput
list_formats, list_flows, list_toolsmcptools.ListFormatsOutput, ListFlowsOutput, ListToolsOutput
Review toolsmcptools.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_filea kapi.check/v2 Report; see the JSON contract
context_notehost.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_summaryhost.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
statsthe same document kapi stats --json emits
A curated framework toolhost.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.