Skip to content

Commit 182947f

Browse files
authored
feat: mcp tool allowlist (#411)
### TL;DR Added the ability to filter which tools are exposed from an MCP server to agents. ### What changed? - Added a new `allowed_tools` configuration option to `MCPServerSettings` that accepts a set of tool names - Modified the server loading logic in `mcp_aggregator.py` to filter tools based on the `allowed_tools` configuration - When `allowed_tools` is specified, only tools with names that exactly match entries in the set will be exposed to agents ### How to test? 1. Configure a server with the `allowed_tools` option in your configuration: ```yaml servers: my_server: allowed_tools: - "tool_one" - "tool_two" ``` 2. Start the MCP agent with this configuration 3. Verify that only the specified tools are available to agents 4. Try accessing a tool that's not in the allowed list and confirm it's not available ### Why make this change? This change provides more granular control over which tools are exposed to agents from each server. This is useful for: - Security purposes - limiting access to potentially dangerous tools - Simplifying the agent experience by only exposing relevant tools - Creating different tool configurations for different agent scenarios without needing separate server instances <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Per-server allow-list to restrict which tools agents can see (exact name matching). * New configuration option for ID reuse policy to control temporal task ID behavior. * **Behavior** * Discovery filters tools so only permitted tools are indexed and shown; an explicitly empty allow-list exposes no tools. * Metrics/reporting include a per-server filtered-tools count. * **Tests** * Added comprehensive tests covering per-server tool filtering and edge cases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 157867e commit 182947f

File tree

4 files changed

+370
-1
lines changed

4 files changed

+370
-1
lines changed

schema/mcp-agent.config.schema.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,23 @@
726726
"default": null,
727727
"title": "Env",
728728
"description": "Environment variables to pass to the server process."
729+
},
730+
"allowed_tools": {
731+
"anyOf": [
732+
{
733+
"items": {
734+
"type": "string"
735+
},
736+
"type": "array",
737+
"uniqueItems": true
738+
},
739+
{
740+
"type": "null"
741+
}
742+
],
743+
"default": null,
744+
"title": "Allowed Tools",
745+
"description": "Allow list for tools of given server"
729746
}
730747
},
731748
"title": "MCPServerSettings",
@@ -1031,6 +1048,17 @@
10311048
],
10321049
"default": null,
10331050
"title": "Rpc Metadata"
1051+
},
1052+
"id_reuse_policy": {
1053+
"default": "allow_duplicate",
1054+
"enum": [
1055+
"allow_duplicate",
1056+
"allow_duplicate_failed_only",
1057+
"reject_duplicate",
1058+
"terminate_if_running"
1059+
],
1060+
"title": "Id Reuse Policy",
1061+
"type": "string"
10341062
}
10351063
},
10361064
"required": [

src/mcp_agent/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
from io import StringIO
88
from pathlib import Path
9-
from typing import Dict, List, Literal, Optional
9+
from typing import Dict, List, Literal, Optional, Set
1010
import threading
1111
import warnings
1212

@@ -105,6 +105,10 @@ class MCPServerSettings(BaseModel):
105105
env: Dict[str, str] | None = None
106106
"""Environment variables to pass to the server process."""
107107

108+
allowed_tools: Set[str] | None = None
109+
"""Set of tool names to allow from this server. If specified, only these tools will be exposed to agents.
110+
Tool names should match exactly. [WARNING] Empty list will result LLM have no access to tools."""
111+
108112
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
109113

110114

src/mcp_agent/mcp/mcp_aggregator.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,26 @@ async def load_server(self, server_name: str):
345345
# Process tools
346346
async with self._tool_map_lock:
347347
self._server_to_tool_map[server_name] = []
348+
349+
# Get server configuration to check for tool filtering
350+
allowed_tools = None
351+
disabled_tool_count = 0
352+
if (self.context is None or self.context.server_registry is None
353+
or not hasattr(self.context.server_registry, "get_server_config")):
354+
logger.warning(f"No config found for server '{server_name}', no tool filter will be applied...")
355+
else:
356+
allowed_tools = self.context.server_registry.get_server_config(server_name).allowed_tools
357+
358+
if allowed_tools is not None and len(allowed_tools) == 0:
359+
logger.warning(f"Allowed tool list is explicitly empty for server '{server_name}'")
360+
348361
for tool in tools:
362+
# Apply tool filtering if configured - O(1) lookup with set
363+
if allowed_tools is not None and tool.name not in allowed_tools:
364+
logger.debug(f"Filtering out tool '{tool.name}' from server '{server_name}' (not in allowed_tools)")
365+
disabled_tool_count += 1
366+
continue
367+
349368
namespaced_tool_name = f"{server_name}{SEP}{tool.name}"
350369
namespaced_tool = NamespacedTool(
351370
tool=tool,
@@ -394,6 +413,7 @@ async def load_server(self, server_name: str):
394413
"server_name": server_name,
395414
"agent_name": self.agent_name,
396415
"tool_count": len(tools),
416+
"disabled_tool_count": disabled_tool_count,
397417
"prompt_count": len(prompts),
398418
"resource_count": len(resources),
399419
}

0 commit comments

Comments
 (0)