Skip to content

Build nodes from content and meta only - #94

Merged
jlrickert merged 5 commits into
mainfrom
refactor/mcp-write-tool-surface
Sep 5, 2026
Merged

Build nodes from content and meta only#94
jlrickert merged 5 commits into
mainfrom
refactor/mcp-write-tool-surface

Conversation

@jlrickert

Copy link
Copy Markdown
Owner

A node is built from exactly two inputs: content, the markdown whose explicit H1 is its only title source, and meta, the complete metadata document. Previously the same information was spread across Title, Lead, Body, Tags, and Attrs, which gave a node's title two sources and its metadata three, with no rule saying which won.

Five commits, each independently buildable and reviewable in order.

refactor(cli,mcp): remove cross-keg import and archive tooling

Drops the import_from_keg and export MCP tools, the tap import command, and Tap.ImportFromKeg. export was already unreachable — registerArchiveTools was defined but never called from NewServer, so its tool could never appear in a tools/list response.

Archive support itself is untouched: tap archive and the pkg/keg machinery behind it stay. What goes is the node-level cross-keg copy, whose agent-facing story overlapped create and whose CLI name collided with archive import.

feat(mcp): return structured error guidance from every failed tool call

Every tool failure now carries a recovery contract — a code, an action naming the next step, and an operationPerformed flag. Only auth and precondition-required had structured content before, so every other failure left an agent guessing whether to retry and whether the write landed.

operationPerformed is three-valued on purpose: false where the request was provably refused before any write, null where the outcome is genuinely unknown. Reporting false for an unclassified mid-write failure would be a guess, and the point of the field is that it can be trusted without reading the node back.

Orientation denials get clearer wording too. A denial on a call that named no flight is now described in terms of what the caller actually did — it named a keg, which selects a target and never grants authority — instead of blaming a "selected flight" the caller never selected.

feat(keg): build nodes from content and meta only

The core change. keg.NodeCreate and keg.CreateOptions take only Body and Meta. keg.RejectFrontmatter becomes the single definition of the rule that content must not open with a --- block, so the CLI, MCP, the remote keg, and Tapper Hub all enforce it identically.

cat gains a structured read whose fields are exactly what edit accepts, so a read-modify-write cycle round-trips without parsing output meant for humans — which is why it returns the two documents separately rather than a composed ---meta---body blob that edit would reject.

The standalone meta tool retires along with Tap.MetaBatch: reading metadata is cat with meta_only, writing it is edit's meta field. Keg.Next leaves the interface — ids are returned by the create that allocates them, so a reserved-but-unwritten id is no longer a hole a caller has to remember to fill.

refactor(mcp): name edit's item list nodes

edit took its items under edits while create, remove, and node_snapshot used nodes. All four batch mutation tools now agree.

docs: record the content/meta node model and flight usage

Updates the integration content and re-renders the Claude and Codex skills. Two tool-inventory corrections came out of smoke-test runs where agents could not reach a KEG through subflight-scoped calls; both were usage errors the docs invited, and one had been logged as a server defect when the access control was working as specified.

Compatibility

Nothing here is marked as a breaking change — no surface in this project is stable yet — but callers are affected: the create item shape changes, edit's parameter is renamed, and the meta, import_from_keg, and export tools are gone. Tapper Hub consumes this API and has a matching branch that is not yet open.

Verification

go build ./... and go test ./... pass at each of the five commits, not just at the tip.

Removes the live-keg copy path and the archive export tool from the agent
surface: the `import_from_keg` and `export` MCP tools, the `tap import`
command, and `Tap.ImportFromKeg` with its supporting package.

`export` was already unreachable. registerArchiveTools was defined but never
called from NewServer, so the tool it registered had no way to appear in a
tools/list response -- deleting the file removes code that only looked like
a feature.

Archive support itself is untouched. `tap archive` and the pkg/keg
export/import machinery it drives stay exactly as they are; what goes is the
node-level cross-keg copy, whose agent-facing story overlapped `create` and
whose CLI name collided confusingly with `archive import`.

The parity coverage table loses the ImportFromKeg row and the note
explaining that collision, since neither has anything left to describe.
Every tool failure now carries a recovery contract: a `code`, an `action`
naming the next step, and an `operationPerformed` flag saying whether state
changed. Previously only two paths -- auth and precondition-required -- had
structured content, so for every other failure an agent got a bare message
and had to guess whether to retry, whether the write landed, and what to fix.

operationPerformed is three-valued on purpose. false means the request was
refused before anything was written, which is knowable for each classified
code because all of them are raised during validation or precondition
checking. null means the outcome is genuinely unknown, which is the honest
answer for an unclassified failure that may have been raised mid-write.
Reporting false there would be a guess, and the point of the field is that an
agent can trust it instead of reading the node back to find out.

Orientation failures get the same treatment, with one wording split that came
out of a smoke-test run. A denial on a call that named no flight is now
described in terms of what the caller did -- it named a keg, which selects a
target and never grants authority -- rather than blaming "the selected
flight" the caller never selected. The old wording read as a wrong flight
name and sent agents through list_flights looking for a flight that was not
the problem. The denial text also deliberately stops telling agents to
reorient: a denial is not disorientation, and the next call resolves live
authority anyway.

Finally, schemaWithFlight now appends a description of the flight parameter
at the same point it injects the property, so schema and prose cannot drift.
Agents read descriptions, not just schemas, and an injected property that
nothing mentions reads as absent -- it had already been reported as a missing
feature.
A node is built from exactly two inputs: Body, the markdown whose explicit H1
is the node's only title source, and Meta, the complete metadata document.
keg.NodeCreate and keg.CreateOptions drop Title, Lead, Tags, and Attrs, which
between them had given a node's title two sources and its metadata three, with
no rule saying which won.

keg.RejectFrontmatter becomes the single definition of the rule that content
must not open with a --- block, so every writer -- the CLI, MCP, the remote
keg, and Tapper Hub -- enforces it identically and metadata keeps one home.
Callers that used to embed frontmatter in the body now split it into Meta.

The MCP surface follows the same shape. create takes content and meta; edit
takes optional content and meta pointers, so a call can replace either
document alone while one expected_hash still covers both. cat gains a
structured read whose fields are exactly what edit accepts, letting a
read-modify-write cycle round-trip without parsing output meant for humans --
which is why it returns the two documents separately rather than a composed
---meta---body blob that edit would reject.

The standalone meta tool is retired along with Tap.MetaBatch. Reading metadata
is cat with meta_only and writing it is edit's meta field, so metadata has one
read path and one write path instead of a second tool that could disagree with
them. The parity table records that the Meta capability is now covered by two
MCP tools rather than one.

Keg.Next leaves the interface. Ids are assigned by the create call that
allocates them and returned in its result, so there is no reason to reserve
one ahead of time, and a reserved-but-unwritten id was a hole a caller had to
remember to fill.

Two smaller correctness fixes ride along in the metadata parser, both reached
by the new single-document path. ParseMeta now names the shape that actually
arrived -- "metadata must be a YAML mapping of keys, got a sequence" -- rather
than surfacing the decoder's complaint about an internal struct it could not
fill, because that message reaches API clients verbatim. NodeMeta.Keys reports
the document's top-level keys so a caller can detect two inputs writing the
same key before either write happens; Get cannot serve that purpose, since it
reports only scalars and silently misses nested keys.
edit took its items under "edits" while create, remove, and node_snapshot all
took theirs under "nodes". The four batch mutation tools now agree on one
parameter name, so an agent that has learned the shape of one has learned all
of them and cannot pick the wrong key by analogy.

A client still sending "edits" is rejected rather than silently accepted,
which is the same answer the schema already gives for any other unknown key.
Updates the agent-facing integration content and guides for the two-input node
model and the renamed MCP edit parameter, and re-renders the Claude and Codex
skills from it.

Two corrections to the tool inventory came out of smoke-test runs where agents
could not reach any KEG through subflight-scoped calls. Both were usage errors
the documentation invited:

`flight` is described as operational, not discovery-only, with a concrete JSON
example carrying the exact canonical name. The inventory had described the
parameter conceptually, so agents passed a bare flight name; unqualified names
resolve against the active KEG, and under a root with an empty cover there is
nothing to resolve against, which surfaces as ORIENTATION_DENIED and reads
like a missing feature. One report concluded from this that the server does
not forward flight context to operational tools, which is the opposite of what
the contract says.

`keg` is now stated plainly to never grant authority -- it selects a target
within the authority a call already has. Naming an uncovered KEG is
ORIENTATION_DENIED because the access control is working, and that denial had
been logged as a defect.

cliff.toml gains a breaking-changes section. CHANGELOG.md is generated from
commit messages, but the template grouped only by type and never read
commit.breaking, so a future `feat(keg)!:` with a BREAKING CHANGE: footer would
render as an ordinary feature bullet and a consumer scanning the changelog
would never learn their client broke. Nothing in this project is a stable API
yet, so no commit is marked breaking today and the section stays empty;
verified that regenerating with the new template leaves every historical entry
byte-identical.
@jlrickert
jlrickert merged commit 594b88c into main Sep 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant