MCP Integration¶
Audience: owners and operators wiring an MCP-aware client (Claude Code, Claude Desktop, IDE agents, custom Pydantic AI / OpenAI Responses agents) into an Axon instance.
Russian mirror: use the language selector at the top of the page → Русский.
What it is and why¶
Inbound MCP is a second control plane mounted at /api/v1/mcp. It
exposes a deliberately small axon.* tool surface that lets an
external agent inspect the platform (workflows, runs, approvals,
projects, conversations, cases) and trigger guarded mutations
(workflow lifecycle, approval decisions, outbound conversation reply)
without bypassing the canonical CommandEnvelope + RBAC + policy +
audit + outbox pipeline.
- Reads delegate to the same query handlers the Console REST API uses (no parallel storage paths).
- Mutations go through the canonical
CommandProcessor; the MCP layer only builds the envelope withsource="mcp",actor_type=SERVICE,actor_id="mcp:<key_id>". - Auth is pre-issued bearer token —
Authorization: Bearer axon_mcp_<key_id>.<secret>. There is no OAuth flow in the MVP (PLAN-MCP-INBOUND §0.4).
Connecting a client¶
Two paths are supported; Claude Code (native HTTP) is the default.
Variant A — Claude Code (native HTTP MCP)¶
claude.json or project/user .mcp.json:
{
"mcpServers": {
"axon": {
"type": "http",
"url": "https://axon.example.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer axon_mcp_<key_id>.<secret>"
}
}
}
}
Variant B — Claude Desktop via the mcp-remote bridge¶
~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) / %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"axon": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://axon.example.com/api/v1/mcp",
"--header",
"Authorization: Bearer ${AUTH_TOKEN}"
],
"env": {
"AUTH_TOKEN": "axon_mcp_<key_id>.<secret>"
}
}
}
}
Pre-requisite: node ≥ 18 + npx. With a self-signed dev cert add
"NODE_TLS_REJECT_UNAUTHORIZED": "0" to env (dev only; remove in
production).
Claude Desktop's built-in custom-connector UI is OAuth-oriented; if
your installed Desktop build cannot inject a static Authorization
header, stay on mcp-remote until B7 OAuth/DCR ships.
IDE agents / custom MCP Python SDK clients¶
Any client that supports
mcp.client.streamable_http.streamablehttp_client(...) with
headers={"Authorization": "Bearer ..."} works without modification.
Issuing a key¶
Short path: Console → Settings → MCP API Keys → New key.
Plaintext is shown once on the create screen — save it now. Key
rotation is revoke + create new; in-place rotation is invariant
I-MCP-5 forbidden. Operational runbook lives at
deploy/runbooks/mcp-api-key-lifecycle.md;
incident-response runbook at
mcp-key-compromised.md.
Canonical tool surface (21 tools)¶
Reads¶
| Tool | What | Permission |
|---|---|---|
axon.workflow.list |
List workflows in a project | READ |
axon.workflow.get |
One workflow by id | READ |
axon.run.get |
Run snapshot + step timeline | READ |
axon.run.history |
Recent runs in a project | READ |
axon.run.steps |
Step timeline scoped to a run | READ |
axon.approval.list |
List approvals | READ |
axon.approval.get |
One approval by id | READ |
axon.project.list |
Projects the key can see | implicit (via scope) |
axon.project.get |
One project | READ |
axon.health |
Health probe (no project scope) | valid key only |
axon.conversation.list |
List conversations | READ |
axon.conversation.get |
Conversation + recent messages | READ |
axon.case.list |
List cases (redacted projection) | READ |
axon.case.get |
One case by id (redacted projection) | READ |
Mutations¶
| Tool | Permission | Notes |
|---|---|---|
axon.workflow.create |
CREATE_WORKFLOW (MANAGER+) | Inline definition or definition_id via payload |
axon.workflow.start |
START_WORKFLOW (OPERATOR+) | Downstream effects may be external_high |
axon.workflow.pause |
PAUSE_WORKFLOW (MANAGER+) | |
axon.workflow.resume |
RESUME_WORKFLOW (MANAGER+) | |
axon.workflow.cancel |
CANCEL_WORKFLOW (MANAGER+) | Destructive |
axon.approval.decide |
APPROVE / REJECT_APPROVAL | Verb approve/reject |
axon.conversation.send_reply |
SEND_MESSAGE (OPERATOR+) | Outbound reply through a durable command (external_low, policy/approval/outbox/audit). Not an alias for send_message — that one stays inbound-append. Phase 4 caveat: the final provider call lives in the send_conversation_reply activity, which is currently a stub shared with the Console-driven flow. The MCP envelope, policy gate, audit chain, and workflow signal all run end-to-end; real Telegram delivery is a follow-up worker extension and is not specific to MCP. |
Excluded from MVP: axon.workflow.retry / .replan / .undo — their
permissions live only on OWNER/ADMIN and are not exposed externally.
Idempotency¶
Every mutating tool requires an idempotency_key: str. Replays with
the same key + payload return the original command_id. Replays
with the same key but a different payload return an MCP-level error
failure_code="idempotency_conflict" (CallToolResult isError=true)
— this is not HTTP 409.
Error semantics¶
- Protocol-level (auth, rate-limit, transport security) — HTTP 401/403/421/429.
- Tool-level failures land as
CallToolResult(isError=true, structuredContent.failure_code=...). Codes:validation_error,rbac_denied,version_conflict,idempotency_conflict,project_suspended,target_not_found,rate_limited,handler_reject,forbidden.
Best practices¶
- Scope keys narrowly. Most automations need a single
project_scopes=[<id>]key with roleOPERATORorREAD_ONLY. - Restrict the tool set per key. If a key only needs
axon.workflow.startandaxon.workflow.get, setallowed_tool_namesaccordingly — this is the second RBAC plane. - Treat plaintext as a secret. Never paste it in chat history, UI fields or git. Revoke + rotate at the first sign of compromise.
- Watch the dashboard. Console → Settings → MCP API Keys → click
a row for derived telemetry:
last_seen_at,requests_last_7d(success/denied split), top tools.
What MCP does NOT do¶
- No long-running execution — agents stay external; the platform runs workflows on Temporal as before.
- No AI calls — LiteLLM remains internal-only.
- No streaming/notifications — request/response only; poll
axon.run.getfor run state.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
| HTTP 401 | Missing/invalid Authorization: Bearer ... header. |
| HTTP 403 | Origin not in AXON_MCP_ALLOWED_ORIGINS. |
| HTTP 421 | Host not in AXON_MCP_ALLOWED_HOSTS. |
| HTTP 429 | Per-key rate limit exceeded; Retry-After header included. |
| Locked out after a wrong-secret burst | Redis transient lockout (≥10 failures / 5 min). Wait the window or revoke + rotate the key. |
failure_code="version_conflict" |
expected_version stale — re-read via axon.*.get. |