Repository navigation
feat: genie mcp server — Warp + Claude Code consume genie state - #2508
Conversation
hand-rolled stdio server satisfies real claude code; contract pinned for g2
Group 3 of genie-mcp. genie init (and launch per worktree) JSON-merge-register the genie mcp server into project .mcp.json + .warp/.mcp.json under mcpServers: preserve all other servers + top-level keys, byte-identical rerun, malformed- JSON-safe (throws, no clobber), alt-wrapper-key aware. Command uses the absolute process.execPath (correct for the shipped compiled binary; bare genie is not on PATH). README documents the pull-not-push tab-info limitation. Reviewed: SHIP. 14 init tests, 21 launch tests.
Group 2 of genie-mcp. Hand-rolled newline-delimited JSON-RPC 2.0 stdio server (no MCP SDK, genie stays 4-deps-lean) exposing genie.db READ-ONLY via 5 tools: genie_board, genie_wish_status, genie_worktree_context (resolves branch wish/<slug>-<group>), genie_task, genie_active. Net-new new Database(readonly) open (not openSqlite) degrades to empty board on absent db; mcp-tools is await-import'd so non-mcp paths never load bun:sqlite (graph-walk probe). mcp added to WORKSPACE_EXEMPT (v5 self-resolving, like task/board/launch/omni). Reviewed: SHIP. 14 mcp tests, 599 full suite, dist protocol verified.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements a zero-dependency, read-only Model Context Protocol (MCP) server over stdio via the genie mcp command, exposing the repository's task and board state to Warp and Claude Code. The implementation includes lazy-loading of database dependencies to prevent overhead on non-MCP paths, automatic registration during initialization, and comprehensive test coverage. Feedback on the implementation focuses on strict compliance with the JSON-RPC 2.0 and MCP specifications, specifically regarding returning proper parse errors for invalid JSON, wrapping tool execution to return MCP-compliant error payloads instead of protocol-level internal errors, and correctly distinguishing notifications from requests with a null ID.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let req: JsonRpcRequest; | ||
| try { | ||
| req = JSON.parse(trimmed) as JsonRpcRequest; | ||
| } catch { | ||
| // Unparseable line → cannot attribute an id; drop it (no id to reply to). | ||
| return; | ||
| } |
There was a problem hiding this comment.
According to the JSON-RPC 2.0 specification, if the server receives invalid JSON (a parse error), it MUST return a Parse Error response (code: -32700) with id: null. Currently, the server silently drops unparseable lines and returns nothing, which can cause the client to hang indefinitely waiting for a response.
Please update the catch block to send a proper JSON-RPC error response, matching the behavior of the G1 spike server.
| let req: JsonRpcRequest; | |
| try { | |
| req = JSON.parse(trimmed) as JsonRpcRequest; | |
| } catch { | |
| // Unparseable line → cannot attribute an id; drop it (no id to reply to). | |
| return; | |
| } | |
| let req: JsonRpcRequest; | |
| try { | |
| req = JSON.parse(trimmed) as JsonRpcRequest; | |
| } catch (e) { | |
| err(null, -32700, "Parse error: " + (e instanceof Error ? e.message : String(e))); | |
| return; | |
| } |
| const payload = tool.handler(ctx, args); | ||
| ok(id, { | ||
| content: [{ type: 'text', text: JSON.stringify(payload) }], | ||
| structuredContent: payload, | ||
| isError: false, | ||
| }); |
There was a problem hiding this comment.
According to the Model Context Protocol (MCP) specification, if a tool execution fails due to a runtime error (e.g., a database query failure or a git command error), the server should return a successful JSON-RPC response with isError: true and the error details in the content array, rather than a JSON-RPC protocol error (INTERNAL_ERROR).
Currently, any error thrown inside tool.handler will propagate to the top-level dispatch catch block and trigger a protocol-level INTERNAL_ERROR response, which can cause some MCP clients to disconnect or treat the entire server as unhealthy.
Wrapping the tool.handler call in a try...catch block and returning isError: true ensures robust error handling and strict compliance with the MCP specification.
try {
const payload = tool.handler(ctx, args);
ok(id, {
content: [{ type: 'text', text: JSON.stringify(payload) }],
structuredContent: payload,
isError: false,
});
} catch (e) {
ok(id, {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'tool_execution_failed',
message: e instanceof Error ? e.message : String(e),
}),
},
],
isError: true,
});
}| // A message with no id (or null id) is a notification: process side effects | ||
| // but send NO reply (e.g. notifications/initialized). | ||
| const isNotification = req.id === undefined || req.id === null; | ||
| const id = req.id ?? null; |
There was a problem hiding this comment.
In JSON-RPC 2.0, a Notification is strictly defined as a Request object without an id member (i.e., id is undefined). If id is present but is null, it is technically a Request (albeit discouraged), and the server MUST send a response with id: null. Treating id: null as a notification (and thus sending no reply) violates the specification.
| // A message with no id (or null id) is a notification: process side effects | |
| // but send NO reply (e.g. notifications/initialized). | |
| const isNotification = req.id === undefined || req.id === null; | |
| const id = req.id ?? null; | |
| // A message with no id is a notification: process side effects | |
| // but send NO reply (e.g. notifications/initialized). | |
| const isNotification = req.id === undefined; | |
| const id = req.id ?? null; |
Scope hygiene (final review): the omni-approval-ux WISH draft was accidentally committed into the G1 spike commit; it is unrelated to genie-mcp and not on dev. Removed so PR #2508 carries only genie-mcp. Content preserved for the separate omni-approval-ux revise.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d5d2327021
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const parsed = branch ? parseWishBranch(branch) : null; | ||
|
|
||
| if (parsed) { | ||
| const tasks = ctx.db ? listTasks(ctx.db, { wish: parsed.wish }).filter((t) => t.group === parsed.group) : []; |
There was a problem hiding this comment.
Map the synthetic main group back to ungrouped tasks
When genie launch handles tasks with no group, it launches them under the synthetic group name main and branch wish/<slug>-main, but the DB rows still have group === null. In that normal ungrouped-wish case, genie_worktree_context resolves the branch and then filters for t.group === 'main', so the pane receives an empty task list even though it was launched with ready tasks. Treat parsed main as null for the launch-created default group, or otherwise keep the branch/group encoding consistent with the stored task rows.
Useful? React with 👍 / 👎.
| const dash = rest.lastIndexOf('-'); | ||
| if (dash <= 0 || dash === rest.length - 1) return null; | ||
| return { wish: rest.slice(0, dash), group: rest.slice(dash + 1) }; |
There was a problem hiding this comment.
Preserve hyphenated group names when parsing branches
This parser assumes the last - separates slug from group, but genie launch accepts hyphens in both slugs and group names and creates branches as wish/${slug}-${group} without escaping. For a valid group such as api-ui, branch wish/payment-api-ui is resolved as wish payment-api and group ui, so genie_worktree_context returns the wrong wish/group and usually no tasks for hyphenated groups. Use an unambiguous encoding or resolve against known wish/group rows instead of splitting on the last dash.
Useful? React with 👍 / 👎.
Summary
Genie's live state (wishes, tasks, board, what each worktree is working on) was invisible to Warp and to the agents running inside it. This adds a
genie mcpstdio MCP server that exposesgenie.dbread-only as MCP tools, auto-registered into the project.warp/.mcp.json+.mcp.jsonbygenie init/launch— so Warp and Claude Code (any MCP client) can read genie's live state, and each Warp pane's agent can ask "what wish/group am I here for." Executes wishgenie-mcp— brainstorm → design → wish, each independently reviewed.The correction behind this
The original "richer Warp integration" ask assumed a Warp plugin with stateful tabs. The real answer (from Warp's docs): Warp's plugin surface is MCP, and it's pull, not push — Warp has no external API to write tab titles. So genie exposes state via MCP and the pane's agent asks. Bigger than Warp: the same server gives every Claude Code session live genie state.
What shipped (3 groups, spike-first)
.warp/.mcp.json/.mcp.jsonschema + theprocess.execPathcommand form.genie mcp: the hand-rolled server + 5 read-only tools (genie_board,genie_wish_status,genie_worktree_context(resolves branchwish/<slug>-<group>),genie_task,genie_active). Net-newnew Database(readonly)open degrades to an empty board on absent db;mcp-toolsisawait import-ed so non-mcp paths never loadbun:sqlite(real graph-walk probe).init/launchJSON-merge-register the server into both config files, preserving existing servers/keys, byte-identical rerun, malformed-JSON-safe; absolute command viaprocess.execPath.Needs your eyes — Warp live-UI connect
The spike confirmed the schema, the MCP protocol, and Claude Code end-to-end (both boxes), but Warp's GUI can't be driven over ssh. To close the last criterion: open Warp on your Mac → SSH
felipe→ a genie-init'd repo → Settings → Agents → MCP servers → expectgeniedetected + green. A scratch repo is staged at~/genie-mcp-spike/on felipe (safe torm -rfafter).Gates
Full
bun run check(599 tests) + build green; the dist server's protocol verified end-to-end (initialize → tools/list → tools/call); the full init→register→serve loop proven.Follow-ups (noted, non-blocking)
process.execPathresolves tobun(correct for the shipped compiled binary; self-evident in dev) — optionalBun.mainhardening.Merge decision: Felipe.