Skip to main content
Version: 1.8.0

Govern tool calls with the MCP Gateway

An agent should not gain unrestricted access to every tool just because it can speak MCP. The DVARA MCP Gateway puts workspace identity, policy, data protection, approval, loop detection, and audit controls between your agent and its tools.

Enterprise only

Not included in DVARA Open Source.

How does DVARA govern a tool call?​

Your MCP client connects to DVARA over Streamable HTTP at /mcp. DVARA resolves the API key to one workspace and returns only that workspace's registered tools. Tool names use {serverId}__{toolName}, such as support__search_articles, so tools from different servers cannot collide.

When the client calls a tool, DVARA:

  1. authenticates the API key and fixes the workspace boundary;
  2. checks that the server and tool are available to that workspace;
  3. evaluates matching tool policy and scans arguments for PII;
  4. applies loop detection and any configured human-approval rule;
  5. sends an allowed call to the registered upstream server;
  6. scans a successful response for PII; and
  7. records the intent and result in the tamper-evident audit trail.

The MCP client still plans the work. DVARA governs the call that crosses the gateway.

What do you need before you connect?​

You need a running Enterprise platform deployment (a licence is optional), an active workspace API key or configured MCP OAuth issuer, and at least one active MCP server with a synced tool catalog. Register and sync a server from MCP → MCP Servers in the DVARA Flightdeck.

The native endpoint is on by default. If an operator has disabled it with dvara.mcp-gateway.native.enabled: false, /mcp is not available.

Connect an MCP client​

The examples below use a workspace API key, which is the default. When MCP OAuth is enabled, use an OAuth access token instead. API keys remain accepted unless the operator sets dvara.mcp-gateway.oauth.api-keys-enabled: false. See Authenticate MCP clients with OAuth 2.1.

Point a Streamable HTTP client at the gateway. Replace <dvara-host> and <your-api-key> with values from your deployment.

{
"mcpServers": {
"dvara": {
"type": "streamableHttp",
"url": "https://<dvara-host>/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>",
"X-Session-Id": "support-run-42"
}
}
}
}

X-Session-Id is optional, but add it when you want DVARA to join tool calls into a session and detect repeated behavior across calls. It is a correlation value, not an identity credential.

List the tools your agent can use​

The following request uses the MCP JSON-RPC contract directly. A POST request must accept both JSON and event-stream responses.

curl --silent https://<dvara-host>/mcp \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json, text/event-stream' \
--header 'X-Session-Id: support-run-42' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'

A workspace with a support server can receive:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "support__search_articles",
"description": "Search approved customer-support articles",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
},
"annotations": { "readOnlyHint": true }
},
{
"name": "support__issue_refund",
"description": "Issue a refund for an order",
"inputSchema": {
"type": "object",
"properties": {
"order_id": { "type": "string" }
},
"required": ["order_id"]
},
"annotations": { "destructiveHint": true },
"_meta": { "dvara/approval_required": true }
}
]
}
}

DVARA scans tool descriptions before returning them. A description detected as tool poisoning is omitted from the list.

From 1.8.4 each tool keeps the annotations its server declared, such as readOnlyHint and destructiveHint; earlier releases dropped them. A tool the workspace holds for human approval is marked with _meta["dvara/approval_required"]: true, so a client can tell in advance that a call to it will wait for a person.

Call a governed tool​

Use the namespaced tool name returned by tools/list:

curl --silent https://<dvara-host>/mcp \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json, text/event-stream' \
--header 'X-Session-Id: support-run-42' \
--data '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "support__search_articles",
"arguments": { "query": "refund policy" }
}
}'

An allowed call returns a standard MCP tool result:

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Returns are accepted within 30 days of delivery."
}
],
"isError": false
}
}

An upstream tool can also complete normally with a tool-level error. DVARA returns that result unchanged so the model can read the tool's message and recover:

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{"type": "text", "text": "file not found: report.csv"}],
"isError": true
}
}

This is not a gateway error: the HTTP and JSON-RPC exchange succeeded. In 1.8, the tool-call evidence therefore records a successful transport even though the returned result carries isError: true. An upstream HTTP or transport failure still returns a gateway error.

How are native governance refusals returned?​

The native endpoint uses stable application-specific JSON-RPC codes:

RefusalJSON-RPC codeHTTP-equivalent decision
Killed session, denied approval, or approval store unavailable-32001403
Approval timeout-32002408
Agent loop detected-32003429

The error data also carries the DVARA code (dvara_code) and the HTTP-equivalent status (http_status). A real, unexpected gateway fault returns -32603 Internal error; exception details are not returned to the client. Native tool-call evidence carries the request trace ID.

From 1.8.4, when the upstream server itself answers a tool call with a JSON-RPC error, such as Unknown tool, DVARA returns that error with the server's own code and message. Its data adds dvara_code (mcp_upstream_rejected for an invalid request, unknown method or invalid params; mcp_upstream_error for any other code), http_status, trace_id and, when the server sent any, upstream_data. Before 1.8.4 this was reported as an unreachable server. See MCP Gateway errors.

Open MCP → Tool Calls in the Flightdeck (in the Flightdeck, Agents → MCP Tool Calls) to verify the workspace, session, server, tool, policy decision, PII flags, status, and latency. A completed call writes MCP_TOOL_INTENT before execution and MCP_TOOL_RESULT afterward.

What happens without extra configuration?​

Control1.8 defaultWhat that means
Workspace authenticationRequired/mcp accepts only an active DVARA API key
Native endpointOnSet dvara.mcp-gateway.native.enabled: false to turn it off
PII scanningOn, action LOGDetected PII is recorded, but the call continues
Response PII scanningOnSuccessful responses are scanned before release
Loop detectionOnThe fifth identical consecutive call in one session is rejected by the default threshold
Automatic session killOffA detected loop rejects that call but does not kill the session
Human approval engineOn, with no matching rulesCalls do not pause until a workspace names required tools or servers
MCP extensionsNone allowedEvery extension a client declares is refused and recorded; DVARA serves none yet
Approval timeout300 seconds, then denyA matching call is refused if nobody decides in time
Tool-call argument size1 MiB, from 1.8.5A call with larger arguments is refused with 413 MCP_ARGUMENTS_TOO_LARGE
Upstream answer size4 MiB whole, 16 MiB streamed, from 1.8.5A larger answer is refused with 502 MCP_RESPONSE_TOO_LARGE
Legacy HTTP+SSE upstreamsWork until 2027-07-28Each use is logged and audited; from that date a call is refused with 410 mcp_transport_sunset
Input requests from a serverSampling refused, elicitation relayed, roots refused, from 1.8.5See what happens when a server asks for input

A session kill is keyed by workspace and session ID. It refuses that ID only for the workspace that issued the kill; the same ID in another workspace keeps working. From 1.8.5 a kill also stops the session's model calls and A2A hops, on every Gateway, for 24 hours. A tool call held for approval when its session is killed is denied at once and refused with SESSION_KILLED. See Stop an agent session.

Policy enforcement is conditional on the active policies for the workspace. A policy denial stops the call before the upstream and returns MCP_POLICY_DENIED. A policy evaluation failure fails closed with MCP_POLICY_ERROR.

From 1.8.4 a call a policy denied has its own tool-call record, with policy decision DENY, status 403 and MCP_POLICY_DENIED, so it appears in the Flightdeck's MCP → Tool Calls and in the session timeline. The policy and rule that decided are on the MCP_POLICY_DENIED audit event with the same trace ID. The call reached no server, so it is not counted as a governed call for limits or billing.

How large can a tool call and its answer be?​

From 1.8.5 both directions have a size limit.

Arguments. A tools/call whose arguments, as JSON, are larger than 1 MiB is refused with 413 MCP_ARGUMENTS_TOO_LARGE. The check runs before the schema check, policy, the PII scan and the upstream server, on the REST surface and on /mcp. The call is recorded as a denial: an MCP_ARGUMENTS_TOO_LARGE audit event and a tool-call row with decision DENY. Before 1.8.5 a call of any size was scanned for PII, put to policy and forwarded.

Answers. DVARA counts an upstream server's bytes as it reads them. A whole answer may be up to 4 MiB and a streamed answer up to 16 MiB. Past a limit the call fails with 502 MCP_RESPONSE_TOO_LARGE before the answer is parsed, is not retried, and is recorded with decision DENY. This covers REST, REST_BRIDGE, STREAMABLE_HTTP and SSE servers and the tool sync. The default is lower than the A2A plane's because a multi-megabyte tool result takes seconds to read; a larger limit would time out before it refused.

An operator sets the fleet values with dvara.mcp-gateway.limits.max-arguments-bytes and dvara.mcp-gateway.response-limits.*; 0 turns a limit off. A workspace can have its own values, set in the Size limits section of the Flightdeck workspace form. The Flightdeck shows them to the workspace admin, read-only. See how large a request or response can be.

On upgrade to 1.8.5

A tool call with more than 1 MiB of arguments, or an answer over 4 MiB, is refused where it used to be forwarded or read whole. Raise the limit for a workspace that needs more before you upgrade.

Ask what a server can do​

From 1.8.5 DVARA answers the MCP server/discover request itself and never forwards it. A workspace is never told of an upstream's tools or capabilities it cannot use.

On /mcp the answer names the Gateway, its protocol version and only what the workspace can use: tools when it has a tool to call, and the extensions its allow-list grants. No tool is named. On the REST surface, POST /mcp/{serverId}/server/discover answers for that one server and runs the governed chain like tools/list, so another workspace's server is not found.

A key with scopes needs mcp:read. On /mcp each call is audited as MCP_SERVER_DISCOVERED; the REST call is recorded as MCP_TOOL_INTENT and MCP_TOOL_RESULT with operation server/discover.

What happens when a server asks for input?​

The MCP revision 2026-07-28 lets a server answer a call with resultType: "input_required": requests the client must answer before it retries. From 1.8.5 DVARA decides each one before it reaches your client:

Input requestDefaultWhat DVARA does
sampling/createMessage, a model callRefused, alwaysThe whole result is refused with MCP_SAMPLING_REFUSED. Relayed, that model call would skip the LLM Gateway's policy, PII scan, budget, cost record and audit.
elicitation/create, a question for the userRelayed on the REST surfaceIts text is scanned for prompt injection like any result.
roots/list, the client's filesystem rootsRefusedRefused because it discloses the client's file layout to the server.

A workspace can change elicitation and roots with its settings mcp.input_requests.elicitation and mcp.input_requests.roots (allow or refuse). Its policies can deny them too: the policy sees operation input_requests/elicitation or input_requests/roots. The fleet defaults are dvara.mcp-gateway.input-requests.*.

The server's opaque requestState is never parsed. It must be a string of at most 16 KiB each way, or the call is refused with MCP_REQUEST_STATE_INVALID. It is recorded only as its size and digest. Every round trip is audited as MCP_INPUT_REQUESTED and MCP_INPUT_RESPONDED, joined by that digest. Each retry is a governed tool call of its own, so budgets, rate limits and loop detection count every one.

Only the REST surface relays input requests. The native /mcp endpoint speaks 2025-06-18, which has no round trips, so it refuses such a result with 502 MCP_INPUT_REQUIRED_UNSUPPORTED.

See progress while a call waits for approval​

A tool call held for human approval can wait minutes. From 1.8.4 a client can ask to hear about it: put a progressToken in the call's _meta.

curl --silent --no-buffer https://<dvara-host>/mcp \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json, text/event-stream' \
--data '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "support__issue_refund",
"arguments": { "order_id": "A-1042" },
"_meta": { "progressToken": "refund-A-1042" }
}
}'

DVARA then answers the call as an event stream. When the call is held, it sends an MCP notifications/progress at once and every 5 seconds after, until a reviewer decides or the approval expires. Each names the approval ID shown in the Approvals queue and its expiry, in message and in _meta:

{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "refund-A-1042",
"progress": 5,
"total": 300,
"message": "Held for approval 7f3c9a2e-5d41-4b8e-9c6a-1e2f3a4b5c6d until 2026-09-30T15:05:00Z",
"_meta": {
"dvara/approval_id": "7f3c9a2e-5d41-4b8e-9c6a-1e2f3a4b5c6d",
"dvara/expires_at": "2026-09-30T15:05:00Z"
}
}
}

progress is the seconds held so far and total the seconds held plus the seconds left until expiry. The last event is the call's answer. No notification is sent after the decision. The interval is dvara.mcp-gateway.agentic.approval.progress-interval-ms (default 5000). A call without a progressToken, and a REST POST /mcp/{serverId}/tools/call, wait with no notifications, as before.

Choose how PII is handled​

Set a workspace's MCP PII action to match its risk. LOG is the default.

ActionTool argumentsSuccessful tool response
LOGRecord the detection and forward the original valuesRecord MCP_PII_OUTPUT_LEAK and release the response
REDACTReplace detected values before the upstream callReplace newly detected values before release
TOKENIZEReplace values with reversible tokens; refuse the call if no token store is availableIrreversibly redact newly detected response values
BLOCKReturn MCP_PII_DETECTED with HTTP 400; do not call the upstreamReturn MCP_PII_DETECTED with HTTP 403; do not release the response

Response values are not tokens DVARA created on the request path, so TOKENIZE uses irreversible redaction for newly detected response PII.

Request-side tokenization writes MCP_PII_TOKENIZED. If tokenization cannot run, DVARA writes the retained MCP_PII_REDACT_DEGRADED event with action: TOKENIZE; the default degraded action refuses the call with MCP_PII_TOKENIZE_UNAVAILABLE. These names preserve the 1.7 audit contract—do not infer reversible redaction from the older event name.

Give an agent only the tools it needs​

A virtual MCP server is a workspace-scoped, default-deny view of tools from one or more registered servers. Its allow list contains namespaced tool names such as support__search_articles.

Select a virtual server for a client with:

X-DVARA-Virtual-Server: support-agent-tools

With this header, tools/list returns only the allow-listed tools. A call to a tool outside the allow list is refused before an upstream receives it. The allowed tools still pass through the same policy, PII, approval, loop, and audit controls. Virtual servers cannot contain other virtual servers.

Forward a user's upstream identity​

Registered servers use stored credentials by default. Set a server's auth mode to DELEGATED when the upstream should enforce the calling user's permissions, then send that user's token separately:

X-DVARA-Delegated-Authorization: Bearer <upstream-user-token>

Authorization still carries the DVARA API key. A delegated server called without the second token returns MCP_DELEGATED_TOKEN_REQUIRED. DVARA uses the delegated token for that upstream call and does not store or log it; audit data contains the delegated auth mode and a fingerprint instead.

Choose an upstream transport​

The client-to-DVARA connection uses Streamable HTTP. Each registered upstream server separately chooses how DVARA reaches it.

TransportUse it for
STREAMABLE_HTTPA modern native MCP server using protocol revision 2025-06-18
SSEA native MCP server that still uses the older HTTP and SSE transport. Deprecated; refused from 2027-07-28
RESTA server that implements DVARA's earlier REST tool contract
REST_BRIDGEA plain REST API described by OpenAPI and exposed as governed tools

For REST_BRIDGE, DVARA turns supported OpenAPI operations into tools. The server's base URL is validated before it is saved, and every generated tool call uses the same governance path as a native MCP call.

From 1.8.5 the legacy HTTP+SSE transport (SSE) has a sunset date. The MCP revision 2026-07-28 deprecates it. An SSE server keeps working until 2027-07-28. Until then each use is logged as a warning and audited as MCP_DEPRECATED_TRANSPORT_USED, at most once a day per server. From that date a call is refused with 410 mcp_transport_sunset and a tool sync fails. Move such servers to STREAMABLE_HTTP. An operator can move the date with dvara.mcp-gateway.legacy-sse-sunset, set on the Gateway and on Flightdeck.

DVARA does not follow redirects from an upstream MCP server. Register the final URL: a 3xx response fails the tool call, tool sync, or health check instead of being followed to another address.

Control which MCP extensions a client may use​

The MCP revision 2026-07-28 adds extensions: optional features outside the core protocol, named vendor-prefix/name, such as io.modelcontextprotocol/tasks. An extension is in use only when both sides declare it.

From 1.8.4 DVARA reads the extensions a client declares, in initialize or in a request's _meta, and decides each one for the workspace:

  1. The workspace allow-list must allow it. Unknown extensions are denied by default: an empty list allows nothing.
  2. Each allowed extension is then put to the workspace's active policies. A policy can narrow the allow-list, never widen it.
  3. DVARA must serve the extension. It serves none yet, so today no extension is granted, and every one a client declares is refused and recorded.

A refusal never fails the request. The client falls back to core MCP behavior. When a client declared any extension, the initialize answer carries capabilities.extensions with the granted ones (today an empty object). A client that declares no extensions gets exactly the answer it got before.

Each refused extension writes an MCP_EXTENSION_DENIED audit event and a warning log line, at most once a day per workspace, extension, version, reason and client on each Gateway pod. The event's reason is not_allowed, version_not_allowed, invalid_identifier, policy_denied (with the policy and rule), policy_error (the policy could not be evaluated, so the extension is refused) or not_supported.

Set the allow-list​

The fleet default is dvara.mcp-gateway.extensions.allowed (empty by default, which allows nothing). A workspace's own list replaces it. Set it in Flightdeck: the Flightdeck workspace form's MCP extensions section (owner), or the Flightdeck's Agents → MCP Extensions page (workspace admin). Put one entry per line or separate entries with commas:

EntryAllows
io.modelcontextprotocol/tasksThat extension, any version
io.modelcontextprotocol/*Every extension from that vendor prefix
com.example/charts@2That extension only when its settings say version 2
noneNothing, even when the fleet default allows some

Leave the field blank to use the fleet default. Flightdeck refuses a malformed entry when you save, and a GitOps import refuses it too. A Flightdeck change is audited as WORKSPACE_MCP_EXTENSIONS_UPDATED. In GitOps the workspace key is mcp.extensions.allowed.

Deny an extension with a policy​

The policy sees operation extensions/negotiate, with mcp.operation, mcp.extension_id and mcp.extension_version in the request metadata (mcp.extension_version is empty when the client names none). Write the rule as a CEL expression:

version: "1"
rules:
- id: no-tasks
expression: request.metadata["mcp.extension_id"] == "io.modelcontextprotocol/tasks"
action: DENY

A rule that does not check mcp.operation applies to extension negotiation too, as it does to every MCP operation.

Know which deprecated MCP features clients still use​

The MCP revision 2026-07-28 deprecates Roots, Sampling and Logging. The specification keeps them until at least 2027-07-28. DVARA has never offered them, and still does not: it never asks a client for roots, never sends a sampling request, and does not advertise logging.

From 1.8.4 DVARA records who still uses them. When a client on /mcp declares the roots or sampling capability, sends notifications/roots/list_changed, calls logging/setLevel, or carries either capability or a log level in a request's _meta, DVARA logs a warning and writes an MCP_DEPRECATED_FEATURE_USED audit event naming the feature, the client and the dates. The same client is recorded at most once a day per feature on each Gateway pod. A failed audit write never fails the request.

Answers do not change, except that the -32601 refusal of logging/setLevel now says Logging is deprecated. From 1.8.5 a server's own sampling or roots request is refused too; see what happens when a server asks for input. The legacy HTTP+SSE upstream transport has its own sunset date. Instead of Roots, pass directories or files as tool arguments, which DVARA governs. Instead of Sampling, have the tool server call a model through the LLM Gateway with its own API key.

Know the native endpoint boundaries​

The native endpoint limits the complete HTTP request body with dvara.mcp-gateway.limits.max-request-bytes, which defaults to 4 MiB from 1.8.6. A larger declared or chunked body receives a bare HTTP 413 before JSON-RPC processing. Authentication runs first, so an invalid credential still receives 401 without its body being read. Set the limit to 0 or less to disable it.

The limit applies only to native POST /mcp; it does not cover the server-scoped REST bridge. The tool-argument limit is separate and defaults to 1 MiB. A request can therefore pass the body limit and still be refused because its serialized arguments value is too large. Likewise, a workspace argument limit above 4 MiB cannot take effect on the native endpoint unless the operator raises this outer limit.

The older server-scoped REST bridge accepts POST /mcp/{serverId}/resources/{*path} and POST /mcp/{serverId}/prompts/{*path}. DVARA forwards the wildcard suffix as the MCP operation: /resources/list becomes resources/list, /resources/read becomes resources/read, /prompts/list becomes prompts/list, and /prompts/get becomes prompts/get. Send that operation's parameter object as the JSON body. These paths are separate from the native JSON-RPC boundary below.

  • The native endpoint advertises the MCP tools capability and implements tools/list and tools/call. It does not expose MCP resources or prompts in 1.8; resources/* and prompts/* requests return a JSON-RPC method-not-found error.
  • DVARA implements MCP protocol revision 2025-06-18. If a client requests a different revision during initialize, DVARA answers with 2025-06-18; the client decides whether it can continue.
  • The tools capability advertises listChanged: false. Catalog changes appear on the next tools/list; DVARA does not push a change notification.
  • A present browser Origin must match dvara.mcp-gateway.native.allowed-origins. Requests without Origin are accepted, and localhost origins are accepted for local development.
  • The native endpoint is exactly /mcp. The earlier REST contract remains on paths beneath /mcp/{serverId}/... for existing integrations.

Where to go next​