ADR-0061 — MCP tool expansion + review bulk-resolve + installable agent skill (README-delta parity, B5/D2)¶
- Status: Accepted
- Date: 2026-07-06
- Sprint: B5 (README-delta parity —
feat/b5-mcp-skill) - Feature: F17-adjacent (MCP surface) + F9 (review queue) · closes UI-ALIGNMENT-PLAN-2026-07 §B5 row D2
- Extends: ADR-0010 (MCP transport + shared write path) · ADR-0029/0032/0033 (remote HTTP surface, runtime toggle, UI token + source-trust gate) · ADR-0044 (review bulk actions +
statusfilter) · ADR-0034 (review proposal model) - Invariants owned: I6 (every new tool reuses an existing provider-neutral seam; NO tool touches
InferenceProvider, NO provider is hardcoded) · I9 (reuse the existing FastMCP server,rag.retrieval, the graph engine, and the watcher rescan — no new search/graph/gateway) · I1 (the write tools are incremental upserts / status writes; the rescan is the incremental watcher, never a full re-scan) · I7 (every read is capped:depth ≤ 2,limit ≤ 100,ids ≤ 200; no new loop) · I5 (write tools keep the ADR-0010 §2 shared validated write path) - Author: solution-architect
- Implementers: backend-engineer (MCP tool bodies +
build_http_mcpgating +POST /review/queue/bulk-resolve+PATCH /review/queue/{id}) · ai-agent-engineer (co-sign the tool contracts +tools/synapse-skill/SKILL.mdtrigger discipline) · tech-writer (D4 MCP reference + OpenAPI regen)
1. Context¶
The current llm_wiki README (2026-07) has evolved past the v0.5.4 audit baseline. Its local
service now exposes an MCP server with a richer tool surface (graph neighbourhood, review
listing/resolution, source-file read, source rescan), a review bulk-resolve + PATCH REST
surface, and an installable agent skill (npx skills add llm_wiki_skill) that lets a Claude
Code / Codex agent operate the wiki from a disciplined trigger.
Synapse's MCP server (backend/app/mcp/server.py) exposes exactly four tools —
search_wiki, write_page, get_page, list_pages — split into read-only bodies plus one
gated write body (write_page), with build_http_mcp(write_enabled=...) deciding whether the
HTTP surface registers the write tool (ADR-0029 §2.3 / ADR-0033 §2.4). The review queue has
per-item actions and a POST /review/queue/bulk (ADR-0044) but no bulk-resolve verb and no
PATCH. There is no packaged agent skill.
This is a parity gap, not a redesign. The abstraction is already correct: read bodies +
one write body, a stdio mcp, an in-process SDK server, and a token-gated HTTP surface. This
ADR extends that surface with more tools of the same two kinds and one narrow REST pair, and
adds a trigger-disciplined skill package. It does not change transport, auth, the write
path, or provider routing.
2. Decision¶
2.1 Three new READ-ONLY MCP tools (bounded, reuse existing seams)¶
Added as new shared _*_body functions and registered on the stdio mcp, the build_http_mcp
read set (always present, like get_page/list_pages), and the SDK server:
| Tool | Signature | Seam reused (I9) | Bound (I7) |
|---|---|---|---|
get_graph_neighborhood |
(title: str, depth: int = 1) |
the existing graph engine / links tables (ADR-0012/0016) — read the persisted edges, never recompute layout (I2) | depth clamped to 1..2; 2 is the hard ceiling |
list_reviews |
(status: str \| None = None, limit: int = 50) |
ops.review list path + the ADR-0044 status filter |
limit clamped to 1..100; excludes nothing new |
read_source_file |
(path: str) |
reads a file under the vault raw/sources/ root only |
path resolved and confined to raw/sources/ (§2.4) |
All three are read-only: they never mutate the vault, so they are registered on the HTTP
surface unconditionally (same class as search_wiki/get_page/list_pages) and require no
write_enabled. get_graph_neighborhood reads the already-computed graph — it must not
trigger a layout or a graph recompute (I2 stands; it is a lookup, mirroring how search_wiki
is a lookup, not the F5 pipeline rebuild).
2.2 Two new WRITE MCP tools (gated exactly like write_page)¶
| Tool | Signature | Effect | Gating |
|---|---|---|---|
resolve_review |
(id: str, action: str) |
set a review item's status via the existing ops.review seam (§2.5 action set) |
write_enabled only — same gate as write_page |
trigger_source_rescan |
() |
enqueue the incremental watcher rescan of raw/sources/ (I1) — never a full re-scan |
write_enabled only |
Both are mutating, so they follow write_page exactly: registered on the stdio mcp and
the SDK server unconditionally, but on the HTTP surface only inside the
if write_enabled: block of build_http_mcp (ADR-0029 §2.3). Remote/HTTP therefore stays
read-only by default (ADR-0033 §2.4 decision table unchanged) — a leaked token cannot
resolve reviews or trigger a rescan unless the operator explicitly opted the write surface in.
resolve_review reuses ops.review._set_status / the existing action handlers, so terminal-
status and idempotency guarantees (ADR-0044) hold identically — no second review writer.
trigger_source_rescan calls the existing watcher entry point; it does not implement a new
scan (I1/I9).
2.3 REST — POST /review/queue/bulk-resolve + PATCH /review/queue/{id}¶
Parity with the llm_wiki "reviews export/PATCH/bulk-resolve" surface, layered on ADR-0044:
POST /review/queue/bulk-resolve {ids: list[str], action: str}— apply one action to up to 200 items (ids ≤ 200, request rejected422above the cap). Bounded DB writes over the existing_set_status/ action handlers; no loop over an unbounded set (I7). This is a distinct verb from ADR-0044'sPOST /review/queue/bulk(which is UI-selection bulk over the live statuses);bulk-resolvetakes an explicit id list + action for API/agent callers.PATCH /review/queue/{id} {action: str}(or status field) — single-item state transition through the same seam, for callers that prefer a REST PATCH over the per-verb routes.
Both route into the existing ops.review handlers — no new review logic, no new table, no
migration (D2/ER unchanged, I8 has nothing to regenerate here beyond OpenAPI).
2.4 read_source_file is confined to raw/sources/ (path safety — CRITICAL)¶
read_source_file(path) MUST resolve the requested path against the vault raw/sources/ root
and reject anything that escapes it (.., absolute paths, symlink traversal). The
implementation resolves to a real path and verifies it is a descendant of the raw/sources/
directory; a miss returns a structured {"error": ...} (never an exception, never file bytes
from elsewhere). It reads source inputs only — never wiki/, never .obsidian/, never
config, never anything outside raw/sources/. This mirrors the reference's allow-list file-read
posture and keeps the tool from becoming an arbitrary-file-read primitive over the token-gated
HTTP surface.
2.5 resolve_review action set = the existing review actions¶
resolve_review(id, action) accepts only the already-defined review actions/statuses from
ADR-0034/0044 (skip, dismiss, mark-resolved/resolved, and — where the item supports it —
create / deep-research via their existing handlers). No new status value is introduced. An
unknown action returns a structured error. bulk-resolve shares this same action validation.
Terminal statuses (ADR-0044) are never re-mutated.
2.6 Agent skill — tools/synapse-skill/SKILL.md, trigger-disciplined¶
Ship an installable skill that drives the REST/MCP surface, mirroring llm_wiki's
trigger-disciplined llm_wiki_skill:
- Trigger discipline (load-bearing): the skill activates only on an explicit reference to "Synapse" or "my wiki" (the user's own vault). It MUST NOT trigger on a generic "search my notes", "look something up", or any ambient note/file phrasing. The skill description states the trigger narrowly so it does not hijack unrelated requests.
- It operates entirely through the existing REST endpoints and MCP tools defined above (search/read/graph/review/rescan/write); it introduces no new capability and no provider logic (I6/I9). It is a client of the surface, not part of it.
- Lives in
tools/synapse-skill/(backend-agent owned); this ADR fixes only its contract and trigger rule, not its prose.
3. Do-NOT list (binding)¶
- Do NOT expose any write tool (
resolve_review,trigger_source_rescan,write_page) on the HTTP surface without thewrite_enabledgate. They belong inside theif write_enabled:block ofbuild_http_mcp, exactly likewrite_page. Remote/HTTP stays read-only by default (ADR-0029 §2.3 / ADR-0033 §2.4). - Do NOT let
read_source_fileescaperaw/sources/. No.., no absolute paths, no symlink traversal, no readingwiki//.obsidian//config. Confinement is verified against a resolved real path; a miss returns a structured error. - Do NOT exceed the caps:
get_graph_neighborhooddepth ≤ 2;list_reviewslimit ≤ 100;bulk-resolveids ≤ 200. Clamp/reject rather than run unbounded (I7). - Do NOT make the skill trigger on generic phrasing ("search my notes", "find a file"). It fires only on explicit "Synapse" / "my wiki".
- Do NOT recompute the graph or run a layout inside
get_graph_neighborhood— read the persisted edges only (I2). - Do NOT add a second review writer or a new status value —
resolve_review/bulk-resolve/PATCHroute through the existingops.reviewseam (ADR-0034/0044). - Do NOT implement a new scan in
trigger_source_rescan— enqueue the existing incremental watcher (I1).
4. Acceptance check (DoD)¶
- stdio
mcpand the SDK server register all tools (4 existing + 5 new = read:search_wiki,get_page,list_pages,get_graph_neighborhood,list_reviews,read_source_file; write:write_page,resolve_review,trigger_source_rescan). build_http_mcp(write_enabled=False)registers only the read tools (noresolve_review, notrigger_source_rescan, nowrite_page);write_enabled=Trueadds all three write tools.get_graph_neighborhood(depth=5)is clamped to2;list_reviews(limit=999)clamped to100;POST /review/queue/bulk-resolvewith>200ids →422.read_source_file("../wiki/index.md")(and absolute/symlink variants) → structured error, no bytes returned; a validraw/sources/*path returns content.resolve_review/bulk-resolve/PATCHroute throughops.review(grep proves no second writer); terminal statuses unchanged.trigger_source_rescanenqueues the incremental watcher — no full re-scan path invoked (I1).- The skill description triggers on "Synapse"/"my wiki" and not on "search my notes" (documented trigger rule).
make openapiincludesPOST /review/queue/bulk-resolveandPATCH /review/queue/{id}(I8). No DB migration ⇒ no ER change.
5. Consequences¶
Positive — closes the B5/D2 parity gap by extending the surface Synapse already has: the
new tools are the same two kinds (read bodies + gated write bodies) wired through the same
build_http_mcp split, so remote-read-only-by-default and the shared validated write path are
preserved for free. The skill is a disciplined client, adding no runtime capability. No
transport, auth, provider-routing, or schema change.
Trade-offs (explicit) — five more tools widen the MCP surface area, so the write-gating and
the read_source_file confinement are the two things that MUST be verified in tests (they are
the only new attack surface). resolve_review over HTTP (when the operator opts write in) lets a
token holder resolve reviews remotely — acceptable and symmetric with the already-gated
write_page. The skill's usefulness depends on trigger discipline; a loose trigger would be a UX
regression, hence the explicit Do-NOT.
Invariant check — I6: no tool touches InferenceProvider; nothing hardcoded. I9:
reuses FastMCP, rag.retrieval, the graph engine, the watcher, and ops.review — no new
search/graph/gateway/writer. I1: rescan is the incremental watcher; review writes are
status writes. I7: every read/bulk is capped (depth ≤ 2, limit ≤ 100, ids ≤ 200); no
new loop. I5: the shared validated write path (ADR-0010 §2) is untouched. I2/I3/I4/I8:
untouched (no layout recompute, no UI-thread work, no schema change). No invariant is traded
for convenience.