Download
Docs/Drive the deck/MCP server

MCP server

A zero-dependency stdio MCP server exposing every Deck API operation as a tool, with the annotations a client's approval policy keys on.

Raw markdown

Register it

scripts/mandeck-mcp.mjs is a stdio MCP server exposing the same operations as tools, for agents that are MCP clients — Claude Code, Codex, Gemini CLI, Cursor. A bot on a shell uses the CLI instead.

{ "mcpServers": { "mandeck": { "command": "node",
    "args": ["/path/to/mandeck-mcp.mjs"] } } }

A running deck serves its own copy at GET /cli/mandeck-mcp.mjs. It is Node with no dependencies, and it reads the same token file as everything else.

It is dual-era: it speaks spec 2026-07-28 (stateless, per-request _meta, server/discover) and still answers a legacy initialize.

Approving tools

Consent lives in the client, not the protocol. Every tool carries annotations, and the profile to install is *read = free, act = ask, close = only your own panes* — and the last one is enforced by the deck (403 without force), not by trust.

  • Claude Code settings.json: "permissions": {"allow": ["mcp__mandeck__deck_sessions", "mcp__mandeck__deck_screen", "mcp__mandeck__deck_messages", "mcp__mandeck__deck_jobs", "mcp__mandeck__deck_job_state", "mcp__mandeck__deck_job_wait", "mcp__mandeck__deck_job_report", "mcp__mandeck__deck_events", "mcp__mandeck__deck_agents", "mcp__mandeck__deck_health"]} — everything else prompts (Claude Code does not auto-approve on annotations).
  • Codex CLI config.toml: [mcp_servers.mandeck] default_tools_approval_mode = "writes" prompts only for tools not marked read-only.
  • Gemini CLI policy: a rule on toolAnnotations = { readOnlyHint = true }allow.

destructiveHint only means something when readOnlyHint is false; a read tool is trivially idempotent; and this deck is one machine, so openWorldHint is always false.

The tools

tools/list is deterministic and follows the route order, so a client can cache it. In the table below read is readOnlyHint, destructive is destructiveHint, and act is everything else — additive, worth an ask.

ToolAnnotationWhat it does
deck_sessionsreadList every pane on the deck: id (address), pane (stable uuid), agent, cwd, state (focused/needsYou/live)
deck_screenreadWhat a pane's terminal shows right now (ANSI-stripped tail)
deck_messagesreadA pane's structured transcript (falls back to screen when the agent has no transcript reader)
deck_senddestructiveType text into a pane. submit=true (default) presses a real Return; false types without submitting (use before slash-command menus)
deck_keydestructivePress a control key in a pane: ctrl-c, escape, ctrl-d, up, down, enter, or a digit 0-9 (digits answer TUI pickers)
deck_spawnactStart an agent CLI in a new pane (slugs from deck_agents; 'shell' for a bare shell — a shell pane does NOT run the prompt, it only records it). cwd is required (no default home directory). Refuses a duplicate live pane for the same agent+cwd unless force=true (409 carries the existing session/pane/job). Returns {session, pane, job}. A spawn with a prompt opens a follow-up contract (needs-review is not job done)
deck_jobsreadTasked-spawn jobs, newest first: id, state (running|waiting-you|done|failed), session, pane, agent, cwd, live
deck_job_statereadJob lifecycle state (running | waiting-you | done | failed). Distinct from pane live/needsYou. done/failed only after an explicit protocol signal, never from silence or needs-review
deck_job_waitreadPoll a job until done, failed, waiting-you, or timeout. Does not settle the follow-up contract. Returns the job JSON plus a wait field (done|failed|waiting-you|timeout)
deck_job_reportreadTranscript (or screen fallback) for a job, for a bot to paste. Reading it settles the follow-up contract
deck_job_doneactExplicitly finish a job as done — with the [mandeck job-done] line, the only way a job ends. Idempotent; 409 if the job already failed
deck_job_failactExplicitly finish a job as failed. Idempotent; 409 if the job is already done
deck_closedestructiveClose a pane. Refused (403, with the pane uuid) when the pane was not spawned through the API — the human opened it — unless force=true
deck_focusactBring a pane to the human's screen
deck_agentsreadAgent CLIs installed on this Mac (the only valid spawn slugs)
deck_eventsreadAppend-only needs-you/spawned/needs-review/done/failed event log; poll with the last seq instead of diffing sessions. done/failed are job-terminal, not follow-up settle
deck_tabsreadThe deck's tabs (workspaces): index, title, pane count
deck_historyreadPast sessions on this deck, searchable by title/cwd/agent
deck_resumeactReopen a past session (ids from deck_history) in a new pane
deck_projectsreadRecent project folders on this Mac
deck_accountsreadProvider accounts this deck can spend
deck_create_accountactEnsure a provider account exists (idempotent)
deck_healthreadIs a deck here? Pane count + TCC permission state (no auth needed)

deck_job_wait polls the job only, so it does not settle the spawn's follow-up contract; deck_job_report does, because that is a real check. See Jobs & hand-offs.