---
title: "Tool Filtering and Approval Policies for MCP Servers"
description: "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."
canonical: https://callsphere.ai/blog/tool-filtering-approval-policies-mcp-servers
category: "Learn Agentic AI"
tags: ["OpenAI", "MCP", "Tool Filtering", "Approval", "Security"]
author: "CallSphere Team"
published: 2026-03-14T00:00:00.000Z
updated: 2026-05-31T14:36:27.021Z
---

# 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:

```mermaid
flowchart LR
    HOST(["MCP host
Claude Desktop or IDE"])
    CLIENT["MCP client"]
    subgraph SERVERS["MCP Servers"]
        S1["Filesystem server"]
        S2["GitHub server"]
        S3["Postgres server"]
        SX["Custom tool server"]
    end
    LLM["LLM session"]
    OUT(["Grounded action"])
    HOST  CLIENT
    CLIENT |stdio or HTTP+SSE| S1
    CLIENT  S2
    CLIENT  S3
    CLIENT  SX
    CLIENT --> LLM --> OUT
    style HOST fill:#f1f5f9,stroke:#64748b,color:#0f172a
    style CLIENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style OUT fill:#059669,stroke:#047857,color:#fff
```

```python
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:

```python
# 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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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.

---

Source: https://callsphere.ai/blog/tool-filtering-approval-policies-mcp-servers
