Skip to content

Dynamic Tool Filtering #703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

pksorensen
Copy link

@pksorensen pksorensen commented Aug 13, 2025

Add comprehensive tool filtering and authorization system with dynamic configuration support, ASP.NET Core
integration, and educational sample demonstrating advanced filtering patterns.

Lowlevel implementation / proposal of IToolFilters.
Compressive Sample implementatino with alot of example/education to demonstrate possibiliites.

Work in progress - higher level abstractions is next step so a attribute based filtering can be done also.

Keeping it in draft until i have had time to try test it out in my own servers.

I would like feedback on the lowlevel IToolFiltering implementation, not so much the sample as it requires more work. But it only make sense if there ar consensus around this could be the lowlevel way we want to support dynamic tool filtering.

Motivation and Context

Problem: The MCP C# SDK currently lacks a comprehensive tool filtering and authorization system, making it
difficult for developers to implement fine-grained access control for MCP tools based on user roles,
permissions, scopes, or business logic.

Solution: This PR introduces a complete tool filtering framework that:

  • Enables dynamic tool filtering based on user context, roles, and custom business logic
  • Integrates seamlessly with ASP.NET Core authorization patterns
  • Provides proper HTTP challenge responses (WWW-Authenticate headers) for unauthorized access
  • Supports multiple filter types working together with configurable priorities
  • Maintains full backward compatibility with existing MCP server implementations

Use Cases:

  • Multi-tenant SaaS applications with per-tenant tool access control
  • Role-based access control (RBAC) systems with hierarchical permissions
  • OAuth2-style scope-based authorization for API access
  • Time-based restrictions (business hours, maintenance windows)
  • Rate limiting and quota management per user/tool
  • Feature flag integration and environment-specific tool availability

How Has This Been Tested?

Comprehensive Test Suite:

  • Unit Tests: 100+ test methods covering all core components with >95% code coverage
  • Integration Tests: End-to-end testing with real MCP client-server communication
  • Performance Tests: Load testing with 10,000+ tools and concurrent operations
  • Authorization Challenge Tests: HTTP challenge response validation for all auth schemes
  • Filter Chain Tests: Priority-based execution and fail-fast behavior validation

Educational Sample Application:

  • DynamicToolFiltering Sample: Complete working example with 19 tools across 4 security levels
  • Multiple Authentication Methods: JWT Bearer tokens and API key authentication
  • Six Different Filter Types: Role-based, scope-based, time-based, rate limiting, tenant isolation, and
    business logic filters
  • Nine Launch Profiles: Testing different scenarios from development to production modes
  • Automated Testing Scripts: Bash and PowerShell scripts for comprehensive validation

Real-World Testing Scenarios:

  • Multi-user access with different permission levels
  • Rate limiting enforcement with quota management
  • Time-based access restrictions during business hours
  • Multi-tenant isolation with tenant-specific configurations
  • OAuth2 scope validation with proper challenge responses
  • Filter priority coordination and exception handling

Breaking Changes

✅ No Breaking Changes - This implementation is fully backward compatible:

  • Existing MCP servers continue to work without any modifications
  • Tool filtering is completely opt-in and disabled by default
  • All new interfaces and services are optional extensions
  • Existing tool registration and execution patterns remain unchanged
  • No changes to existing public APIs or interfaces

New Optional Features:

  • IToolFilter interface for custom filter implementations
  • IToolAuthorizationService for filter coordination
  • Extension methods for easy service registration
  • Enhanced AuthorizationResult and AuthorizationChallenge classes

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Architecture Decisions:

  1. Decorator Pattern Integration: The filtering system integrates with the existing MCP server decorator
    pattern, applying filters to the combined tool result from all sources (original handlers + server collection)
    to ensure consistent authorization across pagination.

  2. Priority-Based Filter Chain: Filters execute in priority order (lower numbers first) allowing for
    performance optimization where expensive filters run after fast-failing security checks.

  3. ASP.NET Core Authorization Integration: Built on proven ASP.NET Core authorization patterns with proper
    HTTP challenge responses, making it familiar to .NET developers.

  4. Fail-Secure Design: Any filter denial immediately blocks access, and filter exceptions default to
    denying access for security.

  5. Service Abstraction: All filtering services use interfaces allowing for easy testing, mocking, and
    production implementations (Redis, database, etc.).

File Structure:
src/ModelContextProtocol.Core/Server/Authorization/
├── Interfaces (IToolFilter, IToolAuthorizationService)
├── Core Services (ToolAuthorizationService, ToolFilterAggregator)
├── Result Classes (AuthorizationResult, AuthorizationChallenge)
├── Built-in Filters (AllowAll, DenyAll, Pattern, Role-based)
└── HTTP Integration (Challenge handling, exception types)

samples/DynamicToolFiltering/
├── Complete working sample with 19 tools
├── 6 advanced filter implementations
├── Multiple authentication schemes
├── Comprehensive documentation and testing guides
└── Production-ready configuration examples

Performance Considerations:

  • Filter instances are registered as singletons for memory efficiency
  • Priority-based execution allows expensive filters to run last
  • Concurrent collection usage ensures thread-safety
  • Built-in cleanup processes prevent memory leaks in long-running services

Security Features:

  • Proper WWW-Authenticate challenge generation for OAuth2, Basic, and custom schemes
  • Input validation and sanitization throughout
  • Fail-secure behavior with comprehensive error handling
  • Audit logging capabilities with structured logging integration
  • No sensitive information exposure in error messages

Educational Value:

  • Complete sample demonstrating real-world authorization patterns
  • Progressive complexity from simple role-based to advanced business logic filtering
  • Production deployment examples with Docker and monitoring
  • Integration guides for common auth providers (Auth0, Azure AD B2C)
  • Performance testing and monitoring configuration examples

This implementation provides a robust foundation for secure MCP tool access control while maintaining the
SDK's simplicity and extensibility principles.

- Implemented unit tests for IToolFilter implementations including AllowAllToolFilter, DenyAllToolFilter, OAuthToolFilter, ToolNamePatternFilter, and RoleBasedToolFilter.
- Added tests for various scenarios such as tool inclusion, execution authorization, priority settings, and exception handling.
- Created integration tests for end-to-end tool filtering in MCP server operations, covering cases with no filters, deny all filters, pattern filters, and multiple filters with priority.
- Ensured proper handling of exceptions during filtering and authorization processes.
- Implemented PowerShell script (test-all.ps1) to test various aspects of the MCP server including health checks, authentication, authorization, tool visibility, rate limiting, feature flags, error handling, and performance.
- Created Bash script (test-all.sh) with similar functionality to ensure cross-platform compatibility.
- Added detailed logging for test results, including success, failure, and warnings.
- Included parameterization for test categories and server URL.
- Implemented robust error handling and response validation for API requests.
- Ensured tests cover both valid and invalid scenarios for API keys and tool execution.
@PederHP
Copy link
Collaborator

PederHP commented Aug 13, 2025

Without having dived too deeply into it yet, I think it might be worth considering a unified mechanism for tools, resources, prompts - as any kind of auth convenience or drop-in filtering/dynamic listing that is relevant to tools is equally relevant to the other two (and potential future capabilities). It might be awkward in practice, or even infeasible. Caveat: I have only skimmed the PR.

@halter73
Copy link
Contributor

Thanks for your contribution! I'm also currently working on adding support for a comprehensive server-side middleware pipeline which is tracked by #267 supporting tools resources and prompts which we will very likely merge instead of this. It's not ready yet but expect a PR later this week, and I'll be sure to mention you so you can review it.

As part of this, I'm working to make ASP.NET Core's [Authorize] attribute work for tool/resource/prompt handlers, and as much is possible I'm trying to rely on ASP.NET's authn and authz primitives (such as IAuthorizationPolicy IAuthenticationService and authentication handlers) to handle things like challenge and forbidden responses.

@pksorensen
Copy link
Author

Thanks both.

This is mostly meant to start a discussion and possible itterate for a design of basically tool filtinger.

Two notes that i find important:

  1. I agree that we should tab into the authorization service of dotnet core.
  2. I wanted to share different usecases from the sample on what kind of filtering we would like to do from a consumer point of view.

For for 1) - i wanted to add the lowlevel internal implementation for tool (i agreee the same thing should exists for other primitimes, however i also belive the interface should be type strong and i see them as seperate things howeever supporting all primitives is wanted).

With lowlevel i mean that i want to abstract away the internal filtering such anyone can override, reimplemente other totaly diffrent usecases if they see them from being able to tab into a filtering pipeline that allow to based on the server, request, user and other services and filter the tools that are being visible.

Authorization is one of these things (i did take a opinioned step here saying if not authorized to a tool i will simply not show it. Usecase to show it and just use authorization concepts when someone call it could also be an alternative that some would want).

I agree that the naming of my services mixes a bit with authorization and should be fixed - the core idea is to talk about filtering. Authorization is an implementation on top. I would use the lowlevel filteing implementation to provide [Authorize] support ontop.

For tools i tried to identity where filtering should happen - concluded that part of McpServer.cs where the tools are being returned would be the right location and made a simple implementation like the following (lets rename it to CreateFilteringContext and toolFilteringService.

// Apply tool filtering to the combined result (from all sources) regardless of pagination
                var authorizationService = Services?.GetService<IToolAuthorizationService>();
                if (authorizationService is not null)
                {
                    try
                    {
                        var authContext = CreateAuthorizationContext(request);
                        var allToolsInResult = result.Tools.ToList();
                        var filteredTools = await authorizationService.FilterToolsAsync(allToolsInResult, authContext, cancellationToken).ConfigureAwait(false);
                        
                        // Replace the tools in the result with the filtered list
                        result.Tools.Clear();
                        foreach (var tool in filteredTools)
                        {
                            result.Tools.Add(tool);
                        }
                    }
                    catch (Exception ex)
                    {
                        // Log error but keep the unfiltered result
                        _logger?.LogError(ex, "Error during tool filtering, returning unfiltered tools");
                    }
                }

                return result;

Next i want to relay the strong part of the design:

image

(again authorization, bad naming)

  "Filtering": {
    "Enabled": true,
    "DefaultBehavior": "deny",
    "RoleBased": {
      "Enabled": true,
      "Priority": 100,
      "RoleClaimType": "role",
      "ToolRoleMapping": {
        "admin_*": [ "admin", "super_admin" ],
        "premium_*": [ "premium", "admin", "super_admin" ],
        "*_user_*": [ "user", "premium", "admin", "super_admin" ],
        "get_*": [ "guest", "user", "premium", "admin", "super_admin" ],
        "echo": [ "guest", "user", "premium", "admin", "super_admin" ],
        "get_utc_time": [ "guest", "user", "premium", "admin", "super_admin" ],
        "*": [ "user", "premium", "admin", "super_admin" ]
      },
      "UseHierarchicalRoles": true,
      "RoleHierarchy": [ "super_admin", "admin", "premium", "user", "guest" ]
    },
    "TimeBased": {
      "Enabled": false,
      "Priority": 200,
      "TimeZone": "UTC",
      "BusinessHours": {
        "Enabled": false,
        "StartTime": "09:00",
        "EndTime": "17:00",
        "BusinessDays": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ],
        "RestrictedTools": [ "admin_*" ]
      },
      "MaintenanceWindows": []
    },
    "ScopeBased": {
      "Enabled": true,
      "Priority": 150,
      "ScopeClaimType": "scope",
      "ToolScopeMapping": {
        "admin_*": [ "admin:tools" ],
        "premium_*": [ "premium:tools" ],
        "*_user_*": [ "user:tools" ],
        "get_*": [ "read:tools" ],
        "echo": [ "basic:tools" ],
        "get_utc_time": [ "basic:tools" ],
        "*": [ "user:tools" ]
      }
    },
    "RateLimiting": {
      "Enabled": true,
      "Priority": 50,
      "WindowMinutes": 60,
      "RoleLimits": {
        "guest": 10,
        "user": 100,
        "premium": 500,
        "admin": 1000,
        "super_admin": -1
      },
      "ToolLimits": {
        "premium_performance_benchmark": 5,
        "admin_*": 50
      },
      "UseSlidingWindow": true
    },
    "TenantIsolation": {
      "Enabled": false,
      "Priority": 75,
      "TenantClaimType": "tenant_id",
      "TenantHeaderName": "X-Tenant-ID",
      "TenantConfigurations": {
        "tenant-a": {
          "Name": "Tenant A",
          "IsActive": true,
          "AllowedTools": [ "*" ],
          "DeniedTools": [ "admin_*" ],
          "CustomRateLimits": {
            "premium_*": 10
          }
        },
        "tenant-b": {
          "Name": "Tenant B",
          "IsActive": true,
          "AllowedTools": [ "get_*", "echo", "*_user_*", "premium_*" ],
          "DeniedTools": [ "admin_*", "premium_performance_benchmark" ],
          "CustomRateLimits": {}
        },
        "enterprise-tenant": {
          "Name": "Enterprise Tenant",
          "IsActive": true,
          "AllowedTools": [ "*" ],
          "DeniedTools": [],
          "CustomRateLimits": {
            "*": 1000
          }
        }
      }
    },
    "BusinessLogic": {
      "Enabled": true,
      "Priority": 300,
      "FeatureFlags": {
        "Enabled": true,
        "ToolFeatureMapping": {
          "premium_*": "premium_features",
          "admin_performance_*": "admin_performance_tools"
        },
        "DefaultFeatureFlagState": false
      },
      "QuotaManagement": {
        "Enabled": false,
        "QuotaPeriodDays": 30,
        "RoleQuotas": {
          "user": 1000,
          "premium": 10000,
          "admin": -1
        },
        "ToolQuotaCosts": {
          "premium_performance_benchmark": 10,
          "premium_*": 2,
          "*": 1
        }
      },
      "EnvironmentRestrictions": {
        "Enabled": true,
        "ProductionRestrictedTools": [
          "admin_force_gc",
          "admin_list_processes"
        ],
        "DevelopmentOnlyTools": []
      }
    }
  }

I think the sample shows the potential of a low level filtering service and how consumers (projects) can implement alot of flexibility on how they applications should do tool filtering (means that we as a sdk is not forcing people in a direction).

But it allows us to provide default implementations of authorization filtering and such that tabs into higher level features of .net.

So in the end i just want to make sure that we can do dynamic filtering of tools beacuse its a important step of creating a good mcp server. Right now many of my servers expose way to many tools that users should be ablee to alter based on usecases and not having them around all the time.

This was just a quick generation - so to me its not important if this get merged but i want to make sure we get a good design for people because right now many people dont know yet how we will be building mcp serveers/soltuions the coming year, so open and flexible from sdk would be the way to go and then opinionated extensions on top.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants