MCP tool definitions for the Evo API (fjs/cas/evo): subjects, revision heads, and the typed read of a single revision over the content-addressable store (fjs/cas), backed by the in-memory cache the core module maintains.
These tools are not their own server. They are served by the same process as cas_add/cas_get/cas_list: fjs/mcp builds one Evo<O> from its own Cas<O> and cache slot (initEvo, scanned once at startup), concatenates evoToolRegistry onto its own tool registry, and serves everything — one ~/.cas/ store, one Evo cache, one server, one npx functionalscript mcp. See fjs/mcp/README.md for how to run it and register it with an MCP client.
| Tool | args | action | result |
|---|---|---|---|
evo_list | { archived? } | e.list(...) | subjects, as a JSON array of strings |
evo_head | { subject } | e.head(...) | head hashes, one per line |
evo_revision | { hash } | e.revision(...) | the revision, as JSON |
evo_add | { parents, snapshot?, subject?, archived?, lock? } | e.add(...) | hash (cBase32) |
evo_list returns JSON rather than the newline-joined line format evo_head/cas_list use: subjects are arbitrary caller-supplied strings, not constrained to a newline-free alphabet the way hashes are, so a subject containing \n (or an empty subject) would be ambiguous in a line-based format.
evo_list's archived argument is forwarded unchanged to Evo.list: omitted lists the active subjects (at least one current head not archived), true lists the archived ones (at least one current head, all of them archived), and a subject with no current heads is in neither. See Subject status for why the status is derived from the heads and why there is no all-subjects mode.
evo_revision is the typed read of a single revision: { subject, parents, snapshot, generation, archived?, lock? } — the JSON of fjs/cas/evo's RevisionData (see fjs/cas/evo/README.md), with dialect dropped and every hash in its canonical cBase32 spelling, so parents and snapshot compare directly against evo_head output. parents[0] is the mainline parent; every further entry is a merged-in branch.
cas_get (fjs/mcp) stays the generic raw-bytes tool for arbitrary blobs — snapshots and any other non-revision content. get means "raw bytes by hash" throughout these tools, which is why this one is not called evo_get: leaving agents to decode, validate and canonicalize a raw revision blob themselves is exactly what it exists to avoid.
evo_add and evo_revision speak the same structure in opposite directions, so a revision read back can be added again as-is. The one field evo_revision returns that evo_add's schema does not name is generation, which the server computes; rtti struct validation ignores properties the schema does not name, so a whole evo_revision result can be passed straight back to evo_add.
evo_add's lock is not a restatement of the format's lock field but fjs/media/revision's own lockField schema, so what the server accepts and what tools/list advertises cannot drift apart. It therefore takes either form the format does: the bindings inline as a map, or the cBase32 hash of a vnd.fjs.lock blob holding them — recorded as given, never followed. The map half is recursive — a value is a hash or a nested scope — which is why it is the one argument whose JSON Schema is not inlined whole: the recursion comes out as a named $defs rule (lockValue) reached through a local $ref, the JSON Schema spelling of a cycle.
Each tool's argument schema is an rtti struct declared once and used twice: toJsonSchema derives the inputSchema advertised in tools/list, and parse decodes the arguments object in tools/call — the same pattern as fjs/mcp.
- Invalid
arguments(rttivalidaterejects the object, e.g. a missingparents) →isError, reported by the sharedtoolEntrymachinery before the domain logic runs. - A domain-level
evo_addfailure — an unresolvablesubject, an invalidvnd.fjs.revision(seefjs/media/revision), a blob too large to encode, or a store write failure — →isErrorwith the message fromEvo.add'sResult. - A domain-level
evo_revisionfailure →isErrorwith the message fromEvo.revision'sResult, which keeps every case distinct: ahashthat is not cBase32, a hash the store has nothing under, a read that failed for another reason (so an unreadable blob is never reported as a missing one), and a blob that is not a revision. - A response too large to encode → the transport's
-32603, not a tool-level error.evo_listandevo_revisioncarry JSON as MCP text content, so the JSON-RPC serializer escapes it again and an encoded line can outgrow the cap even when the value itself is small (a subject of quote characters is the worst case).fjs/protocol/mcp/stdiothen retries with a small internal-error body that keeps the request'sid— the same envelope every tool has,cas_getincluded. A tool cannot turn this into a descriptive error of its own: whether the encoded response fits is known only by encoding it, and predicting that from an unencoded size is the kind of estimate this codebase does not make.
evoToolRegistry is generic in the store's operation type O and takes a plain Evo<O>, so proof.f.mjs exercises each tool entry's handle directly against an in-memory Evo — no MCP session or live process required.