Skip to content

feat: genie mcp server — Warp + Claude Code consume genie state - #2508

Merged
namastex888 merged 3 commits into
devfrom
wish/genie-mcp
Jul 3, 2026
Merged

namastex888 merged 3 commits into
devfrom
wish/genie-mcp

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

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 mcp stdio MCP server that exposes genie.db read-only as MCP tools, auto-registered into the project .warp/.mcp.json + .mcp.json by genie 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 wish genie-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)

  • G1 spike — verdict hand-rolled: a zero-dep newline-delimited JSON-RPC 2.0 stdio server satisfies real Claude Code end-to-end and speaks MCP 2024-11-05 (Warp's protocol). genie stays 4-deps-lean — no MCP SDK. Pinned the .warp/.mcp.json/.mcp.json schema + the process.execPath command form.
  • G2 — genie mcp: the hand-rolled server + 5 read-only tools (genie_board, genie_wish_status, genie_worktree_context (resolves branch wish/<slug>-<group>), genie_task, genie_active). Net-new new Database(readonly) open degrades to an empty board on absent db; mcp-tools is await import-ed so non-mcp paths never load bun:sqlite (real graph-walk probe).
  • G3 — init/launch JSON-merge-register the server into both config files, preserving existing servers/keys, byte-identical rerun, malformed-JSON-safe; absolute command via process.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 → expect genie detected + green. A scratch repo is staged at ~/genie-mcp-spike/ on felipe (safe to rm -rf after).

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)

  • Dev-mode process.execPath resolves to bun (correct for the shipped compiled binary; self-evident in dev) — optional Bun.main hardening.
  • MCP WRITE tools + Codex TOML wiring are later wishes (this is read-only, Warp + Claude Code).

Merge decision: Felipe.

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.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 322591c7-4f00-4450-989c-79d21b520cca

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wish/genie-mcp

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/term-commands/mcp.ts
Comment on lines +140 to +146
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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;
}

Comment thread src/term-commands/mcp.ts
Comment on lines +94 to +99
const payload = tool.handler(ctx, args);
ok(id, {
content: [{ type: 'text', text: JSON.stringify(payload) }],
structuredContent: payload,
isError: false,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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,
      });
    }

Comment thread src/term-commands/mcp.ts
Comment on lines +103 to +106
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
// 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;

@namastex888
namastex888 merged commit 8055a5b into dev Jul 3, 2026
11 checks passed
namastex888 added a commit that referenced this pull request Jul 3, 2026
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/lib/v5/mcp-tools.ts
const parsed = branch ? parseWishBranch(branch) : null;

if (parsed) {
const tasks = ctx.db ? listTasks(ctx.db, { wish: parsed.wish }).filter((t) => t.group === parsed.group) : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/lib/v5/mcp-tools.ts
Comment on lines +108 to +110
const dash = rest.lastIndexOf('-');
if (dash <= 0 || dash === rest.length - 1) return null;
return { wish: rest.slice(0, dash), group: rest.slice(dash + 1) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant