MCP Connectors

Validate MCP input against policies

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.

post/api/v1/mcp/check-input

Request

Headers

X-Axonflow-Approval-Idstring uuid

The approval a retry spends (#4370). A call held for a person's approval is answered a pending_approval naming an id; once a person approves it in the portal, the caller retries the SAME call naming that id here (or in the body's approval_id field, for a client that cannot set headers - a header and a field naming different ids are refused approval_not_found).

The retry is decided exactly as the first call was. Only when it is again held for approval, and the approval is approved by a person who is not the caller, before its expiry, unspent, and granted for this very call (the same input, tool, route, requester and requirement), does it pass - and the approval is spent: it admits exactly one call. A call that is allowed anyway spends nothing; no approval lifts a deny. Every other outcome is refused with one of the ApprovalHoldReason codes. Enterprise; the Community build has no approval queue and ignores the header.

WHAT "THE SAME CALL" MEANS. On the MCP routes: the connector, the tool, the operation, the statement and its parameters (and the row limit on /mcp/resources/query). On /api/v1/decide: the WHOLE request the engine decided - stage, target, query AND context. A retry that changes any of it is refused bound_input_changed: resend the same context, and carry per-request values (request ids, timestamps) in headers, never in context - the correlation id is a header.

Idempotency-Keystring
Example:n8n-exec-abc123-node-Approve

Optional per-request dedup token. When supplied on POST /api/v1/mcp/check-input, POST /api/v1/audit/tool-call, or POST /api/v1/hitl/queue, the platform caches the original response for 24h and returns it byte-for-byte on subsequent requests carrying the same key + same authenticated tenant + same endpoint.

Format: 1-256 chars, ^[A-Za-z0-9_.:\-/]+$. Workflow IDs from n8n, ADK, or generic SDKs all fall inside this set. A malformed key returns 400 before the handler runs.

Cache rules: 2xx + 4xx responses are cached; 5xx is NOT cached so the caller's retry can hit a fresh attempt. A cache hit returns the original response plus an Idempotent-Replayed: true response header.

Cross-tenant collisions are impossible: tenant_id participates in the primary key + an RLS policy on the storage table. Two tenants using the same key value get distinct rows.

X-Axonflow-PEP-Handshakestring

base64url (padding optional) of the PEPHandshake document.

The ADR-065 PEP capability handshake: base64url of a compact JSON document in which an external enforcement point declares what it is and which obligations it can discharge. See PEPHandshake for the document.

Absent is the default and changes nothing. A caller that omits the header takes byte-for-byte the path it took before this header existed.

What an absent header means for a redaction depends on the plane, by design (PRD v11 section 1 item 16, #4257). The MCP passes discharge a redaction of the content they hand back: the request pass (check-input, check_policy) masks the statement, and a redaction that masks nothing in the statement is refused unsupported_obligation to every caller, and one that masks a request parameter to every caller that has not declared field_redact at version 2 (on Community, to every caller) (#4264); the response passes (check-output, the MCP server's check_output) mask the rows or the message. /api/v1/decide and the gateway pre-check return a decision rather than content, so a required redaction is a field_redact obligation for the enforcement point, and a caller that has not declared field_redact is refused unsupported_obligation. A caller that declares NO redaction (capabilities: []) is refused on each of these planes; on Community the MCP passes still return a checksum validator's masked content to it (reachable only through a directly inserted detection_action_overrides row) until #4122.

A header that is PRESENT and cannot be read is refused, never treated as absent: degrading a malformed declaration to "legacy caller" would go on handing an enforcement point obligations it had just said it cannot discharge. The refusal is 400 and its message names this header, which matters on /api/v1/access/evaluation where the refusal is rendered through that surface's existing incomplete_evaluation code and the message is the only thing distinguishing a malformed HEADER from a malformed body ENVELOPE.

Present more than once is refused: RFC 7230 permits an intermediary to join repeated field lines with a comma, and a comma is outside the base64 alphabet, so a joined pair can only decode to malformed. That is why the document is base64 rather than raw JSON, which would join into something a lenient parser might accept.

When a decision carries a mandatory obligation the declared set does not cover, the request is answered 200 with verdict: deny and the reason unsupported_obligation (ADR-065 invariant 8) - a decision about the request, not a transport error. That holds on every edition. An Enterprise deployment adds a second reason beginning pep_capability_unsupported naming the gap, and refuses a checksum validator's mask on the MCP passes; a Community deployment hands that masked content over (#4257 split 2, #4122).

Request body

approval_idstring 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_idstring

Client identifier (required in Enterprise mode)

user_tokenstring

JWT user token (required in Enterprise mode)

tenant_idstring

Tenant identifier (required in Enterprise mode, defaults to "default" in Community)

user_idstring

Optional end-user identifier. Honored only for internal-service callers, which may assert the end user; it attributes the decision's audit row

user_rolestring

Optional end-user role (e.g., "admin", "analyst"). Honored only for internal-service callers; recorded on the decision's audit row

connector_typestring required

MCP connector/server type (e.g., "postgres", "snowflake", "salesforce")

toolstring

Optional tool identifier being invoked, distinct from connector_type/server (#2904). Feeds capability-scoped policy evaluation when set.

statementstring required

The SQL query or command to validate against policies

parametersobject

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_typestring

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

Input passed all policy checks

approval_idstring uuid

On an allow, the approval that admitted this call (#4370).

allowedboolean

Whether the input passed all policy checks

block_reasonstring

Human-readable reason if blocked (omitted when allowed)

policies_evaluatedinteger

Total number of policies evaluated

decision_idstring

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

override_availableboolean

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_idstring

Not set from v11.0.0: no override is offered, and the step gate no longer reads a session override (#4252).

redactedboolean

Whether the engine masked any PII in the request statement or in any parameter. Omitted (false) when nothing was redacted.

redacted_statementstring

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_parametersobject

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_evaluatedboolean

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_typestring

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_bundlestring

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_packsstring[]

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.

Changes

No recorded changes to this operation across all 1 revision of this API.