Primer-as-a-Service over MCP
Goal
You have an external agent, an IDE assistant, Claude Desktop, claude.ai, or any MCP-speaking system, and you want it to use primer as a remote execution backend. Instead of running a long task in its own context, the external agent hands it to primer: it spins up a workspace session, lets the session run, polls for the result, reads it back, and can cancel a run it no longer needs. The external agent drives all of this over primer's built-in MCP endpoint.
There are two distinct surfaces here, and it is worth keeping them apart:
- The operator setup (turning the endpoint on, choosing which tools it publishes, minting a token) is done by you, in the console or with the CLI, exactly like every other recipe.
- The runtime drive (create a session, poll it, read the result, cancel it)
is done by the external MCP client. That client is the product surface, the
caller you are enabling, like the sender on the other end of a webhook. You do
not drive it with
primectl; you point an MCP client at the endpoint and let it call the tools.
For the full description of the endpoint, see the MCP Server feature.
Ingredients
- The built-in MCP endpoint at
/v1/mcp. It is off by default; you opt in by enabling it and publishing an allowlist. - An allowlist naming the session-drive tools. The minimal set is:
workspaces__create_workspace_session, start an agent (or graph) session.workspaces__get_workspace_session, poll its lifecycle state.workspaces__read_workspace_file, read its output.workspaces__cancel_workspace_session, stop a run.
- An agent and a workspace for the external client to run. Any existing
agent on a
local(or container / kubernetes) workspace works. - An MCP client that speaks the StreamableHTTP transport, authenticated to
the endpoint with a bearer token that has the
mcpscope.
All four allowlisted tools are non-yielding and do not require an active agent session, so they pass the MCP exposability floor. Tools that park (such as
system__ask_user) or that need a livesession_idare rejected from the allowlist by design, see the exposure model for why.
Walkthrough
1. Enable MCP exposure with the session-drive allowlist
Turn the endpoint on and publish exactly the four session tools. Saving a new
allowlist replaces the previous one atomically, and only the listed tools appear
on the client's tools/list.
In the console:
- Go to MCP Server in the left nav and click Enable in the MCP
server endpoint panel. The status pill changes to
enabled. - In the Exposed tools table, use the toolset filter to find the
workspacestools, then tick exactly the four session-drive tools:create_workspace_session,get_workspace_session,read_workspace_file, andcancel_workspace_session. - Click Save. Saving re-runs the exposability floor, so an attempt to publish a yielding or session-only tool is rejected here with a reason, not silently dropped later.
The MCP exposure config is an operator-only, console-driven surface: it is
edited from the console (cookie session) and cannot be changed by a bearer
token, even one with the mcp scope. So this enable + allowlist step has no
primectl equivalent; do it in the console. The CLI's role in this recipe is
the rest of the setup (the agent, the workspace, and the token below).
2. Mint the client token and connect
The external client authenticates with a bearer token that carries the mcp
scope. Mint it from Settings > API tokens in the console: click New
token, give it the mcp scope, and copy the one-time plaintext (this is the
same token-minting flow the CLI uses for its own --token, see "Connecting the
CLI" in the RAG knowledge base recipe).
Point your MCP client at <primer-base-url>/v1/mcp over the StreamableHTTP
transport with that token. On connect, the client lists tools and sees only
the four allowlisted ids, nothing else from primer's catalogue. That is the
exposure gate: a tool you did not allowlist (for example
workspaces__delete_workspace) is absent from tools/list and cannot be
called.
3. Create a session, the external agent offloads the task
From here on the external MCP client drives, calling the published tools. It
calls create_workspace_session with the workspace, an agent binding, and the
task as the initial instruction. auto_start: true runs it immediately. The
call returns the created session, including its id.
tools/call workspaces__create_workspace_session
{
"workspace_id": "<your workspace id>",
"binding": { "kind": "agent", "agent_id": "<your agent id>" },
"initial_instructions": "Summarise README.md and write the summary to summary.txt.",
"auto_start": true
}
To offload a graph instead of a single agent, bind to a graph and pass its input:
{
"workspace_id": "<your workspace id>",
"binding": { "kind": "graph", "graph_id": "<your graph id>" },
"graph_input": { "ticket": "INC-1" },
"auto_start": true
}
4. Poll for the result
The client polls get_workspace_session until the session reaches a terminal
state. The tool returns {info, status}; the session is finished when status
(and info.status) is ended, with info.ended_reason one of completed,
failed, or cancelled.
tools/call workspaces__get_workspace_session
{ "workspace_id": "<your workspace id>", "session_id": "<session id>" }
This is the same session row the console Sessions page shows and that the session tools wrap, so a client driving over MCP sees the identical lifecycle an operator would. See the sessions API reference for the full lifecycle.
5. Read the output
Once the session has ended, the client reads any file the run produced (or the
transcript itself) with read_workspace_file. The transcript lives at
.state/sessions/<session id>/messages.jsonl; a file the agent wrote lives
wherever the agent put it.
tools/call workspaces__read_workspace_file
{ "workspace_id": "<your workspace id>", "path": "summary.txt" }
6. Cancel a long run
If the external agent decides it no longer needs a result, it cancels the
session. A created or paused session ends immediately; a running one is
preempted at the next safe point. Either way the session converges to terminal
ended.
tools/call workspaces__cancel_workspace_session
{ "workspace_id": "<your workspace id>", "session_id": "<session id>" }
A subsequent get_workspace_session shows the session ended with
ended_reason: "cancelled".
Testing
A scripted end-to-end test drives the endpoint exactly as an external client
would (tests/e2e/test_cookbook_mcp_service.py, SMK-COOKBOOK-16): it connects
over the StreamableHTTP transport, lists tools, creates a session bound to a
trivial agent, polls it, reads it back, and cancels a second one. A companion
test (tests/e2e/test_cookbook_mcp_service_cli.py, SMK-COOKBOOK-CLI-16) does
the operator-side setup the published path supports, creating the agent and
workspace with primectl create -f, then drives the runtime over the MCP client
(the product surface), since the MCP client is how this capability is
consumed. The MCP exposure enable is done over the console cookie session in
both tests, because that surface is console-only by design.
Expected outcome (verified):
- Exposure gate.
tools/listreturns exactly the four allowlisted ids and nothing else; a non-allowlisted workspace tool (such asworkspaces__delete_workspace) is absent. - The offloaded session runs.
create_workspace_sessionreturns a session id; pollingget_workspace_sessionreachesstatus: "ended"withended_reason: "completed". The same session read from the console Sessions page shows the identicalendedstatus. - The result is retrievable over MCP.
read_workspace_fileon the session's transcript (or a produced file) returns the agent's output. - Cancel is honoured. A cancelled session converges to terminal
ended.
The whole flow runs headlessly over the in-process MCP transport, so it is a faithful stand-in for a real IDE assistant or desktop agent driving primer as a backend.