Skip to main content

C-06: Context retrieval

Summary

A project's content context is retrieved through two primitives, offered identically on the CLI and over MCP:

ShapeQuestionCLIMCP
By locationwhat applies here?kapi context <path>context://<path> resource
By contentwhat do we know about this?kapi context search <query>context_search tool

Every asset-shaped lookup folds into one of the two. There is no retrieval command per store (none for voice, none for terms, none for content memory), because which store holds the answer is an implementation detail the caller should never have to know.

The two surfaces are held in parity by construction: the MCP tools and resources are thin wrappers over the same host functions the CLI verbs call, and conformance tests assert they agree.

Context

A caller does not have a store in mind. It has a question. The assistant asking what applies here, the check asking does this rule fire, and the engine asking may this approved wording be reused are one question asked by three callers.

Asset-shaped retrieval forces the caller to know the answer's location before asking, which inverts that. Worse, it returns partial answers that read as whole ones: a command that renders a voice profile's own preferred and forbidden terms visibly contains terminology, while the project's terms store, a separate source reaching the engine by a separate path, goes unread. A caller writing against that output has terminology it did not get. Silence would have been safer.

The same pressure applies across surfaces. The agent skill drives the CLI, so a surface an assistant learns from the skill and a surface it reaches over MCP must be the same surface, or the highest-leverage prose in the repository teaches a model that does not exist.

Decision

One question, two shapes

Retrieval is addressed by location or by content, never by store.

By location answers what applies here: the point the location resolves to, the profile in force, its rendered guidance, the terms bound at that point, and the governance windows around them. It resolves through KapiProject.ResolveGovernanceFor (C-02), the seam a run, a check and a push resolve through, so the voice a writer reads for a file is the voice a run applies to it, including a content item's own channel: and a profile whose window has closed.

By content answers what do we know about this: one query, every store the scope holds (terms and concepts, content memory, profile examples). Each term it reports carries how often the extracted content uses it, read from the context graph's uses_term edges (C-03) rather than computed when asked, so the number is the one the platform's concept page shows and every surface labels it as of the last extraction.

Two address forms, one thing

The by-location primitive is addressable two ways, because an ad-hoc caller has no path to resolve:

context://{+path}{?format} what applies at this location
context://profile/{name}{?format} what a named profile holds

The CLI mirrors this as kapi context <path> and kapi context --profile <name>. The MCP server publishes the two as the resource templates context-at-location and context-for-profile.

profile/ is reserved under the scheme, which is what lets one address space carry both forms. A name no recipe declares falls back to a voice profile of that name (the local store, then a built-in pack), because otherwise the by-name address would be unusable outside a project, which is the case it exists for.

Asking is a read, not a call. The by-content primitive takes a query and is a tool; the by-location primitive names something that already exists and is a resource. That is not cosmetic: a resource is addressed, and an address is what makes the rendering a property rather than a second tool.

Rendering is a property, not a command

The by-location primitive renders as prose for a model (text/markdown) or structured for a program (application/json). MCP resources carry a mime type, so this is a property of the resource rather than a second entry point; the CLI expresses it as --json. On the wire the rendering rides on the address as ?format=json, and the mime type on the response states which was served. A format nobody recognises is an error: a caller that asked for a shape it can parse must not be handed prose it cannot.

That is what dissolves a voice_guide-shaped tool. Such a tool conflates three concerns (voice only, by name only, markdown only) into one narrow point. Once content is the whole context, addressing covers by-name, and rendering is a property, nothing is left over.

What folds in, and what does not

The fold is on the MCP surface. An agent gets the two primitives and no per-store retrieval tool: no voice-guide tool, no term-lookup tool, no memory-search tool. host/mcp_tools_curation_test.go guards the surface so a registry tool cannot re-shadow the primitives.

The CLI keeps its per-store verbs. kapi voice guide and kapi voice show render a resolved profile as a guide; kapi terms lookup / terms search and kapi memory lookup / memory search query one store directly. They answer a narrower question than kapi context (what does this store hold rather than what applies here), which is a reasonable thing to ask of a store you opened on purpose. What changes is what an agent is taught: the skill drives kapi context search, so the narrow verbs stay available to a person without becoming the model an assistant learns.

Management verbs are not retrieval and do not fold. terms import/export/stats, memory import/export/audit, voice new/validate/import/pack operate on a store, which is a different act from asking a question. The same holds for the voice tools an agent does get: voice_check and voice_rewrite judge or change a piece of text, so neither is a retrieval question.

Content memory is recycled, not searched

Recycling is the loop's job and is invisible by design: kapi up pre-fills from content memory as the structural cost control. So the retrieval surface does not offer search memory for prior translations. That duplicates work already done, and inviting a caller to do it by hand is inviting it to hand-crank the loop.

What is served is precedent: how has this project said this before, when authoring source content. That is a different question from recycling, it is not answered anywhere else, and it is what makes a caller's own writing sound like the project. It reaches callers through context search, not through a memory-specific verb.

Scope, not capability

The same call returns less at a narrower scope, and says so. Three scopes are named (host.ContextScope), and every answer carries one in its scope field:

ScopeWhat stands behind the answer
projectthe local project's stores
workspacea connected concept graph, with relations, revisions and market scoping
profileone voice profile and nothing else: the by-name answer, with no project behind it

A result set is explicitly scoped in its response rather than silently thinner: a caller must be able to tell this project holds no answer from this scope cannot hold one. Reporting the third case as an empty project scope would tell a caller the project holds no terminology when no project was consulted at all, so the by-name answer says in as many words that no recipe point stands behind it.

Half an answer plus a statement of what was unreachable is more useful than an answer that quietly omits a store it could not open.

Results are grouped, never merged into one ranking

A term match and a memory match are not comparable scores. Results are grouped by kind, each group ranked within itself. A single blended list would impose an order that means nothing.

Every answer reports its own freshness

The first of an answer's notes says whether the governing context, the terminology or the committed decisions moved since this process last read them. The comparison is against the freshness ref this project last observed, held on disk (C-05), so a retrieval costs no round trip, and the baseline is per process and advances on every read.

It reports and never resolves. What a moved context means for work already written is a judgement, and the retrieval surface is not in a position to make it. kapi status carries the same fact for a person on its governance axis; kapi check --ship is the enforcing half, where a target produced under a superseded context fails the staleness gate.

A governance window that closed is reported the same way: the by-location answer carries the transition (which profile stopped governing, when, and what governs in its place) as a note.

The generated surface is opt-in

MCP exposes a curated set by default: the two retrieval primitives, the check tools (check_text, check_file), stats, the convergence verbs (up, up_plan), the write verb (apply_edits), the two offline voice tools, and three registry tools that have no porcelain equivalent (translate, term-check, redact). kapi mcp --all-tools restores the full generated surface for debugging and power use.

The tools that execute arbitrary commands and scripts are not part of that flag. Show me every tool and let a caller run anything are different classes of decision, and bundling them means enabling the first silently grants the second (E-06). They are not exposed over MCP at all; kapi exec still runs them.

Cutting MCP exposure removes nothing from the CLI: kapi exec <tool> runs every registry tool regardless.

Consequences

  • The skill and the MCP client learn the same model, which is what stops the repository's highest-leverage prose from teaching a surface that does not exist.
  • Callers stop needing a map of the stores. The question is the interface; where the answer lives is ours to change.
  • A stale answer is visible to the caller holding it, rather than being a read with no memory.
  • Partial answers stop reading as whole ones, the failure that makes a store-shaped retrieval tool actively misleading rather than merely narrow.
  • A new registry tool does not become an agent tool by accident. Exposure is a decision with a name attached.
  • The by-location primitive resolves to the file, not to the passage. A content item's own channel: is the finest declared point, so one file in a collection can answer differently from its neighbours. A point beneath the file is not yet built (C-02).
  • Parity is a test, not a convention. The CLI verb is pinned to the answer's own rendering, the MCP resource body is pinned to the same bytes and its JSON to the same document, and a snapshot test locks both the tool names and the addresses as a contract.
  • An address is a contract in the way a tool name is. A caller writes context:// into its own prompts and configuration, so the URIs may be added to and never renamed or dropped without an explicit decision.

See also