MCP & tools

rupu cp serve serves an MCP endpoint at /mcp beside the web UI and the HTTP API. An MCP client such as Claude Code, Cursor or Claude Desktop connects to it and operates rupu: it lists and launches workflows and agents, watches runs, approves gates, drives sessions and messages a live run. It also gets rupu's tool catalog — files, shell, SCM, issues, findings — as plain tools.

What /mcp is

The endpoint is an adapter, not a second implementation. The operator tools call the control plane's own endpoints in process and return the same JSON as the HTTP API; the catalog tools are the same tool bodies rupu's agents and workflow action: steps call. On top of those it serves a wait tool that stands in for the API's SSE streams, rupu's own documentation as read-only resources, and four prompts.

MCP client Claude Code · Cursor · Desktop /mcp rupu cp serve operator tools · wait resources · prompts CP routes /api, called in process Tool catalog fs · shell · scm · findings
One endpoint: every control-plane route as an operator tool, plus the catalog the agents themselves use.
Full access. The MCP gives a connected client the same reach as the control plane's operator. rupu adds no permission layer of its own on /mcp: it does not prompt and it has no --mode. What runs is decided by the client's harness — Claude Code's permission prompts and --allowedTools, Cursor's approvals. Each tool carries read-only / destructive / open-world hints so a client's permission UI can tell runs.list from runs.cancel. Treat the bearer token like a key to the whole control plane, and narrow what is served with --tools if you want less.

Serve modes

CommandServes
rupu cp serveweb UI + /api + /mcp
rupu cp serve --no-web/api + /mcp, no web UI (implies --no-open)
rupu cp serve --no-mcpweb UI + /api, no /mcp

--no-web --no-mcp is refused (nothing to serve), and so is --tools with --no-mcp. A bad --tools grant fails before the socket is bound. At startup the server prints the MCP URL and the command that connects Claude Code to it.

/mcp sits behind the same bearer token as /api (--token), and the server checks two things on every request: the Origin header, when present, must equal the Host; and on a loopback bind the Host must be a loopback name (127.0.0.1, localhost, [::1]), which closes DNS rebinding. Binding beyond loopback is allowed; do it only with --token (see Control plane).

Connecting a client

Claude Code

# no token (loopback, default bind)
claude mcp add --transport http rupu http://127.0.0.1:7878/mcp

# with a token
claude mcp add --transport http rupu http://127.0.0.1:7878/mcp \
  --header "Authorization: Bearer $CP_TOKEN"

claude mcp add --scope user makes it available in every project. Claude Code names each tool mcp__rupu__<tool> with dots turned to underscores (runs.list is mcp__rupu__runs_list). claude mcp list shows whether it connected.

Cursor

~/.cursor/mcp.json (or the project's .cursor/mcp.json); leave headers out when the server runs without a token:

{
  "mcpServers": {
    "rupu": {
      "url": "http://127.0.0.1:7878/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

Claude Desktop

Add the URL as a custom connector if your plan offers remote connectors that can reach it. Otherwise bridge it with mcp-remote in claude_desktop_config.json:

{
  "mcpServers": {
    "rupu": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://127.0.0.1:7878/mcp",
               "--header", "Authorization: Bearer <token>"]
    }
  }
}

Authentication to GitHub, GitLab and the model providers is shared with the CLI through the same credential store, so rupu auth login once unlocks the catalog tools for everything.

Operator tools

188 tools over the control plane's own endpoints, one per route (GET /api/tools and tools/list give each tool's full input schema). Each takes the arguments of the endpoint it wraps (path parameters, the handler's query struct, the JSON body), an optional host where the API takes ?host=, refuses unknown arguments, and returns the API's JSON as text. A JSON object is also sent as structuredContent; a list is sent as text only, since MCP allows only an object there. Names are <area>.<verb>; hints follow the HTTP method (GET read-only and idempotent, PUT idempotent, DELETE destructive) and gate nothing.

AreaTools
Workflow runsruns.list runs.list_all runs.archived runs.get runs.approve runs.reject runs.cancel runs.pause runs.resume runs.archive runs.restore runs.delete runs.graph runs.children runs.board runs.usage runs.usage_timeline runs.source runs.ast runs.autoflow runs.retry
Agent runs and transcriptsagent_runs.list agent_runs.continue transcripts.get transcripts.archive transcripts.delete events.recent
Autoflow runsautoflow_runs.list autoflow_runs.problems autoflow_runs.cycles autoflow_runs.events
Sessions and messagessessions.list sessions.get sessions.turns sessions.usage_timeline sessions.start sessions.send sessions.archive sessions.restore sessions.delete sessions.stop sessions.compact messages.list messages.send
Agentiflowsagentiflows.list agentiflows.get agentiflows.events agentiflows.children agentiflows.unit_output agentiflows.board agentiflows.generated agentiflows.steer agentiflows.run agentiflows.stop agentiflow_defs.list agentiflow_defs.get agentiflow_defs.create agentiflow_defs.save agentiflow_defs.delete agentiflow_defs.validate
Definitionsworkflows.list workflows.get workflows.create workflows.save workflows.delete workflows.validate workflows.generate workflows.run workflows.run_yaml agents.list agents.get agents.create agents.save agents.delete agents.generate agents.run agents.validate agents.validate_all generate.models autoflows.list autoflows.get autoflows.enable autoflows.disable autoflows.run autoflows.explain autoflows.doctor autoflows.wakes claims.list claims.release claims.requeue claims.repair skills.list skills.get skills.install skills.update skills.validate tools.list models.list models.refresh
Findings, coverage, assetsfindings.list findings.get findings.tags findings.tag findings.export findings.export_many findings.artifact findings.import findings.schema coverage.list coverage.get coverage.audit coverage.catalog coverage.runs coverage.diff coverage.templates coverage.template coverage.gap coverage.rerun assets.list query.fields query.check
Observabilitynetflow.run netflow.agentiflow netflow.project netflow.global netflow.graph netflow.explorer netflow.index netflow.latest usage.get usage.timeline usage.runs usage.outliers dashboard.get
Fleet, config, customershosts.list hosts.registered hosts.add hosts.add_ssh hosts.add_bucket hosts.enroll_node hosts.remove host.info workers.list config.get config.put_global config.put_customer config.put_project config.put_policy customers.list customers.create customers.get customers.update customers.delete customers.archive customers.unarchive customers.assign_project customers.unassign_project launch.preview repos.list repos.tracked repos.attach repos.prefer repos.forget scm_accounts.list scm_rules.list scm_rules.bind auth.status
Projectsprojects.list projects.get projects.runs projects.sessions projects.agents projects.workflows projects.autoflows projects.coverage projects.coverage_assessed projects.tree projects.source projects.files projects.init
Setup and housekeepingfleet.list fleet.install engagement_profiles.list engagement_profiles.get cron.list cron.events cron.tick maintenance.prune

Every control-plane route is a tool except the SSE streams (wait replaces them), the host-to-host transfer routes (workspace sync, skill bundles, coverage, netflow and artifact pulls), the node WebSocket, /healthz, the web UI's filesystem browser and POST /mcp itself.

Results

The wait tool

An MCP client cannot read the control plane's SSE streams, so wait long-polls:

{ "watch": [ {"kind": "run_settled", "run": "run_01J..."} ],
  "cursor": {"run:run_01J...": 12}, "timeout_ms": 30000 }
ConditionFires whenhost?
{kind: "run_settled", run}the run reached a terminal statusyes
{kind: "run_gate", run}the run has a parked approval gateyes
{kind: "run_events", run, types?}the run wrote an event (optionally only these types)yes
{kind: "messages", root, kinds?}the root's message log has a new message (optionally only these kinds)yes
{kind: "agentiflow", id, types?}the agentiflow wrote a line (optionally only these kinds: round, run_stopped, …)local only
{kind: "agent_run_end", run}a standalone agent run finished (read its final_answer from the reply)local only
{kind: "session_turn", session, run?}a session turn ended; pass the run id sessions.send returned, else it waits for the active turn (or the next one if the session is idle)local only
{kind: "findings", project?, run?, q?}new finding records were recorded (a tag change on an older finding is not one); q is the findings query languagelocal only

Operating rupu from an agent

A loop that launches a workflow with a gate and finishes it:

  1. workflows.run {name: "release"} returns the run id.
  2. wait {watch: [{kind: "run_gate", run: ID}, {kind: "run_settled", run: ID}]} returns with run_gate fired.
  3. runs.approve {id: ID}.
  4. wait {watch: [{kind: "run_settled", run: ID}], cursor} (no run_gate) returns when the run settles, or with timed_out: true, in which case call it again.

And one that runs an agent: agents.run returns the run id, then wait {watch: [{kind: "agent_run_end", run: ID}]} returns when it ends. To steer a run while it works, messages.send reaches any participant of its message space (see Messaging); to learn rupu's formats before authoring, read the resources and use the author-agent / author-workflow prompts.

Catalog tools

Beside the operator tools the endpoint serves the rupu-tools catalog: the fs/shell tools (read_file, write_file, edit_file, grep, glob, bash, …), the SCM and issue tools (scm.*, issues.*, github.*, gitlab.* — each described in Integrations) and findings.report / findings.verify.

The fs/shell and findings tools take a required workspace argument: a project id, or the absolute path of a registered project. The call runs with that project's layered config (global, then customer, then project), its SCM registry and its findings ledger. A project's config and registry are cached until the server restarts.

Where a catalog tool has the name of an operator tool (agents.list, workflows.list, agents.get, …), the operator tool is the one served. Every other catalog tool the server can serve is listed, findings.query, findings.tag and catalog.search included. A catalog tool whose service the server lacks is left out and named once in the server log at startup (RUST_LOG=info). Findings filed over MCP record under the mcp scope. A served bash's network flows are not captured: the call is no run, so there is no run ledger for them.

Choosing the tools: --tools

rupu cp serve --tools <grant> narrows the MCP to the tools it names. It filters every tool: the operator tools and wait as well as the catalog tools. It uses the agent tools: grammar (exact names, legacy aliases, ns.*, core.* and *, comma-separated or repeated). Without --tools every tool is served.

# operator tools, wait, messages and findings only
rupu cp serve --no-web --tools 'runs.*,wait,messages.*,findings.*'
# a read-only code browser: no operator tools at all
rupu cp serve --no-web --tools read_file,grep

A narrow grant such as read_file,grep therefore drops the operator tools and wait; name them (runs.*,wait,…) to keep them. A name that matches no tool fails startup, listing the namespaces served and, for a near miss, a did-you-mean (--tools run.list suggests runs.list).

Resources and prompts

/mcp also serves read-only resources, built into the binary (never read from RUPU_HOME), so an agent can learn rupu from the MCP alone and sees the docs of the version it talks to:

URIContent
rupu://docs/<name>The 18 docs under docs/: using-rupu, agent-format, agent-authoring, workflow-format, workflow-authoring, agentiflows, triggers, engagement-profiles, coverage, messaging, skills, transcript-schema, cp-api, control-plane, response-outcomes, netflow, configuration, mcp (Markdown)
rupu://schema/finding-reportThe finding report JSON Schema (what findings.schema answers)
rupu://query/<list>A list query's field registry: findings, workflow_runs, agent_runs, sessions, autoflow_runs, events (what query.fields answers)
rupu://profiles/<id>Each built-in engagement profile, as TOML. engagement_profiles.list also lists layered ones
rupu://toolsThe tool catalog, the rows of GET /api/tools
rupu://examples/<path>The repo's examples/ agents, workflows and contracts

resources/list and resources/templates/list are cursor-paged; resources/read of an unknown URI answers -32002 with data.uri.

Four prompts (prompts/list, prompts/get): author-agent, author-workflow, author-agentiflow and triage-findings (with a project argument). Each is a short instruction that names the resources and tools to use; the links are text, not embedded resources, because the docs run 5–73 KB.

Attribution

An operator message sent through messages.send, and a finding tag event written through findings.tag, are recorded with client: "mcp" beside via: "cp", so the Messages tab and the logs can tell an MCP client from the web UI. A gate decision or cancel through runs.approve, runs.reject or runs.cancel on the local host records the actor mcp where the web UI records web. The mark comes from the /mcp endpoint itself, never from a request header, so a plain HTTP call can't claim it.

Protocol

MCP streamable HTTP, JSON responses:

rupu mcp serve is deprecated

rupu mcp serve used to run a stdio server. It now prints a deprecation notice and runs rupu cp serve --no-web on 127.0.0.1:7878; --transport and --mode are ignored and --tools is passed on. stdio is gone. Point clients at the HTTP URL as above (for a client that only speaks stdio, use mcp-remote).

Troubleshooting

SymptomLikely cause
401 from /mcpThe server runs with --token and the client sent none
403 from /mcpOrigin differs from Host, or a loopback bind got a non-loopback Host (a proxy or tunnel rewriting it)
405 on GET / DELETEExpected: /mcp is POST only
A fs / shell / findings tool says workspace is requiredPass a project id or the absolute path of a registered project
wait returns instantly with run_gateThe gate is still parked; approve it or drop run_gate from watch
wait on a findings, session_turn, agentiflow or agent_run_end condition with a host is refusedhost applies only to run_settled, run_gate, run_events and messages
wait with a remote host takes a few seconds even with timeout_ms: 0It waits up to 5 s for the host's first answer
A result has an extra x-rupu-... text lineA fan-out left hosts out; the note names them
A tool errors with "over 16 MiB"Fetch the named API path directly with the token
Tool is missing from tools/list--tools excludes it, or it is a catalog tool whose service the server lacks (logged at startup with RUST_LOG=info)
--tools fails at startup with a "did you mean"A name matches no tool; use the suggestion

Tools inside rupu

The catalog served on /mcp is the same one rupu's agents use. An agent's frontmatter tools: is its grant — what its model may call — and each entry is a canonical name (bash, findings.report, scm.prs.get), a legacy alias (report_finding, dispatch_agent), a namespace (scm.*, board.*), core.* (the core tools bash, read_file, write_file, edit_file, grep, glob, ast_grep) or *. There are no other wildcards. An unknown name fails the agent's load with a did-you-mean; rupu agent validate checks every file. Omitting tools: gives the default grant: core.*, dispatch, join and every connector namespace.

# agent frontmatter
tools:
  - read_file
  - grep
  - "scm.*"          # every scm.* tool
  - issues.get       # one exact tool

Every tool declares one effect — read, record (rupu's own bookkeeping: findings, coverage, board posts), write (the workspace, or running code), external (acting outside this machine) or spawn (starting another run) — and the run's permission mode decides each call from the effect alone, never from the tool's name:

Effect ↓ / Mode →bypassask (interactive)ask (no operator)readonly
readallowallowallowallow
recordallowallowallowallow
writeallowpromptallow, with a noticedeny
externalallowpromptallow, with a noticedeny
spawnallow; child keeps its own modeallow; child capped at askallow; child capped at askallow; child capped at readonly

A tool is offered only where it works — the connector tools need configured SCM accounts, the board and mailbox tools a message space, coverage.* a concerns: block — and every run's transcript opens with a tool_grant event listing exactly the tools the model was offered and why, with every call audited as tool_audit. The authority tools board.directive and board.retract are never granted by *. The full grammar, the effect table and the alias list are in Agents; the launch tools dispatch / join / workflows.generate are described there too.