Console as an MCP server
The runspec-console is normally an MCP client (see External MCP servers). This is the mirror: the running console hosts its own MCP server on localhost, so an external coding agent — Claude Code, Cursor, Cline, or any MCP client — can run the console's fleet runnables as tools, with the console's confirm gate in the loop.
Claude Code ──HTTP MCP (127.0.0.1:PORT)──▶ runspec-console (running)
confirm card ◀── park & approve ─────────┤ autonomous → runs now
(you click Approve in the console) │ confirm → parks in the console UI
History (agent + human runs) ◀────────────┘ manual → refused
Why route the agent through the console rather than a standalone gateway
(runspec-mcp)? Because the console already owns the SSH fleet,
credentials, discovery, the confirm-card UI, and the History/audit trail — so
a confirm-level call from the agent surfaces as the same approval card the
console's own agent uses, and every run (agent- or human-initiated) lands in one
History. It's the governed front door.
Requires the mcp extra: pip install runspec-console[mcp].
Enable it
Off by default — it opens a listening socket, so it's opt-in.
Settings → MCP Server:
- Enable server — starts it (applies immediately).
- Host —
127.0.0.1(loopback is the primary security bound; changing it off loopback is not recommended). - Port — default
8765. The actual value is written to a discovery file (see below) so the client's config need not hard-code it. - Bearer token (optional) — when set, required on every request
(
Authorization: Bearer <token>). Localhost is the primary bound; the token guards against other local processes on the machine. - Default autonomy floor (optional) — force every tool to at least this level (most-restrictive wins). Unset ⇒ each runnable's own declared autonomy.
- Confirm wait — how long a parked call waits for your approval before returning a resumable result (see Autonomy below).
- Tool style — how runnables with subcommands are presented (see
Tool style below).
leaf(default) is serve-identical;runnablecollapses each runnable to one tool with acommandargument.
Or in config.toml:
[mcp_server]
enabled = true
host = "127.0.0.1"
port = 8765
# token = "…" # optional bearer token
# default_autonomy = "confirm"
confirm_wait = 50
# tool_style = "leaf" # or "runnable" — one tool per runnable + a command arg
# expose_builtins = false # also publish the console's console-specific built-in tools
[mcp_server] lives in config.toml, so it rides Config Sync / the config seed
like every other section (the token stays local — set it per machine).
Connect Claude Code
While the console is running it writes a discovery file at
{app_dir}/runspec_mcp_server.json with the live URL (and token, when set):
{ "url": "http://127.0.0.1:8765/mcp", "token": "…", "header": "Authorization: Bearer …" }
When a bearer token is set it's written here in the clear, so the file is created owner-only (0600) — see Security & the token below.
Add it to Claude Code (the Settings tab shows the exact command):
claude mcp add --transport http runspec http://127.0.0.1:8765/mcp
# with a token:
claude mcp add --transport http runspec http://127.0.0.1:8765/mcp \
--header "Authorization: Bearer <token>"
Any MCP client that speaks streamable-HTTP works the same way. The console must be running for the endpoint to be up — this is a desktop, not a daemon; open the console when you want the agent to reach the fleet.
On a white-label build, the server advertises its [console.theme] brand_name as
the protocol-level server name a connecting client sees (falling back to
runspec-console when no brand is set), so the coding agent's server listing
matches the brand.
The tools are serve-identical to runspec serve / runspec-mcp:
subcommands are flattened to parent_child leaf tools, password args never
appear in a tool schema, and each tool carries its runnable's autonomy. The same
runnable discovered on several fleet hosts is exposed once per host (the extra
hosts get a <host>__ prefix). [tool_visibility] hides carry over (a runnable
hidden from the agent is not published here either).
Tool style — leaf vs per-runnable
A fleet of multi-verb runnables can expose a lot of leaf tools: jira alone
flattens to ~37 (jira_get-issue, jira_create-issue, …), confluence ~20,
gitlab ~17 — and every host the runnable is installed on multiplies that
again (5 hosts × runspec-linux's ~31 runnables = a lot). Serve-identical, that's
how runspec serve presents them — but some MCP clients struggle with hundreds of
tools in one list. tool_style picks the shape:
tool_style |
what the client sees | when |
|---|---|---|
leaf (default) |
one tool per subcommand per host — jira_get-issue, …, and <host>__-prefixed copies for extra hosts (serve-identical) |
precise per-verb arg schemas; fine when the client handles many tools |
runnable |
one tool per distinct runnable — jira with a command argument (the verbs) and, when it's on more than one host, a host argument (the hosts) |
far fewer tools; matches the console's own deduped view; use when a client chokes on a big list |
runnable style collapses all the multiplying dimensions — one tool per
distinct runnable name:
- Subcommands → a
commandenum argument. The runnable's own (top-level) args are typed; per-subcommand args ride through as additional properties (validated by the executor at run time). - Hosts → the same runnable discovered on several hosts is one tool with a
hostenum argument (required when it's on >1 host; absent for a single-host runnable). So 5 hosts × 31 runnables is ~31 tools, not ~155. - Venvs / packages → a cross-platform runnable that ships in more than one
package (e.g. the web/TLS/
whichrunnables in bothrunspec-windowslocally andrunspec-linuxon the fleet, discovered under different venvs/groups) collapses to one tool too — not a bare +<venv>__-prefixed pair. The correct venv is chosen per host at run time. So the count matches the console's distinct-runnable tally (the number in the sidebar), not a per-venv split.
Autonomy is still resolved per chosen subcommand — picking create-issue gates
exactly as the jira_create-issue leaf tool would, while get-issue stays
autonomous — so the confirm gate behaves identically to leaf; only the tool
count changes.
Another lever for a large fleet is [tool_visibility] hidden_from_agent — hide
whole packages/groups you don't need over MCP (the hides carry over to this
server). Combine both: collapse to runnable style and hide what you don't use.
Built-in tools (opt-in)
By default the server publishes only your fleet runnables. Set
[mcp_server] expose_builtins = true (Settings → MCP Server → Expose built-in
tools) to also publish the console's own console-specific built-in agent
tools — the ops/fleet powers a coding agent can't do itself:
console_status,search_history- memory + runbooks (
remember,search_memory,list_memories,forget_memory,save_runbook,load_runbook,run_runbook) - host→local file transfer (
download_file,relay_file) - Outlook (
outlook_*, when Outlook COM is available) - the action tools (
create_document,create_schedule,emit_event,open_url,play_sound, …)
Each keeps its own autonomy — reads run immediately, writes (send mail, save a
runbook, create a schedule, …) park in the console confirm card just like a
confirm runnable, and the bounded-wait/resumable behaviour is identical.
Never exposed, regardless of this setting: the console-config tools
(manage_hosts/manage_triggers/…) and the search meta-tools (automation must
not rewrite itself; the meta-tools are internal indirection), and the generic
workspace / code-exec / web / repo tools — a coding agent already has its own
file/bash/web tools, and the workspace/exec ones are tied to a chat session. The
set is filtered against what's actually available on this install, so e.g. Outlook
tools only appear on a Windows box with Outlook.
Off by default: it widens what an external process can drive through your console beyond running fleet runnables.
Live tool list (discovery)
The server advertises tools.listChanged and pushes a
notifications/tools/list_changed to the connected client whenever the console's
fleet discovery updates. So if you open the console (or reconnect the client)
while it's still connecting to remote hosts, the client refreshes to the full set
on its own as discovery completes — no manual reconnect needed. (A client that
ignores the notification can still re-list manually.)
Autonomy — the confirm gate
Each tool is gated by its runnable's declared autonomy (escalated by any
per-arg autonomy and the optional floor):
| autonomy | over the console's MCP server |
|---|---|
autonomous |
runs immediately, no prompt |
confirm / supervised |
parks in the console's confirm card — you Approve / edit args / Deny |
manual |
refused (operator-only; run it yourself in the console) |
password args |
omitted from the tool schema entirely |
The denylist and enforce_run_as still apply on the executing host, regardless.
Parked calls and the client timeout. An MCP client imposes its own per-tool
timeout (Claude Code's MCP_TOOL_TIMEOUT defaults to 60s), and progress
notifications do not extend it. So a parked confirm waits only up to
confirm_wait (default 50s, deliberately under that ceiling): if you approve
in time, the runnable runs and its output returns; if not, the tool returns a
graceful, resumable message —
⏳ Awaiting operator approval in the console (request
ab12cd34). Approve it there, then call this tool again to run it.
— and the parked card clears. Nothing runs late or orphaned; the agent simply
calls again (raising a fresh confirm) once you've approved. If you raise the
client's MCP_TOOL_TIMEOUT, raise confirm_wait to match for a smoother
single-call approval.
Credentials
Runnables that need a secret (a Jira PAT, a service-account password, an API token) get it automatically, by scope — the same model as the console's own agent. You never hand a credential to the MCP client, and the client never sees a secret:
- Credentials are matched to a run by their scope (
scope_hosts/scope_runnablesglobs, set in Settings → Credentials; an empty scope matches everything). Every in-scope credential for the(host, runnable)is applied server-side, its secret read from the OS keychain at run time. - Both delivery channels fire. The name-derived env vars (a
Jirauserpass credential →JIRA_USERNAME/JIRA_PASSWORD; aDeploytoken →DEPLOY_TOKEN/DEPLOY_PAT) for runnables that read the environment, and the arg-bindings (username_arg→ the argument,secret_arg→ the secret env channel) for runnables whose interface is--user/--passwordargs — so both styles of runnable work over MCP. password-typed args are omitted from the tool schema entirely, so the agent can neither see nor supply them — the console fills them from the credential.
To make a credential available to MCP runs, scope it to the host(s)/runnable(s) it should serve (or leave the scope empty to apply it everywhere). A runnable with no matching credential simply runs without one (and fails auth if it needed it).
Two deliberate limits versus the in-console chat agent:
- No per-call credential selection. The chat agent can name a
credential_id; an external MCP client can't (the tool schema exposes no such field). If two credentials could match one runnable, tighten their scopes. - No sole-password fallback. Filling a lone
passwordarg from a single matching credential without asecret_argbinding is chat-agent-only; over MCP, bind it explicitly withsecret_arg(or use the env-var channel).
Security & the token
The server binds to loopback (127.0.0.1), which is the primary boundary:
loopback traffic never touches a network interface, so there's no network
man-in-the-middle to worry about, and the optional bearer token guards against
other local processes connecting to the port. That's why plain HTTP is fine here
and TLS isn't needed on loopback (a client that demands https:// is better
served by a local stdio bridge than by adding a cert the client won't trust).
Given that, the bearer token's real exposure is at rest, not on the wire — so:
- The discovery file (
{app_dir}/runspec_mcp_server.json) is written owner-only (0600): on POSIX the permissions are set before the token is written (no readable window) and an older looser file is tightened; on Windows the per-user app directory's ACLs govern. - The token is never written to
config.toml's synced copy in a way that leaves Config Sync — set it per machine ([mcp_server] token), it stays local. - Runnable secrets (credential-supplied passwords/tokens) never travel to the client and are never audited — see Credentials above.
If you don't run other untrusted local processes, the token is optional; set one when you want to be sure only your own client can reach the port.
History & provenance
Agent-initiated runs execute through the same executor as Forms runs and are
tagged trigger = "mcp", so the History tab shows them alongside human runs and
you can tell which came from the coding agent. Secrets are never audited (as
everywhere).
See also
- External MCP servers — the console as an MCP client.
runspec-mcp— the standalone (headless) gateway; this is the governed, in-console alternative.docs/design/console-mcp-server.md— the design note (confirm-gate reuse, the keep-alive prototype + real-Claude-Code verification, the file:symbol map).