MCP Server Allowlist Policy
The mcp-server-allowlist policy category constrains which MCP servers an agent may register or connect to.
Today's MCP ecosystem lets an agent register arbitrary third-party servers -- a malicious or compromised tool catalog can be a serious attack surface. This category is the lever to constrain that.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
allowed_servers | string[] | [] | Positive allowlist of server names. Empty = no allowlist (all allowed unless in blocked_servers) |
blocked_servers | string[] | [] | Always-deny list. Wins over allowlist |
action_on_violation | string | block | Action on violation: block or warn |
How It Works
The handler runs at before_workflow, mid_execution, and after_workflow -- on every phase, so a server attached mid-run is caught.
Reads context.metadata["mcp_servers"] -- a list of server names this run has connected to. The MCP manifest endpoint (or whichever subsystem registers servers) is responsible for populating this. Empty / missing list = no enforcement.
Semantics:
- Empty
allowed_servers-> onlyblocked_serversapplies - Non-empty
allowed_servers-> ONLY those servers are permitted blocked_serverswins (deny overrides allow)
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.metadata["mcp_servers"] | all | List of MCP server names attached to this run |
Example Policy
Strict allowlist for enterprise tenants
{
"name": "Approved MCP servers only",
"category": "mcp-server-allowlist",
"rules": {
"allowed_servers": ["github-mcp", "jira-mcp", "internal-tools-mcp"],
"blocked_servers": ["unverified-public-mcp"],
"action_on_violation": "block"
},
"scope": {
"agents": ["*"]
},
"enabled": true
}
Deny-only mode
If you trust most MCP servers but want to block known-bad ones:
{
"rules": {
"blocked_servers": ["typosquat-mcp", "scraper-mcp"],
"action_on_violation": "block"
}
}
SDK Integration
import waxell_observe as waxell
waxell.init()
# The MCP manifest endpoint should populate metadata["mcp_servers"]
# when an agent registers servers.
@waxell.observe(
agent_name="dev-assistant",
enforce_policy=True,
metadata={"mcp_servers": ["github-mcp", "jira-mcp"]},
)
async def dev_agent(task: str) -> str:
return await run(task)
Observability
| Field | Example (BLOCK) |
|---|---|
| Category | mcp-server-allowlist |
| Action | block |
| Reason | MCP server 'untrusted-mcp' is not on the allowlist |
| Metadata | {"blocked_server": "untrusted-mcp", "allowed_servers": [...], "phase": "before"} |
Common Gotchas
- Requires
context.metadata["mcp_servers"]to be populated. If your MCP manifest endpoint doesn't write this field, the policy is a no-op. Empty / missing list = ALLOW. - Exact name match. Use the canonical server name -- no globs or regex.
- Empty allowlist = NO allowlist. Same trap as the Tool Allowlist -- setting
allowed_servers: []does not block all servers. - Fires on every phase. Cheap (set lookup), so the policy catches servers attached mid-run, not just at startup.
- Three handlers, one module.
mcp-server-allowlist, tool-allowlist, and prompt-allowlist share the sameallowlists.pyhandler module. Each is a separate category with separate rules.
Next Steps
- Tool Allowlist -- Positive list governance for tools
- Prompt Allowlist -- Positive list governance for prompt templates
- Safety Policy -- Generic blocked_tools + content filters
- Code Execution Policy -- Sandbox controls for executable tools
- Policy Categories & Templates -- All categories