The MCP server¶
The Hub can serve its estate to an AI agent over the Model Context Protocol — the same Manage rows, the same Scouts, the same actions an operator has, behind the same refusals. It is off by default, and it is the installation file that turns it on: no environment variable, no setting in the dashboard, no runtime toggle.
The MCP is built so that an agent working through it knows what every tool
does under the hood. Each tool's description states its mechanism and its
consequences, and the doctrine pages of this wiki are embedded in the Hub
binary and served through list_docs / read_doc, so a client on an
air-gapped network still reads the rules before it proposes anything.
Enabling it¶
Add an [mcp] section to /etc/cyphers/installation.toml and restart the
Hub:
[mcp]
enabled = true
# What the MCP may change. "none" (the default) is read-only.
mutations = "none" # "none" | "safe" | "all"
# May it mint Scout enrolment tokens? Independent of `mutations`.
enroll_scouts = false
| Key | Default | Effect |
|---|---|---|
enabled |
false |
Mounts /mcp on the dashboard listener. When false the route is absent, not 401. |
mutations |
"none" |
none: reads only. safe: non-issuing, non-destructive verbs — scan_now, probe_now on endpoints; scan_now, rename on Scouts. all: the full action vocabulary, including renew. |
enroll_scouts |
false |
Allows enroll_scout. Deliberately independent of mutations: enrolling a Scout does not need renewal authority, and requiring all for it would be the wrong trade. |
The Hub logs the resolved posture at startup (MCP posture resolved). An
unknown key in the section aborts startup, the same rule as the crypto
section.
Renewals are the operator's call
mutations = "all" is what lets an agent place a certificate order. The
gate exists so that a deployment decides, once, whether an agent may ever
do that. The server's own instructions tell a client that a renewal it was
not asked for is never to be triggered — but the gate, not the
instruction, is the control.
Tokens¶
Every request to /mcp carries a bearer token. Tokens are minted on the
dashboard's Access page (+ Mint MCP token: a label, a scope and a
lifetime in days; the token is shown once, beside a ready-to-paste
claude mcp add --transport http cyphers-hub <hub url>/mcp -H "Authorization:
Bearer <token>" line — see Settings → Access)
or on the Hub host with the CLI. Both go through one mint path and leave the
same audit row (mcp_token_minted):
cyphers-hub mcp-token --label "ops-assistant" --scope read --days 90
cyphers-hub mcp-token --label "ops-assistant-act" --scope act --days 30
cyphers-hub mcp-token --list
cyphers-hub mcp-token --revoke <token-id>
--scope readcan call every read tool.--scope actis additionally required formanage_action,scout_actionandenroll_scout— the scope and the installation gate are independent checks and both must pass.- The raw token is shown once and stored hashed; no read returns it again. A revoked or expired token gets 401 on the next request.
- The Access page lists live MCP clients (label, scope, expiry, last use) with a Revoke button, which takes effect on the very next request.
- Everything a token does is attributed: audit rows and activity events carry
the actor
mcp:<token-id>, which falls outside theuser:view so an agent's action is distinguishable from a human's forever. A token's first successful use raises an activity event ("MCP client connected").
Connecting a client¶
The transport is Streamable HTTP, stateless: point a client at
https://<hub>:<dashboard-port>/mcp with Authorization: Bearer <token>. A
plain JSON-RPC call works without a session handshake. Each answer comes back
as one server-sent event, so a script reads the data: line:
curl -sS https://hub.example.com:9443/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
| sed -n 's/^data: //p'
The tools¶
| Tool | Needs | What it does |
|---|---|---|
list_scouts |
read | Every enrolled Scout — id, name, status, last seen, endpoint count. |
get_scout {scout} |
read | One Scout by id or exact name. |
get_estate {q?} |
read | Every Manage row, from the same read the page uses, flattened so a SAN sibling or a host under a wildcard is listed with its own fleet_id and its group_primary. q narrows by hostname substring. |
get_endpoint {fleet_id} |
read | One endpoint in full — state, certificates with provenance, and available_mutations: the only actions the Hub will accept for it right now. |
list_docs / read_doc {slug} |
read | The embedded doctrine pages (below). |
manage_action {fleet_id, action} |
act + safe/all |
Run an endpoint action. The action must be replayed verbatim from available_mutations — an action that is not on offer is refused, whatever the gate says. Runs the dashboard's own dispatcher and answers with its result or its refusal. Only offers of kind mutate are replayable; a form offer (Adopt, Policy, Delete, Revoke, Emergency renew, Verify grace, Wildcard attach, Rename Scout, Renewal config) needs operator input and is done in the dashboard drawer. |
scout_action {scout, action} |
act + safe/all |
scan_now, rename (safe); delete, set_scan_interval, declare_serving, retract_serving (all). Same handler as the Scouts page. |
enroll_scout {name, cert_dirs, targets} |
act + enroll_scouts |
A one-hour enrolment token, the CA pin in build.sh's sha256:<hex> form, the NATS URL a target must dial, and installer_command - the complete build.sh line carrying one --cert-dir/--target per entry. Both lists are required: the client must ask the operator (pass [] for none). |
Why actions are replayed, not composed¶
Estate strings — certificate subjects, SANs, hostnames — are chosen by remote
parties, and an MCP client is a language model whose context those strings
enter. Fencing the text and asking the model to respect the fence would be
weaker than making a forged verb impossible: manage_action accepts an
action only if it appears, byte for byte, among the offers get_endpoint
currently advertises for that row. At most a poisoned string can name an
action the Hub is already offering, under a gate the operator turned on,
attributed to a token.
A refusal carrying refused: true is a decision that repeats on retry (a
pre-flight finding, held_by_scout, not_revocable —
see Recover from refusals); a bare
refusal means the offer went stale between the read and the run, and the
client re-reads.
What enroll_scout returns, and what it does not¶
The Hub cannot reach the target machine, so the response is the Hub's half of an install, not a runnable command:
token(valid one hour),ca_fingerprint_flag(sha256:<64 hex>, the exact forminstaller/build.sh --ca-fingerprinttakes — pass it, becausebuild.shotherwise tries to fetch the Hub's own chain from the Hub box, which often cannot reach its advertised name), andnats_url.nats_urlis resolved for the target. If the Hub's configured Scout NATS URL is a loopback address (the Hub talking to itself), the Hub's first advertised NATS host stands in andnats_url_notesays so; if nothing is advertised, the loopback is returned withnats_url_warningset — never silently.cert_dirsandtargetsare inputs the client must collect from the operator — the directories the box's server reads certificates from, and thehost[:port]endpoints it serves. They become the lines ofcert-dirs.confandtargets.conf(see install). The Hub validates them the way the Scout's parsers andbuild.shwould (absolute paths; fully qualified names, no wildcard, port 1-65535) and refuses the call otherwise, minting nothing.[]is a valid answer;cert_dirs_notethen says whatbuild.shseeds instead. Nothing here is derived from a certificate, a web server, or DNS: serving is declared.installer_commandis the completeinstaller/build.shline to run on the Hub host — real token, pin, and every entry as a quoted flag.next_stepswalks the install procedure and states the mechanism a client must know before touching the box:cert-dirs.conflists the directories the Scout watches (there is no--cert-dirflag), deliveries land in<data-dir>/certs/<primary-domain>/and are published into the operator's own files,targets.confdeclares what the box serves, and hooks are operator-placed.
The embedded docs¶
list_docs returns the curated set with a one-line summary and each page's
URL on this site; read_doc {slug} returns the page's Markdown. The set is
the doctrine an agent must have read before acting on certificates, renewals,
Scouts or hooks:
| Slug | Page |
|---|---|
concepts/certificate-files |
Certificate files: naming & takeover — read before touching any certificate file or web-server config |
concepts/endpoints-proof-management |
Endpoints, proof & management |
concepts/renewal-lanes |
Renewal lanes |
concepts/key-reuse |
Key reuse & pinning |
concepts/replicated-certificates |
Replicated certificates |
hub/manage-page |
The Manage page |
hub/renewal-profiles |
Renewal profiles |
hub/mcp |
this page |
scout/install |
Scout install procedure |
scout/security |
Scout security — hooks, pinning, scan_target |
how-to/dns-01-route53 |
Automatic DNS-01: AWS Route 53 |
how-to/one-certificate-on-two-servers |
One certificate on two servers |
how-to/recover-from-refusals |
Recover from refusals |
how-to/reset-a-password |
Reset or change a password — the password policy, and the root-only host command for a lost super admin password |
The MCP has no password tool, on purpose. A dashboard password is changed
by its owner, reset by a super admin, or — when the super admin's is lost —
reset by root on the Hub host with cyphers-hub reset-password. An MCP token
is not a person and must never be a way around that: a client asked to reset a
password should read how-to/reset-a-password and hand the operator the
command, not look for an action that performs it.
The pages are embedded at build time from this wiki, so what the MCP serves is what this site publishes for the same release.
What the server tells every client¶
The server's instructions — read once, at connection — summarise the rules
the tool descriptions then repeat in place:
- Proof and management are two independent facts per endpoint; an unproven endpoint is offered nothing but a probe.
- One certificate is one renewal: a SAN or wildcard certificate is one row, and Renew, Adopt, autopilot and the profile belong to the certificate.
- Which Scout serves a hostname is declared, never detected — a
targets.confline on the box. Ask the operator; do not infer from certificate names. - Certificate files are found by content and published shape-preserved into the files the server already reads. Nothing is renamed, moved, copied or edited in nginx/Apache configuration — a client must never propose it.
- Hooks are operator-placed at
<data-dir>/hooks/{pre,deploy,post}; the Hub never ships one. A custom web server needs only a reload-only deploy hook. - Renewals and every other mutation are the operator's call.
- Actions are replayed from
available_mutations, never composed.