sources/base.py plus tools/source_tools.py is the same problem phase 5 option (A) contemplates, already solved in ADK-specific form: a data system exposed as model-callable tools. Recorded under Q2 because that question calls the tool surface the real design work, and this is a worked example of it. Three signals, each with the reason it is a signal: enumerate and read are abstract while sample_rows defaults to None, so sampling is advisory; the surface is per-concept, which is evidence that Door B's per-run collision gate and index generation are the side that has to give; and two things not to copy — an untyped dict[str, Any] return that only a model can interpret, and a find() that linear-scans list_concepts() and so assumes total enumeration is cheap, against this library's rule that nothing enumerates unless a profile says derived. Read at the pinned commit, not vendored.
107 lines
5.8 KiB
Markdown
107 lines
5.8 KiB
Markdown
# 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.
|