Tool Filtering and Approval Policies for MCP Servers
Implement secure MCP agents with static and dynamic tool filtering using create_static_tool_filter, ToolFilterContext-based dynamic filters, and granular approval policies for safe tool execution.
Why Tool Filtering Matters
MCP servers expose tools — sometimes dozens of them. The filesystem server alone provides read, write, delete, search, and more. But you rarely want an agent to have unrestricted access to every tool. A customer support agent should not be able to delete files. A research agent should not be able to write to the database.
Tool filtering lets you control exactly which MCP tools an agent can use. Approval policies add a second layer — even for allowed tools, you can require human confirmation before execution. Together, these mechanisms let you build agents that follow the principle of least privilege.
Static Tool Filtering with create_static_tool_filter
The simplest approach is a static filter that allows or blocks tools by name:
flowchart TD
START["Tool Filtering and Approval Policies for MCP Serv…"] --> A
A["Why Tool Filtering Matters"]
A --> B
B["Static Tool Filtering with create_stati…"]
B --> C
C["Dynamic Filtering with Callable Functio…"]
C --> D
D["ToolFilterContext Fields"]
D --> E
E["Agent-Specific Filtering"]
E --> F
F["Approval Policies"]
F --> G
G["Handling MCPToolApprovalRequest"]
G --> H
H["Programmatic Approval"]
H --> DONE["Key Takeaways"]
style START fill:#4f46e5,stroke:#4338ca,color:#fff
style DONE fill:#059669,stroke:#047857,color:#fff
from agents.mcp import MCPServerStdio
from agents.mcp.util import create_static_tool_filter
# Allow only specific tools
server = MCPServerStdio(
name="Filesystem",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
},
tool_filter=create_static_tool_filter(
allowed_tool_names=["read_file", "list_directory", "search_files"]
),
)
With this filter, the agent can read and search files but cannot write, delete, or move them. The LLM never even sees the blocked tools — they are excluded from the tool list sent to the model.
You can also specify a block list instead:
# Block specific dangerous tools, allow everything else
server = MCPServerStdio(
name="Filesystem",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
},
tool_filter=create_static_tool_filter(
blocked_tool_names=["write_file", "edit_file", "move_file", "create_directory"]
),
)
Block lists are useful when a server has many tools and you only need to restrict a few dangerous ones.
Dynamic Filtering with Callable Functions
For more sophisticated filtering, pass a callable that receives context about the current run:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from agents.mcp.util import ToolFilterContext
from agents import RunContextWrapper
# Define a user role context
class UserContext:
def __init__(self, role: str, department: str):
self.role = role
self.department = department
# Dynamic filter based on user role
async def role_based_filter(
context: ToolFilterContext[UserContext],
tool_name: str,
) -> bool:
"""Filter tools based on the user's role."""
user = context.run_context.context
# Admins get access to everything
if user.role == "admin":
return True
# Read-only tools for all users
read_tools = {"read_file", "list_directory", "search_files", "get_file_info"}
if tool_name in read_tools:
return True
# Write tools only for editors and admins
write_tools = {"write_file", "edit_file", "create_directory"}
if tool_name in write_tools and user.role in ("editor", "admin"):
return True
# Destructive tools only for admins (already handled above)
return False
server = MCPServerStdio(
name="Filesystem",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
},
tool_filter=role_based_filter,
)
ToolFilterContext Fields
The ToolFilterContext object provides rich context for filtering decisions:
from agents.mcp.util import ToolFilterContext
async def contextual_filter(
context: ToolFilterContext,
tool_name: str,
) -> bool:
"""Demonstrates available context fields."""
# The run context wrapping your custom context type
run_ctx = context.run_context
# The agent that is requesting the tool
agent = context.agent
agent_name = agent.name
# The MCP server providing the tool
server_name = context.server_name
# Use these for fine-grained decisions
# Example: only the "Admin Agent" can use write tools from the filesystem server
if server_name == "Filesystem" and tool_name.startswith("write"):
return agent_name == "Admin Agent"
return True
This is powerful for multi-agent systems where different agents have different permission levels.
Agent-Specific Filtering
In a multi-agent system with handoffs, each agent can have different tool access:
flowchart TD
CENTER(("Core Concepts"))
CENTER --> N0["Least privilege — Only expose the minim…"]
CENTER --> N1["Static filters first — Use create_stati…"]
CENTER --> N2["Dynamic filters for context — Add role-…"]
CENTER --> N3["Approval for writes — Any tool that mod…"]
CENTER --> N4["Path restrictions — For filesystem serv…"]
CENTER --> N5["Query restrictions — For database serve…"]
style CENTER fill:#4f46e5,stroke:#4338ca,color:#fff
from agents import Agent, handoff
from agents.mcp import MCPServerStdio
from agents.mcp.util import create_static_tool_filter
async def build_multi_agent_system():
# Shared MCP server with different filters per agent
read_only_server = MCPServerStdio(
name="FS-ReadOnly",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
},
tool_filter=create_static_tool_filter(
allowed_tool_names=["read_file", "list_directory", "search_files"]
),
)
read_write_server = MCPServerStdio(
name="FS-ReadWrite",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
},
tool_filter=create_static_tool_filter(
blocked_tool_names=["move_file"] # Allow everything except move
),
)
async with read_only_server, read_write_server:
# Research agent can only read
research_agent = Agent(
name="Research Agent",
instructions="Research and analyze files. You have read-only access.",
mcp_servers=[read_only_server],
)
# Editor agent can read and write
editor_agent = Agent(
name="Editor Agent",
instructions="Edit and create files based on research findings.",
mcp_servers=[read_write_server],
)
# Coordinator delegates to specialists
coordinator = Agent(
name="Coordinator",
instructions="""You coordinate between research and editing tasks.
Hand off to the Research Agent for analysis and the Editor Agent
for file modifications.""",
handoffs=[
handoff(research_agent),
handoff(editor_agent),
],
)
return coordinator
Approval Policies
Approval policies complement tool filtering. While filters remove tools entirely, approval policies allow tools but require confirmation before execution:
from agents.tool import HostedMCPTool
# Per-tool approval configuration
tool = HostedMCPTool(
tool_config={
"type": "mcp",
"server_label": "filesystem",
"server_url": "https://fs.example.com/mcp",
"require_approval": {
"never": {
"tool_names": ["read_file", "list_directory", "search_files"]
},
"always": {
"tool_names": ["write_file", "edit_file", "delete_file", "move_file"]
}
},
}
)
This configuration lets read operations execute automatically but pauses for write operations, giving a human the chance to review and approve.
Handling MCPToolApprovalRequest
When an approval-required tool is invoked, the runner pauses and yields an approval request:
from agents import Agent, Runner
async def run_with_approvals(agent: Agent, user_input: str):
"""Run an agent and handle tool approval requests interactively."""
result = await Runner.run(agent, input=user_input)
# Process any pending approval requests
while hasattr(result, "pending_approvals") and result.pending_approvals:
for request in result.pending_approvals:
print(f"\nApproval required for tool: {request.tool_name}")
print(f"Server: {request.server_label}")
print(f"Arguments: {request.arguments}")
decision = input("Approve this action? (yes/no/details): ").strip().lower()
if decision == "yes":
request.approve()
elif decision == "details":
print(f"Full tool config: {request.tool_config}")
second = input("Approve? (yes/no): ").strip().lower()
if second == "yes":
request.approve()
else:
request.deny(reason="User declined after reviewing details")
else:
request.deny(reason="User declined")
# Continue execution after all approvals are processed
result = await Runner.run(agent, input=result)
return result.final_output
Programmatic Approval
For automated pipelines where human approval is not practical, implement programmatic approval rules:
async def auto_approve(agent: Agent, user_input: str, rules: dict):
"""Automatically approve or deny based on predefined rules."""
result = await Runner.run(agent, input=user_input)
while hasattr(result, "pending_approvals") and result.pending_approvals:
for request in result.pending_approvals:
tool = request.tool_name
args = request.arguments
# Apply rules
if tool in rules.get("auto_approve", []):
request.approve()
elif tool in rules.get("auto_deny", []):
request.deny(reason=f"Tool {tool} is auto-denied by policy")
elif tool == "write_file" and _is_safe_path(args.get("path", "")):
request.approve()
else:
request.deny(reason="No matching approval rule")
result = await Runner.run(agent, input=result)
return result.final_output
def _is_safe_path(path: str) -> bool:
"""Check if a file path is in an allowed directory."""
allowed_prefixes = ["/workspace/output/", "/tmp/"]
return any(path.startswith(prefix) for prefix in allowed_prefixes)
# Usage
rules = {
"auto_approve": ["read_file", "list_directory", "search_files"],
"auto_deny": ["delete_file", "move_file"],
}
output = await auto_approve(agent, "Organize the project files", rules)
Building a Secure MCP Agent — Full Example
Putting it all together:
import asyncio
import os
from agents import Agent, Runner
from agents.mcp import MCPServerStdio, MCPServerStreamableHTTP
from agents.mcp.util import create_static_tool_filter
from agents.tool import HostedMCPTool
async def build_secure_agent():
# Local filesystem — read only
fs_server = MCPServerStdio(
name="Filesystem",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
},
tool_filter=create_static_tool_filter(
allowed_tool_names=["read_file", "list_directory", "search_files"]
),
)
# Remote database — filtered to SELECT-only tools
db_server = MCPServerStreamableHTTP(
name="Database",
params={
"url": "https://db-tools.internal.com/mcp",
"headers": {"Authorization": f"Bearer {os.environ['DB_MCP_TOKEN']}"},
},
cache_tools_list=True,
tool_filter=create_static_tool_filter(
allowed_tool_names=["query", "describe_table", "list_tables"]
),
)
# Hosted wiki access — with approval for edits
wiki_tool = HostedMCPTool(
tool_config={
"type": "mcp",
"server_label": "company-wiki",
"server_url": "https://wiki-mcp.company.com/mcp",
"require_approval": {
"never": {"tool_names": ["search", "read_page"]},
"always": {"tool_names": ["edit_page", "create_page"]},
},
}
)
async with fs_server, db_server:
agent = Agent(
name="Secure Research Agent",
instructions="""You are a research agent with restricted access:
- Filesystem: read-only (can read files and search)
- Database: read-only (can query and describe tables)
- Wiki: can read freely, edits require approval
Never attempt to circumvent these restrictions.
If you need write access, tell the user to contact an admin.""",
mcp_servers=[fs_server, db_server],
tools=[wiki_tool],
)
return agent
async def main():
agent = await build_secure_agent()
result = await run_with_approvals(
agent,
"Find all configuration files in the workspace, check the database schema, and update the wiki architecture page",
)
print(result)
asyncio.run(main())
Security Checklist for MCP Agents
- Least privilege — Only expose the minimum tools each agent needs
- Static filters first — Use
create_static_tool_filteras the baseline - Dynamic filters for context — Add role-based or context-aware filtering when needed
- Approval for writes — Any tool that modifies state should require approval
- Path restrictions — For filesystem servers, limit the root directory
- Query restrictions — For database servers, block DDL operations
- Audit logging — Log every tool invocation with the agent name, tool name, and arguments
- Regular review — Periodically audit which tools agents are actually using and remove unnecessary ones
Tool filtering and approval policies are not optional features — they are essential for any MCP agent that operates on real data. Start restrictive and loosen permissions based on observed need.
Written by
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.