---
title: "MCP JSON-RPC endpoint"
method: POST
path: "/api/v1/mcp-server"
tags: ["MCP Server"]
---

# MCP JSON-RPC endpoint

`POST /api/v1/mcp-server`

## Request

- Base URL: `https://agent.getaxonflow.com`
- URL: `https://agent.getaxonflow.com/api/v1/mcp-server`
- Auth: none declared. Required headers: `Authorization`
- TypeScript: <https://skmtc.dev/getaxonflow/apis/axonflow-agent-api/docs/api/v1/mcp-server/post.ts> (a function with typed inputs; its response is checked against the schema)

Single Streamable-HTTP endpoint for the built-in MCP server. Accepts
MCP JSON-RPC 2.0 messages (`initialize`, `tools/list`, `tools/call`,
…) and returns JSON-RPC responses. Governance tools exposed:
`check_policy`, `check_output`, `audit_tool_call`, `list_policies`,
`get_policy_stats`, `explain_decision`, plus Pro-tier tools.

Authentication: HTTP Basic `org:license-key` on every request. An
Enterprise deployment MAY additionally send a per-user token in
`X-User-Token` (ADR-043/044). It is optional unless the organization's
`require_user_token` posture demands one (see `BasicAuth`).

**A call held for approval (#4370, Enterprise).** When the
organization's active document requires a person's approval for a
`check_policy` call, the tool answers `allowed: false` with a
`pending_approval` object (see `PendingApproval`) and a
`block_reason` that leads with `approval_pending`; the approval is
queued in the portal's Approvals view. Once a person approves it,
call `check_policy` again with the SAME arguments, naming the
approval in the `X-Axonflow-Approval-Id` header or the `approval_id`
argument: it answers `allowed: true` once and names the approval in
`approval_id`. Any other outcome is refused with an
`ApprovalHoldReason` code. Nothing is queued on the Community build,
which keeps `approval_required`. An approval requirement that also
binds the response (`check_output`) still refuses there with
`approval_required` until the requirement is scoped to the request
plane (#4371).

Governance segments no longer gate `check_policy` or `check_output`
(they did from #3430, refusing with `blocked_by:
"segment_identity_unresolved"` or `"segment_resolution_failed"`).
Neither tool consults segments: each is decided by the anchored
engine, which reads none (PRD v11 §1.1, §1.2), so a segment
resolution that would fail changes nothing.

Two identity rules are narrower than an operator usually expects,
and both are deliberate:

- **`X-User-Email` does not substitute for a validated per-user
  token, even when `AXONFLOW_TRUST_IDENTITY_HEADERS` is on.** Under
  the organization's `require_user_token` posture, a caller without a
  validated token is refused before the header can supply an
  identity. The header
  keeps its attribution role; the ADR-044 override writes answer
  the freeze from v11.0.0 (#4252).
- **A shared synthetic identity never holds a per-user session
  override.** ADR-044 overrides are scoped to (tenant, user, policy),
  so an override created under an identity many callers share would
  flip deny to allow for every one of them (#2896). `create_override`
  refuses such a session for its identity first (every other session is
  answered `LEGACY_POLICY_WRITE_FROZEN` from v11.0.0, #4252), and no
  override is offered to it; its audit rows still carry the identity. The census is
  `IsSharedSyntheticIdentity` in `platform/shared/identity`: the
  reserved `axonflow.local` and `axonflow.internal` addresses, the MCP
  client pseudo-identity prefix, and the Community-SaaS evaluator
  `evaluator@try.getaxonflow.com`; in Community mode the local-dev
  identity is exempt. Token minting validates only that
  the subject contains `@`, so a token for such an identity
  validates, and `check_policy` and `check_output` accept it like any
  other validated token.

The Community SaaS per-minute limit and daily quota are answered
with HTTP 429, a `Retry-After` header, and the `RateLimitEnvelope` as JSON text in
`result.content[0].text`, with `result.isError: true` (#4261;
before it, the same result came with a 200). The per-minute limit
answers `limit_type: per_minute`, window `minute`, `resets_at` one
minute out and `Retry-After: 60`; the daily quota answers
`limit_type: daily_quota`, window `daily_utc`, resetting at
midnight UTC. A credential refused by the pre-credential
per-minute limiter gets the same per-minute answer, with an empty
`tier` because the tier is not resolved yet, instead of a 401.
The `tools/call` tier gates (`feature_pro_only`,
`hitl_approvals_window`) answer the same result shape with HTTP 403,
`X-Axonflow-Tier-Limit` and `X-Axonflow-Upgrade-URL`, as the REST
routes do (#4274; before it, a 200 with no headers). Only
`hitl_approvals_window` has a reset time, so only it is sent with
`Retry-After`: the seconds until its `resets_at`.

JSON-RPC error codes: `-32001` is the one application code, an
authentication failure, sent with HTTP 401 and a
`WWW-Authenticate: Basic` challenge. The rest are the JSON-RPC 2.0
standard codes: `-32700` (parse error) with HTTP 400, `-32600`
(invalid request) with HTTP 400, or 415 when the `Content-Type` is
not `application/json`, and `-32601` (method not found) and
`-32602` (invalid params) with a 200.

The JSON-RPC `check_output` tool result DOES include
`redacted_message` — unlike the standalone REST
`/api/v1/mcp/check-output` endpoint (#2870).

## Headers

- `X-Axonflow-Approval-Id` string, uuid
- `Authorization` string, required
- `X-Axonflow-PEP-Handshake` string: base64url (padding optional) of the PEPHandshake document.

## Request body

- object: MCP JSON-RPC 2.0 message
  - `jsonrpc` '2.0'
  - `id` unknown
  - `method` string
  - `params` object

## Response `200`

JSON-RPC response (errors are carried in the JSON-RPC error member)

- object

## Other responses

- `401`: Missing or invalid authentication. Handler-written 401s use the `{success, error}` envelope; 401s written by the auth middleware use the `{"error": {"code", "message"}}` envelope (JSONError).
- `403`: Community SaaS `tools/call` tier gate (#4274): a tool above the caller's tier (`limit_type: feature_pro_only`) or the rolling approval window (`limit_type: hitl_approvals_window`). The body is a JSON-RPC response whose `result.content[0].text` is the `RateLimitEnvelope` as JSON text, with `result.isError: true`. Sent with `X-Axonflow-Tier-Limit` and `X-Axonflow-Upgrade-URL`, and with `Retry-After` only for `hitl_approvals_window`. Also, on any method (`initialize` included), a tier admission refusal (#4249 row 5682255301): a valid credential whose organization is at its service-principal ceiling (`limit_type: service_principal`), or a validated per-user token past the human-principal ceiling (`limit_type: human_principal`). The envelope's `code` is `ERR_TIER_LIMIT_<DIMENSION>` and its `error` the refusal's message; no `Retry-After`. The REST routes answer the same refusal 402. It is never a 401: the credential was valid.
- `429`: Community SaaS rate limit (#4261): the per-minute limit (`limit_type: per_minute`), including a credential refused by the pre-credential limiter, or the daily quota (`limit_type: daily_quota`). Also, on any method, a tier admission refusal while the admission ledger cannot be reached (`limit_type: service_principal` or `human_principal`, `code` `ERR_TIER_LIMIT_<DIMENSION>`, #4249 row 5682255301). The body is a JSON-RPC response whose `result.content[0].text` is the `RateLimitEnvelope` as JSON text, with `result.isError: true`. Sent with `Retry-After`, `X-Axonflow-Tier-Limit` and `X-Axonflow-Upgrade-URL`.

---

[API](https://skmtc.dev/getaxonflow/apis/axonflow-agent-api.md) - [All operations](https://skmtc.dev/getaxonflow/apis/axonflow-agent-api/llms.txt) - [OpenAPI document](https://skmtc.dev/getaxonflow/apis/axonflow-agent-api/revisions/34d30fcb7fa1?raw)
