MCP gateway
POST /v1/gateway/mcp decides an MCP tool call. The tool name is mapped to an action by substring. Vulnify does not execute arguments.
The response is the same decision body as POST /v1/events, plus gateway. Unlike the HTTP gateway, this object has no upstream. gateway.upstream is not in the schema. mappedAction is the action the tool name mapped to. allowed is whether that decision allows the call.
The Node.js guardMcpHandler adapter is a different path. It guards a handler inside your own MCP server and calls POST /v1/events. Use that when the server runs in your process. Use this endpoint when the caller sends the tool call to Vulnify.
Request
Section titled “Request”curl -X POST https://api.vulnify.io/v1/gateway/mcp \ -H "Authorization: Bearer $VULNIFY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "routeId": "ROUTE_ID", "tool": "crm.export_contacts", "resource": "Customer Database", "arguments": { "limit": 500 } }'| Name | In | Required | Type | Description |
|---|---|---|---|---|
Idempotency-Key | header | no | string | Retries with the same key (per organization and endpoint, 24 hours) return the stored decision instead of deciding again. The replay always includes finalDecision, derived from the stored decision and the current review. |
| Field | Type | Required | Description |
|---|---|---|---|
agent | string (max length 100) | no | Agent name as registered in Vulnify. |
agentId | string (uuid) | no | Agent id. Use this or `agent`. |
resource | string (max length 100) | no | Resource name as registered in Vulnify. |
resourceId | string (uuid) | no | Resource id. Use this or `resource`. |
destination | INTERNAL | EXTERNAL_EMAIL | EXTERNAL_API | no | |
recordsAffected | integer (0–100000000) | no | |
content | string (max length 100000) | no | Optional text scanned for sensitive data. Scanned in memory and never stored. |
routeId | string (uuid) | no | Registered MCP route (supplies a default agent). |
tool | string (min length 1, max length 200) | yes | MCP tool name. Mapped to an action by substring. |
arguments | object | no | Tool arguments. Not executed by Vulnify. |
tool is required. A registered MCP route supplies a default agent. The 400 response still describes a validation error when agent and resource are not identified. arguments is an object of any keys. It is not executed by Vulnify.
Response
Section titled “Response”Decision: { id, decision, finalDecision, evaluatedDecision, monitored, riskLevel, riskScore, reasons, policy, dlpFindings, lgpdCategories, review, quotaExceeded, sandbox }. decision is the stored outcome and does not change when a review is resolved. finalDecision is the effective outcome (ALLOW after approval, BLOCK after denial or expiry, REVIEW while pending) and is always present, including an idempotent replay. quotaExceeded flags a plan overrun without blocking; sandbox is true for TEST keys.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes | |
decision | ALLOW | REVIEW | BLOCK | yes | Decision recorded on the event. It does not change when a review is resolved. |
finalDecision | ALLOW | REVIEW | BLOCK | yes | Effective outcome. Equals `decision` when there is no review. REVIEW while a review is pending. ALLOW after approval, BLOCK after denial or expiry. Always present, including an idempotent replay of a response stored before this field existed: the replay derives it from the stored decision and the current review. |
evaluatedDecision | ALLOW | REVIEW | BLOCK | yes | What full enforcement would have decided. Differs from `decision` in monitor mode. |
monitored | boolean | yes | |
riskLevel | LOW | MEDIUM | HIGH | CRITICAL | yes | |
riskScore | integer (0–100) | yes | |
reasons | array of string | yes | |
policy | object or null | yes | |
policy.id | string (uuid) | yes | |
policy.name | string | yes | |
dlpFindings | array of string | yes | Sensitive-data types found in `content`. |
lgpdCategories | array of IDENTIFICATION | CONTACT | LOCATION | FINANCIAL | HEALTH | COMPANY | CREDENTIALS | yes | LGPD categories derived from `dlpFindings`. |
review | object or null | yes | Null when the event has no human review. |
review.status | PENDING | APPROVED | DENIED | EXPIRED | yes | |
review.expiresAt | string or null (date-time) | yes | |
review.decidedAt | string or null (date-time) | yes | |
review.note | string or null | yes | |
quotaExceeded | boolean | yes | Plan quota is over the limit. The decision is still made. |
sandbox | boolean | yes | True when the event was created with a TEST key. |
gateway | object | no | |
gateway.protocol | MCP | yes | |
gateway.routeId | string or null (uuid) | no | |
gateway.tool | string | yes | |
gateway.mappedAction | READ_DATA | WRITE_DATA | DELETE_DATA | EXPORT_DATA | SEND_EMAIL | yes | |
gateway.allowed | boolean | yes |
| Header | Description |
|---|---|
X-Request-Id | Request id for support and log correlation (a caller-sent plain id is kept). |
x-api-version | Machine API contract (v1). |
x-vulnify-version | API build (SemVer, see /changelog). |
Idempotent-Replay | "true" when the body is a stored replay |
Errors
Section titled “Errors”| Status | Meaning |
|---|---|
400 | Validation error (agent/agentId and resource/resourceId are required) |
401 | Missing, invalid, revoked or expired API key |
403 | Key bound to another agent, or caller IP not in the key allowlist |
404 | Unknown agent, resource or gateway route |
413 | JSON body larger than 200 KB |
429 | Rate limit exceeded |
A 413 means the JSON body is larger than 200 KB. It is a client error, including when a caller hoped to fail open. See Errors.
The call is listed in the gateway call log with protocol MCP.

