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.
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.
| Tool | Annotation | What it does |
|---|---|---|
deck_sessions | read | List every pane on the deck: id (address), pane (stable uuid), agent, cwd, state (focused/needsYou/live) |
deck_screen | read | What a pane's terminal shows right now (ANSI-stripped tail) |
deck_messages | read | A pane's structured transcript (falls back to screen when the agent has no transcript reader) |
deck_send | destructive | Type text into a pane. submit=true (default) presses a real Return; false types without submitting (use before slash-command menus) |
deck_key | destructive | Press a control key in a pane: ctrl-c, escape, ctrl-d, up, down, enter, or a digit 0-9 (digits answer TUI pickers) |
deck_spawn | act | Start 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_jobs | read | Tasked-spawn jobs, newest first: id, state (running|waiting-you|done|failed), session, pane, agent, cwd, live |
deck_job_state | read | Job 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_wait | read | Poll 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_report | read | Transcript (or screen fallback) for a job, for a bot to paste. Reading it settles the follow-up contract |
deck_job_done | act | Explicitly 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_fail | act | Explicitly finish a job as failed. Idempotent; 409 if the job is already done |
deck_close | destructive | Close 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_focus | act | Bring a pane to the human's screen |
deck_agents | read | Agent CLIs installed on this Mac (the only valid spawn slugs) |
deck_events | read | Append-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_tabs | read | The deck's tabs (workspaces): index, title, pane count |
deck_history | read | Past sessions on this deck, searchable by title/cwd/agent |
deck_resume | act | Reopen a past session (ids from deck_history) in a new pane |
deck_projects | read | Recent project folders on this Mac |
deck_accounts | read | Provider accounts this deck can spend |
deck_create_account | act | Ensure a provider account exists (idempotent) |
deck_health | read | Is 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.