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

# Validate MCP output against policies

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

## Request

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

Validate MCP response data against configured policies **without having executed the query
through AxonFlow**.

This endpoint enables external orchestrators to apply AxonFlow's output-side policy
enforcement (PII redaction, exfiltration limits, SQLi response scanning) to data they
fetched from MCP connectors independently.

## Policy Evaluation

The following checks are applied in order:
1. **SQLi response scanning**: Detects SQL injection artifacts in response data
2. **Static response policies**: PII detection and redaction (SSN, credit card, Aadhaar, etc.)
3. **Exfiltration limits** (query-style only): Row count and byte size limits

If PII is detected, the response includes `redacted_data` with masked values.
If exfiltration limits are exceeded, the response returns `allowed: false`.

## Query vs Execute Responses

- **Query responses** (`response_data`): Full policy evaluation including exfiltration checks
- **Execute responses** (`message`): SQLi scanning and PII checks only (no exfiltration limits)

## Audit Logging

All check-output evaluations are logged to the `mcp_query_audits` table with
`operation: "check-output"` for compliance tracking.

## Headers

- `X-Axonflow-Client` string
- `X-Axonflow-PEP-Handshake` string: base64url (padding optional) of the PEPHandshake document.

## Request body

- union
  - object
    - `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 user identifier
    - `connector_type` string, required: MCP connector/server type (e.g., "postgres", "snowflake", "salesforce")
    - `tool` string: Optional tool identifier whose output is being validated, distinct from connector_type/server (#2904/#2955). Feeds capability-scoped response evaluation when set (a text-document tool's output skips execution-class detectors); omitted → full (fail-closed) evaluation, no fallback from connector_type.
    - `response_data` object[], required: Query-style response rows to validate
    - `message` string: Execute-style response message (e.g., "5 rows affected")
    - `metadata` object: Connector metadata for SQLi response scanning (e.g., query echo, database name)
    - `row_count` integer: Total number of rows returned (used for exfiltration limit checks)
  - object
    - `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 user identifier
    - `connector_type` string, required: MCP connector/server type (e.g., "postgres", "snowflake", "salesforce")
    - `tool` string: Optional tool identifier whose output is being validated, distinct from connector_type/server (#2904/#2955). Feeds capability-scoped response evaluation when set (a text-document tool's output skips execution-class detectors); omitted → full (fail-closed) evaluation, no fallback from connector_type.
    - `response_data` object[]: Query-style response rows to validate
    - `message` string, required: Execute-style response message (e.g., "5 rows affected")
    - `metadata` object: Connector metadata for SQLi response scanning (e.g., query echo, database name)
    - `row_count` integer: Total number of rows returned (used for exfiltration limit checks)

## Response `200`

Output passed all policy checks (may include redacted data)

- MCPCheckOutputResponse
  - `allowed` boolean: Whether the output passed all policy checks
  - `block_reason` string: Human-readable reason if blocked (omitted when allowed)
  - `redacted_data` unknown
  - `policies_evaluated` integer: Total number of policies evaluated
  - `exfiltration_info` 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
  - `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.
  - `redaction_evaluated` boolean: Whether the response-phase redaction pipeline actually ran (#2865). Mirrors MCPCheckInputResponse.redaction_evaluated for the response leg. A PEP fulfilling a response-phase redact_pii obligation MUST fail closed when this is false/absent — the redactor did not run (detection disabled for the connector, or no policy engine), so absence of `redacted_data` cannot be trusted as "nothing to mask." Omitted (false) preserves the pre-#2865 shape.
  - `engine` 'anchored': Which policy engine authored this verdict: the ADR-065 decision plane, the only author on this route (PRD v11 §1.1). It rides this body and the 403 envelope of a refusal by the same pass. 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 the response pass, 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 an organization's recorded `pii=block` or `pii=redact` detection override, the Indonesia or India validator blocked the request or masked the response ahead of the decision plane. Omitted when none did, which is every request without such an override.
    - `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`: Output blocked by policy (exfiltration limit exceeded or SQLi detected). **`segment_resolution_failed` is no longer a cause** (it was one from #3447). The route resolved the caller's governance segments and refused the request when that failed, on behalf of an organization's segment-scoped static rows, and those rows no longer decide: the anchored engine authors this verdict and reads no segments (PRD v11 §1.1, §1.2). So the shared engine's detector pass runs with no segment set, and a segment resolution that would fail changes nothing.

---

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