Skip to content
Learn Agentic AI
Learn Agentic AI13 min read12 views

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

  1. Least privilege — Only expose the minimum tools each agent needs
  2. Static filters first — Use create_static_tool_filter as the baseline
  3. Dynamic filters for context — Add role-based or context-aware filtering when needed
  4. Approval for writes — Any tool that modifies state should require approval
  5. Path restrictions — For filesystem servers, limit the root directory
  6. Query restrictions — For database servers, block DDL operations
  7. Audit logging — Log every tool invocation with the agent name, tool name, and arguments
  8. 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.

Share
C

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.