Documentation

MCP Server — the Agent Plane

Status: Implementedpackages/mcp (arkaik-mcp), a dependency-free stdio server: the SDK caveat below resolved to its own fallback (the workspace's zod 4 vs the SDK's zod 3), so the server speaks newline-delimited JSON-RPC directly; the tool catalog is the contract, and adopting the SDK later changes plumbing, not behavior. File IO comes from the arkaik/io subpath export (§ Reuse Seams); the plugin ships the generated plugin/.mcp.json; tests/mcp/run-mcp-tests.js is the harness (§ Testing). This document remains the normative contract. The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as in RFC 2119.

Purpose

The skill makes agents good writers of the map; the format makes them competent readers. What's missing is a conversational surface: an agent asked "what's blocked on iOS?" or "link this new endpoint to the checkout view" should not need to parse a 4,000-line JSON file into context or shell out to a CLI with flags. The MCP server exposes the map as tools — the same projections humans see as pages (maps.md, journal.md § Projections), plus validated dual-write mutations.

This is the audience-symmetry principle (vision.md § Core Product) made concrete: every human surface has an agent-consumable twin, produced by the same @arkaik/schema functions.

Packaging & Transport

DecisionDetail
PackageNew workspace package packages/mcp, npm name arkaik-mcp (unscoped, symmetric with arkaik), MIT — toolchain side of the license split
Bin / install storynpx -y arkaik-mcp is the whole setup; esbuild-bundled single file like the CLI
Transportstdio (v1). The server is spawned per session by the agent host; no daemon, no port
Not a CLI subcommandThe arkaik CLI stays dependency-free; the MCP SDK would end that. The CLI usage text gains a pointer line; an arkaik mcp alias MAY wrap the package later
SDK note@modelcontextprotocol/sdk was the default choice; its zod-3 pin against the workspace's zod 4 resolved this to the fallback, taken all the way: the server speaks newline-delimited JSON-RPC directly (src/protocol.ts, ~150 lines — initialize, tools/list, tools/call, ping) with raw JSON-Schema tool definitions whose enums come from @arkaik/schema ids. @arkaik/schema remains the only validation authority; the SDK MAY be adopted later without changing the tool catalog

Graph Discovery — two modes, one catalog

The server serves a graph from one of two backends. The tool catalog is identical in both, which is the point: the mode is a Store implementation (src/store.ts), not a second set of tools, so repo and hosted behaviour cannot drift.

Repo bundle (Kommit-first, the default)

The map lives next to the code, maintained as a side effect of development. Resolution order:

  1. --bundle <path> argument
  2. ARKAIK_BUNDLE environment variable
  3. docs/arkaik/bundle.json under the current working directory

The journal is the sidecar resolved by the rules in journal.md § Storage Shapes. The server MUST reload the bundle per tool call (files are small; external edits — a human, another agent, git pull — must be picked up).

Hosted project (the account backend)

The map lives in an arkaik account and is reached over HTTP, so the app, this server, and the GitHub App all read and write one copy — no sync, no merge, no conflict UI (hosted-projects.md, services.md § Boundary 1 no longer holds for hosted projects).

Selected by --remote, or automatically by a docs/arkaik/arkaik.json written by arkaik link. Resolution:

Order
Project--project, then $ARKAIK_PROJECT, then arkaik.json
Origin$ARKAIK_URL, then arkaik.json, then https://arkaik.app
Token$ARKAIK_TOKEN only — never a file, because a credential in a repo file is a credential in a git history

--bundle always wins, so an explicit path overrides a link file.

Three rules this mode MUST hold:

  • The client sends OPS, not a mutated graph. The server recomputes under its own row lock, so two writers cannot lose each other's work. Store.persist therefore takes an intent for the hosted backend where the file backend takes an outcome.
  • A linked repo with no token exits non-zero. It MUST NOT fall back to a repo bundle: a silent fallback serves a stale graph that looks fine, which is the failure an agent cannot notice.
  • load() returns a real BundleValidation, not a look-alike. Most read tools touch only bundle/nodes/edges/journal, so a hosted loaded carrying just those passed unnoticed until validate_bundle — which also reads result, valid and sidecarFindings — crashed on it, putting the skill's hard validator gate out of reach for every hosted project. Hosted mode fetches the whole bundle plus its journal, so the findings come from running the same validateBundle over the same graph the server validated: one validator, one verdict, either side of the wire.

propose_idea and file_request are refused against a hosted project — the hosted write path has no journal-only operation yet — with an explicit message rather than a silent drop.

Tool Catalog (v1)

All tool results are JSON text content. Read tools are projections; write tools follow the Write Path below.

ToolInputReturnsJournal events
list_nodesspecies?, status?, platform?, value?, anchor?, parity_gap?, query?, limit?node summaries {id, title, species, status, platforms} (acceptances add platform_statuses, values)
get_nodenode_idfull node + edges with neighbor titles + where-used flows + computeNodeTimeline + (views/flows) covered_by
create_nodespecies, title, description?, status?, platforms, metadata?created node (id via deriveNodeId) + any synthesized composes edgesnode.created (+ one edge.added per synthesized composes edge — see Playlist Composition)
update_nodenode_id, patchupdated node + any synthesized composes edgesvia diffNodeUpdate: node.updated / node.status_changedplatform) / ref.added / ref.removed (+ edge.added per synthesized composes edge)
delete_nodenode_idremoved node + cascaded edge idsnode.deleted (edge cascade implied per journal.md)
add_edgesource_id, target_id, edge_typecreated edge (id via edgeId)edge.added
remove_edgeedge_idackedge.removed
propose_ideatitle, description?, node_id?eventidea.proposed
file_requesttitle, description?, source?, node_id?eventrequest.filed
get_changelogversion?release list, or one computeChangelog slice
get_backlogcomputeBacklog (open ideas & requests)
list_mapsbuilt-in + stored MapDefinitions with node/edge counts
get_mapmap_idcomputeMapSubgraph result
validate_bundlevalidateBundle findings (errors + warnings, with paths)

list_nodes additionally accepts:

  • value — only acceptances tagged with this value element (enum: the 30 VALUE_IDS).
  • anchor — a node id; only acceptances covering it via a covers edge.
  • parity_gap — boolean; only acceptances delivered (live) on ≥1 applicable platform but not all (spec §3.5).

Acceptance summaries additionally carry platform_statuses (resolved per applicable platform) and values. get_node on a view or flow includes covered_by: summaries of the acceptances covering it.

Known gap — get_map and list_maps ignore MapDefinition.product. A stored map may be scoped to one product (maps.md § Product Scope), but both tools call computeMapSubgraph directly, and the product restriction is applied by the app (lib/utils/product-scope.ts, mapScopedNodes) before that call rather than inside it. So an agent asking for a product-scoped map gets the unscoped subgraph, and list_maps reports the unscoped node and edge counts alongside it — silently, with nothing in the result saying so.

This breaks audience symmetry, the principle the rest of this document is built on: every human surface is supposed to have an agent-consumable twin, and here the twin answers a different question than the canvas does. The fix is to move membership resolution into @arkaik/schema so computeMapSubgraph can take the product itself — not a second implementation in the MCP layer, which is precisely how two surfaces come to disagree about the same node.

arkaik release (tagging, note drafting, compaction) stays CLI-only in v1 — it is a ceremony with side effects beyond the bundle, owned by packages/cli/src/commands/release.ts.

Write Path (dual-write, validator-gated)

Every mutating tool MUST follow, in order:

  1. Load bundle + journal (fresh).
  2. Apply the mutation in memory and derive the matching journal events (shared derivation — see Reuse Seams).
  3. Run validateBundle on the mutated bundle with the new events folded in. Any error → return the pathed findings and write nothing. Warnings pass through in the tool result.
  4. Persist: append each event to the journal sidecar (JSONL, one line per event, actor: "arkaik-mcp"), then rewrite the snapshot with serializeBundle (canonical form — clean git diffs).

This is the skill's dual-write doctrine (journal.md § Authority) enforced structurally: an MCP mutation is incapable of the snapshot-without-history drift that free-form file edits invite.

Playlist Composition (composes-edge synthesis)

A flow's playlist and its composes edges are two views of one relationship: the validator's playlist-composes-coherence rule requires a composes edge from a flow to every view/sub-flow its playlist references. Under the gate above this created a deadlock — a flow created with a populated playlist fails coherence because the edges don't exist yet, but add_edge cannot create them until the flow node exists, so no single call could produce a populated flow (issue #263).

create_node and update_node therefore synthesize the required edges: when the mutated node is a flow, they add a composes edge (flow → referenced node) for every playlist reference (recursing through condition/junction branches) that lacks one, fold those edges and their edge.added events into the same validated write, and return them in the tool result under edges. Edges that already exist are never duplicated; a reference to a missing node still fails the gate (nothing is written). This mirrors the app's own playlist editor, which adds the edge and the entry together. A populated flow is thus a single create_node call.

Reuse Seams (two enabling moves)

  1. CLI file-IO becomes importable. packages/cli exposes a subpath export arkaik/io (bundle read/write, journal sidecar IO, validation wrapper) built as a second esbuild entry. packages/mcp depends on arkaik and imports these verbatim — no drift between what the CLI and the MCP server consider "the bundle on disk". Filesystem code stays out of @arkaik/schema, which remains browser-safe.
  2. Dual-write derivation moves to the schema package. lib/data/emit-events.ts is already pure; its core moves to packages/schema/src/derive.ts with the actor as a parameter. The app keeps a thin re-export binding actor: "arkaik-app"; the MCP server binds "arkaik-mcp"; the skill doctrine stays the human-readable statement of the same rules.

Distribution

  • npm: npx -y arkaik-mcp — zero-install for any MCP host.
  • Claude Code plugin: the plugin gains a generated plugin/.mcp.json declaring the server (command: "npx", args: ["-y", "arkaik-mcp"]), emitted by scripts/generate/generate-plugin.js and covered by the CI drift check. Installing the plugin then delivers skill + MCP together: the doctrine and the tools, one install.

Testing

tests/mcp/run-mcp-tests.js follows the CLI harness pattern: spawn the built server against a tmpdir fixture bundle + journal sidecar, speak JSON-RPC over stdio (initialize, tools/list, tools/call), and assert: read-tool shapes; a write round-trip (update → journal line appended → validate_bundle clean → snapshot canonical); and the gate (a mutation that would dangle an edge is refused with pathed findings and the files untouched).

tests/mcp/remote-store.test.js runs the same built server against a stub of the hosted API. Assertions that a tool behaves the same through both stores belong here rather than in the file-mode suite, which by construction cannot see hosted-only drift: validate_bundle returning a verdict identical to repo mode — clean and broken — is checked that way.

Non-Goals (v1)

  • Hosted MCP / REST over Synk projects. The natural Klub-tier follow-up: the same tool catalog served over authenticated transport against account-backed projects, enforced on the existing lib/services/limits.ts seam. Requires a device-token auth flow (shared open question with arkaik push --to synk, services.md). Specified when scheduled — nothing in this document precludes it.
  • Multi-bundle workspaces — one server instance, one bundle.
  • Release ceremony — CLI-only, above.