From e21f1226c6609fafb45ffa1b4645c046be6efc2b Mon Sep 17 00:00:00 2001 From: lmiranda Date: Tue, 3 Feb 2026 16:08:07 -0500 Subject: [PATCH] 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 --- src/gitea_http_wrapper/filtering/__init__.py | 4 +- src/gitea_http_wrapper/filtering/filter.py | 108 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/gitea_http_wrapper/filtering/filter.py diff --git a/src/gitea_http_wrapper/filtering/__init__.py b/src/gitea_http_wrapper/filtering/__init__.py index 59bf8df..710a375 100644 --- a/src/gitea_http_wrapper/filtering/__init__.py +++ b/src/gitea_http_wrapper/filtering/__init__.py @@ -1,3 +1,5 @@ """Tool filtering module for Claude Desktop compatibility.""" -__all__ = [] +from .filter import ToolFilter + +__all__ = ["ToolFilter"] diff --git a/src/gitea_http_wrapper/filtering/filter.py b/src/gitea_http_wrapper/filtering/filter.py new file mode 100644 index 0000000..01999c6 --- /dev/null +++ b/src/gitea_http_wrapper/filtering/filter.py @@ -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", + }