Implement tool filtering module

This commit implements a flexible tool filtering system for Claude Desktop compatibility.

Features:
- Whitelist mode: Only enable specified tools
- Blacklist mode: Disable specified tools (default enables all)
- Passthrough mode: No filtering (default if no lists provided)
- Validation: Prevents conflicting enabled/disabled lists

Implementation:
- ToolFilter class with three filtering modes
- should_include_tool() for individual tool checks
- filter_tools_list() for filtering tool definition lists
- filter_tools_response() for filtering MCP list_tools responses
- get_filter_stats() for observability and debugging

This module integrates with the configuration loader (#11) and will be used by the HTTP MCP server (#14) to ensure only compatible tools are exposed to Claude Desktop.

Closes #12

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 16:08:07 -05:00
parent 42d625c27f
commit d11649071e
2 changed files with 111 additions and 1 deletions

View File

@@ -1,3 +1,5 @@
"""Tool filtering module for Claude Desktop compatibility."""
__all__ = []
from .filter import ToolFilter
__all__ = ["ToolFilter"]

View File

@@ -0,0 +1,108 @@
"""Tool filtering for Claude Desktop compatibility."""
from typing import Any
class ToolFilter:
"""
Filter MCP tools based on enabled/disabled lists.
This class handles tool filtering to ensure only compatible tools are exposed
to Claude Desktop, preventing crashes from unsupported tool schemas.
"""
def __init__(
self,
enabled_tools: list[str] | None = None,
disabled_tools: list[str] | None = None,
):
"""
Initialize tool filter.
Args:
enabled_tools: List of tool names to enable. If None, all tools are enabled.
disabled_tools: List of tool names to disable. Takes precedence over enabled_tools.
Raises:
ValueError: If both enabled_tools and disabled_tools are specified.
"""
if enabled_tools is not None and disabled_tools is not None:
raise ValueError(
"Cannot specify both enabled_tools and disabled_tools. Choose one filtering mode."
)
self.enabled_tools = set(enabled_tools) if enabled_tools else None
self.disabled_tools = set(disabled_tools) if disabled_tools else None
def should_include_tool(self, tool_name: str) -> bool:
"""
Determine if a tool should be included based on filter rules.
Args:
tool_name: Name of the tool to check.
Returns:
True if tool should be included, False otherwise.
"""
# If disabled list is specified, exclude disabled tools
if self.disabled_tools is not None:
return tool_name not in self.disabled_tools
# If enabled list is specified, only include enabled tools
if self.enabled_tools is not None:
return tool_name in self.enabled_tools
# If no filters specified, include all tools
return True
def filter_tools_list(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Filter a list of tool definitions.
Args:
tools: List of tool definitions (dicts with at least a 'name' field).
Returns:
Filtered list of tool definitions.
"""
return [tool for tool in tools if self.should_include_tool(tool.get("name", ""))]
def filter_tools_response(self, response: dict[str, Any]) -> dict[str, Any]:
"""
Filter tools from an MCP list_tools response.
Args:
response: MCP response dict containing 'tools' list.
Returns:
Filtered response with tools list updated.
"""
if "tools" in response and isinstance(response["tools"], list):
response = response.copy()
response["tools"] = self.filter_tools_list(response["tools"])
return response
def get_filter_stats(self) -> dict[str, Any]:
"""
Get statistics about the filter configuration.
Returns:
Dict containing filter mode and tool counts.
"""
if self.disabled_tools is not None:
return {
"mode": "blacklist",
"disabled_count": len(self.disabled_tools),
"disabled_tools": sorted(self.disabled_tools),
}
elif self.enabled_tools is not None:
return {
"mode": "whitelist",
"enabled_count": len(self.enabled_tools),
"enabled_tools": sorted(self.enabled_tools),
}
else:
return {
"mode": "passthrough",
"message": "All tools enabled",
}