# Phase 5 — MCP as a way to populate a bundle **Status: scoped, not designed.** Committed by the operator 2026-07-27. This document records the commitment, the fork it turns on, and the constraints any design has to satisfy. It deliberately does not choose an implementation: the fork below decides who owns the work, and choosing it wrongly is expensive in a way writing it later is not. ## The fork — settle this first "Support MCP to populate a bundle" names two different pieces of work. They share a protocol and nothing else. **(A) We are the MCP server.** An agent connects to us and calls tools — `add_concept`, `list_bundle`, `generate_index` — and the bundle fills up through our doors. The natural home is **Door B**, which already answers "here is content, make it OKF": MCP would be a protocol surface over an existing door rather than a new ingestion path. **(B) We are an MCP client.** A manifest names an MCP server as a source, the same way it names `file`/`sql`/`http` today, and Door A pulls concepts from it. **Default reading, for confirmation rather than assumption:** the phrase "to populate a bundle" points at (A). Recorded as a reading, not a decision — the operator has not been asked which one, and the two have different owners. ## Why the fork decides the owner (B) adds a **source type**, and the source type set is normative in commons' `ingest-spec.md`. This repo implements that spec and does not edit it. So (B) cannot start here: it starts as a request to commons, and it lands here only after they have specified it. (A) adds no source type and no spec surface — the concepts arrive through Door B exactly as dropped files do — so (A) is ours to design. That asymmetry is the whole reason this document exists before any code. ## Constraints any design inherits None of these are negotiable by this feature, and each one has already refused something: 1. **Exactly one runtime dependency, ever** (`llm-ingestion-guard`), enforced by a packaging test. An MCP SDK would be the second. Either the protocol is hand-rolled over stdlib JSON-RPC on stdio, or it lives behind an optional extra the way binary parsers do in `[extract]` — never in the core install. 2. **No model calls anywhere in the run path.** MCP is model-facing, and that is fine in exactly one direction: under (A) the model is the *caller* and we stay deterministic tools. A design where this library calls a model to decide what to write is out of scope, and would be a different product. 3. **The persist gate still applies.** Content arriving over MCP is untrusted by construction. It goes through Door B's injected guard gate like every other dropped file; MCP is a transport, never a reason to bypass a gate. Security stays the guard's, as always. 4. **Network access is an explicit per-run opt-in.** A server listening on a socket, or a client dialing one, is network. The existing fail-fast refusal is the baseline, not something to reinvent. 5. **Determinism.** `ingested_at` is a required argument with no wall-clock default. A protocol handler does not get to invent one because a call arrived "now". ## Open questions - **Q1 — (A) or (B), or both in order?** Blocks everything else. - **Q2 — If (A): what is the tool surface?** Door B's public surface is `process_inbox` over a directory. An MCP tool taking one concept at a time is a different granularity, and the collision gate and index generation are currently per-run rather than per-concept. This is the real design work. **Prior art, measured 2026-07-27 at the pinned upstream commit.** Upstream has already built this surface in ADK-specific form, and it is worth reading before we draw our own. `sources/base.py` (38 lines, stdlib only) defines a `Source` ABC; `tools/source_tools.py` wraps its methods as model-callable tools, which `agent.py` registers. Three things in it are design signals rather than incidental: - **`list_concepts` and `read_concept` are abstract; `sample_rows` is optional with a `None` default; `find` ships a default implementation.** So the surface's obligations are *enumerate* and *read*, and sampling is advisory. A tool surface that made sampling mandatory would be claiming more than the problem needs. - **It is per-CONCEPT** (`read_concept(ref)`), which is the granularity this question is about. That an independent implementation landed on per-concept is evidence that the per-run shape — Door B's collision gate and index generation — is the side that has to give, not the tool surface. - **What not to copy.** `read_concept` returns `dict[str, Any]`, untyped because a model interprets it; we render bytes and cannot. And `find` is a linear scan over `list_concepts()`, which assumes total enumeration is cheap — colliding directly with this library's rule that nothing enumerates unless a profile says derived. Read, not vendored. The point is the shape of the question they answered. - **Q3 — Transport.** stdio is the vendorable, zero-dependency answer and matches how the marketplace consumes things. HTTP would be a second network surface with its own gate question. - **Q4 — Which half?** Phase 4 puts a Node/ESM half next to the Python one. MCP servers in the plugin world are overwhelmingly Node. If (A) lands in Python only, the second-brain consumers cannot use it; if it lands in both, the "share contract and fixtures, not code" rule applies to the tool surface too. - **Q5 — Does an MCP-populated bundle differ observably** from one Door B wrote from files? It must not, or the transport has leaked into the artifact. ## Non-goals - Reimplementing any security check the guard owns. - Calling a model from this library, under any transport. - Editing commons' source-type set locally to make (B) possible sooner.