Skip to content

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 read can call every read tool. --scope act is additionally required for manage_action, scout_action and enroll_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 the user: 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 form installer/build.sh --ca-fingerprint takes — pass it, because build.sh otherwise tries to fetch the Hub's own chain from the Hub box, which often cannot reach its advertised name), and nats_url.
  • nats_url is 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 and nats_url_note says so; if nothing is advertised, the loopback is returned with nats_url_warning set — never silently.
  • cert_dirs and targets are inputs the client must collect from the operator — the directories the box's server reads certificates from, and the host[:port] endpoints it serves. They become the lines of cert-dirs.conf and targets.conf (see install). The Hub validates them the way the Scout's parsers and build.sh would (absolute paths; fully qualified names, no wildcard, port 1-65535) and refuses the call otherwise, minting nothing. [] is a valid answer; cert_dirs_note then says what build.sh seeds instead. Nothing here is derived from a certificate, a web server, or DNS: serving is declared.
  • installer_command is the complete installer/build.sh line to run on the Hub host — real token, pin, and every entry as a quoted flag.
  • next_steps walks the install procedure and states the mechanism a client must know before touching the box: cert-dirs.conf lists the directories the Scout watches (there is no --cert-dir flag), deliveries land in <data-dir>/certs/<primary-domain>/ and are published into the operator's own files, targets.conf declares 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.conf line 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.