MCP servers

MCP servers reach the firewall in two completely different shapes, and a rule that works on one does nothing to the other. This is the single most important thing to understand in this section.

Remote MCP: visible, with a URL

The client hands the provider a server URL and asks it to do the calling. Anthropic carries this in a top-level mcp_servers array; OpenAI Responses carries it as a {"type": "mcp"} entry in tools.

{
  "mcp_servers": [
    {
      "type": "url",
      "name": "jira",
      "url": "https://mcp.atlassian.com/v1/sse",
      "authorization_token": "…",
      "tool_configuration": { "allowed_tools": ["create_issue"] }
    }
  ]
}

This is a protocol fact. The firewall reads the label and the URL, records both, and mcp_deny / mcp_allow match against the label:

vulnetix ai-firewall policy guardrail no-remote-mcp \
  --rule-type mcp_deny --action strip --pattern '*' \
  --priority 100 --enable

strip removes the whole entry, so the provider is never given the URL or the token. That is the connection, not one call.

Note The URL is recorded as scheme://host/path only. Query string and userinfo are discarded before the value is stored, because the authorization_token sits right beside it and query-parameter authentication is a real pattern. Nothing token-shaped reaches the database.

Local MCP: flattened, and only identifiable by name

A stdio MCP server runs on the developer’s machine. The client connects to it, asks it for its tools, and merges those tools into the ordinary tools array before sending anything. By the time a request exists, there is no MCP server in it — only tools with suggestive names.

ClientNaming convention
Claude Codemcp__<server>__<tool>
Codex CLI<server>__<tool>
Cursormcp_<server>_<tool>

So governing a local MCP server means writing a tool rule against its prefix:

vulnetix ai-firewall policy guardrail no-local-jira \
  --rule-type tool_deny --action strip --pattern 'mcp__jira__*' \
  --priority 100 --enable

mcp_deny will not touch it. There is nothing on the wire for that rule to match.

In the inventory, local MCP servers appear as tool rows with source: declared — the tool name really was in the request — while the derived server name is marked inferred, because that part is a convention rather than a field. A client that names its tools differently is invisible to the prefix, and nothing stops one doing so.

The shadow-MCP problem

This is the case worth naming plainly, because it is why the section exists.

A developer adds an MCP server to a local config. It authenticates to the issue tracker, or the CI system, or a production database, with a long-lived token they generated themselves. From that moment, every prompt their agent sends carries the ability to reach that system, and the model decides when to use it.

Nobody in security hears about it. There is no pull request, no procurement, no change ticket, no entry in any asset register. A repository scan finds nothing: the config is in ~, not in the repo.

The one place it is unavoidably visible is the request. Which is what the inventory reads, and why running it in flag for a fortnight before writing a single rule is the recommended first step.

Allowlisting servers you have approved

vulnetix ai-firewall policy guardrail approved-mcp \
  --rule-type mcp_allow --action block --pattern 'jira' --priority 100 --enable

That covers remote servers only. To do the same for local ones, allowlist their tool prefixes:

vulnetix ai-firewall policy guardrail approved-local-mcp \
  --rule-type tool_allow --action block --pattern 'mcp__jira__*' --priority 100 --enable

Be careful: a tool_allow rule puts the entire tool family into allowlist mode, including the agent’s own Read and Edit. Allowlisting one MCP prefix and nothing else will refuse every session. Allowlist the agent’s own tools in the same breath, or use denies.