Skip to main content

MCP Governance

Concepts

AIControls intercepts every MCP tool call from Claude Code and other MCP-enabled agents — applying policies, logging arguments and results, and routing high-risk operations for human approval.

How MCP proxying works​

When Claude Code (or another MCP client) connects through the AIControls proxy, MCP tool calls are intercepted at the protocol layer before being forwarded to the upstream MCP server. The proxy evaluates policies against the tool name and arguments, then either allows, denies, or holds the call for approval.

No changes are needed on the MCP server side — the proxy is transparent to both the agent and the server.

What gets logged​

Every MCP tool call produces an audit event containing:

FieldDescription
Tool namee.g. bash, read_file, write_file, kubectl
ArgumentsFull argument map passed to the tool
ResultTool output (configurable — can be redacted for sensitive tools)
DecisionAllow, Deny, Approve, Ask, Warn, or Audit — and which policy triggered
DurationTime from call to response
ResourceFile path, URL, or resource identifier extracted from arguments

Built-in SSRF protection​

Any tool that accepts a URL argument (web fetch, browser automation, HTTP clients) can be directed by an adversarial prompt toward internal endpoints — cloud metadata services, internal APIs, or loopback addresses. AIControls blocks this by default: every tool-call argument that is a URL is resolved via DNS at call time and checked against private, loopback, link-local, and other internal ranges, across every tool and every argument — no per-tool configuration needed.

Resolving DNS at call time (rather than only pattern-matching the URL string) also catches DNS rebinding, where a hostname is deliberately made to resolve to an internal address.

If an agent legitimately needs to reach an internal address (e.g. an internal search API), add it to your workspace's SSRF allowlist rather than disabling the protection. See Policy Context for the object.mcp.ssrf.* fields available if you want to layer custom policy rules on top of the default protection.

Policy-based tool authorization​

MCP tool calls are governed the same way as any other request: a policy declares a matchCondition that scopes it to specific tools or servers, and one or more validation rules that inspect the tool name and arguments to decide whether the call is allowed, denied, audited, or held for approval.

From the MCP Servers page, you can create these governance policies scoped to a specific server directly from the server list. Click the action menu (⋮) on any server to see two options:

ActionWhat it does
Create a policyOpens the policy editor pre-populated with a matchCondition scoped to this server's tool namespace. Add your validation logic and save — the policy applies immediately.
Add to existing policyAdds a new rule to an existing policy, pre-scoped to this server. Use this to consolidate related rules into a single policy rather than creating many single-rule policies.

This is the recommended starting point for governing a newly registered server. A typical workflow:

  1. Register the MCP server and confirm it appears in the server list.
  2. Use Create a policy to add an Audit rule that logs all tool calls from this server. Observe the audit log for a few days to understand usage patterns.
  3. Use Add to existing policy to layer in Deny or Approve (warn-approve) rules for specific high-risk tools, using the argument patterns you observed.

Policies generated this way are full CEL ValidatingPolicy documents — you can edit them freely in Policies after creation. The server-scoped match condition is a starting point, not a constraint.

MCP resource subscriptions​

Newer MCP clients (speaking protocol version 2026-07-28 or later) can subscribe to a resource on an MCP server and receive push notifications whenever it changes, instead of polling with repeated reads. AIControls governs subscriptions the same way it governs everything else: establishing one runs through the same identity, policy, content-safety, and enforcement checks as a tool call or resource read, and every pushed update is itself evaluated and audited.

Two limits protect against a runaway or misbehaving subscriber:

LimitDefaultEffect when exceeded
Concurrent subscriptions per agent10The next resources/subscribe call is denied (subscription-concurrency-cap, see Decision Types) until the agent releases an existing subscription.
Update events per second, per subscription50Events over the limit are dropped silently (no error returned to the client) and audited (subscription_rate_exceeded); the subscription itself stays open.

Each pushed update is evaluated against your policies independently — a policy can deny specific update content (e.g. a change to a sensitive file) without denying the subscription as a whole. Denying an individual update drops just that one push; the subscription itself stays open.

The MCP Servers page shows the MCP protocol version each server actually negotiated (e.g. "MCP 2026-07-28") next to its health status — useful for confirming which of your upstream servers support the newer subscription-capable protocol versus the classic request/response-only one.

Tool call results (tools/call) on MCP 2026-07-28+​

On MCP protocol version 2026-07-28 and later, tools/call responses include a resultType discriminator:

  • complete — the tool call is finished (normal one-shot responses).
  • input_required — the server needs more client input before it can finish the call (multi round-trip request protocol / MRTR).

In MRTR, an empty inputRequests object is also meaningful: it indicates load-shedding (the server can’t handle the request right now), and the client should retry later.

AIControls currently rejects non-empty upstream inputRequests and client inputResponses rather than relaying ungoverned sampling, elicitation, or roots operations. Governed MRTR fulfillment is not yet supported; empty-map load-shedding remains available.

Tool-definition pinning (rug-pull defense)​

MCP servers can change behavior after you've already reviewed them — a tool's description or input schema can be silently rewritten between one tools/list response and the next. Because tool descriptions are fed directly to the model, a changed description can steer an agent into unintended behavior without anyone touching a config file. This is sometimes called a "rug-pull."

AIControls defends against it by treating a server's tool set as a snapshot to compare against, not just a reachable endpoint:

  • At scan time, AIControls hashes every tool's name, description, and input schema from the server's tools/list response and stores the snapshot.
  • On every later scan, the current tools/list response is compared against the pinned snapshot, tool by tool.

Each tool carries a drift state:

StateMeaning
PinnedMatches the approved snapshot — unchanged.
DriftedThe tool existed in the snapshot, but its description, name, or input schema changed since it was approved.
NewThe tool wasn't in the snapshot — the server started advertising it after pinning.

Drift states are sticky: once a tool is drifted or new it stays that way — through any number of later scans — until an admin explicitly re-pins it. A drift alert fires once, when the state changes, not on every scan that re-observes it.

Only snapshots taken by a real scan of the server count as its baseline. Snapshots written by demo mode are never compared against, displayed for, or counted toward a real server that shares the demo server's name. A snapshot stored before an upgrade that recorded where snapshots came from is set aside the same way — but the two are handled differently: the demo snapshot is deleted outright, while a pre-upgrade snapshot of unrecorded ("unknown") provenance is left in place and merely filtered out of every reader (drift comparison, the Elevated Tools count, the Tools tab, the Tools inventory), so it never counts toward a real server's baseline without ever needing a destructive migration. Either way, a retained snapshot from an earlier real scan of that same server is never touched, and keeps serving as its drift baseline, keeps counting toward the Elevated Tools number, and keeps appearing on the Tools tab and in the Tools inventory. Only a server with no such retained real snapshot goes empty on these three surfaces (and skips drift blocking) until its next scan. That next scan (scheduled or manual) pins a fresh baseline either way.

Drift and new-tool states appear as badges on a server's Tools tab — reachable by any role from Inventory → MCP Servers → Observed → (click a server), or by an admin from the Registration tab's own server details — and every detected change is written to the audit log so you can see exactly what changed and when. The current snapshot and hash for a server are also available read-only at GET /api/v1/mcp-servers/{name}/tool-schemas if you want to check it from a script — that same response also carries each tool's access type and Elevated Privileged status, described next.

Scheduled re-scan​

Scans don't only happen when a server is added or manually refreshed — AIControls re-scans every connected server's tools/list on a schedule, so a rug-pull between manual refreshes is caught within the interval. The interval defaults to 60 minutes (0 disables) and is set under Inventory → MCP Servers → Registration tab → Details → Governance, or via PUT /api/v1/settings/mcp-rescan-interval. Servers using per-developer credentials are skipped — their inventory updates when a developer next uses them or an admin refreshes with a token.

Drift response mode​

Each server has a drift response mode, defaulting to a workspace-wide setting (Alert unless changed) and overridable per server:

ModeEffect
AlertA drifted or new tool raises an admin alert but stays callable — the default.
BlockDrifted and new tools are hidden from tools/list — clients can't see or call them — until an admin re-pins them.

Re-pinning accepts a tool's current definition as the new approved baseline: its state returns to pinned (and, under Block mode, it becomes visible again). Re-pin from the tool list in Inventory → MCP Servers → Registration tab, or via POST /api/v1/mcp-servers/{name}/tool-schemas/{tool}/repin. Re-pinning is an admin action — reviewing the schema diff before accepting it is the point.

Drift enforcement on tool calls is cached briefly on each replica. A change to the drift response mode, a server's registration, or a tool's pinned state — including re-pinning a tool — applies immediately on the replica that handled the change, and within 10 seconds on every other replica.

note

Block mode hides a drifted tool at the catalog (tools/list) layer. It doesn't park individual calls to a drifted tool for approval. While Block mode is on, the tool stays hidden until an admin re-pins it.

Tool access types and Elevated Privileged tools​

Every declared MCP tool is tagged with an access type — what kind of operation it performs — so you can aim controls at "the dangerous tools" without hand-listing tool names:

Access typeMeaningExamples
ReadObserves state without changing anythingget_issue, list_pull_requests, search
WriteCreates or updates — additive, or reversible in principlecreate_ticket, update_record, apply_patch
DeleteRemoves or destructively overwritesdelete_repository, drop_table, truncate
ExecuteRuns caller-supplied code or commands whose effect isn't bounded by the tool's own contractbash, run_command, sql_execute
UnknownNo signal available to classify it—

AIControls derives this automatically, in order of preference:

  1. Server-declared hints. Modern MCP servers can mark a tool read-only or destructive in its definition; when a server says so, that's authoritative.
  2. A keyword match over the tool's name and description (whole words, so apply_filter doesn't match apply) — used when the server declares nothing.
  3. A built-in table for AI coding assistants' own tools (file read/write/edit, shell execution) that never go through a server's tool list.
  4. Otherwise the tool is tagged Unknown — a gap to close, not a class the system guesses at. By default an Unknown tool is still treated as Elevated Privileged against confidential and restricted data, so the gap doesn't quietly exempt it from review.

Every tool's access type, and which rule produced it, is visible on the Tool Schema Pinning table (Inventory → MCP Servers → a server → Tools) and on the Tools inventory page, and is available to policy authors as object.tool.accessType (see Policy Context).

If a tool is misclassified — or lands on Unknown because nothing matched — an admin can override its access type directly from the Tool Schema Pinning table: open the row's ⋮ menu and choose Set access type override. An override always wins over automatic classification and is labeled (override) everywhere the access type appears; choosing Inherit (classify automatically) clears it. See Fix or override a tool's access type.

Elevated Privileged tools​

A tool is flagged Elevated Privileged when its access type, combined with how sensitive the data behind it is (see Data classification below), crosses a threshold worth a human's attention:

Access typeElevated when the datasource's classification is
ReadRestricted
WriteConfidential or Restricted
DeleteAny classification, including not classified
ExecuteAny classification, including not classified
UnknownConfidential or Restricted

In other words: a destructive or arbitrary-code tool is always elevated, because that kind of operation is high-risk regardless of what data it touches. A write tool only becomes elevated once it's pointed at sensitive data — a comment on a public wiki isn't in the same class as a write to a production database. A read tool is elevated only against the most sensitive tier, because that's exfiltration risk, not routine lookup. An Unknown tool is treated exactly like a write, because a tool nothing could classify is a reason for more scrutiny, not less.

This default rule — the elevation matrix — is workspace-tunable from Setup → Settings → Risk Scoring → Tools: tick which classifications make each access type elevated, or check Always for an access type that should be elevated regardless of classification (including against an unclassified datasource). An admin can also force a single tool's Elevated status on or off directly via the tool_access_overrides settings API — an override always wins over the matrix.

A saved matrix keeps its own Unknown row​

The table above is the shipped default — the matrix a workspace uses while nobody has saved one of their own. Once an admin saves a matrix, that saved matrix is used exactly as stored, including an Unknown row that was left empty. Adopting a new default is not retroactive: a workspace that tuned its matrix before Unknown became elevated keeps unclassifiable tools un-elevated, so every policy that matches object.mcp.elevated — including the built-in mcp-require-approval-elevated-tool — skips those calls entirely.

Nothing fails when that happens; the calls simply aren't covered. The Tool Schema Pinning panel therefore warns on any server that has tools with an Unknown access type that aren't elevated, and counts them. See Cover unclassifiable tools for the three ways to close the gap.

Elevated status, and the reason it was assigned, appears next to a tool everywhere access type does — the Tool Schema Pinning table, the Tools inventory, the Datasources server card's Elevated Tools count, and the Approvals drawer when an elevated call is held for review.

Data classification​

A data classification tells AIControls how sensitive the data behind an MCP server is — it's a property you assign to the datasource (the MCP server), not something inferred from a tool's schema or the content it returns:

ClassificationTypical use
PublicNo sensitivity — public documentation, open data
InternalOrdinary business data, not meant for outside sharing
ConfidentialSensitive business data — financial records, customer data, source code
RestrictedThe most sensitive tier — secrets, regulated data, production credentials
(not classified)No classification has been assigned yet

Set it from Inventory → MCP Servers → edit a server → Data Classification, or via the API:

curl -X PATCH "$YOUR_WORKSPACE_URL/api/v1/mcp-servers/payments-db/data-classification" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"classification": "restricted"}'

A server with mixed data — most tools return internal data but one returns secrets — can override classification per tool through the same override document used for access-type overrides (Settings → Tool Access Overrides, or GET/PUT /api/v1/settings/tool-access-overrides, admin-only for writes). The per-tool value always wins over the server-level one.

A server with no classification set shows as Not classified rather than defaulting to a middle tier — an unclassified datasource is a visible gap on the Datasources page and the Tools inventory, not something that quietly looks safe. object.tool.dataClassification is empty in policy for an unclassified datasource, exactly as it is for an LLM call.

For a managed server, its saved classification is authoritative. Clearing it explicitly makes the server Not classified, even if a deployment configuration declares a classification for the same name. The deployment value is used only when no managed server record exists.

Tool inventory and server details use this same precedence. The Elevated tools count is recalculated from the current classification, tool overrides, and elevation matrix without requiring another scan. If classification or override settings cannot be read, these views report an error instead of displaying a fallback as authoritative. Changes affect current tool assessments; historical audit events and their risk bands remain unchanged.

Enforcement caches server classifications for up to 10 seconds. After a successful edit, inventory reads reflect the saved value immediately, while new calls converge when that cache expires. During a storage outage, enforcement retains its existing cached or deployment fallback behavior.

Call risk band​

Every MCP tool call is also stamped with a risk band — Low, Medium, High, or Critical — computed from its access type and its datasource's classification together. This is a label for at-a-glance triage on the audit trail, not a numeric score: filter Security Events to High or Critical to see the calls worth reviewing first, independent of whether any policy actually held or denied them. A call against an unclassified datasource bands the same as Internal so a newly connected, not-yet-classified server doesn't paint every dashboard red — the gap itself is what the Not classified badge and the unclassified-datasource policy below are for.

Each server card on the Datasources page also shows a Risk posture tag: the highest band any of the server's pinned tools would get given the server's current classification. It describes what the server could do, not what has been called. A delete tool on a confidential server makes its posture Critical even if nobody has called it yet. When attack patterns involving a server's tools were first detected in the last 7 days, the card also shows an Attack patterns (7d) count. A match that touched the server more than once counts once, as does the same match detected at both identity and session scope. If the count can't be read, the card shows Unavailable rather than hiding the count, so a read failure isn't mistaken for no matches. See Attack pattern detection.

Built-in policies for elevated tool use​

Two built-in policies, under the Access Control category, ship in Audit mode so connecting them never changes behavior on upgrade:

PolicyWhat it does
mcp-require-approval-elevated-toolMatches every MCP tool call flagged Elevated Privileged. Switch it to Require Approval to hold elevated calls for a human decision — see Human-in-the-loop and Approval Workflow for how an approver scopes the grant.
mcp-audit-unclassified-datasourceFlags an elevated call made against a datasource with no data classification assigned. This is a gap-surfacing nudge, not an enforcement gate — it's intended to stay in Audit mode permanently as a reminder to classify the datasource, not to be graduated to a blocking mode.

A third Audit-mode built-in, workload-data-ceiling, uses the same classification. It flags an MCP call whose server or tool classification ranks above the calling workload's data classification ceiling. It's audit-only: it never denies a call, and calls to unclassified servers and tools pass. Enable it when you set data classification ceilings on workloads and want an audit record of every call that exceeds a ceiling.

Enable mcp-require-approval-elevated-tool and mcp-audit-unclassified-datasource for elevated tool use, and add workload-data-ceiling if you use data classification ceilings. Enable each policy from Policies → Library like any other built-in. To narrow one further, for example by scoping mcp-require-approval-elevated-tool to a specific team or environment, see Clone and customize a built-in.

Shadow MCP traffic & quarantine​

Every MCP tool call targets a server. If that server isn't one you've configured as a datasource, AIControls still sees the traffic — the proxy sits inline — and treats it as a signal instead of silently ignoring it.

The first time a tool call reaches a server that is in neither your configured datasources nor the server catalog, AIControls:

  1. Automatically creates a catalog entry for that server in the quarantined state.
  2. Raises a one-time admin alert — you're notified once per newly discovered server, not once per call.

A quarantined entry is an audit-only placeholder: traffic to it keeps being logged, but the server has no owning team, no bindings, and no policy of its own — it exists so the discovery isn't lost while an admin decides what to do with it.

State (quarantine_state)Meaning
(empty)Normal — a known, admin-managed server.
quarantinedTraffic was observed to an unregistered server; pending admin resolution.
deniedAn admin reviewed the quarantined server and rejected it — further traffic is blocked by policy.

An admin resolves a quarantined entry from Inventory → MCP Servers → Registration tab by either promoting it into a normal governed server (eligible for policies and Access Bindings) or denying it. See Resolve a quarantined server for the exact steps.

Directly connected MCP servers​

An agent can connect to an MCP server directly instead of through AIControls /mcp. Quarantine and MCP policies don't apply to those calls, because the calls never pass through the MCP proxy. When the agent's model traffic goes through the LLM gateway, LLM policies can still restrict those tools. They match on the tools the agent advertises to the model and on the tool calls the model makes. See Restrict tools through the LLM gateway.

Unrecognized tools​

Inventory → MCP Servers → Unrecognized tools lists tool names that AIControls classified as unknown across sessions: names that are not on the list of Claude Code built-in tools and not a recognizable snake_case or server__tool name. Use it to tell a new or custom client tool from a misconfigured MCP server or an unexpected tool call.

Each row shows the tool name, when it was first and last seen, how many sessions called it, the total number of recorded events (calls), and a View session link to an example session. Search matches part of a tool name, every column except the example link sorts, and the list pages through the server, so the total reflects every matching name. Choose 7, 30 or 90 days; the default is 30 days. Admins and viewers can open the tab.

Calls recorded before Claude Code built-in recognition shipped keep their original classification, but a legacy call to a listed built-in is not shown here. The same data is available at GET /api/v1/tools/unknown (q, sortBy of name, firstSeen, lastSeen, sessions or calls, order, days, page, pageSize).

Observed server inventory​

Inventory → MCP Servers opens on the Observed tab: every server that has actually received traffic through the proxy, built from audit data rather than the registration list, so it reflects what's really being called instead of what's been configured. Each row shows first/last-seen timestamps, total call count, and distinct tool count, and carries one of three statuses:

StatusMeaning
GovernedThe server has been observed in traffic and is registered — policies apply to it.
UngovernedThe server has been observed in traffic but was never registered — no policy scope covers it. Raises an ungoverned_mcp_server security event.
StaleThe server is registered but has never been observed in traffic.

Clicking a row opens its Tools tab — the same per-tool drift/inventory view described above — so any role can see what a server declares and who it's granted to, not just admins.

An Ungoverned row is your cue to register the server (see Register the server) so it comes under policy. The Registration tab, visible to admins only, holds the server list, connection details, and the drift/quarantine tooling described above.

See also​