axis-agent-mcp
axis-mcp
A Model Context Protocol server that wraps the AXIS agent-identity protocol. It packages both resolution-side motions of the protocol into one install:
- Carry & Present (operator side) — the agent holds its credential locally, mints AXIS Identity Tokens (AITs), and surfaces a signup link to its operator when it isn't yet registered.
- Look Up (platform side) — resolve any inbound agent to its operator and authority, and verify a presented token. Free, account-less, read-only.
Beyond resolution, it is a self-hosted operator tool: the human operator manages the agent's identity and permissions entirely on their own machine — local operator-key custody, a local permission policy, and local enforcement — so you run an agent's identity and authorization with no hosted Governor and no Kipple infra beyond the public registry. See the Operator CLI and Operator-key local custody below.
It wraps the existing axis-protocol-sdk (signing,
local verification) and the live AXIS Prime registry's public read endpoints
(https://registry.axisprime.ai). No new backend. (Hosted, recoverable,
multi-device custody + managed policy remain a separate explicit upgrade — the
Governor — not required here.)
MCP is a surface, not the identity layer. AXIS agent identity lives in the protocol and
axis-protocol-sdk; this MCP server is the surface that lets an MCP-speaking agent (Claude, etc.) use it as tools. An agent on any other stack — a plain HTTP client, a Python agent, the next protocol — usesaxis-protocol-sdkdirectly and gets the same identity. AXIS isn't tied to MCP; MCP is simply the most ergonomic surface for LLM agents today.
Tools
Motion B — Look Up (zero local state, read-only)
| Tool | What it does |
|---|---|
axis_verify(token, expected_audience?) |
Verify an AIT via the registry; optionally check its aud claim. |
axis_resolve_agent(agent_id, presenting_ait?) |
Resolve an agent's public identity record. |
axis_resolve_operator(operator_id) |
Resolve an operator's public record. |
Motion A — Carry & Present (local custody)
| Tool | What it does |
|---|---|
axis_status() |
Report the local credential + the mandatory custody disclosure (or a signup affordance). |
axis_requirements(platform_url) |
Read a platform's /.well-known/axis-access (+ axis-scopes): audience, tier, required scopes, registration URL. |
axis_present(audience_or_platform_url, scope?, ttl?=300, dlg?) |
Mint an AIT and return it + the Authorization: Bearer <ait> header. Refuses any scope not granted in local policy. When a scope is asserted and no dlg is passed, auto-attaches the newest registered delegation covering that scope (dlg_source: "auto") — so a gate can resolve it without the caller knowing to pass dlg. |
axis_delegate(scope, ttl?) |
Sign a delegation (operator → agent) with the local operator key and register it via the registrar path so a platform gate can resolve it. Refuses any scope not granted in local policy. |
post_comment(url, text, scope?) |
One-call engage: post a comment on an AXIS-gated platform. Discovers requirements, ensures a registered delegation for the comment scope, mints the token, and POSTs it — hiding the endpoint, header, and payload. Returns the exact axis grant … to run (never self-grants) if the operator hasn't granted the scope. |
axis_request_onboarding(platform_url?) |
Generate the agent keypair locally and return a one-click device-code operator signup link. Platform URL optional — omit to onboard platformless at the default (email) tier and engage specific platforms later. The link also carries the operator public signing key + a registration proof so the registrar registers it (POST /operators/:id/key), making operator-signed grants registry-verifiable. |
axis_complete_onboarding(device_code?) |
Poll the registrar device-token endpoint once for a pending onboarding: pending (re-invoke), complete (binds the returned agent_id to the local key), or denied/expired. |
axis_dashboard(activity_limit?=20) |
The operator dashboard in one call: agents, per-agent permissions (scope/expiry/approval), and recent signed activity, with a render hint. The in-chat equivalent of the axis dashboard localhost page — read-only, never returns key material. |
Prompts
| Prompt | What it does |
|---|---|
my_agents |
Discoverable entry in the client's prompt menu; drives the model to call axis_dashboard and render the operator dashboard inline — so you find it in the UI instead of learning a CLI command. |
Operator CLI (axis)
The MCP server serves your agent's LLM. The axis CLI is for you, the
human operator: it manages your agent's identity and permissions entirely
locally — no hosted Governor, no Kipple infra beyond the public registry. Both
share the ~/.axis-mcp store.
npm run build # builds dist/cli.js (bin: `axis`)
axis status # held credential(s): ids, tier, granted scopes, custody, op-key
axis agents # list local agents
axis operator-key init # generate the operator key + register the public key
axis grant content:comment --ttl 30 # grant a scope (policy + operator-signed delegation)
axis approve content:publish # approve a requires-approval scope
axis require-approval content:publish # flag a scope deny-by-default
axis revoke content:comment # revoke one scope
axis revoke --all # revoke the whole agent
axis audit --tail 20 # show the signed local audit log
axis dashboard # open the localhost click-instead-of-type dashboard
axis help
Run without installing the bin via npm run axis -- <cmd> (e.g. npm run axis -- status).
Local operator dashboard (axis dashboard)
For operators who'd rather click than type, axis dashboard starts a tiny
localhost-only web page that does what the CLI does — list agents, grant /
revoke / approve scopes, flag requires-approval, and tail the audit log — backed
by small JSON endpoints on the same Node server. It reuses the exact same
policy / store / scope / delegation logic as the CLI (policy.ts,
tools/delegate.ts), so a grant from the page mints the same operator-signed
delegation that axis grant does.
axis dashboard # http://127.0.0.1:7878/?token=… (auto-opens the browser)
axis dashboard --port 9000 # pick a port
axis dashboard --no-open # don't auto-open the browser
Security: the server binds 127.0.0.1 only (never 0.0.0.0), so it is not
reachable from other machines. A random per-session token is minted at start
and embedded in the printed URL; every control endpoint requires it (header
x-axis-token or ?token=), and mutating endpoints additionally reject
cross-site requests (same-origin / Sec-Fetch-Site guard) — so a random web page
open in the same browser can't drive your local permission state. The dashboard
is for the human operator; it's distinct from the stdio MCP server (which serves
the agent's LLM), and both share ~/.axis-mcp.
Permission model. axis grant <scope> writes the scope into
~/.axis-mcp/policy.json and mints/refreshes an operator-signed delegation
covering the agent's full granted set. axis_present and axis_delegate then
refuse to mint or sign for any scope that isn't granted (clear error pointing
at axis grant). Scopes flagged axis require-approval are deny-by-default
until axis approve. The scope grammar is AXIS v0.3 (resource:action, with
resource:* and * wildcards); malformed scopes are rejected with a did-you-mean
hint.
Operator-key local custody
axis operator-key init generates an operator Ed25519 keypair (distinct from
the agent identity key, in a separate keychain account). Registering the
public key with the registry via POST /operators/:id/key is
registrar-authenticated and proof-bearing, so it follows whichever registry the
MCP points at — cloud or your own (see
Self-hosting). Either way grants are
operator-signed locally regardless of registration; registration only enables
third parties to verify the delegation proof against a registry-held key.
One-click onboarding registers the operator key too. You don't have to run
axis operator-key init separately: axis_request_onboarding ensures the
operator key exists and carries its public key + a registration proof
(operator_pubkey, operator_proof) into the activation link. When the operator
signs in, the registrar registers it via POST /operators/:id/key — so the
operator key is registry-verifiable from the same one-click flow. The proof is an
Ed25519 signature over the RFC 8785 JCS canonicalization of { publicKey }
(proofType jcs-eddsa-2026), the same regime the registry uses for POST /register. This path is live — the registrar reads those params from the link
and registers the operator key during onboarding. The registrar's outcome
(registered / conflict / pending) is surfaced as operator_key_status by
axis_complete_onboarding and in axis_status.
Self-hosting against your own registry
Self-host mode behaves exactly like cloud: you run your own axis-registry
instance (your own ledger) and register operator keys through a registrar
credential in that instance — the same code path as registry.axisprime.ai.
- Deploy
axis-registryand seed a registrar row (the shippedseed-key.sqlgives you a one-time registrar API key). - Point the MCP at it, supply that key, then provision the operator key:
export AXIS_REGISTRY_URL=https://registry.example.com # or http://localhost:8787 export AXIS_REGISTRAR_API_KEY=<your registrar key> axis operator-key init
The MCP registers your operator public key directly (registrar Bearer +
JCS-EdDSA proof of ownership). Without AXIS_REGISTRAR_API_KEY the command
still provisions and holds the key locally but skips registry registration — a
no-registrar write to the central registry is correctly refused, so use the
cloud one-click onboarding for that path.
Key custody (LOCAL mode)
The agent's Ed25519 identity key is generated on this machine and stored in
the OS keychain (via keytar). If the keychain is unavailable, it falls
back to an AES-256-GCM-encrypted file under ~/.axis-mcp/. The private key is
never returned by any tool and never leaves the machine.
Disclosure (always surfaced by
axis_statusand onboarding): Your agent's identity key is stored on this computer. Anyone who can use this computer can act as your agent.
Every present/sign action is recorded to a local append-only signed audit log
at ~/.axis-mcp/audit.jsonl (default-on).
Run
npm install
npm run build # tsc → dist/
npm start # node dist/server.js (stdio transport)
# or, no build step:
npm run dev # tsx src/server.ts
Register with an MCP client
claude_desktop_config.json (or any MCP client config):
{
"mcpServers": {
"axis": {
"command": "node",
"args": ["C:/Users/josha/dev/axis-mcp/dist/server.js"]
}
}
}
Environment overrides
| Var | Default | Purpose |
|---|---|---|
AXIS_REGISTRY_URL |
https://registry.axisprime.ai |
Registry base URL. |
AXIS_REGISTRAR_URL |
https://signup.axisprime.ai |
Registrar base for onboarding links (live). |
AXIS_REGISTRAR_API_KEY |
(unset) | Self-host only. A registrar key seeded in your own registry so the MCP can register the operator key and delegations directly against AXIS_REGISTRY_URL (Bearer + proof). Unset on the cloud path. (AXIS_REGISTRAR_KEY is accepted as an alias.) |
AXIS_DELEGATION_REGISTRAR_URL |
(unset) | Product path for delegation registration. A cloud registrar mint endpoint that holds the registrar key server-side; the MCP POSTs the operator-signed delegation to it, so no registrar credential lives on the operator's machine. Preferred over AXIS_REGISTRAR_API_KEY when set. |
AXIS_COMMENTS_GATE_URL |
https://axis-comments.editor-9a4.workers.dev |
Comment gate base for post_comment on the axis-comments-ghost audience (until AXIS discovery advertises an actions manifest). |
AXIS_MCP_HOME |
~/.axis-mcp |
Local data home (audit log, key fallback, metadata, policy). |
AXIS_MCP_DISABLE_KEYCHAIN |
(unset) | Set to 1 to force the encrypted-file key fallback (never the OS keychain). Used by the test suite; available where the keychain is undesirable. |
AXIS_HTTP_TIMEOUT_MS |
30000 |
Registry / well-known fetch timeout. |
Tests
npm test # all (unit + live registry + server smoke)
npm run test:unit # offline: present/delegate signing round-trips
npm run test:live # hits registry.axisprime.ai public read endpoints
See BUILD-STATUS.md for what's verified and the registrar onboarding contract.
License
TBD. No license is set yet (this repo is private). Likely Apache 2.0 to match the open AXIS stack (protocol, registry, SDK); but MUST be MIT / MIT-0 if this is published as a ClawHub skill (ClawHub external mandate). Decide before the repo goes public or is published (GitHub / npm / ClawHub). Tracked in the Cross-Project Coordination DB (Notion).