--- title: Driving vincent from an agent (MCP) --- # Driving vincent from an agent The daemon speaks the **Model Context Protocol** at `http://127.0.0.1:{port}/mcp`, on the same listener as the [REST API](../reference/api.md) and behind the same bearer token. Point any MCP client at it and it gets the whole vincent API as tools — with discovery, argument schemas and typed errors, instead of curl and a guess. ## Connecting Read the port from `{data_dir}/daemon.json` and the token from `{data_dir}/token`, then configure your client with a streamable-HTTP server: ```json { "mcpServers": { "vincent": { "type": "http", "url": "http://127.0.0.1:7777/mcp", "headers": { "Authorization": "Bearer " } } } } ``` `vincent daemon status` prints the port. There is no TLS and no second auth story: this is loopback, and the token is the one every other client uses. ## What the tools are The tool list **is** the route table: projects, workflows, `resolve`, tasks and every human action (`task_cancel`, `task_pause`, `task_approve`, `task_answer`, `task_archive`, `task_follow_up`, …), steps, step status, transcripts, diffs, the GitHub reads, the [trigger](triggers.md) reads, `trigger_validate` and both dry runs, the [issue](../reference/api.md#issues) routes (`issue_list`, `issue_create`, `issue_get`, `issue_patch`, `issue_close`, `issue_reopen`, `issue_comments`, `issue_comment`, `project_issue_labels`), the issue import status `project_issue_sync_status`, and the read-only `health`, `info`, `config_get`, `agent_list`, `doctor`, `orphan_list`. Twenty-four routes are deliberately **not** tools: - `POST /v1/daemon/stop` - `POST /v1/agents/{name}/quota` - `POST /v1/daemon/backup` - `DELETE /v1/projects/{id}` - `DELETE /v1/tasks/{id}` - `DELETE /v1/chats/{id}` - `DELETE /v1/issues/{id}` - `POST /v1/projects/{id}/issues/sync` - `POST /v1/tasks/import` - `POST /v1/maintenance/gc` - `POST /v1/doctor/fix` - `PATCH /v1/config` - `POST /v1/workflows` - `PATCH /v1/workflows` - `POST /v1/triggers` - `PATCH /v1/triggers/{id}` - `DELETE /v1/triggers/{id}` - `POST /v1/triggers/{id}/events` - `POST /v1/tasks/{id}/github/pull/create` - `POST /v1/tasks/{id}/github/pull/merge` - `POST /v1/tasks/{id}/github/pull/close` - `POST /v1/tasks/{id}/github/pull/reopen` - `POST /v1/tasks/{id}/github/pull/comment` - `POST /v1/tasks/{id}/github/pull/checks/rerun` An agent should not be able to stop, garbage-collect or reconfigure the daemon that is supervising it. Those stay CLI-and-curl only. The two [permanent deletes](../reference/api.md#permanent-delete) are on the project delete's line: a row a human archived is history nobody else may discard. The issue delete joins them: it is permanent, and an imported issue's tombstone outlives it. Sync now (`POST /v1/projects/{id}/issues/sync`) stays a human act too: it asks the importer to poll early and flushes vincent's pending state writes to GitHub. Its `GET`, `project_issue_sync_status`, is an ordinary tool. `issue_close` and `issue_reopen` are tools, but on an issue imported from GitHub they are refused with `forge_write_needs_human`: that issue's state is written back to GitHub, and only a person does that. The same refusal meets a `vincent issue` command a workflow step or a chat agent runs, which the CLI marks with a header. `issue_comment` adds a comment as your task (`task N`), or `agent` outside a step; on an issue whose GitHub remote is live it is refused with `issue_mirrored`, because that thread is GitHub's and nothing is posted there. The [task import](../reference/api.md#importing-a-task) that undoes one sits beside them: it reads a file the caller names and writes rows no agent should be able to create. `task_archive` stays an ordinary tool, because the row and its transcripts survive an archive. The configuration one is the sharpest of them: a patch can change the argv the daemon spawns (`notify.command`, `agents.*.path`), what its children inherit (`environment`), and whether steps are wired to MCP at all (`mcp.wire_steps` — a step could otherwise switch these tools off for everyone). The two workflow writes are the same line drawn one step out: a workflow file is what the daemon runs, so an agent must not edit one. Nothing regresses — the built-in `create-workflow` workflow writes its deliverable through the filesystem, not through this API — and the read side is untouched: `workflow_list`, `workflow_definition`, `workflow_validate` and `workflow_schema` are all ordinary tools. The four trigger routes draw it once more. A [trigger](triggers.md) starts agents from outside, so an agent must not author, edit, arm or delete one (enabling is a `PATCH`), and must not push an event into one either: an agent that can inject events can start agents. The reads, `trigger_validate` and both dry runs stay tools, because a dry run fires nothing. The quota push is the same line drawn around a *fact* rather than an action: a step that could report its own adapter at 99% would paint every board and status line in the installation with a wall that does not exist, and nothing downstream could tell that from the real thing — the [reading carries a source, not a caller](../reference/api.md#usage-quota). Nothing is lost, because the two things that push are a status line and an app-server probe, neither of which is an agent step reaching for a tool. The last six are the routes that [write to GitHub](../features.md#open-a-pull-request): one pushes a task's branch and opens its pull request, and the other five merge, close, reopen, comment on and re-run the failed checks of a task's linked one. The only thing gating any of them is a human asking — no config switch, no confirmation the daemon can check. An agent-callable version would be consent nobody gave, and because `mcp.wire_steps` is on by default it would put those writes on the step path. Nothing is lost: an agent that wants a pull request has a shell in its own worktree and runs `git push`, `gh pr create` or `gh pr merge` there. `config_get` is the one tool whose body differs from its route's. The HTTP response serves `config.yaml` in full; the tool masks `environment.set`'s values and `notify.command`'s argv, keeping the names, because a tool result lands in the model's context and in the step's transcript. Nothing else is changed — see the [security model](../security-model.md). **Nor is any `/v1/chats` route** — the whole [chat](../reference/api.md#chats) family, reads included, a chat's skill list among them. A chat turn starts an agent CLI without going through admission, so a tool that could send one would let an agent start unqueued agent processes, which is the exact thing `max_tasks` below bounds. The recursion bounds cannot help either: they walk `created_by_task_id`, and a chat is not in that chain. Nothing is lost — an agent calling these tools already has a session of its own. The same goes for `POST /v1/tasks/{id}/chat`, which lives under `/v1/tasks` but opens a [chat on a stopped task](../reference/api.md#a-chat-on-a-stopped-task). What such a chat does reach is the tools' results: while it is open its task is locked, so `task_retry`, `task_skip` and every other action tool but `task_cancel` answers `409 task_locked_by_chat`, exactly as it would for a human. Each tool takes the route's path parameters by name, plus `body` (for `POST` and `PATCH`) or `query` (for `GET`): ```json { "name": "task_create", "arguments": { "body": { "project_id": 1, "workflow": "adhoc", "prompt": "fix the flaky test" } } } ``` ```json { "name": "task_transcript", "arguments": { "id": 12, "run_id": "34", "query": { "format": "normalized", "limit": "200" } } } ``` `task_create` and `issue_create` also take an optional `idempotency_key`, which becomes the `Idempotency-Key` header: re-sending the same arguments with the same key returns the original task or issue instead of creating a second one. A tool call has no header surface, so it is an argument. An issue a step files records that step's task as its author. `task_create`'s body takes `issue_id` to create the task from a vincent issue and link the two; `task_list`'s `query` takes `issue_id` to list the tasks created from one; `issue_get`'s `query` takes `workflow` to preview what that create would prefill; and `issue_list`'s `query` takes `project_id` and `remote_number` to find the issue a project imported a GitHub issue number as, and `lane` — `open`, `in_progress`, `hand_off` or `done`, the last being every closed issue — to list one board lane. A parameter the route repeats, such as `lane`, `state` or `label`, takes an array of strings: `{"query": {"lane": ["open", "hand_off"]}}`. Every issue row, and `issue_get`, carries `lane` and `attention`. There is no `github_issue` on `task_create`: a body that carries it is refused with `400 invalid_json`. Errors come back as the API's own envelope, so a `409` still carries `details.state` and you can branch on it rather than reading prose. One result is capped at 256 KiB, with a note saying so — page with the route's own `offset`/`limit`. ## Following a run Task runs take minutes, so polling burns turns. `task_wait` blocks until a task reaches a terminal or human-blocking state: ```json { "name": "task_wait", "arguments": { "task_id": 12, "timeout_seconds": 600 } } ``` It returns when the task is `done`, `aborted`, `archived`, `awaiting_input`, `blocked` or `awaiting_gate`, or when the timeout elapses — default 5 minutes, **hard ceiling 30**, so a call cannot hang forever. Check `woke` rather than `state` to tell a wake from a timeout: a timed-out wait truthfully reports a task that is still running. Step transitions arrive as MCP progress notifications while the call is open, but the result is complete without them. If your client drops every notification you lose nothing but the live commentary. ## Your own steps get this too By default the daemon registers the endpoint with the agent CLI it spawns for an [agent step](workflows.md), so a step's agent has the vincent tools with no configuration from you — it can file follow-up work, read a sibling lane's transcript, or answer a gate. Each step gets its own endpoint, with a secret that dies when the step does. Turn it off with one line: ```yaml mcp: wire_steps: false ``` Two things to know when it is on: - **A step waiting on another task will be refused, not hung**, if the task it is waiting for cannot start while the step holds its concurrency slot. `task_wait` returns a `would_deadlock` error immediately. Raise `max_parallel_tasks` or wait on something already running. - **Cursor steps get a `.cursor/mcp.json` in the task worktree** for the duration of the run, because `cursor-agent` has no per-run MCP flag. It is untracked, so it is visible in `git status` and in the task diff while the step runs, and it is removed when the step ends. Your global `~/.cursor/mcp.json` is never touched. An adapter that cannot carry an MCP server fails the step with `mcp_unsupported` rather than running an agent that silently has no tools. That is deliberate: a prompt written against the vincent tools should fail loudly. Turn `wire_steps` off if you would rather it ran anyway. ### Steps in a container An agent step of a task with a [`container.image`](../reference/configuration.md#container) runs inside the container, where `127.0.0.1` is not the daemon. Its endpoint is `http://host.docker.internal:{port}/mcp/step/{run_id}` instead — in claude's and codex's per-run config and in cursor's `.cursor/mcp.json` alike — and the tools are the same. - **On Linux with Docker Engine**, the daemon's loopback listener cannot be reached from a container, so vincent opens a second listener on the container network's gateway address for as long as a containerized step needs it. It answers `/mcp/step/{run_id}` and nothing else: `/v1` and `/mcp` are `404` there, and every request needs that step run's secret. Anything else on the same bridge network can reach the port, so the secret is what keeps it to the one step. - **On Docker Desktop** (macOS, and Docker Desktop for Linux) and runtimes such as rootless podman, that bind is not possible, and `host.docker.internal` already leads to the daemon's own loopback port, which is what is used. - **A container with no network reaches neither.** `container.network: false` with `wire_steps` on is refused when you create a task whose workflow has an agent step. A workflow of command steps only is unaffected. - **`vincent status` does not work in a container** — the image has no vincent binary and no route to the daemon on `127.0.0.1`. Tell a containerized agent to report its progress with the `step_status` tool instead; the task and step ids are in `VINCENT_TASK_ID` and `VINCENT_STEP_ID`, as usual. ## Bounds A step's agent can create a task whose step runs an agent that creates a task. `mcp.max_depth` (default 3) and `mcp.max_tasks` (default 32) bound that chain, enforced when the task is created; the refusal names the key it hit. See the [configuration reference](../reference/configuration.md#mcp). ## What this is not The per-step endpoint is **not a sandbox**. A full-auto agent can read the daemon token and reach `/mcp` directly, and `permission_mode: restricted` bounds the filesystem and the shell — not what a step does to vincent. A restricted step can create and cancel tasks. For a containerized step on Linux, the endpoint is also the one thing vincent serves off loopback, [as above](#steps-in-a-container). See the [security model](../security-model.md). ## See also - [HTTP API](../reference/api.md) — the routes the tools are. - [Configuration](../reference/configuration.md#mcp) — `mcp:`. - [Security model](../security-model.md).