-
Notifications
You must be signed in to change notification settings - Fork 1
Mcp Parity
Thatch supports three integration paths sharing one core (src/tool-defs.ts):
- OpenCode plugin — runs inside opencode's Bun runtime. Full access to plugin hooks: system prompt injection, session events, in-process tool buffering, compaction context, and skill installation.
-
Claude Code MCP server — runs as a stdio JSON-RPC process. Tools are
exposed via MCP
tools/list+tools/call. Session behavior is driven by Claude Code hooks (SessionStart,PostToolBatch,UserPromptSubmit). -
Cursor MCP server — same stdio MCP server as Claude Code. Session
behavior is driven by Cursor hooks (
sessionStart,postToolUse,beforeSubmitPrompt) in a flathooks.jsonformat.
This document maps the feature parity and documents the remaining gaps.
| Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
|---|---|---|---|---|
| Tools (single source of truth) | Plugin tool registration, thatch_ prefix |
MCP tools/list + tools/call, mcp__thatch__ prefix (opencode-only tools filtered out) |
MCP tools/list + tools/call, mcp__thatch__ prefix (opencode-only tools filtered out) |
Full for the shared tools; get_session_info (session identity) and session_search/session_get (conversation archaeology) are opencode-only — MCP hosts have no session concept. The thatch session CLI subcommands cover the same archaeology on any host with an opencode.db file. The watch_* tools (PR, branch, and command watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel. The chat_* tools (cross-session chat) are opencode-only — no host session identity and no wake-up channel |
| System prompt (store names, usage rules) |
experimental.chat.system.transform — dynamic, repo baked in at runtime |
CLAUDE.md static text appended by thatch setup; repo auto-detected by the MCP server at startup |
AGENTS.md static text appended by thatch setup; repo auto-detected at startup |
Approximate — static text persists through compaction, so the agent retains the usage instructions, but the dynamic per-turn refresh is lost |
| Session-start reminder (recall nudge + hygiene heartbeat) |
session.created event → client.session.prompt injects a synthetic message |
SessionStart hook → thatch reminder; stdout becomes context |
sessionStart hook → thatch reminder --json; output is additional_context JSON |
Full |
| Compaction context (re-familiarize after compaction) |
experimental.session.compacting hook appends context to the compaction output |
PostCompact hook — side-effects only, cannot inject context
|
No equivalent hook | Gapped — see below |
Wrap-up commands (/thatch/compact, /thatch/exit) |
Command markdown synced to $XDG_CONFIG_HOME/opencode/command/thatch/ at plugin init; command.execute.before arms the session and the next idle resolves the greenlight token into a TUI action (session.compact via executeCommand, app.exit via publish) |
No equivalent — no plugin host to arm the check or trigger the action | No equivalent | Gapped — needs the plugin's SDK client and TUI control routes |
On-demand actions (/thatch/defrag, extract, hygiene, reflect, refine) |
Command markdown synced at plugin init; bodies rendered from the shared prompt cores with thatch_ tool spellings; extract is opencode-only (needs session identity via get_session_info and the plugin's payload provider) |
Command markdown synced by thatch setup --claude into <claudeDir>/commands/thatch/; same cores with mcp__thatch__ spellings; extract excluded |
MCP Prompts (prompts/list + prompts/get) from the same cores; Cursor surfaces them as slash commands; extract excluded |
Full for the shared actions (same cores, per-host tool spellings); extract opencode-only |
| Extraction (buffer tool calls, extract durable facts) |
tool.execute.after buffers non-meta tool calls in-process (isMetaToolName in src/extraction.ts: thatch_*, skill, task, agent, subagent — execute calls unwrapping only thatch tools are also excluded); direct extraction is the primary path — when the parent session goes idle with pending buffer entries, the plugin creates a child session via the SDK (client.session.create with parentID) and prompts it to call get_extraction_payload (nudge suppressed via the extracting set); the chat.message nudge path is a fallback used when direct extraction throws |
PostToolBatch → thatch buffer-batch (file-backed JSONL queue); UserPromptSubmit → thatch flush-tools peeks the queue and prints the nudge with session ID + fetch tool name (does not drain — the queue persists until a memory write or extraction_done) |
postToolUse → thatch buffer-tool (single-tool, file-backed queue); beforeSubmitPrompt → thatch flush-tools --json
|
Partial — opencode uses direct extraction (parent idle → child session) as the primary path, with the nudge as fallback; MCP hosts are nudge-only. Both paths use get_extraction_payload to fetch the queued interactions as a tool response, keeping the full payload out of the main session's context window. Nudge timing differs: the nudge arrives at the start of the next turn in Claude Code/Cursor; opencode's direct extraction fires at end of turn (session idle), and the nudge fallback fires at the start of the next turn like MCP |
| Extraction feedback (toast notifications) |
client.tui.showToast fires when the extraction child goes idle — shows [thatch] new: N, updated: M, deleted: K (success) or extraction complete — nothing to save (info); best-effort, silently ignored in headless mode |
No TUI connection — MCP hosts have no equivalent | No TUI connection | Gapped — MCP hosts cannot show toast notifications |
| Automatic alerts (notify on LLM pause / turn end) | Plugin event hook drives the alerts.ts state machine: question.asked/permission.asked → pause alert; session.status idle with a real-work round → done alert; unrecovered errors → needs-attention. Delivered via the shared notify.ts dispatcher, channel per event from the alerts config section |
No event stream — only thatch_notify_user when the agent chooses |
No event stream — only thatch_notify_user when the agent chooses |
Gapped — MCP hosts have no plugin event hook to watch; Claude Code's Notification/Stop hooks could feed the same classifier later |
| Prompt-aware recall nudge |
chat.message hook embeds the prompt with the in-process warm model, searches db.search(), pushes a nudge part if matches ≥ threshold |
UserPromptSubmit hook → thatch flush-tools connects to the MCP server's sideband socket; the warm server embeds + searches; hook prints the nudge or falls back to the write nudge |
beforeSubmitPrompt hook → thatch flush-tools --json (same sideband path) |
Full — sideband socket gives cold hook processes access to the warm MCP server's model |
| Prediction auto-fire (user decision model) |
chat.message hook scores the prompt embedding against prediction matchers; injects a [thatch] User decision model nudge alongside the recall nudge (separate synthetic part, same embedding call) |
UserPromptSubmit hook → thatch flush-tools fires prediction query via the sideband socket's predictions method in parallel with the recall nudge |
beforeSubmitPrompt hook → thatch flush-tools --json (same sideband path) |
Full — same scorePredictionNudge entry point in both paths |
| Behavior auto-fire (self-discipline rules) |
chat.message hook scores the prompt embedding against behavior matchers; injects a [thatch] Situational behaviors nudge (separate synthetic part, same embedding call) |
UserPromptSubmit hook → thatch flush-tools fires behavior query via the sideband socket's behaviors method in parallel with recall and predictions |
beforeSubmitPrompt hook → thatch flush-tools --json (same sideband path) |
Full — same scoreBehaviorNudge entry point in both paths |
| Skills | Installed to $XDG_CONFIG_HOME/opencode/skills at plugin init (shared + opencode-only) |
Installed by thatch setup (shared only) to the repo's .claude/skills/ (local) or $CLAUDE_CONFIG_DIR/skills/ (global) |
Installed by thatch setup (shared only) to the repo's .cursor/skills/ (local) or ~/.cursor/skills/ (global) |
Full — same SKILL.md format; the code-review coordinator is opencode-only (needs sub-agents) |
| Store detection (repo identity from git remote) |
worktree parameter from the opencode plugin |
CLAUDE_PROJECT_DIR env (set by Claude Code for stdio MCP servers) |
CURSOR_PROJECT_DIR then CLAUDE_PROJECT_DIR then cwd |
Full |
| Setup detection at startup | n/a — plugin auto-installs at init |
checkSetup in MCP server: detects missing or broken instructions in CLAUDE.md |
Same as Claude Code (checks AGENTS.md) |
Full — warns the agent to tell the user to run thatch setup
|
| Hook config format | n/a — plugin registers hooks in code | Nested: .claude/settings.json {hooks:{Event:{hooks:[{type,command}]}}}
|
Flat: .cursor/hooks.json {version:1,hooks:{event:[{command}]}}
|
n/a |
The shared tool definitions in src/tool-defs.ts use bare names
(memory_remember, memory_recall, ...). Each host prefixes them differently:
-
opencode:
thatch_memory_remember— the prefix is added by thetool()wrapper insrc/tools.ts. -
Claude Code:
mcp__thatch__memory_remember— Claude Code auto-prefixes with the server name; the MCP server exposesmemory_remember. -
Cursor:
mcp__thatch__memory_remember— same MCP server.
The CLAUDE.md / AGENTS.md instructions generated by thatch setup reference
both forms — the full mcp__thatch__* names for the host, and bare names for
readability.
OpenCode's experimental.session.compacting hook lets the plugin append text
to the compaction output — the agent sees a "you are using thatch, here's what
you've learned" message after compaction. Claude Code's PostCompact hook is
side-effects only (logging, external state); it has no additionalContext
field and no decision control. Cursor has no equivalent hook.
Mitigation: CLAUDE.md / AGENTS.md is loaded at session start and persists
through compaction, so the agent retains the usage instructions. The
SessionStart / sessionStart hook with source: "compact" fires after
compaction in Claude Code, so the recall reminder runs again. There is no
explicit "re-familiarization" nudge for hosts without a compaction hook.
OpenCode's extraction pipeline has two paths — direct extraction (primary) and the nudge (fallback):
Direct extraction (primary path):
-
tool.execute.afterbuffers every non-meta tool call (isMetaToolNameinsrc/extraction.ts) into a per-session in-memory array (max 20 interactions). - When the parent session goes idle (
session.statusidle event) with pending buffer entries, the plugin callstriggerExtraction: creates a child session viaclient.session.create(withparentID), then prompts it viaclient.session.promptAsync(orprompt) with the extraction prompt (the parent's session ID is interpolated into the prompt by the plugin). Theextractingset gates re-triggering while the child runs. - The child session runs the
thatch-fact-extractorskill, writes memories viathatch_memory_remember. The child's payload fetch records its claim; its memory write (orextraction_done) completes that claim - consuming only the entries it received. - When the child goes idle, the handler finalizes the claim-scoped
completion, deletes the child session, and fires a toast via
client.tui.showToast.
Failure recovery (no model-facing nudge exists on opencode):
- If
triggerExtractionthrows (session creation or prompt fails), the catch block clears theextractingflag. The pending buffer is untouched, and the next idle re-triggers extraction - the plugin retries without any model cooperation. - If the child errors or is deleted before completing, what it held is requeued to pending. If its completion signal never comes at all, the 15-minute stale reaper returns accepted entries to pending. Either way the next idle re-extracts - no interaction is silently lost.
Claude Code and Cursor use a file-backed queue (src/extract-queue.ts)
because hooks fire one-shot with no cross-call state:
-
PostToolBatch(Claude Code) /postToolUse(Cursor) appends each tool interaction to a per-session JSONL file under$XDG_CACHE_HOME/thatch/queue/(max 20, oldest dropped). These commands are silent — no stdout — so the agent loop is not delayed. -
UserPromptSubmit/beforeSubmitPromptrunsthatch flush-tools, which peeks the queue (does not drain it) and prints the JSON payload. The queue persists until the agent writes a memory or callsthatch_extraction_done, so ignored nudges accumulate and escalate (polite → insistent → ALL-CAPS) via the file-backed missed-nudge counter. This arrives at the start of the next turn (before the model processes the prompt), not at the end of the current one.
The two paths produce the same JSON shape via buildExtractionPayload(), so
the fact-extractor skill receives an identical contract regardless of host.
The prompt-aware recall nudge needs the embedding model to embed the user's
prompt text. In opencode, the model is warm in-process — the chat.message
hook calls model.queryEmbed() directly. In Claude Code/Cursor, hooks are
one-shot bun spawns that can't reach the MCP server's in-memory model.
Loading the ~34 MB model on every UserPromptSubmit would add ~300-700 ms to
every prompt in the critical path before the agent responds. The sideband
socket eliminates this cost; see the next section.
Claude Code / Cursor session
├── MCP server (long-lived, warm model)
│ ├── stdio JSON-RPC (tool calls)
│ └── Unix socket sideband (embed + search for hooks)
├── UserPromptSubmit / beforeSubmitPrompt hook → thatch flush-tools [--json]
│ ├── peeks the file-backed extraction queue (does not drain)
│ ├── connects to sideband socket
│ ├── sends prompt text
│ ├── receives match labels + scores
│ └── prints recall nudge or falls back to write nudge
├── PostToolBatch → thatch buffer-batch (Claude Code)
│ └── postToolUse → thatch buffer-tool (Cursor)
└── SessionStart / sessionStart hook → thatch reminder [--json]
└── reads the hook's stdin session id: chat identity + mail line
The socket path is derived from a SHA-256 hash of the DB path — both the MCP
server and hook processes resolve the same DB path independently (from
THATCH_DB_PATH or the default under XDG_CONFIG_HOME), so they arrive at the
same socket path without out-of-band coordination. The path lives under
os.tmpdir().
The protocol is newline-delimited JSON: one request per connection, one
response. Requests are {"method":"match|predictions|behaviors", "text":"...","stores":[...],"threshold":N,"limit":N}. Responses are
{"ok":true,"matches":[...]} / {"ok":true,"predictions":[...]} /
{"ok":true,"behaviors":[...]} or {"ok":false,"error":"..."}.
Graceful degradation: if the socket isn't available (MCP server not running,
old version, stale socket from a crash, or a >2 s timeout), sidebandMatch
returns null and flush-tools falls back to the static write nudge. The
recall nudge is best-effort — its absence never blocks the agent's workflow. A
stale socket file left by a crash is cleaned up on connection error.
The shared tool definitions in src/tool-defs.ts are the single source of
truth for all three paths. The opencode plugin wraps them in tool() with zod
schemas and a thatch_ prefix; the MCP server (used by both Claude Code and
Cursor) wraps them in z.object() for validation and z.toJSONSchema() for
the protocol response. See docs/dev/setup-and-hooks.md for the concrete
files, hook event names, and command shapes each host writes.
User
- Guide: Behavior Engine
- Guide: Cli
- Guide: Code Review
- Guide: Commands
- Guide: Cross Session Chat
- Guide: Deduplication
- Guide: Default Behaviors
- Guide: Extraction
- Guide: Hygiene
- Guide: Memory
- Guide: Notifications
- Guide: Prediction Engine
- Guide: Overview
- Guide: Setup
- Guide: Skills
- Guide: Watchers
Developer
Dev Feature Guides
- Feature: Behavior Engine
- Feature: Cicd
- Feature: Cli
- Feature: Commands
- Feature: Compaction Recovery
- Feature: Cross Session Chat
- Feature: Database
- Feature: Deduplication
- Feature: Extraction
- Feature: Hygiene
- Feature: Memory Store
- Feature: Multi Host
- Feature: Notifications
- Feature: Nudge Pipeline
- Feature: Opencode Plugin
- Feature: Prediction Engine
- Feature: Qa System
- Feature: Overview
- Feature: Repo Identity
- Feature: Session Lifecycle
- Feature: Setup
- Feature: Sideband
- Feature: Watchers