feat: add ToolFilterMiddleware for per-request tool filtering#16
Conversation
Add middleware-based per-request tool filtering using FastMCP's middleware hooks: - ToolFilterMiddleware class with on_list_tools and on_call_tool hooks - ToolFilterFn type alias for filter function signatures - Filter function receives (Tool, FastMCP) to access request-specific config via get_mcp_config() - Comprehensive tests for middleware functionality - Add mcp to deptry ignore list (transitive dependency through fastmcp) Co-Authored-By: AJ Steers <aj@airbyte.io>
Original prompt from AJ Steers |
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
🎉 Thanks for opening this pull request! Your contribution is appreciated. Here are some helpful commands you can use: Quick Commands
Available Poe TasksYou can run any of these tasks using the slash command: Core Tasks
Quick Fixes
Build & Install
Other Commands
The CI will automatically run tests when you push commits. Happy coding! 🚀 |
Add tool_filters parameter to mcp_server() that accepts a list of filter functions. Each filter function is wrapped in ToolFilterMiddleware and registered with the app. This provides a cleaner API for users to add per-request tool filtering. Co-Authored-By: AJ Steers <aj@airbyte.io>
…rameter - Add include_standard_tool_filters parameter to mcp_server() - Add readonly_mode and no_destructive_tools standard filters - Add module filtering (exclude_modules, include_modules) - Add tool name exclusion (exclude_tools) - Update middleware.py docstring with simplified example - Add comprehensive tests for new functionality Co-Authored-By: AJ Steers <aj@airbyte.io>
|
Done! I've updated the "Basic Usage" example in middleware.py to use the simplified The implementation now includes:
All 55 tests pass locally and CI is green. |
- Create tool_filters.py with all constants, config args, and public filter functions - Define constants for config names, env vars, HTTP headers, and annotation keys - Move STANDARD_CONFIG_ARGS and STANDARD_TOOL_FILTERS to tool_filters.py - Rename middleware.py to _middleware.py to hide from public API - Update __init__.py exports to include new public symbols - Remove ToolFilterMiddleware from public exports (implementation detail) - Update tests to import from correct modules Co-Authored-By: AJ Steers <aj@airbyte.io>
|
Done! I've addressed all the feedback:
All 55 tests pass locally and lint checks are green. |
Co-Authored-By: AJ Steers <aj@airbyte.io>
Co-Authored-By: AJ Steers <aj@airbyte.io>
Co-Authored-By: AJ Steers <aj@airbyte.io>
- Create server_config.py with MCPServerConfigArg, MCPServerConfig, get_mcp_config - Move ToolFilterFn from _middleware.py to tool_filters.py - Update imports in server.py, tool_filters.py, _middleware.py, __init__.py - Update test patches to reference server_config module - Removes circular dependency: tool_filters no longer imports from server.py Co-Authored-By: AJ Steers <aj@airbyte.io>
There was a problem hiding this comment.
Pull request overview
This PR adds middleware-based per-request tool filtering to FastMCP servers, allowing different MCP clients to see different tools based on HTTP headers or other request context. It extracts configuration classes into a new server_config.py module to break circular dependencies.
Changes:
- Created
server_config.pymodule withMCPServerConfigArg,MCPServerConfig, andget_mcp_config(extracted fromserver.py) - Added
tool_filters.pywith standard filtering logic, config args, and filter functions - Added
_middleware.pywithToolFilterMiddlewareimplementation - Extended
mcp_server()withtool_filtersandinclude_standard_tool_filtersparameters
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_server.py |
Updated test patches to reference new server_config.get_http_headers, added comprehensive tests for tool filtering |
tests/test_middleware.py |
New test file for ToolFilterMiddleware functionality |
tests/test_fastmcp_extensions.py |
Updated imports after moving annotation constants |
src/fastmcp_extensions/tool_filters.py |
New module with standard tool filters and configuration |
src/fastmcp_extensions/server_config.py |
New module with config classes extracted from server.py |
src/fastmcp_extensions/server.py |
Removed config classes (moved to server_config.py), added tool filter middleware registration |
src/fastmcp_extensions/_middleware.py |
New private module with ToolFilterMiddleware implementation |
src/fastmcp_extensions/__init__.py |
Updated exports to reflect new module structure |
pyproject.toml |
Added mcp to deptry's DEP003 ignore list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Adds middleware-based per-request tool filtering using FastMCP's middleware hooks. This allows different MCP clients to see different tools based on their HTTP headers or other request-specific context.
Key components:
ToolFilterMiddlewareclass withon_list_toolsandon_call_toolhooksToolFilterFntype alias:Callable[[Tool, FastMCP], bool]get_mcp_config(app, "config_name")to access request-specific configuration valuestool_filtersparameter onmcp_server()for convenient middleware registrationinclude_standard_tool_filtersparameter for one-line setup of common filtering patternsThis replaces the previous registration-time filtering approach (from PR #15's earlier iteration) with a middleware-based approach that supports per-request filtering based on HTTP headers.
Updates since last revision
Extracted
server_config.pymodule to break circular dependency:server_config.pywithMCPServerConfigArg,MCPServerConfig, andget_mcp_configToolFilterFnfrom_middleware.pytotool_filters.py(now possible without circular import)server.py,tool_filters.py,_middleware.py, and__init__.pyserver_config.get_http_headersinstead ofserver.get_http_headerstool_filters.pyno longer imports fromserver.pyPrevious refactoring (still applies):
tool_filters.pywith all constants, config args, and public filter functionsmiddleware.pyto_middleware.pyto hide implementation details from public APIToolFilterMiddlewarefrom public exports (users should usemcp_server()withtool_filtersparameter)When
include_standard_tool_filters=True, the following standard filters are added:readonly_modeMCP_READONLY_MODEX-MCP-Readonly-ModereadOnlyHint=Trueno_destructive_toolsMCP_NO_DESTRUCTIVE_TOOLSX-No-Destructive-ToolsdestructiveHint=Trueexclude_modulesMCP_EXCLUDE_MODULESX-MCP-Exclude-Modulesinclude_modulesMCP_INCLUDE_MODULESX-MCP-Include-Modulesexclude_toolsMCP_EXCLUDE_TOOLSX-MCP-Exclude-ToolsReview & Testing Checklist for Human
server_config.pymodule extracts config classes fromserver.py. Verify imports work correctly and there are no circular dependencies._get_tool_by_namemethod accessesapp._tool_manager._tools(private attributes). This could break if FastMCP changes internals.mcp_moduleannotation is accessed viagetattr(annotations, "mcp_module", None)which relies on pydantic'smodel_extra. Confirm this works with actual tools registered via@mcp_tool.Suggested test plan:
include_standard_tool_filters=True@mcp_toolannotations (read_only, destructive, different modules)X-MCP-Readonly-Mode: 1,X-MCP-Exclude-Modules: cloud)Notes
mcp_serverhelper (PR feat: add mcp_server() helper with built-in server info and credential resolution #9)mcppackage is added to deptry's DEP003 ignore list since it's a transitive dependency throughfastmcpused for MCP protocol typesfastmcp_extensions.tool_filtersfor users who want to customize or extend themToolFilterMiddlewareis now in the private_middleware.pymodule; users should usemcp_server()withtool_filtersparameter for custom filtersserver_config.pymodule now contains all config-related classes, breaking the previous circular dependency betweenserver.pyandtool_filters.pyLink to Devin run: https://app.devin.ai/sessions/3e7dd2bb599e4aa496c56fa479f05892
Requested by: Aaron ("AJ") Steers (@aaronsteers)