PROP-015 — MCP server and agent integration
01Milestone: M1.7 (ROADMAP.md). The server slice
shipped first; the agent-integration surface (vibe mcp install /
status / upgrade / uninstall) followed.
02Status: IMPLEMENTED — this PROP is the retroactive spec home the
CONVERT-PLAN v0.1 §7 endgame opened for the vibe-mcp crate and the
vibe mcp command family. Units typed at REQ grain; the code carries the
matching scope! / #[spec(implements)] edges.
03Related: PROP-004 §5 / §6
(the comparative research that motivated treating agent context as a
managed, distributable artefact), PROP-002 §2.1
(content_hash identity the query_package tool surfaces),
PROP-003 §2.5 (the subskill
delivery modes the read_subskill / materialise_subskill tools read),
VIBEVM-SPEC.md §5 (the product's AI-integration
scope), and PROP-023 (the
bridge-packages design that added the #skill-include req on
2026-06-24 — additive; §2.6 #skill is unchanged).
1. Motivation
04vibevm installs spec-and-discipline packages into a project; the consuming agent then has to find and read what landed. Two integration surfaces close that loop:
- 05A Model Context Protocol server (
vibe mcp serve) that exposes the project's lockfile-derived state to any MCP-speaking agent as callable tools — so the agent queries package identity and pulls subskill content on demand instead of guessing from the file tree. - An agent-integration command family (
vibe mcp installand friends) that wires that server into each agent's own configuration and writes a per-agent skill manifest, so an operator runs one command instead of hand-editing five different config files.
- 06Both are product scope (
VIBEVM-SPEC.md§5). - Neither changes wire formats, the lockfile schema, or install behaviour — they are read-mostly surfaces over state the rest of vibevm already owns.
2. Decisions
2.1 The server: JSON-RPC 2.0 over stdio
07req r1
08Decision. vibe-mcp is a transport-agnostic MCP server.
- 09It speaks
JSON-RPC 2.0 over line-delimited stdin/stdout (the MCP stdio form),
handling the
initializehandshake,tools/list,tools/call, andping. - The protocol version is a one-line
const(PROTOCOL_VERSION); the server name/version surface in the handshake. - Transport is a trait (
Transport) — production usesStdioTransport, tests injectMemoryTransportfor deterministic round-trips. - Each
tools/callreloads the project lockfile fresh, so a concurrentvibe installsurfaces on the next call without a server restart. - A missing
vibe.lockis an empty lockfile, not an error — the agent sees empty state through the normal tool response.
2.2 The tool surface: one seam, three tools
10req r1
11Decision. Every tool implements one seam (McpTool): it describes
itself (name, human description, JSON-Schema input shape) and runs
against parsed arguments plus the read-only ServerContext.
- 12Tools register at one point; the dispatcher routes by registered name and does not know a tool's identity beyond it.
- The seam is the cell boundary — a new tool is a new cell, not an edit to the dispatcher.
13The shipped tools (all group-qualified <group>/<name> pkgrefs in;
structured JSON + a text rendering out):
- 14
query_package— the full lockfile entry for an installed package (kind, version,content_hash, registry, source,files_written, features, active subskills,describesPURL, language). Read-only. read_subskill— the concatenated content of an active subskill's files.eager/lazy-pushsubskills read from the project tree;lazy-pullsubskills read from the package cache (PROP-003 §2.5.0), so the agent gets bytes regardless of delivery mode. Read-only.materialise_subskill— copy alazy-pullsubskill's content into the project tree. No-op foreager/lazy-push(already on disk); refuses to overwrite withoutforce. The one writing tool.
15A tool failure renders as isError: true in the result payload (a
tool-level failure), distinct from a transport-level JSON-RPC error.
2.2.1 Searching the map — the set-returning twin of explain
16query finds nodes; explain
looks at one. Until 2026-08-06 the map could only be asked about a target
already known by name, so «which of these exist?» was unanswerable — not in
the host, not in any language stack, not in the engine. query is that
question, and the two are deliberately separate verbs rather than one verb with
a mode: a point lookup and a set filter render differently, cap differently, and
fail differently.
17The filter level is a permanent level, not a first version to be replaced (owner ruling, 2026-08-06). An agent accustomed to grep reaches for filters; a query language demands a form it will not build without need. So the filters must work on their own and must never become a degenerate case of a grammar — in the library they are their own entry point, so a broken parser could not take them down with it.
18Three filters, combined with AND, under a hard ceiling: exact spec address, substring of a code symbol, element kind. None is required; those given narrow. The ceiling is not a convenience and cannot be removed — the answer is read by an agent with a bounded context, and an unbounded one is useless rather than generous. When it truncates it says so, with the total, in both renderings.
19Results are nodes, never edges. «Find me an edge» is not a question anyone asks; «find me what has, or lacks, an edge of this kind» is — so edges are a filter dimension for the query level above, and the result set stays spec units and code items.
20Every hit records where it came from, because a second producer is already designed: the code-quality engine's findings join these results at query time, by the owner's ruling that two engines must not merge their data. Nothing of that engine is built here; the result shape simply does not close the door — a discriminated field rather than a closed variant set, so a third source adds a value instead of breaking every reader.
21One library, two thin
surfaces, per the omnichannel floor: the filtering lives in the host's trace
crate, vibe query renders it for a person and the query tool renders it for
an agent, and both call the same function. The MCP tool's description says when
to reach for it instead of explain — that description is the surface's own,
which is exactly what the agent-facing class owns and all it owns.
22kind filters over
a measured vocabulary. The committed map carries nine code kinds, and a spec
unit answers on its own kind rather than borrowing the code one; the two
vocabularies are disjoint, so one filter serves both without ambiguity. Caveat
worth carrying: every spec unit in this tree is legacy-unmarked, so a spec kind
matches nothing here today. The path is open and exercised only by fixtures —
stated because a filter that silently returns nothing is indistinguishable from
one that is broken.
23The map is built fresh in memory per
call, never read from the committed artefact — the same posture explain takes,
for the same reason: a query answers for the tree as it is.
24The query language is designed and not built. Its shape is the filters plus graph traversal — depth, and «has no edge of kind X», which is what answers «which rules does nothing verify» — and it introduces a grammar that will need versioning. It stands on this level rather than replacing it. SUPERSEDED 2026-08-06 — it is built; the contract is §2.2.2 below.
2.2.2 The query language — traversal on top of the filters
25select is a third verb, for the same
reason query and explain are two. explain looks at one target, query
filters a set, and select walks the graph from a set. Three questions, three
caps, three failure modes — a mode flag on one verb would make the result type
depend on the argument, and every consumer would carry three readers.
26It stands on
the filter level and cannot take it down with it. The three filter predicates
are the ones §2.2.1 ships, reached rather than redefined; the parser lives in its
own module behind its own entry point, so a broken grammar leaves query
answering. That separation is the owner's ruling in
##MAP-QUERY-THE-SIMPLE-LEVEL-IS-PERMANENT made structural instead of promised.
27Seven predicates, whitespace
separated, joined by AND, and no operators at all: uri:, symbol:, kind:
(the filter level's own), scope: (prefix of a spec address), has: / lacks:
(an edge verb), depth: (0..3). No disjunction, no parentheses, no precedence —
each of those is a permanent versioning liability, and a predicate can be added
without renumbering a language that has no operator layer to renumber.
28scope: is in the
set because the level's own canonical question is unanswerable without it.
Measured before the build: 5 742 of 5 825 spec units carry no verifies edge,
and nothing that shipped could narrow that — kind is carried by 0 units in
this tree and uri is exact, so composing the negative predicate with the filter
level yields 5 742 or 1. A document prefix brings 67 of the corpus's 72 documents
inside the ceiling; the five that do not are named in the design record rather
than left to be rediscovered.
29has: and
lacks: select seeds, and «touches» reads from each family's own side —
incoming for a spec unit, outgoing for a code item. On a directed bipartite
graph that is the only reading under which one predicate serves both families,
and applying them after the walk instead would answer a question nobody asked.
30depth:N expands the seed
set along edges, undirected, and the seeds stay in the answer. depth:0 is the
default and the identity, so a query without it is exactly the seed selection —
which is what keeps this level a strict superset of the one below rather than a
different thing wearing its name. Every hit carries the hop count it was reached
at, so a caller can tell what it asked for from what the walk brought with it.
31The depth bound is 3, and the number came from the graph. Exhaustively over all 1 205 edge-bearing nodes, 71.7 % reach more at depth 2 than at depth 1 and only 5.9 % reach more at depth 3; the largest connected component is 44 nodes. So depth is a precision control on this map rather than a safety one — and the result ceiling stays hard regardless, because it protects against a future map, not this one.
32An unknown predicate, an unknown verb, a repeated predicate, an out-of-range depth or an empty query is an ERROR that names the offending token and lists what was expected — never a silently ignored clause. Same law as the markup's typed fences, for the same reason: a grammar that ignores what it does not understand promises everything and checks nothing, and whoever trusted the promise is the one who finds out. An empty query is refused rather than read as «everything», because that answer already exists one verb away.
33The grammar carries a version and reports it in every answer rather than demanding it in every query. A query string stays free of ceremony; the structured answer states the version it was parsed under, so a consumer that cares can branch and one that does not is unaffected. Requiring a prefix would tax every caller forever to buy nothing until the first breaking change.
34The rejected shapes, the
measurements above with their commands, and the correction of a sampled reading
that was wrong about depth are in
vibevm/vibespecs/design/map-query-language.xml — the
lore this contract is the short form of.
2.3 Tool and server errors cite their REQ
35req r1
- 36Decision.
ToolErrorandServerErrorare the crate's two error layers. - Each variant's Display text carries the violated
spec://unit and a fix surface (the Class-F product-error grammar), so a failing tool call is navigable back to this PROP without source access.
2.4 Agent detection
37req r1
38Decision. The integration surface supports a fixed set of MCP-capable coding agents (Claude Code, Claude Code Desktop, Cursor, OpenCode, Codex).
- 39An agent is present in a project when its project-level markers exist
(e.g.
.claude/CLAUDE.md,.cursor/.cursorrules) or its user-level host config directory exists. - Detection drives the default
target set for
vibe mcp install; the operator can always override with an explicit agent filter.
2.5 Per-agent configuration
40req r2
41Decision. Each agent declares its config shape, and the writer is agent-aware but format-generic:
- 42Format — JSON or TOML per agent (Codex is TOML-only).
- Section key — the agent's MCP-servers table name (
mcpServers,mcp,mcp_servers). - Scope — project (
.<agent>/…in the repo) and/or user (the host config dir). Some agents are user-only (Claude Code Desktop, Codex). - Config path — resolved per (agent, scope), cross-platform. The
path must be the file the agent actually reads for MCP discovery,
not merely a settings file it happens to own. For Claude Code that is
<project>/.mcp.json(project) and the top-levelmcpServersof~/.claude.json(user) — neversettings.json, which only gates.mcp.jsonservers (enabledMcpjsonServers) and does not define them. - Merge discipline — installing upserts vibevm's one entry under the
section key and preserves every foreign key, and their order: the
JSON writer round-trips order-preserving (
serde_json/preserve_order), so a merge into a large~/.claude.jsonappends rather than re-alphabetising the operator's whole file. Uninstalling strips only vibevm's entry and leaves the rest. The operator's other MCP servers and unrelated config survive every operation.
- 43The vibevm entry is scope-independent:
vibe mcp servewith no--path, resolving its project root from the launcher's CWD (an MCP client sets CWD to the project directory for a project-scope server), so one shape serves every scope and a committed.mcp.jsonstays portable. - On Windows the launcher is wrapped as
cmd /c vibe …becausevibeis avibe.cmdshim that an MCP client's bare process-spawn cannot exec directly.
2.6 Skill materialisation
44req r1
45Decision. For agents that support a skill manifest (Claude Code,
OpenCode, Codex — not the JSON-config-only Cursor / Claude Code Desktop),
vibe mcp install also writes a SKILL.md describing how to use vibevm
through the MCP tools.
- 46The skill body is data, not code — a vendored
template (
include_str!) rendered into each agent's skill directory (.<agent>/skills/vibevm/SKILL.md). - Writing is idempotent: identical
content is left untouched (
unchanged); a divergent file is updated.
2.7 The integration lifecycle
47req r1
48Decision. The agent-integration command family is a coherent
lifecycle over the (agent × scope) matrix, every verb idempotent and
every mutating verb offering --dry-run and a confirmation:
- 49
install— detect (or accept a filter), preview, confirm, write MCP entries and skills. status— read-only: what would install / upgrade / uninstall do.upgrade— refresh stale MCP blocks andSKILL.mdfiles in place.uninstall— strip vibevm's MCP entries and skill files, preserving foreign config.
50Per-(agent, scope) outcomes are reported as structured records
(AgentInstallReport / SkillInstallReport) the CLI renders or emits as
JSON.
2.8 Selective skill projection
51req r1
52Decision. SkillDecl gains an optional include — a list of glob
patterns relative to the skill's path.
- 53When present, only matching files are
projected into the agent's skill directory, preserving their relative
structure; when absent or empty, the whole
pathtree is projected — the existing §2.6 behaviour, unchanged. - Selection composes with the
already-working nested
path: a skill can point at a subdirectory and pick specific files out of it. - This is available to any skill but is load-bearing for bridge packages
(PROP-023): a bridged upstream
tree is full of unrelated files, and the maintainer projects just the
SKILL.mdand whatever it references without vendoring the noise. - Glob matching is deterministic; a pattern that matches nothing is a declared-but-empty selection (surfaced, not a silent no-op).
54[[skill]]
name = "vim"
path = "upstream/skills/vim"
include = ["SKILL.md", "references/**/*.md"] # omit → whole tree (§2.6)
3. Out of scope
- 55No new wire formats or lockfile changes. The server reads the existing lockfile schema; the tools surface existing fields.
- No agent-specific behaviour beyond config shape. The integration knows each agent's file format and paths, not its runtime semantics.
- HTTP / SSE transports. Stdio is the shipped transport; the
Transporttrait leaves room without committing to more today. - LLM-provider tools. PROP-003 §F virtual-capability emission waits on
a real
vibe-llm(VIBEVM-SPEC.md§10.4).
4. Acceptance
- 56The server answers
initialize/tools/list/tools/callover aMemoryTransportround-trip; each tool has a behavioural oracle. query_package/read_subskill/materialise_subskilleach behave per §2.2 against a lockfile fixture (found / not-found / invalid-pkgref; the delivery-mode split; the force / no-overwrite contract).vibe mcp installwrites the expected entry under each agent's section key, preserves foreign keys, and is idempotent;uninstallis its inverse;statuswrites nothing.