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.
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.
| Client | Naming convention |
|---|---|
| Claude Code | mcp__<server>__<tool> |
| Codex CLI | <server>__<tool> |
| Cursor | mcp_<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.
Related
- Tool rules — the family that actually governs local MCP.
- The inventory — where to find out which servers you have.
- Limitations — what none of this catches.