---
title: "Validate MCP input against policies"
method: POST
path: "/api/v1/mcp/check-input"
tags: ["MCP Connectors"]
---

# Validate MCP input against policies

`POST /api/v1/mcp/check-input`

## Request

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

Validate an MCP request (query or command) against configured policies **without executing it**.

This endpoint enables external orchestrators (LangGraph, CrewAI, custom pipelines) to use
AxonFlow as a policy gate while managing MCP connector execution themselves.

## Policy Evaluation

The anchored decision engine (ADR-065) decides the request from the organization's active typed policy document: the system detectors (SQL injection, dangerous queries, PII) run over the statement and its parameters, and the engine's verdict is the response. Tenant dynamic policies no longer decide MCP requests (PRD v11 §1.2).

If the engine denies the request, the response returns `allowed: false` with a `block_reason`. A request the engine cannot decide is refused with 503; it is never let through (PRD v11 §1.7).

## When to Use

Use `check-input` + `check-output` when your orchestrator manages MCP execution natively.
Use `/mcp/resources/query` or `/mcp/tools/execute` when you want AxonFlow to handle both
policy enforcement and connector execution.

## Audit Logging

All check-input evaluations are logged to the `mcp_query_audits` table with
`operation: "check-input"` for compliance tracking. Audit entries include
`parameters_hash` (SHA-256) and `parameter_count` for forensic analysis.

## Headers

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

## Request body

- MCPCheckInputRequest
  - `approval_id` string, uuid: The approval a retry spends (#4370), for a client that cannot set the `X-Axonflow-Approval-Id` header (see that parameter). Never part of what the approval binds.
  - `client_id` string: Client identifier (required in Enterprise mode)
  - `user_token` string: JWT user token (required in Enterprise mode)
  - `tenant_id` string: Tenant identifier (required in Enterprise mode, defaults to "default" in Community)
  - `user_id` string: Optional end-user identifier. Honored only for internal-service callers, which may assert the end user; it attributes the decision's audit row
  - `user_role` string: Optional end-user role (e.g., "admin", "analyst"). Honored only for internal-service callers; recorded on the decision's audit row
  - `connector_type` string, required: MCP connector/server type (e.g., "postgres", "snowflake", "salesforce")
  - `tool` string: Optional tool identifier being invoked, distinct from connector_type/server (#2904). Feeds capability-scoped policy evaluation when set.
  - `statement` string, required: The SQL query or command to validate against policies
  - `parameters` object: Optional query parameters. Values are individually scanned for SQLi, PII, and compliance violations by the static policy engine. String values are scanned directly; nested objects/arrays are JSON-serialized before scanning; numeric values are converted to strings for PII/compliance detection. Boolean values are skipped.
  - `operation` 'query' | 'execute': Operation type. Under the read-only posture it classifies the call as a read or a write (`classifyMCPCall` in `platform/agent/mcp_handler.go`)
  - `content_type` string: Declared content type of `statement` (ADR-056). Defaults to `text/plain` when omitted. When set to a value no registered detector handles, the request is rejected with **415** and a canonical blocked audit row tagged `content_type_unsupported` is written (fail-closed; see the 415 response). Source of truth: `platform/agent/mcp_handler.go` (MCPCheckInputRequest).

## Response `200`

Input passed all policy checks

- MCPCheckInputResponse
  - `pending_approval` PendingApproval: A call held for a person's approval (#4370, PRD v11 §1.13). Pending is NOT allow: nothing ran, and the enforcement point must not forward. An approver approves the queue entry in the portal (Approvals), and the caller retries the same call naming `approval_id` (see the `X-Axonflow-Approval-Id` parameter). The approval expires at `expires_at`: the approval requirement's own deadline, which the engine stamps 15 minutes after the decision on v11. It is never extended; a retry after it is refused `approval_expired`.
    - `approval_id` string, uuid, required: The queue entry's id; the retry names it.
    - `status` 'pending' | 'approved', required: `pending`: nobody has decided it yet. `approved`: a person approved it and it is waiting for this caller's retry, which must name the id.
    - `plane` 'mcp:request' | 'decide', required
    - `expires_at` string, date-time: When the approval lapses. Omitted on a retry of a still-pending approval.
    - `retry` object, required
      - `header` 'X-Axonflow-Approval-Id', required
      - `argument` 'approval_id': The MCP tool argument / MCP route body field that carries the id.
      - `body_field` 'approval_id': The decide request field that carries the id.
  - `approval_id` string, uuid: On an allow, the approval that admitted this call (#4370).
  - `allowed` boolean: Whether the input passed all policy checks
  - `block_reason` string: Human-readable reason if blocked (omitted when allowed)
  - `policies_evaluated` integer: Total number of policies evaluated
  - `policy_info` PolicyInfo: Policy evaluation information included in MCP responses
    - `policies_evaluated` integer: Number of policies evaluated during request/response processing
    - `blocked` boolean: Whether the request was blocked by policy
    - `block_reason` string: Reason if the request was blocked
    - `redactions_applied` integer: Number of field redactions applied to the response
    - `processing_time_ms` integer: Time spent on policy evaluation in milliseconds
    - `matched_policies` PolicyMatchInfo[]: Policies that matched during evaluation
      - `policy_id` string: Unique policy identifier
      - `policy_name` string: Human-readable policy name
      - `category` string: Policy category (e.g., "pii-us", "security-sqli")
      - `severity` string: Match severity (low, medium, high, critical)
      - `action` string: Action taken (block, redact, warn, log)
    - `exfiltration_check` ExfiltrationCheckInfo: Information about exfiltration limit checks (v3.2.0+)
      - `rows_returned` integer: Number of rows in the response
      - `row_limit` integer: Configured row limit (MCP_MAX_ROWS_PER_QUERY)
      - `bytes_returned` integer: Response size in bytes
      - `byte_limit` integer: Configured byte limit (MCP_MAX_BYTES_PER_QUERY)
      - `within_limits` boolean: True when the response stayed within every configured limit
  - `decision_id` string: Unique audit correlator for this policy decision. Links the gate response to its row in the audit log; surfaceable to end users for explainability ("decision: dec_abc123").
  - `risk_level` 'low' | 'medium' | 'high' | 'critical': Highest risk level across all matched policies. Plugins use this to map the block reason to severity (warning vs hard error).
  - `policy_matches` RicherPolicyMatch[]: Per-policy explainability records (ADR-043). Surfaced on a refusal: the controls that determined the anchored engine's verdict. Source of truth: `platform/agent/mcp_request_enforcing_seam.go` (`anchoredPolicyMatches`).
    - `policy_id` string: Unique policy identifier.
    - `policy_name` string: Human-readable policy name. Omitted when unknown.
    - `risk_level` 'low' | 'medium' | 'high' | 'critical': Risk level configured on this policy. Omitted when unset.
    - `allow_override` boolean: Whether this policy permits a session override.
    - `policy_version` integer: Policy version that matched. Omitted when zero.
  - `override_available` boolean: Not set from v11.0.0: session overrides are retired (PRD v11 §1.5, #4252), so the block offers none and a plugin renders no override hint.
  - `override_existing_id` string: Not set from v11.0.0: no override is offered, and the step gate no longer reads a session override (#4252).
  - `redacted` boolean: Whether the engine masked any PII in the request statement or in any parameter. Omitted (false) when nothing was redacted.
  - `redacted_statement` string: The request statement with PII fields masked. A PEP fulfilling a Decision Mode redact_pii obligation forwards THIS value instead of the original. Omitted when the statement was not masked, including a redaction of the parameters alone (`redacted: true` with only `redacted_parameters`). Source of truth: `platform/agent/mcp_handler.go` (MCPCheckInputResponse).
  - `redacted_parameters` object: Each request parameter the redaction masked, keyed by parameter, as the text the request pass scanned it as: the string itself, or a map or list parameter's JSON serialisation, masked as one text so a span across the serialisation is masked as it was matched. The caller decodes it where it decodes the parameter and forwards the masked value. A string or number parameter comes back as its text (a number's decimal text) masked in place, unquoted, as a string. Present only for an enforcement point whose PEP handshake declares `field_redact` at version 2, on Enterprise; any other caller whose parameter the redaction masks is refused 403 `unsupported_obligation`. Omitted when no parameter was masked, so a caller that never carries PII in its parameters gets a byte-identical response (#4264, since v11.1.0).
  - `redaction_evaluated` boolean: Whether the redaction detector actually ran (regardless of whether it masked anything). A PEP fulfilling a redact_pii obligation MUST fail closed when this is false — it means no detection config was enabled, so `redacted: false` is indistinguishable from "looked, found nothing" and the request must not be forwarded as clean.
  - `engine` 'anchored': Which policy engine authored this verdict: the ADR-065 decision plane, the only author on this route (PRD v11 §1.1). Omitted on a refusal no engine decided - an authentication failure, or a request refused before the policy pass ran.
  - `subject_type` string: The type of principal the verdict was decided for (PRD v11 §1.6): `User` for a verified user token, `Client` when the request presented no user identity and its client credential is the principal. Omitted wherever `engine` is.
  - `policy_bundle` string: The digest of the policy set that decided: the system corpus's restriction for this route and the organization root - the organization's active typed document composed with the deployment's baseline permission pack, or, while it has published nothing, the implicit bundle of that pack and the organization template. A rollback reinstates an earlier digest. Omitted wherever `engine` is.
  - `policy_packs` string[]: The add-on policy packs (PRD v11 §1.9) whose controls composed into `policy_bundle` on this route, each as `<pack id>@<digest of the pack document the deployment instantiated>`, sorted. Omitted when the deployment installs no pack or none binds on this route.
  - `legacy_validators` object[]: A checksum validator that acted BEFORE the anchored engine decided (#4122): under the organization's recorded `pii=redact` detection override, the Indonesia validator masked the statement ahead of the decision plane. Omitted when none did.
    - `validator` 'indonesia_pii' | 'india_pii', required
    - `action` 'blocked' | 'masked', required

## Other responses

- `400`: Invalid request body or parameters
- `401`: Missing or invalid authentication on an MCP REST route. FOUR distinct causes; the status code is the same for all four, and the audit row is what distinguishes the last two. 1. **Credentials.** No usable Basic auth / license credential. 2. **A tenant the credential may not act for.** 3. **`user_token_rejected`** -- a `user_token` WAS supplied in the request body and failed to validate: malformed, expired, signed with the wrong algorithm, carrying a bad signature, or revoked by `jti`. **This is new behaviour on these four routes.** Previously ANY user-token failure in enterprise mode was answered by synthesizing a service identity and serving the request, so revocation and expiry had no effect here; a presented token that fails to validate is now a refused access attempt, audited and returned as `401`. This is not opt-in and no flag restores the old behaviour. 4. **`user_token_required`** (#3476) -- NO `user_token` was supplied and the caller's organisation requires one. **Off by default**; this cause cannot occur unless an operator has set `organizations.require_user_token` for that organisation, or the deployment-wide `AXONFLOW_REQUIRE_USER_TOKEN` default. With the posture off, a token-less enterprise caller is still served under a synthetic service identity, which is the correct answer for an infrastructure gateway that has no end-user token to forward. Causes 3 and 4 are recorded as reserved identifiers in the audit row's policy ids and MUST NOT be collapsed: they have opposite operator remedies (repair the caller's token, versus provision one at all). Query them with JSONB containment against `policy_details->'policy_ids'`, never with `LIKE` -- `_` is a single-character wildcard, so a `%user_token_required%` pattern also matches unrelated prose in the same row. These routes read the per-user token from the `user_token` field of the request body. They do not read the `X-User-Token` header; that header belongs to the MCP-server JSON-RPC envelope. Handler-written 401s use the `{success, error}` envelope; 401s written by the auth middleware use the `{"error": {"code", "message"}}` envelope (JSONError).
- `403`: Input blocked by policy. **`segment_resolution_failed` is no longer a cause** (it was one from #3447). No segment gate stands here any more: the anchored engine authors the request pass's verdict (PRD v11 §1.1) and reads no segments, as on decide. **`unsupported_obligation`: a redaction this route cannot discharge** (#4264). Under an organization's `pii=redact` posture a permit composes a mandatory redaction of the request. This route hands back the masked statement (`redacted_statement`), so a redaction of the statement alone is answered 200. A redaction that masks a request parameter is answered 200, with each masked parameter in `redacted_parameters`, ONLY to an enforcement point whose PEP handshake (`X-Axonflow-PEP-Handshake`) declares `field_redact` at version 2 as well as version 1, and only on Enterprise: a caller that does not substitute the masked parameters would run its tool on the originals. Every other caller is refused, the masked statement included. So is a redaction that masks nothing in the statement or in any parameter. The refusal is `allowed: false` with a `block_reason` that starts `unsupported_obligation:` and names the obligation, the policy that attached it and why: for a parameter, its key (never its value or its masked text) and the missing `field_redact@2`. A redaction the platform could not attempt (no redaction engine installed, the policies unreadable, a requirement or detector the engine no longer holds) is a 503.
- `415`: Unregistered `content_type` (ADR-056). The request named a content type that no registered detector handles. A canonical blocked audit row tagged `content_type_unsupported` is written before the error returns — the request is treated as blocked, not silently skipped.
- `503`: The policy engine could not decide the request (for example, the organization's active policy document could not be read). The request is refused, never let through (PRD v11 §1.7).

---

[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)
