Skip to content

MCP server

The CLI stays the primary interface. The MCP server is a second, additive surface for hosts that speak MCP natively but expose no shell/exec tool to the agent (Claude Desktop outside of Code, several IDE-integrated assistants), and for subagent-to-subagent handoffs where brokering every call through a parent agent's shell is itself the friction. Use the CLI directly whenever your harness already shells out to corvee ...; reach for the MCP server only when it can't.

Install

The MCP dependency is an optional extra, not part of the base install:

uv tool install 'corvee[mcp]'

If corvee is already installed without it:

uv tool install --upgrade 'corvee[mcp]'

Start the server

corvee mcp serve

With no flags at all, this auto-detects a project exactly the way the CLI does: it walks up from the server's own working directory looking for .corvee/config.toml, the same lookup corvee task list does. If your MCP host lets you set the spawned process's working directory (most host configs do, via a cwd field, or you can wrap the command in cd /path/to/your/project && corvee mcp serve), that's all you need.

If your host doesn't set the working directory correctly — Claude Desktop, for one, commonly launches from / — use --project-root as an explicit override:

corvee mcp serve --project-root /path/to/your/project

--project-root is resolved once, at startup, and the server refuses to start if it's given but doesn't resolve to a valid project. Auto-detection never fails that hard: if nothing is found at cwd, the server falls back to global-only mode below instead of refusing to start.

Either way, the server prints one line to stderr naming what it resolved (the project's path, or that it's in global-only mode) before it starts accepting calls, and folds the same into the instructions it hands the MCP client. Neither touches stdout, which stays pure JSON-RPC framing.

--actor works like the CLI's own --actor flag or $CORVEE_ACTOR (see Quickstart): a stable identity for whatever is running this server, read once at startup.

corvee mcp serve --actor agent:claude

--session-id (or $CORVEE_SESSION_ID) sets the server's default session id, used by any write tool call that leaves out its own session_id argument. Omit both and the server mints one itself, unique to that process — every write tool still has something to stamp on the events it writes even if you never touch this flag.

corvee mcp serve --session-id session-42

Run one server process per agent conversation. A single process can technically outlive one conversation, but everything it writes shares one actor identity for as long as it's alive — two unrelated conversations sharing a process share claim ownership, the same collision two people sharing one $CORVEE_ACTOR on a machine would hit (see Specification §10.1). If your host restarts the server per conversation already, you get this for free.

Working without a project

Global-only mode is reached either way: no project found at cwd with --project-root omitted, or explicitly by pointing the host at a directory with no .corvee/config.toml anywhere above it. It's the MCP equivalent of corvee --global task add ... from such a directory:

corvee mcp serve

In this mode only TASK-GLOBAL-<n>/FACT-GLOBAL-<n> ids and global-scoped calls work; anything that needs a local project fails with a clear error.

Configure your host

Register the command once and it works across every project: both hosts below spawn the server with your terminal's own working directory as its cwd, which auto-detection (above) then walks up from — no --project-root needed, and nothing to re-register when you switch projects.

Claude Code

claude mcp add --transport stdio --scope user corvee -- corvee mcp serve

--scope user registers it once for every project rather than just the one you're in when you run this. Verify it registered with claude mcp list or claude mcp get corvee — see claude mcp add --help for the full set of scopes.

Codex CLI

codex mcp add corvee -- corvee mcp serve

Or add the equivalent block directly to ~/.codex/config.toml:

[mcp_servers.corvee]
command = "corvee"
args = ["mcp", "serve"]

Confirm it loaded with codex mcp list or /mcp inside a Codex session.

Desktop apps

Claude Desktop and Codex Desktop are a different case: both are known to spawn stdio MCP servers with cwd set to / or your home directory rather than whatever project you have open, so auto-detection never finds anything there. Pass --project-root explicitly for these:

corvee mcp serve --project-root /path/to/your/project

Since a desktop app config can't vary "the project you currently have open," this only really works for one project at a time per registered entry — register one corvee-named entry per project if you work on several, or prefer a terminal-based host instead.

Any other host

Most hosts read a JSON config naming the command to spawn. The shape is host-specific — check your host's own docs for where this file lives — but the entry itself is just the command and its arguments:

{
  "mcpServers": {
    "corvee": {
      "command": "corvee",
      "args": ["mcp", "serve"],
      "cwd": "/path/to/your/project"
    }
  }
}

If your host's config has no cwd field, pass --project-root explicitly instead:

{
  "mcpServers": {
    "corvee": {
      "command": "corvee",
      "args": ["mcp", "serve", "--project-root", "/path/to/your/project"]
    }
  }
}

What it exposes

Sixteen tools, narrow and single-purpose rather than one wide task_update (see Specification §10.4 for why). Every tool that writes something accepts an optional session_id argument — a per-conversation token, distinct from the actor set at startup (see Specification §10.1) — falling back to the server's own default (--session-id/$CORVEE_SESSION_ID, or a generated id) when a call leaves it out. If you do pass your own, keep it stable across your calls in one conversation.

Tool Writes? What it does
task_show no Read one task by id
task_search no Search tasks (default 20 results)
brief no Session-start snapshot: mine, ready, stale, labels
task_add yes File a new task (never claims it)
task_claim yes Claim a task (force to steal someone else's)
task_unclaim yes Release a claim (force to release someone else's stale one)
task_comment yes Leave a note on a task
task_start yes Claim and move to in_progress
task_done yes Move to done
task_cancel yes Move to cancelled
task_review yes Move to review, keeping the current claim
task_block yes Move to blocked — requires a comment explaining why
fact_show no Read one fact by id
fact_search no Search facts (default 20 results)
fact_add yes Record a new fact
fact_verify yes Mark a fact checked-true, with proof

force lives only on task_claim/task_unclaim — not on task_start/ task_done/task_cancel/task_review/task_block, whose names should never quietly let a caller steal someone else's claim. Call task_claim(force=true) first if you need to take one over.

No purge, delete, import, or export on this surface — those stay CLI-only. Nor is there a task_update/task_label/task_link/ task_assign, or a fact_revise/fact_retract/fact_unverify: once a task or fact is filed, this surface can only work it forward (claim, comment, transition it, verify a fact) or leave it as-is, never correct a mistake in its title, description, claim, or labels/links. Reach for the CLI (or a human) for that. See Specification §10.3 for full argument shapes and the reasoning behind each tool.

Responses

A successful call's return value lands in the tool result's structuredContent. Most tools return one object — task_claim, for instance:

{
  "id": "TASK-14",
  "title": "Fix the flaky auth test",
  "description": "repro: run make test twice in a row",
  "type": "bug",
  "priority": "high",
  "state": "open",
  "claimed_by": "agent:claude",
  "claimed_at": "2026-01-01T00:00:00.000Z",
  "assigned_to": null,
  "created_at": "2026-01-01T00:00:00.000Z",
  "updated_at": "2026-01-01T00:00:00.000Z",
  "scope": "local"
}

fact_search/task_search return an object too, not a bare array — the matches live under result, alongside omitted, a count of further matches limit cut off (0 when nothing was cut off):

{
  "result": [
    {"id": "TASK-14", "title": "Fix the flaky auth test", "...": "..."}
  ],
  "omitted": 0
}

Errors

A failed call comes back as an isError tool result carrying the same JSON body a CLI caller gets on stderr, plus one field the CLI's exit status normally covers:

{
  "error": {
    "code": "claim_conflict",
    "message": "TASK-14 is claimed by agent:other",
    "exit_code": 4
  }
}

exit_code maps to the same exit codes the CLI documents. No raw traceback is ever passed through — an unexpected error still comes back in this shape, with exit_code: 1.

A claim_conflict carries one more field, hint, phrased for this surface rather than the CLI's own --force flag:

{
  "error": {
    "code": "claim_conflict",
    "message": "TASK-14 is claimed by agent:other",
    "exit_code": 4,
    "hint": "call task_claim(force=true) on this ref to steal the claim, then retry"
  }
}

Learn the rest

Specification §10 is the exact contract: process model, output shape, every tool's arguments, and the testing plan.