Skip to main content

S-03: Agent surfaces: MCP and skills

Summary

An AI assistant reaches kapi through two surfaces over one implementation. The Agent Skill, a SKILL.md router plus progressive-disclosure reference files sourced at cli/skills/data/kapi/, teaches an assistant that runs shell commands when to reach for kapi and which verb to run. The MCP server (kapi mcp) serves clients that call tools rather than shell out, exposing a deliberately curated set plus the context:// resource space. Both converge on the same asymmetry: the assistant writes the content; kapi supplies the context, applies edits through format-aware writers, and runs configured checks. Content and asset edits land through one write verb, kapi apply, as a typed JSONL change-set, which is also the MCP apply_edits tool. A review decision has its own verbs on both surfaces, sharing the host's decision path.

Context

The assistant already writes the prose and the code. What it lacks is the project's own context (what a thing is called here, what wording is approved, what tone applies at this location) and the ability to write an edit back into a .docx or an XLIFF without corrupting it. Those are exactly what kapi holds.

In the attended workflow, the assistant authors the text and uses scoped findings to repair it. Format readers and writers determine the supported round-trip behavior; the resulting diff remains reviewable. Provider tools also support unattended work. A completed check gate covers its configured analyzers. Applicable guidance that requires semantic judgment is explicitly unassessed when no analyzer implements it.

The connective tissue has to be cheap. An assistant's context is finite, and a document describing every kapi command would crowd out the task. Hence a router that stays small and reference files that load only when the task matches.

Decision

A skill is a directory, and its source lives beside the CLI

cli/skills/data/kapi/
├── SKILL.md the router: frontmatter (name, description) + a short
│ body that decides scope and points at references
└── references/ progressive-disclosure how-to, loaded on demand
├── edit.md read → edit → write → verify
├── create.md author → parse → check → revise
├── voice.md retrieve guidance, score a draft, fix it
├── translate.md translation and terminology
├── project.md the project model
├── context-discovery.md first-visit discovery, and the refresh path
├── toolbox.md
├── i18n.md routes by detected stack into…
└── i18n/ …per-ecosystem playbooks + a machine-readable registry

The router's job is triage: judge whether the request is ad hoc or ongoing, then point at one reference. The references carry the task detail, one per concern. Terminology folds into the voice and translate references rather than standing alone, because a term is something you apply while writing or translating, not a task you set out to do.

The context-discovery reference covers two visits. On the first, the assistant assembles a project's context from the user's material. On a later one it diffs new material against what the project already holds and proposes a refresh: a change-set the user approves (kapi apply refresh.jsonl), so terms and voice rules land atomically and nothing is rewritten behind the user's back.

The i18n concern is itself a tree. references/i18n.md detects the stack and routes into references/i18n/, driven by a machine-readable framework registry (frameworks.yaml) carrying detection signals, catalog layouts, kapi presets, and a maintenance-cost grade per framework.

The source lives in cli/skills/data because the skill names specific commands and flags. A verb change and its skill update are then one reviewed change, and the reviewer sees both.

Skill content is agent-actionable only: when to trigger, which command, what footgun to avoid. Architecture and implementation belong in these documents, not in a file an assistant loads into a live context window.

The skill is a copy, never a second tree

The binary neither carries nor installs the skill: there is no kapi skills command. Four make targets copy the one source tree, so the copies cannot diverge:

TargetWhat it produces
make plugin-bundlethe Claude Code plugin bundle under packages/kapi-claude-plugin
make publish-pluginmirrors that bundle to the neokapi-plugins marketplace repo
make publish-skillmirrors the portable skill into the agent-skills collection, for any SKILL.md-aware tool
make dev-skillscopies it into this repo's own .claude/skills for dogfooding

The marketplace and collection repos are generated distribution artifacts, like a package-manager tap: never hand-edited. Publication is on kapi release, not on merge. A plugin cannot pin a CLI version, so a skill that named an unreleased command would break an up-to-date plugin against a released binary.

The skill's description is the sole triggering lever, and it is loaded at startup by every SKILL.md-aware tool. Whether it fires on the right tasks is measured rather than remembered: scripts/skilleval drives a real assistant session (claude -p) per scenario in a throwaway workspace and records what the agent did. make skill-eval scores triggering over positive and negative prompts, make skill-eval-completion drives each positive scenario to a green gate, and make mcp-eval measures whether an agent picks the right MCP tool. The results and the whole transcripts are published on the skill eval page (A-01); none of the three runs in CI, because they spend and need local credentials.

The paired study runs identical tasks through each agent host with ordinary file tools, the CLI skill, or MCP. The skill condition excludes the kapi MCP server; the MCP condition excludes the skill and direct kapi CLI execution. Independent artifact checks assess completion, while natural prompts measure whether the agent discovers the available integration. Results retain failed attempts and distinguish these outcomes from human judgments of the content. The paired evaluation runner records the model and integration configuration for each attempt.

Two hooks, and a protocol for failing open

The Claude Code plugin ships two project-scoped hooks that drive kapi rather than being kapi:

  • a Stop hook running kapi hook stop, which runs the project's ship gates and keeps the assistant working until they are green;
  • a PreToolUse hook running kapi hook pre-edit, which blocks direct hand-edits of files the project generates as translation targets.

Both fail open. A guard that cannot evaluate must never block work that has nothing wrong with it: failing closed on an unparseable payload would stop an unrelated edit, or trap an assistant that has finished.

What fail-open needs is a way to say so, because the zero value of a hook payload is indistinguishable from a legitimate one. An empty file path reads as "nothing to guard"; an empty working directory lets the project walk run from wherever the hook process started. Both allow, and neither leaves a trace, so without the protocol below a guard that never ran looks exactly like a guard that passed.

SituationstdoutExit
Guard evaluated, verdict negativethe decision shape, with a reason0
Guard evaluated, nothing to reportnothing0
Guard could not run{"systemMessage":"…"}, and the same warning on stderr, naming the hook0
Guard evaluated, gates did not run{"systemMessage":"…"} naming the did_not_run_cause, and the same warning on stderr0

Three consequences follow.

The verdict travels in the JSON, never in the exit code. A non-zero exit reads as a broken hook, so carrying a denial there converts an enforced decision into a hook error. A denial is exit 0 with the verdict in the payload. In particular it never uses ExitGate (3): that code belongs to a human or a CI step running the gate directly (S-01). The hook drives the gate; it is not the gate.

A payload carrying only systemMessage carries no decision, so the normal permission flow is untouched and fail-open is preserved exactly.

The warning goes to stderr as well, naming the hook, because a hand-run or CI invocation has no session to surface a systemMessage into.

The discriminating case: no payload at all is not a failure. When stdin is a character device, the command was run by hand with nothing piped in; there is nothing to read and nothing to report. Likewise "there is no kapi project here" is nothing to gate, not a guard that failed. Warning on either would make every session outside a project noisy, which is how a guard gets uninstalled.

These are the assistant-integration hooks. They are unrelated to a recipe's hooks: block, a separate lifecycle mechanism.

The two loops the skill drives

Readkapi inspectEditthe assistant writesWritekapi applyCheckkapi check <path>

The edit loop: the assistant supplies the words; kapi reads, applies and checks the content. Findings guide revisions, while unsupported guidance remains for review.

Editing existing content. kapi inspect is the read leg: it parses any editable format into one record per block, carrying the text with inline codes rendered as <x id="…"/> placeholders so an edit can round-trip, the block's structural role and nesting level, a stable id, and a content_hash, the canonical block identity (F-03). The assistant rewrites the text and returns a typed change-set; kapi apply writes it.

Creating new content. With no frozen source, the assistant authors in a generative format, one whose writer can produce a document from the content model alone, and uses kapi inspect or kapi stats to parse it back as the first check, then kapi check as the voice-and-terminology gate, revising until green. Binary office formats are editable but not generative: authored elsewhere, edited in place.

Both loops are provider-free by default. The assistant is the writer; kapi is the format engine and the checker.

The ordinary authoring loop uses kapi context <path> before editing and kapi check <path> afterwards. The file path determines the applicable voice channel and terms. The assistant reads analyzer coverage alongside findings; a passing score establishes only the checks that ran. In a repository the loop checks the change rather than the project: kapi check --diff-against HEAD checks each content block the edit touched, whole, and reports the lines it spans, so the check costs one read per changed file and runs after every edit. kapi check --ship enforces project release policy when that is part of the task.

For MCP, context retrieval uses the existing context://<path> resource. check_file checks saved content in its project scope. A draft can be checked with check_text and context_path, the intended project-relative destination. This binds the same voice and terms without reading the destination file. Explicit profile overrides belong to unscoped snippet checks and cannot be combined with context_path. A bound-context failure is an operation error.

kapi apply, the write verb for content and assets

Every deliberate, reviewed change to content or to an asset is one typed JSONL entry discriminated by kind, and every one lands through kapi apply:

kindWhat it editsHow it lands
contenta block's text in a named filebyte-faithful round-trip, drift- and inline-code guarded
terma termthe committed terms source → import → the terms tables of the project store
memorya content-memory pairthe committed memory source → import → the memory tables of the project store
voicea voice vocabulary rulethe committed voice profile → voice-store import (C-07)
reviewa unit's review outcomestaged in the working set, published by kapi commit (C-04)
recipean allowlisted recipe fieldthe kapi.yaml recipe, via project load and save

Two properties make this one verb rather than six.

An asset edit writes the committed source, then compiles the projection. The edit lands in the git-tracked artifact the recipe binds (the terms or memory bundle, the voice profile, the recipe) and the existing importer refreshes the gitignored database from it. The backing store therefore has exactly one writer, git diff is the uniform review surface for every kind, and the operation is idempotent, so re-running a partly-applied change-set is safe.

A content edit carries its own guards. Each content entry pins a content_hash; if the block drifted since it was inspected, the edit is stale and skipped. An edit that drops, invents, or unbalances an inline code is rejected by the fidelity guard rather than written as broken markup. Either outcome exits non-zero so the fix loop re-inspects and retries.

A mixed change-set (a content fix plus the term or voice rule that justifies it) lands atomically, so the draft and the rule that governs future drafts move together.

A review decision is the one write that also has verbs of its own. On MCP, approve_unit, reject_unit and sign_off_unit record a unit's outcome through the same host decision path the CLI uses, with the agent's identity attached; apply_edits with a review entry reaches the same record. Both land in the working set and are published by kapi commit.

Format editability is declarative

A skill needs to know, before it edits, whether a format can be written back. kapi formats carries an Edit column, and its JSON adds editable, round_trip, and generative. A format is editable when it has a reader and a writer and is not a bilingual interchange format, including binary office formats, because the faithful round-trip is precisely what makes editing a binary container safe. Round-trip means the writer reconstructs from a skeleton, so an edit changes only the edited text. Generative gates authoring from scratch. All three resolve declaratively, without loading a plugin (E-02).

The MCP server is curated

An MCP session binds to an explicit recipe (kapi mcp -p project.kapi.yaml) or an implicitly discovered project. An explicit path wins when discovery is disabled. Invalid bound context fails startup or the affected check operation; it cannot silently become an ungoverned successful result. File checks retain the recipe path the call resolved and resolve the file's effective profile at that path. CLI and MCP reports share finding, gate and execution semantics. Their transport and timing boundaries remain distinct.

The project is an argument of the call

An assistant works in more than one project, and an MCP server outlives any one of them. So every project-scoped tool takes an optional project, and the context:// resource takes a ?project= parameter. The value names the project's kapi.yaml, its root directory, or any path inside it: a call can pass the file it is editing.

Resolution runs per call, through the seam the CLI uses (host.ResolveProjectPathcore/project.ResolveRecipePathcore/project.ResolveLayout), so a call and a kapi -p … invocation reach the same recipe. KAPI_NO_PROJECT is honoured exactly as it is on the CLI: it disables discovery, and an explicit path still resolves. A path that holds no project is refused with a typed error naming the path, rather than falling back to whichever project the server's working directory sits in.

The project the server resolved at start remains the default, so a client configured for one project keeps the behaviour it had.

Three properties follow from resolving per call rather than per process.

Nothing about a call lands on the server. The resolved recipe travels on the command the handler builds, which is what every embedded surface already does, so two calls for two projects run at once without interfering.

Each project gets its own store handle. App.ProjectDB memoizes one handle per project root, and Shutdown closes all of them, so a second project opens a second connection pool and neither outlives the server.

The tool list stays the start project's. A client reads tools/list once, so which tools exist is fixed when the server starts. A per-call project decides what governs the content, and for a registry tool it decides the target-language default; it does not add or remove tools.

kapi mcp starts a stdio JSON-RPC server. Its surface is a decision with a name attached, never a consequence of a tool being CLI-visible. Exposing every registry tool produced an agent surface nobody chose, most of it pipeline steps (whitespace-correct, encoding-detect, xml-validation) that no caller should be assembling by hand, plus verbs like recycling that the catch-up loop does automatically and invisibly.

The default surface is therefore the hand-authored porcelain (reading and sizing content, checking text or a file, voice scoring and offline rewriting, context search, the catch-up verbs and their dry run, the review-queue verbs, and apply_edits) plus a short curated list of registry tools that produce something a caller cannot produce itself or check something with no porcelain equivalent: translate, term-check and redact. The listing and format-detection helpers, extract_content, pseudo_translate and the flow-running verbs sit behind --all-tools and --all-flows; --all is the shorthand for both. The full generated list is in the MCP reference.

Two curation rules are asserted by tests rather than remembered:

  • Nothing that executes caller-supplied code is ever agent-facing, not even under --all-tools. "Show me every tool" and "let a caller run arbitrary commands and JavaScript" are different classes of decision, and bundling them would mean enabling the first silently grants the second. Neither is removed from the CLI: kapi exec still runs both.
  • No curated tool shadows a porcelain one. Two names for one job means the caller picks wrong half the time.

In project mode the set narrows further to tools whose source the recipe declares, and the project's first target language becomes the default.

Asking a location is a resource

Context retrieval is split by the shape of the question (C-06). Asking what a word means is a call with arguments, so it is a tool (context_search). Asking what applies at a location is reading something that already exists at an address, so it is a resource, served in the context:// space:

AddressAnswers
context://{+path}{?format,project}what applies at a project-relative location: the voice profile in force with its guidance, the terms bound there, and the governance windows around them
context://profile/{name}{?format,project}the same, addressed by governance profile name, for a caller with no file in hand

Both render markdown by default; ?format=json returns the structured shape. Making the rendering a property of the read, a MIME type, is what avoids a second entry point for the same question. One reserved path prefix carries the by-name form, so a single scheme carries both address forms. ?project= names the project the read acts on, the way the tools take a project argument.

Both MCP primitives are thin wrappers over the same host functions the kapi context verbs call. The skill drives the CLI, so a capability that existed on only one surface would teach an assistant a kapi the other half does not have.

Parity is a suite, not a claim

Sharing an implementation makes the two surfaces agree in principle. What establishes it in practice is a conformance suite that drives the real kapi mcp over stdio the way a client does (initialize, tools/list, tools/call, resources/read) against scratch projects, and compares each answer with what kapi check, kapi exec or kapi context reports for the same input.

Every check-type tool carries a fixture that must pass and a fixture that must fail. The must-fail half is the point: the harness that builds a block from a snippet built it source-only, so every bilingual check reached over MCP returned a clean result whatever the translation said, and the CLI path was correct the whole time. A suite that only asserts a clean result on clean content cannot see that.

The suite lives in kapi/e2e and runs pre-merge in the Kapi CLI E2E job on every pull request touching cli/, host/ or kapi/.

The skill drives the CLI

The bundled skill issues shell commands. The CLI is the richer surface (the model-backed checks, the credential store, project resolution, the full toolbox), and an assistant that can run shell already has it. MCP exists for clients that cannot, and the shared host implementation is what keeps the two honest.

Because a skill issues commands, it consumes the exit-code contract (S-01): a distinct gate code lets a loop branch on "the draft scored below the bar, rewrite it" versus "the command failed" without parsing output.

Consequences

  • One source tree feeds the plugin bundle, the portable skill, and the in-repo dogfood by copy, and it sits beside the CLI it documents, so a command change and its skill update are one reviewed change.
  • The binary neither carries nor installs the skill; distribution and updates are the assistant's plugin manager's job.
  • Progressive disclosure keeps the router cheap and loads detail only on a match.
  • The attended loops call no provider: the assistant writes, kapi round-trips, drift-checks, and gates.
  • One write verb covers content and asset edits, a mixed change-set lands atomically, and git diff is the uniform review surface for all of it.
  • A curated MCP surface means the agent-facing tool list is a reviewed decision; the code-execution exclusion is a test, so widening the surface can never silently grant shell access.
  • One long-lived server serves an assistant that moves between projects, because the project is an argument of the call rather than a property of the process.
  • A check tool that stops reporting a violation over MCP fails a pre-merge job, rather than surviving until someone notices a clean result on broken content.
  • Splitting retrieval into a tool and a resource lets rendering be a property of the read rather than a second address, and keeps both forms of the question in one address space.
  • Whether the skill fires, and whether an agent picks the right tool, is a measured number with a transcript behind it rather than a checklist someone remembers to run.