diff --git a/samples/DynamicToolFiltering/.dockerignore b/samples/DynamicToolFiltering/.dockerignore
new file mode 100644
index 00000000..3433a5f3
--- /dev/null
+++ b/samples/DynamicToolFiltering/.dockerignore
@@ -0,0 +1,70 @@
+# Build artifacts
+bin/
+obj/
+*.dll
+*.exe
+*.pdb
+out/
+
+# Development files
+.vs/
+.vscode/
+*.user
+*.suo
+.idea/
+
+# Logs and temporary files
+logs/
+*.log
+*.tmp
+*.temp
+
+# OS files
+.DS_Store
+Thumbs.db
+*.swp
+*.swo
+*~
+
+# Git
+.git/
+.gitignore
+.gitattributes
+
+# Documentation (exclude to reduce image size)
+docs/
+*.md
+LICENSE
+
+# Test files and results
+tests/
+TestResults/
+coverage/
+*.http
+
+# Node modules (if any)
+node_modules/
+npm-debug.log*
+
+# Docker files (don't include in context)
+Dockerfile*
+docker-compose*.yml
+.dockerignore
+
+# Scripts (exclude deployment scripts)
+scripts/
+*.sh
+*.ps1
+
+# Environment files (may contain secrets)
+.env*
+appsettings.Development.json
+appsettings.Local.json
+
+# Data and cache directories
+data/
+cache/
+
+# Monitoring configuration
+monitoring/
+nginx/
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Authorization/Filters/BusinessLogicFilter.cs b/samples/DynamicToolFiltering/Authorization/Filters/BusinessLogicFilter.cs
new file mode 100644
index 00000000..60a402bb
--- /dev/null
+++ b/samples/DynamicToolFiltering/Authorization/Filters/BusinessLogicFilter.cs
@@ -0,0 +1,311 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server.Authorization;
+using DynamicToolFiltering.Configuration;
+using DynamicToolFiltering.Services;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+using System.Text.RegularExpressions;
+
+namespace DynamicToolFiltering.Authorization.Filters;
+
+///
+/// Business logic filter that implements complex business rules including feature flags,
+/// quota management, and environment-based restrictions.
+///
+public class BusinessLogicFilter : IToolFilter
+{
+ private readonly BusinessLogicFilteringOptions _options;
+ private readonly IFeatureFlagService _featureFlagService;
+ private readonly IQuotaService _quotaService;
+ private readonly IWebHostEnvironment _environment;
+ private readonly ILogger _logger;
+
+ public BusinessLogicFilter(
+ IOptions options,
+ IFeatureFlagService featureFlagService,
+ IQuotaService quotaService,
+ IWebHostEnvironment environment,
+ ILogger logger)
+ {
+ _options = options.Value.BusinessLogic;
+ _featureFlagService = featureFlagService;
+ _quotaService = quotaService;
+ _environment = environment;
+ _logger = logger;
+ }
+
+ public int Priority => _options.Priority;
+
+ public async Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return true;
+ }
+
+ var canAccess = await CanAccessToolAsync(tool.Name, context, cancellationToken);
+
+ _logger.LogDebug("Tool inclusion check for {ToolName}: CanAccess: {CanAccess}", tool.Name, canAccess);
+
+ return canAccess;
+ }
+
+ public async Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return AuthorizationResult.Allow("Business logic filtering disabled");
+ }
+
+ // Check environment restrictions first
+ var environmentCheck = CheckEnvironmentRestrictions(toolName);
+ if (!environmentCheck.IsAuthorized)
+ {
+ return environmentCheck;
+ }
+
+ // Check feature flags
+ var featureFlagCheck = await CheckFeatureFlagsAsync(toolName, context, cancellationToken);
+ if (!featureFlagCheck.IsAuthorized)
+ {
+ return featureFlagCheck;
+ }
+
+ // Check quota limits
+ var quotaCheck = await CheckQuotaLimitsAsync(toolName, context, cancellationToken);
+ if (!quotaCheck.IsAuthorized)
+ {
+ return quotaCheck;
+ }
+
+ _logger.LogDebug("Tool execution authorized by business logic filter: {ToolName}", toolName);
+ return AuthorizationResult.Allow($"Tool '{toolName}' passes all business logic checks");
+ }
+
+ private async Task CanAccessToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken)
+ {
+ // Check environment restrictions
+ if (!CheckEnvironmentRestrictions(toolName).IsAuthorized)
+ {
+ return false;
+ }
+
+ // Check feature flags
+ if (_options.FeatureFlags.Enabled)
+ {
+ var featureFlag = GetFeatureFlagForTool(toolName);
+ if (featureFlag != null)
+ {
+ var userId = GetUserId(context);
+ var isEnabled = await _featureFlagService.IsEnabledAsync(featureFlag, userId, cancellationToken);
+ if (!isEnabled)
+ {
+ return false;
+ }
+ }
+ }
+
+ // Check quota availability (basic check for visibility)
+ if (_options.QuotaManagement.Enabled)
+ {
+ var userId = GetUserId(context);
+ var userRole = GetUserRole(context);
+ var hasQuota = await _quotaService.HasAvailableQuotaAsync(userId, userRole, toolName, cancellationToken);
+ if (!hasQuota)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private AuthorizationResult CheckEnvironmentRestrictions(string toolName)
+ {
+ if (!_options.EnvironmentRestrictions.Enabled)
+ {
+ return AuthorizationResult.Allow("Environment restrictions disabled");
+ }
+
+ var environmentName = _environment.EnvironmentName;
+
+ // Check production restrictions
+ if (string.Equals(environmentName, "Production", StringComparison.OrdinalIgnoreCase))
+ {
+ if (IsToolMatched(toolName, _options.EnvironmentRestrictions.ProductionRestrictedTools))
+ {
+ var reason = $"Tool '{toolName}' is restricted in production environment";
+
+ _logger.LogWarning("Tool execution denied in production: {ToolName}", toolName);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Environment",
+ ("realm", "mcp-api"),
+ ("environment", environmentName),
+ ("restriction", "production_restricted"));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+ }
+
+ // Check development-only tools
+ if (!string.Equals(environmentName, "Development", StringComparison.OrdinalIgnoreCase))
+ {
+ if (IsToolMatched(toolName, _options.EnvironmentRestrictions.DevelopmentOnlyTools))
+ {
+ var reason = $"Tool '{toolName}' is only available in development environment";
+
+ _logger.LogWarning("Tool execution denied - development only: {ToolName}", toolName);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Environment",
+ ("realm", "mcp-api"),
+ ("environment", environmentName),
+ ("restriction", "development_only"));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+ }
+
+ return AuthorizationResult.Allow("Environment restrictions passed");
+ }
+
+ private async Task CheckFeatureFlagsAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken)
+ {
+ if (!_options.FeatureFlags.Enabled)
+ {
+ return AuthorizationResult.Allow("Feature flags disabled");
+ }
+
+ var featureFlag = GetFeatureFlagForTool(toolName);
+ if (featureFlag == null)
+ {
+ return AuthorizationResult.Allow("No feature flag required");
+ }
+
+ var userId = GetUserId(context);
+ var isEnabled = await _featureFlagService.IsEnabledAsync(featureFlag, userId, cancellationToken);
+
+ if (!isEnabled)
+ {
+ var reason = $"Tool '{toolName}' is disabled by feature flag '{featureFlag}'";
+
+ _logger.LogWarning("Tool execution denied by feature flag: {ToolName}, Flag: {FeatureFlag}", toolName, featureFlag);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "FeatureFlag",
+ ("realm", "mcp-api"),
+ ("feature_flag", featureFlag),
+ ("tool_name", toolName));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ return AuthorizationResult.Allow($"Feature flag '{featureFlag}' enabled");
+ }
+
+ private async Task CheckQuotaLimitsAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken)
+ {
+ if (!_options.QuotaManagement.Enabled)
+ {
+ return AuthorizationResult.Allow("Quota management disabled");
+ }
+
+ var userId = GetUserId(context);
+ var userRole = GetUserRole(context);
+
+ // Check if user has available quota
+ var hasQuota = await _quotaService.HasAvailableQuotaAsync(userId, userRole, toolName, cancellationToken);
+ if (!hasQuota)
+ {
+ var currentUsage = await _quotaService.GetCurrentUsageAsync(userId, cancellationToken);
+ var quotaLimit = await _quotaService.GetQuotaLimitAsync(userId, userRole, cancellationToken);
+
+ var reason = $"Quota exceeded for tool '{toolName}'. Usage: {currentUsage}/{quotaLimit}";
+
+ _logger.LogWarning("Tool execution denied - quota exceeded: {ToolName}, User: {UserId}, Usage: {Usage}/{Limit}",
+ toolName, userId, currentUsage, quotaLimit);
+
+ var resetDate = await _quotaService.GetQuotaResetDateAsync(userId, cancellationToken);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Quota",
+ ("realm", "mcp-api"),
+ ("current_usage", currentUsage.ToString()),
+ ("quota_limit", quotaLimit.ToString()),
+ ("reset_date", resetDate.ToString("O")),
+ ("tool_name", toolName));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ // Consume quota for this operation
+ var quotaCost = GetQuotaCost(toolName);
+ await _quotaService.ConsumeQuotaAsync(userId, toolName, quotaCost, cancellationToken);
+
+ var remainingQuota = await _quotaService.GetRemainingQuotaAsync(userId, userRole, cancellationToken);
+
+ _logger.LogDebug("Quota consumed for tool: {ToolName}, User: {UserId}, Cost: {Cost}, Remaining: {Remaining}",
+ toolName, userId, quotaCost, remainingQuota);
+
+ return AuthorizationResult.Allow($"Quota available. Cost: {quotaCost}, Remaining: {remainingQuota}");
+ }
+
+ private string GetUserId(ToolAuthorizationContext context)
+ {
+ return context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
+ ?? context.User?.FindFirst("sub")?.Value
+ ?? context.User?.FindFirst("user_id")?.Value
+ ?? "anonymous";
+ }
+
+ private string GetUserRole(ToolAuthorizationContext context)
+ {
+ return context.User?.FindFirst(ClaimTypes.Role)?.Value
+ ?? context.User?.FindFirst("role")?.Value
+ ?? (context.User?.Identity?.IsAuthenticated == true ? "user" : "guest");
+ }
+
+ private string? GetFeatureFlagForTool(string toolName)
+ {
+ foreach (var mapping in _options.FeatureFlags.ToolFeatureMapping)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value;
+ }
+ }
+
+ return null;
+ }
+
+ private int GetQuotaCost(string toolName)
+ {
+ foreach (var mapping in _options.QuotaManagement.ToolQuotaCosts)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value;
+ }
+ }
+
+ return 1; // Default cost
+ }
+
+ private bool IsToolMatched(string toolName, string[] patterns)
+ {
+ return patterns.Any(pattern => IsPatternMatch(pattern, toolName));
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Convert glob pattern to regex
+ var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
+ return Regex.IsMatch(toolName, regexPattern, RegexOptions.IgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Authorization/Filters/RateLimitingToolFilter.cs b/samples/DynamicToolFiltering/Authorization/Filters/RateLimitingToolFilter.cs
new file mode 100644
index 00000000..90fd4861
--- /dev/null
+++ b/samples/DynamicToolFiltering/Authorization/Filters/RateLimitingToolFilter.cs
@@ -0,0 +1,177 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server.Authorization;
+using DynamicToolFiltering.Configuration;
+using DynamicToolFiltering.Services;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+using System.Text.RegularExpressions;
+
+namespace DynamicToolFiltering.Authorization.Filters;
+
+///
+/// Rate limiting tool filter that implements quota and rate limiting functionality.
+/// Supports both role-based and tool-specific rate limits with sliding or fixed windows.
+///
+public class RateLimitingToolFilter : IToolFilter
+{
+ private readonly RateLimitingOptions _options;
+ private readonly IRateLimitingService _rateLimitingService;
+ private readonly ILogger _logger;
+
+ public RateLimitingToolFilter(
+ IOptions options,
+ IRateLimitingService rateLimitingService,
+ ILogger logger)
+ {
+ _options = options.Value.RateLimiting;
+ _rateLimitingService = rateLimitingService;
+ _logger = logger;
+ }
+
+ public int Priority => _options.Priority;
+
+ public Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ // Rate limiting doesn't affect tool visibility, only execution
+ return Task.FromResult(true);
+ }
+
+ public async Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return AuthorizationResult.Allow("Rate limiting disabled");
+ }
+
+ var userId = GetUserId(context);
+ var userRole = GetUserRole(context);
+
+ // Get applicable rate limit for this user/tool combination
+ var rateLimit = GetApplicableRateLimit(toolName, userRole);
+
+ if (rateLimit == -1)
+ {
+ // Unlimited access
+ _logger.LogDebug("Tool execution authorized (unlimited): {ToolName} for user {UserId}", toolName, userId);
+ return AuthorizationResult.Allow($"User has unlimited access to tool '{toolName}'");
+ }
+
+ // Check current usage
+ var windowStart = GetWindowStart();
+ var currentUsage = await _rateLimitingService.GetUsageCountAsync(userId, toolName, windowStart, cancellationToken);
+
+ if (currentUsage >= rateLimit)
+ {
+ var reason = $"Rate limit exceeded for tool '{toolName}'. Limit: {rateLimit} requests per {_options.WindowMinutes} minutes. Current usage: {currentUsage}";
+
+ _logger.LogWarning("Rate limit exceeded: {ToolName} for user {UserId}. Limit: {Limit}, Current: {Current}",
+ toolName, userId, rateLimit, currentUsage);
+
+ // Calculate reset time
+ var resetTime = windowStart.AddMinutes(_options.WindowMinutes);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "RateLimit",
+ ("realm", "mcp-api"),
+ ("limit", rateLimit.ToString()),
+ ("remaining", Math.Max(0, rateLimit - currentUsage).ToString()),
+ ("reset_time", resetTime.ToString("O")),
+ ("window_minutes", _options.WindowMinutes.ToString()));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ // Record the usage
+ await _rateLimitingService.RecordUsageAsync(userId, toolName, DateTime.UtcNow, cancellationToken);
+
+ var remaining = rateLimit - currentUsage - 1; // -1 for the current request
+
+ _logger.LogDebug("Tool execution authorized: {ToolName} for user {UserId}. Remaining: {Remaining}/{Limit}",
+ toolName, userId, remaining, rateLimit);
+
+ return AuthorizationResult.Allow($"Tool '{toolName}' execution authorized. Remaining: {remaining}/{rateLimit}");
+ }
+
+ private string GetUserId(ToolAuthorizationContext context)
+ {
+ // Try to get user ID from claims
+ var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
+ ?? context.User?.FindFirst("sub")?.Value
+ ?? context.User?.FindFirst("user_id")?.Value;
+
+ if (!string.IsNullOrEmpty(userId))
+ {
+ return userId;
+ }
+
+ // For anonymous users, use a combination of IP and user agent as identifier
+ var clientInfo = context.Session?.ClientInfo?.Name ?? "unknown";
+ return $"anonymous_{clientInfo.GetHashCode():X8}";
+ }
+
+ private string GetUserRole(ToolAuthorizationContext context)
+ {
+ var role = context.User?.FindFirst(ClaimTypes.Role)?.Value
+ ?? context.User?.FindFirst("role")?.Value;
+
+ if (!string.IsNullOrEmpty(role))
+ {
+ return role;
+ }
+
+ // Default role based on authentication status
+ return context.User?.Identity?.IsAuthenticated == true ? "user" : "guest";
+ }
+
+ private int GetApplicableRateLimit(string toolName, string userRole)
+ {
+ // Check for tool-specific limits first (these override role limits)
+ foreach (var toolLimit in _options.ToolLimits)
+ {
+ if (IsPatternMatch(toolLimit.Key, toolName))
+ {
+ return toolLimit.Value;
+ }
+ }
+
+ // Fall back to role-based limits
+ if (_options.RoleLimits.TryGetValue(userRole, out var roleLimit))
+ {
+ return roleLimit;
+ }
+
+ // Default to guest limits if role not found
+ return _options.RoleLimits.TryGetValue("guest", out var guestLimit) ? guestLimit : 10;
+ }
+
+ private DateTime GetWindowStart()
+ {
+ var now = DateTime.UtcNow;
+
+ if (_options.UseSlidingWindow)
+ {
+ // Sliding window: go back WindowMinutes from now
+ return now.AddMinutes(-_options.WindowMinutes);
+ }
+ else
+ {
+ // Fixed window: align to window boundaries
+ var windowMinutes = _options.WindowMinutes;
+ var minutesSinceEpoch = (long)(now - DateTime.UnixEpoch).TotalMinutes;
+ var windowStart = minutesSinceEpoch - (minutesSinceEpoch % windowMinutes);
+ return DateTime.UnixEpoch.AddMinutes(windowStart);
+ }
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Convert glob pattern to regex
+ var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
+ return Regex.IsMatch(toolName, regexPattern, RegexOptions.IgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Authorization/Filters/RoleBasedToolFilter.cs b/samples/DynamicToolFiltering/Authorization/Filters/RoleBasedToolFilter.cs
new file mode 100644
index 00000000..019afcf2
--- /dev/null
+++ b/samples/DynamicToolFiltering/Authorization/Filters/RoleBasedToolFilter.cs
@@ -0,0 +1,254 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server.Authorization;
+using DynamicToolFiltering.Configuration;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+using System.Text.RegularExpressions;
+
+namespace DynamicToolFiltering.Authorization.Filters;
+
+///
+/// Role-based tool filter that restricts access based on user roles.
+/// Supports hierarchical roles and pattern-based tool matching.
+///
+/// ARCHITECTURAL DECISION RECORD (ADR-001):
+/// ========================================
+/// Decision: Implement hierarchical role-based access control with pattern matching
+///
+/// Context:
+/// - Need to control tool access based on user roles (guest < user < premium < admin < super_admin)
+/// - Different tools require different permission levels
+/// - Must support both exact tool name matching and pattern-based matching (e.g., "admin_*")
+/// - Should be configurable and extensible for new roles/tools
+///
+/// Decision Drivers:
+/// 1. Security: Principle of least privilege - users should only access tools appropriate for their role
+/// 2. Scalability: Pattern matching reduces configuration overhead for large tool sets
+/// 3. Flexibility: Hierarchical roles allow role inheritance (admin can use user tools)
+/// 4. Performance: Role checking should be fast (Priority 100 - after rate limiting but before scope checking)
+///
+/// Implementation Details:
+/// - Uses Claims-based authentication to extract user roles
+/// - Supports multiple roles per user (for flexibility)
+/// - Pattern matching with wildcards (*, prefix matching)
+/// - Configurable role hierarchy and tool mappings
+/// - Detailed logging for audit and debugging
+///
+/// Consequences:
+/// + Simple to understand and configure
+/// + Efficient for common use cases
+/// + Follows standard RBAC patterns
+/// - Requires careful role hierarchy design
+/// - Pattern matching could become complex with many tools
+///
+/// Alternatives Considered:
+/// 1. Attribute-based access control (ABAC) - Too complex for initial implementation
+/// 2. Simple boolean permissions - Not flexible enough for hierarchical access
+/// 3. External authorization service - Adds complexity and latency
+///
+public class RoleBasedToolFilter : IToolFilter
+{
+ private readonly RoleBasedFilteringOptions _options;
+ private readonly ILogger _logger;
+
+ public RoleBasedToolFilter(IOptions options, ILogger logger)
+ {
+ _options = options.Value.RoleBased;
+ _logger = logger;
+ }
+
+ ///
+ /// Filter execution priority. Lower numbers execute first.
+ /// Priority 100 places this after rate limiting (50) but before scope checking (150).
+ ///
+ /// DESIGN DECISION: Role-based filtering occurs early in the pipeline because:
+ /// 1. It's fast to execute (simple claim lookup)
+ /// 2. It can quickly filter out unauthorized tools
+ /// 3. It reduces load on downstream filters
+ ///
+ public int Priority => _options.Priority;
+
+ ///
+ /// Determines if a tool should be visible to the user based on their roles.
+ /// This method implements the "fail-fast" principle - if a user doesn't have
+ /// the required role, the tool won't appear in their tool list.
+ ///
+ /// DESIGN DECISION: Tool visibility vs execution separation
+ /// - Visibility check is more permissive to allow discovery
+ /// - Execution check is more restrictive for security
+ /// - This provides better UX while maintaining security
+ ///
+ /// The tool to check for visibility
+ /// The authorization context containing user information
+ /// Cancellation token for async operations
+ /// True if the tool should be visible to the user
+ public Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ // PERFORMANCE OPTIMIZATION: Early exit if filtering is disabled
+ // This avoids unnecessary processing when role-based filtering is turned off
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(true);
+ }
+
+ // SECURITY PRINCIPLE: Extract and validate user roles from claims
+ // Uses the standard Claims-based authentication model from ASP.NET Core
+ var userRoles = GetUserRoles(context);
+ var requiredRoles = GetRequiredRoles(tool.Name);
+
+ // AUTHORIZATION LOGIC: Check if user has any of the required roles
+ // Uses hierarchical role checking - higher roles can access lower-level tools
+ var hasAccess = HasRequiredRole(userRoles, requiredRoles);
+
+ // AUDIT LOGGING: Detailed logging for security monitoring and debugging
+ // Logs both successful access and denials for security analysis
+ _logger.LogDebug("Tool inclusion check for {ToolName}: User roles [{UserRoles}], Required roles [{RequiredRoles}], HasAccess: {HasAccess}",
+ tool.Name, string.Join(", ", userRoles), string.Join(", ", requiredRoles), hasAccess);
+
+ return Task.FromResult(hasAccess);
+ }
+
+ public Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(AuthorizationResult.Allow("Role-based filtering disabled"));
+ }
+
+ var userRoles = GetUserRoles(context);
+ var requiredRoles = GetRequiredRoles(toolName);
+
+ if (HasRequiredRole(userRoles, requiredRoles))
+ {
+ _logger.LogDebug("Tool execution authorized for {ToolName}: User has required role", toolName);
+ return Task.FromResult(AuthorizationResult.Allow($"User has required role for tool '{toolName}'"));
+ }
+
+ var reason = $"Tool '{toolName}' requires role(s): {string.Join(" or ", requiredRoles)}. User has role(s): {string.Join(", ", userRoles)}";
+
+ _logger.LogWarning("Tool execution denied for {ToolName}: {Reason}", toolName, reason);
+
+ // Create a role-based challenge
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Role",
+ ("realm", "mcp-api"),
+ ("required_roles", string.Join(",", requiredRoles)),
+ ("user_roles", string.Join(",", userRoles)));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+
+ private List GetUserRoles(ToolAuthorizationContext context)
+ {
+ var roles = new List();
+
+ // Try to get roles from claims principal
+ if (context.User?.Identity?.IsAuthenticated == true)
+ {
+ roles.AddRange(context.User.Claims
+ .Where(c => c.Type == _options.RoleClaimType || c.Type == ClaimTypes.Role)
+ .Select(c => c.Value));
+ }
+
+ // If no roles found and user is not authenticated, assign guest role
+ if (roles.Count == 0 && context.User?.Identity?.IsAuthenticated != true)
+ {
+ roles.Add("guest");
+ }
+
+ // If no roles found but user is authenticated, assign default user role
+ if (roles.Count == 0 && context.User?.Identity?.IsAuthenticated == true)
+ {
+ roles.Add("user");
+ }
+
+ return roles;
+ }
+
+ private List GetRequiredRoles(string toolName)
+ {
+ foreach (var mapping in _options.ToolRoleMapping)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value.ToList();
+ }
+ }
+
+ // Default to requiring authentication (user role or higher)
+ return new List { "user" };
+ }
+
+ private bool HasRequiredRole(List userRoles, List requiredRoles)
+ {
+ if (requiredRoles.Count == 0)
+ {
+ return true; // No specific role required
+ }
+
+ if (_options.UseHierarchicalRoles)
+ {
+ return HasHierarchicalRole(userRoles, requiredRoles);
+ }
+ else
+ {
+ return userRoles.Intersect(requiredRoles, StringComparer.OrdinalIgnoreCase).Any();
+ }
+ }
+
+ private bool HasHierarchicalRole(List userRoles, List requiredRoles)
+ {
+ // Get the highest privilege level for user roles
+ var userMaxLevel = GetMaxRoleLevel(userRoles);
+
+ // Get the minimum required privilege level
+ var requiredMinLevel = GetMinRoleLevel(requiredRoles);
+
+ // User must have equal or higher privilege level
+ return userMaxLevel <= requiredMinLevel; // Lower index = higher privilege
+ }
+
+ private int GetMaxRoleLevel(List roles)
+ {
+ var minLevel = int.MaxValue;
+
+ foreach (var role in roles)
+ {
+ var level = Array.IndexOf(_options.RoleHierarchy, role);
+ if (level >= 0 && level < minLevel)
+ {
+ minLevel = level;
+ }
+ }
+
+ return minLevel == int.MaxValue ? _options.RoleHierarchy.Length : minLevel;
+ }
+
+ private int GetMinRoleLevel(List requiredRoles)
+ {
+ var maxLevel = -1;
+
+ foreach (var role in requiredRoles)
+ {
+ var level = Array.IndexOf(_options.RoleHierarchy, role);
+ if (level > maxLevel)
+ {
+ maxLevel = level;
+ }
+ }
+
+ return maxLevel == -1 ? _options.RoleHierarchy.Length - 1 : maxLevel;
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Convert glob pattern to regex
+ var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
+ return Regex.IsMatch(toolName, regexPattern, RegexOptions.IgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Authorization/Filters/ScopeBasedToolFilter.cs b/samples/DynamicToolFiltering/Authorization/Filters/ScopeBasedToolFilter.cs
new file mode 100644
index 00000000..24846773
--- /dev/null
+++ b/samples/DynamicToolFiltering/Authorization/Filters/ScopeBasedToolFilter.cs
@@ -0,0 +1,185 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server.Authorization;
+using DynamicToolFiltering.Configuration;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+using System.Text.RegularExpressions;
+
+namespace DynamicToolFiltering.Authorization.Filters;
+
+///
+/// Scope-based tool filter that implements OAuth2-style scope checking.
+/// Restricts tool access based on granted scopes in JWT tokens or claims.
+///
+public class ScopeBasedToolFilter : IToolFilter
+{
+ private readonly ScopeBasedFilteringOptions _options;
+ private readonly ILogger _logger;
+
+ public ScopeBasedToolFilter(IOptions options, ILogger logger)
+ {
+ _options = options.Value.ScopeBased;
+ _logger = logger;
+ }
+
+ public int Priority => _options.Priority;
+
+ public Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(true);
+ }
+
+ var userScopes = GetUserScopes(context);
+ var requiredScopes = GetRequiredScopes(tool.Name);
+
+ var hasAccess = HasRequiredScope(userScopes, requiredScopes);
+
+ _logger.LogDebug("Tool inclusion check for {ToolName}: User scopes [{UserScopes}], Required scopes [{RequiredScopes}], HasAccess: {HasAccess}",
+ tool.Name, string.Join(", ", userScopes), string.Join(", ", requiredScopes), hasAccess);
+
+ return Task.FromResult(hasAccess);
+ }
+
+ public Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(AuthorizationResult.Allow("Scope-based filtering disabled"));
+ }
+
+ var userScopes = GetUserScopes(context);
+ var requiredScopes = GetRequiredScopes(toolName);
+
+ if (HasRequiredScope(userScopes, requiredScopes))
+ {
+ _logger.LogDebug("Tool execution authorized for {ToolName}: User has required scope", toolName);
+ return Task.FromResult(AuthorizationResult.Allow($"User has required scope for tool '{toolName}'"));
+ }
+
+ var reason = $"Tool '{toolName}' requires scope(s): {string.Join(" or ", requiredScopes)}";
+
+ _logger.LogWarning("Tool execution denied for {ToolName}: Insufficient scope. User scopes: [{UserScopes}], Required: [{RequiredScopes}]",
+ toolName, string.Join(", ", userScopes), string.Join(", ", requiredScopes));
+
+ // Determine the most appropriate scope to request
+ var suggestedScope = requiredScopes.FirstOrDefault() ?? "basic:tools";
+
+ // Create OAuth2-style Bearer challenge with insufficient_scope error
+ return Task.FromResult(AuthorizationResult.DenyInsufficientScope(suggestedScope, "mcp-api"));
+ }
+
+ private List GetUserScopes(ToolAuthorizationContext context)
+ {
+ var scopes = new List();
+
+ // Try to get scopes from claims principal
+ if (context.User?.Identity?.IsAuthenticated == true)
+ {
+ // Check for scope claim (OAuth2 standard)
+ var scopeClaims = context.User.Claims
+ .Where(c => c.Type == _options.ScopeClaimType)
+ .ToList();
+
+ foreach (var scopeClaim in scopeClaims)
+ {
+ // OAuth2 scopes can be space-separated in a single claim
+ var scopeValues = scopeClaim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ scopes.AddRange(scopeValues);
+ }
+
+ // Also check for individual scope claims (some implementations use this pattern)
+ scopes.AddRange(context.User.Claims
+ .Where(c => c.Type.StartsWith("scope:", StringComparison.OrdinalIgnoreCase))
+ .Select(c => c.Type["scope:".Length..]));
+ }
+
+ // If no scopes found and user is not authenticated, assign basic public scope
+ if (scopes.Count == 0 && context.User?.Identity?.IsAuthenticated != true)
+ {
+ scopes.Add("basic:tools");
+ }
+
+ // If no scopes found but user is authenticated, assign basic authenticated scope
+ if (scopes.Count == 0 && context.User?.Identity?.IsAuthenticated == true)
+ {
+ scopes.Add("user:tools");
+ }
+
+ return scopes.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+ }
+
+ private List GetRequiredScopes(string toolName)
+ {
+ foreach (var mapping in _options.ToolScopeMapping)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value.ToList();
+ }
+ }
+
+ // Default to requiring basic tools scope
+ return new List { "basic:tools" };
+ }
+
+ private bool HasRequiredScope(List userScopes, List requiredScopes)
+ {
+ if (requiredScopes.Count == 0)
+ {
+ return true; // No specific scope required
+ }
+
+ // User needs at least one of the required scopes
+ return requiredScopes.Any(requiredScope =>
+ userScopes.Any(userScope =>
+ IsScopeMatch(userScope, requiredScope)));
+ }
+
+ private static bool IsScopeMatch(string userScope, string requiredScope)
+ {
+ // Exact match
+ if (string.Equals(userScope, requiredScope, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // Hierarchical scope matching (e.g., "admin:tools" implies "user:tools")
+ // This implements a simple hierarchical model where broader scopes include narrower ones
+ var scopeHierarchy = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "admin:tools", new[] { "admin:tools", "premium:tools", "user:tools", "read:tools", "basic:tools" } },
+ { "premium:tools", new[] { "premium:tools", "user:tools", "read:tools", "basic:tools" } },
+ { "user:tools", new[] { "user:tools", "read:tools", "basic:tools" } },
+ { "read:tools", new[] { "read:tools", "basic:tools" } },
+ { "basic:tools", new[] { "basic:tools" } }
+ };
+
+ if (scopeHierarchy.TryGetValue(userScope, out var impliedScopes))
+ {
+ return impliedScopes.Contains(requiredScope, StringComparer.OrdinalIgnoreCase);
+ }
+
+ // Wildcard matching for custom scopes (e.g., "tools:*" matches "tools:read")
+ if (userScope.EndsWith(":*"))
+ {
+ var scopePrefix = userScope[..^1]; // Remove the "*"
+ return requiredScope.StartsWith(scopePrefix, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Convert glob pattern to regex
+ var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
+ return Regex.IsMatch(toolName, regexPattern, RegexOptions.IgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Authorization/Filters/TenantIsolationFilter.cs b/samples/DynamicToolFiltering/Authorization/Filters/TenantIsolationFilter.cs
new file mode 100644
index 00000000..70774c8e
--- /dev/null
+++ b/samples/DynamicToolFiltering/Authorization/Filters/TenantIsolationFilter.cs
@@ -0,0 +1,201 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server.Authorization;
+using DynamicToolFiltering.Configuration;
+using Microsoft.Extensions.Options;
+using System.Security.Claims;
+using System.Text.RegularExpressions;
+
+namespace DynamicToolFiltering.Authorization.Filters;
+
+///
+/// Tenant isolation filter that provides multi-tenant tool access control.
+/// Restricts tool access based on tenant membership and tenant-specific configurations.
+///
+public class TenantIsolationFilter : IToolFilter
+{
+ private readonly TenantIsolationOptions _options;
+ private readonly ILogger _logger;
+
+ public TenantIsolationFilter(IOptions options, ILogger logger)
+ {
+ _options = options.Value.TenantIsolation;
+ _logger = logger;
+ }
+
+ public int Priority => _options.Priority;
+
+ public Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(true);
+ }
+
+ var tenantId = GetTenantId(context);
+ var canAccess = CanAccessTool(tool.Name, tenantId);
+
+ _logger.LogDebug("Tool inclusion check for {ToolName}: Tenant {TenantId}, CanAccess: {CanAccess}",
+ tool.Name, tenantId ?? "none", canAccess);
+
+ return Task.FromResult(canAccess);
+ }
+
+ public Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(AuthorizationResult.Allow("Tenant isolation disabled"));
+ }
+
+ var tenantId = GetTenantId(context);
+
+ if (string.IsNullOrEmpty(tenantId))
+ {
+ var reason = "Tenant ID is required for tool access";
+
+ _logger.LogWarning("Tool execution denied: {ToolName} - No tenant ID provided", toolName);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("tenant_header", _options.TenantHeaderName),
+ ("tenant_claim", _options.TenantClaimType));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+
+ if (!_options.TenantConfigurations.TryGetValue(tenantId, out var tenantConfig))
+ {
+ var reason = $"Unknown tenant: {tenantId}";
+
+ _logger.LogWarning("Tool execution denied: {ToolName} - Unknown tenant {TenantId}", toolName, tenantId);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("error", "unknown_tenant"),
+ ("tenant_id", tenantId));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+
+ if (!tenantConfig.IsActive)
+ {
+ var reason = $"Tenant {tenantId} is currently inactive";
+
+ _logger.LogWarning("Tool execution denied: {ToolName} - Inactive tenant {TenantId}", toolName, tenantId);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("error", "tenant_inactive"),
+ ("tenant_id", tenantId));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+
+ // Check if tool is explicitly denied for this tenant
+ if (IsToolDenied(toolName, tenantConfig.DeniedTools))
+ {
+ var reason = $"Tool '{toolName}' is not available for tenant {tenantId}";
+
+ _logger.LogWarning("Tool execution denied: {ToolName} - Explicitly denied for tenant {TenantId}", toolName, tenantId);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("error", "tool_denied"),
+ ("tenant_id", tenantId),
+ ("tool_name", toolName));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+
+ // Check if tool is allowed for this tenant
+ if (!IsToolAllowed(toolName, tenantConfig.AllowedTools))
+ {
+ var reason = $"Tool '{toolName}' is not in the allowed tools list for tenant {tenantId}";
+
+ _logger.LogWarning("Tool execution denied: {ToolName} - Not in allowed list for tenant {TenantId}", toolName, tenantId);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("error", "tool_not_allowed"),
+ ("tenant_id", tenantId),
+ ("tool_name", toolName));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+
+ _logger.LogDebug("Tool execution authorized for tenant: {ToolName}, Tenant: {TenantId}", toolName, tenantId);
+
+ return Task.FromResult(AuthorizationResult.Allow($"Tool '{toolName}' is available for tenant {tenantId}"));
+ }
+
+ private string? GetTenantId(ToolAuthorizationContext context)
+ {
+ // Try to get tenant ID from claims first
+ var tenantId = context.User?.FindFirst(_options.TenantClaimType)?.Value;
+
+ if (!string.IsNullOrEmpty(tenantId))
+ {
+ return tenantId;
+ }
+
+ // Try to get tenant ID from HTTP headers (if available in context)
+ // Note: This would require extending ToolAuthorizationContext to include HTTP context
+ // For now, we'll rely on claims-based approach
+
+ return null;
+ }
+
+ private bool CanAccessTool(string toolName, string? tenantId)
+ {
+ if (string.IsNullOrEmpty(tenantId))
+ {
+ return false; // No tenant, no access
+ }
+
+ if (!_options.TenantConfigurations.TryGetValue(tenantId, out var tenantConfig))
+ {
+ return false; // Unknown tenant
+ }
+
+ if (!tenantConfig.IsActive)
+ {
+ return false; // Inactive tenant
+ }
+
+ // Check denied tools first
+ if (IsToolDenied(toolName, tenantConfig.DeniedTools))
+ {
+ return false;
+ }
+
+ // Check allowed tools
+ return IsToolAllowed(toolName, tenantConfig.AllowedTools);
+ }
+
+ private bool IsToolAllowed(string toolName, string[] allowedPatterns)
+ {
+ return allowedPatterns.Any(pattern => IsPatternMatch(pattern, toolName));
+ }
+
+ private bool IsToolDenied(string toolName, string[] deniedPatterns)
+ {
+ return deniedPatterns.Any(pattern => IsPatternMatch(pattern, toolName));
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Convert glob pattern to regex
+ var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
+ return Regex.IsMatch(toolName, regexPattern, RegexOptions.IgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Authorization/Filters/TimeBasedToolFilter.cs b/samples/DynamicToolFiltering/Authorization/Filters/TimeBasedToolFilter.cs
new file mode 100644
index 00000000..aaf4b0cd
--- /dev/null
+++ b/samples/DynamicToolFiltering/Authorization/Filters/TimeBasedToolFilter.cs
@@ -0,0 +1,196 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server.Authorization;
+using DynamicToolFiltering.Configuration;
+using Microsoft.Extensions.Options;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace DynamicToolFiltering.Authorization.Filters;
+
+///
+/// Time-based tool filter that restricts access based on business hours and maintenance windows.
+///
+public class TimeBasedToolFilter : IToolFilter
+{
+ private readonly TimeBasedFilteringOptions _options;
+ private readonly ILogger _logger;
+ private readonly TimeZoneInfo _timeZone;
+
+ public TimeBasedToolFilter(IOptions options, ILogger logger)
+ {
+ _options = options.Value.TimeBased;
+ _logger = logger;
+
+ try
+ {
+ _timeZone = TimeZoneInfo.FindSystemTimeZoneById(_options.TimeZone);
+ }
+ catch (TimeZoneNotFoundException)
+ {
+ _logger.LogWarning("Time zone '{TimeZone}' not found, falling back to UTC", _options.TimeZone);
+ _timeZone = TimeZoneInfo.Utc;
+ }
+ }
+
+ public int Priority => _options.Priority;
+
+ public Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(true);
+ }
+
+ var canAccess = CanAccessTool(tool.Name);
+
+ _logger.LogDebug("Tool inclusion check for {ToolName}: CanAccess: {CanAccess}", tool.Name, canAccess);
+
+ return Task.FromResult(canAccess);
+ }
+
+ public Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(AuthorizationResult.Allow("Time-based filtering disabled"));
+ }
+
+ var currentTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _timeZone);
+
+ // Check maintenance windows first (highest priority)
+ foreach (var maintenanceWindow in _options.MaintenanceWindows)
+ {
+ if (maintenanceWindow.IsActive && IsInMaintenanceWindow(currentTime, maintenanceWindow))
+ {
+ if (IsToolBlocked(toolName, maintenanceWindow.BlockedTools))
+ {
+ var reason = $"Tool '{toolName}' is blocked during maintenance window: {maintenanceWindow.Description}";
+
+ _logger.LogWarning("Tool execution denied during maintenance: {ToolName}, Window: {Description}",
+ toolName, maintenanceWindow.Description);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Maintenance",
+ ("realm", "mcp-api"),
+ ("maintenance_start", maintenanceWindow.StartTime.ToString("O")),
+ ("maintenance_end", maintenanceWindow.EndTime.ToString("O")),
+ ("description", maintenanceWindow.Description));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+ }
+ }
+
+ // Check business hours restrictions
+ if (_options.BusinessHours.Enabled && IsToolRestrictedToBusinessHours(toolName))
+ {
+ if (!IsWithinBusinessHours(currentTime))
+ {
+ var reason = $"Tool '{toolName}' is only available during business hours: {_options.BusinessHours.StartTime}-{_options.BusinessHours.EndTime} on {string.Join(", ", _options.BusinessHours.BusinessDays)}";
+
+ _logger.LogWarning("Tool execution denied outside business hours: {ToolName}", toolName);
+
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "BusinessHours",
+ ("realm", "mcp-api"),
+ ("business_start", _options.BusinessHours.StartTime),
+ ("business_end", _options.BusinessHours.EndTime),
+ ("business_days", string.Join(",", _options.BusinessHours.BusinessDays)),
+ ("current_time", currentTime.ToString("O")),
+ ("timezone", _timeZone.Id));
+
+ return Task.FromResult(AuthorizationResult.DenyWithChallenge(reason, challenge));
+ }
+ }
+
+ _logger.LogDebug("Tool execution authorized by time-based filter: {ToolName}", toolName);
+ return Task.FromResult(AuthorizationResult.Allow($"Tool '{toolName}' is available at current time"));
+ }
+
+ private bool CanAccessTool(string toolName)
+ {
+ var currentTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _timeZone);
+
+ // Check maintenance windows
+ foreach (var maintenanceWindow in _options.MaintenanceWindows)
+ {
+ if (maintenanceWindow.IsActive &&
+ IsInMaintenanceWindow(currentTime, maintenanceWindow) &&
+ IsToolBlocked(toolName, maintenanceWindow.BlockedTools))
+ {
+ return false;
+ }
+ }
+
+ // Check business hours
+ if (_options.BusinessHours.Enabled &&
+ IsToolRestrictedToBusinessHours(toolName) &&
+ !IsWithinBusinessHours(currentTime))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool IsInMaintenanceWindow(DateTime currentTime, MaintenanceWindowOptions maintenanceWindow)
+ {
+ var windowStart = TimeZoneInfo.ConvertTimeFromUtc(maintenanceWindow.StartTime, _timeZone);
+ var windowEnd = TimeZoneInfo.ConvertTimeFromUtc(maintenanceWindow.EndTime, _timeZone);
+
+ return currentTime >= windowStart && currentTime <= windowEnd;
+ }
+
+ private bool IsToolBlocked(string toolName, string[] blockedPatterns)
+ {
+ return blockedPatterns.Any(pattern => IsPatternMatch(pattern, toolName));
+ }
+
+ private bool IsToolRestrictedToBusinessHours(string toolName)
+ {
+ return _options.BusinessHours.RestrictedTools.Any(pattern => IsPatternMatch(pattern, toolName));
+ }
+
+ private bool IsWithinBusinessHours(DateTime currentTime)
+ {
+ // Check if current day is a business day
+ var currentDayName = currentTime.DayOfWeek.ToString();
+ if (!_options.BusinessHours.BusinessDays.Contains(currentDayName, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Parse business hours
+ if (!TimeOnly.TryParse(_options.BusinessHours.StartTime, CultureInfo.InvariantCulture, out var startTime) ||
+ !TimeOnly.TryParse(_options.BusinessHours.EndTime, CultureInfo.InvariantCulture, out var endTime))
+ {
+ _logger.LogError("Invalid business hours format. Start: {StartTime}, End: {EndTime}",
+ _options.BusinessHours.StartTime, _options.BusinessHours.EndTime);
+ return false;
+ }
+
+ var currentTimeOnly = TimeOnly.FromDateTime(currentTime);
+
+ // Handle cases where end time is before start time (spans midnight)
+ if (endTime < startTime)
+ {
+ return currentTimeOnly >= startTime || currentTimeOnly <= endTime;
+ }
+ else
+ {
+ return currentTimeOnly >= startTime && currentTimeOnly <= endTime;
+ }
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Convert glob pattern to regex
+ var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
+ return Regex.IsMatch(toolName, regexPattern, RegexOptions.IgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Configuration/FilteringOptions.cs b/samples/DynamicToolFiltering/Configuration/FilteringOptions.cs
new file mode 100644
index 00000000..8ee7104f
--- /dev/null
+++ b/samples/DynamicToolFiltering/Configuration/FilteringOptions.cs
@@ -0,0 +1,443 @@
+namespace DynamicToolFiltering.Configuration;
+
+///
+/// Configuration options for dynamic tool filtering system.
+///
+public class FilteringOptions
+{
+ public const string SectionName = "Filtering";
+
+ ///
+ /// Gets or sets whether filtering is enabled globally.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the default behavior when no filters match (allow or deny).
+ ///
+ public string DefaultBehavior { get; set; } = "deny";
+
+ ///
+ /// Gets or sets role-based filtering configuration.
+ ///
+ public RoleBasedFilteringOptions RoleBased { get; set; } = new();
+
+ ///
+ /// Gets or sets time-based filtering configuration.
+ ///
+ public TimeBasedFilteringOptions TimeBased { get; set; } = new();
+
+ ///
+ /// Gets or sets scope-based filtering configuration.
+ ///
+ public ScopeBasedFilteringOptions ScopeBased { get; set; } = new();
+
+ ///
+ /// Gets or sets rate limiting configuration.
+ ///
+ public RateLimitingOptions RateLimiting { get; set; } = new();
+
+ ///
+ /// Gets or sets tenant isolation configuration.
+ ///
+ public TenantIsolationOptions TenantIsolation { get; set; } = new();
+
+ ///
+ /// Gets or sets business logic filtering configuration.
+ ///
+ public BusinessLogicFilteringOptions BusinessLogic { get; set; } = new();
+}
+
+///
+/// Configuration for role-based filtering.
+///
+public class RoleBasedFilteringOptions
+{
+ ///
+ /// Gets or sets whether role-based filtering is enabled.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the priority of the role-based filter.
+ ///
+ public int Priority { get; set; } = 100;
+
+ ///
+ /// Gets or sets the claim type that contains user roles.
+ ///
+ public string RoleClaimType { get; set; } = "role";
+
+ ///
+ /// Gets or sets the mapping of tool patterns to required roles.
+ ///
+ public Dictionary ToolRoleMapping { get; set; } = new()
+ {
+ { "admin_*", new[] { "admin", "super_admin" } },
+ { "premium_*", new[] { "premium", "admin", "super_admin" } },
+ { "*_user_*", new[] { "user", "premium", "admin", "super_admin" } },
+ { "*", new[] { "guest", "user", "premium", "admin", "super_admin" } }
+ };
+
+ ///
+ /// Gets or sets whether to use hierarchical roles (admin inherits user permissions).
+ ///
+ public bool UseHierarchicalRoles { get; set; } = true;
+
+ ///
+ /// Gets or sets the role hierarchy from highest to lowest privilege.
+ ///
+ public string[] RoleHierarchy { get; set; } = { "super_admin", "admin", "premium", "user", "guest" };
+}
+
+///
+/// Configuration for time-based filtering.
+///
+public class TimeBasedFilteringOptions
+{
+ ///
+ /// Gets or sets whether time-based filtering is enabled.
+ ///
+ public bool Enabled { get; set; } = false;
+
+ ///
+ /// Gets or sets the priority of the time-based filter.
+ ///
+ public int Priority { get; set; } = 200;
+
+ ///
+ /// Gets or sets the timezone for time-based filtering.
+ ///
+ public string TimeZone { get; set; } = "UTC";
+
+ ///
+ /// Gets or sets business hours when certain tools are available.
+ ///
+ public BusinessHoursOptions BusinessHours { get; set; } = new();
+
+ ///
+ /// Gets or sets maintenance windows when tools are restricted.
+ ///
+ public MaintenanceWindowOptions[] MaintenanceWindows { get; set; } = Array.Empty();
+}
+
+///
+/// Configuration for business hours.
+///
+public class BusinessHoursOptions
+{
+ ///
+ /// Gets or sets whether business hours restrictions are enabled.
+ ///
+ public bool Enabled { get; set; } = false;
+
+ ///
+ /// Gets or sets the start time for business hours (24-hour format).
+ ///
+ public string StartTime { get; set; } = "09:00";
+
+ ///
+ /// Gets or sets the end time for business hours (24-hour format).
+ ///
+ public string EndTime { get; set; } = "17:00";
+
+ ///
+ /// Gets or sets the days of week for business hours.
+ ///
+ public string[] BusinessDays { get; set; } = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" };
+
+ ///
+ /// Gets or sets tool patterns that are restricted to business hours.
+ ///
+ public string[] RestrictedTools { get; set; } = { "admin_*" };
+}
+
+///
+/// Configuration for maintenance windows.
+///
+public class MaintenanceWindowOptions
+{
+ ///
+ /// Gets or sets the start time of the maintenance window.
+ ///
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// Gets or sets the end time of the maintenance window.
+ ///
+ public DateTime EndTime { get; set; }
+
+ ///
+ /// Gets or sets tool patterns that are blocked during maintenance.
+ ///
+ public string[] BlockedTools { get; set; } = { "*" };
+
+ ///
+ /// Gets or sets whether this maintenance window is active.
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Gets or sets the description of the maintenance window.
+ ///
+ public string Description { get; set; } = "";
+}
+
+///
+/// Configuration for scope-based filtering.
+///
+public class ScopeBasedFilteringOptions
+{
+ ///
+ /// Gets or sets whether scope-based filtering is enabled.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the priority of the scope-based filter.
+ ///
+ public int Priority { get; set; } = 150;
+
+ ///
+ /// Gets or sets the claim type that contains scopes.
+ ///
+ public string ScopeClaimType { get; set; } = "scope";
+
+ ///
+ /// Gets or sets the mapping of tool patterns to required scopes.
+ ///
+ public Dictionary ToolScopeMapping { get; set; } = new()
+ {
+ { "admin_*", new[] { "admin:tools" } },
+ { "premium_*", new[] { "premium:tools" } },
+ { "*_user_*", new[] { "user:tools" } },
+ { "get_*", new[] { "read:tools" } },
+ { "*", new[] { "basic:tools" } }
+ };
+}
+
+///
+/// Configuration for rate limiting.
+///
+public class RateLimitingOptions
+{
+ ///
+ /// Gets or sets whether rate limiting is enabled.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the priority of the rate limiting filter.
+ ///
+ public int Priority { get; set; } = 50;
+
+ ///
+ /// Gets or sets the time window for rate limiting in minutes.
+ ///
+ public int WindowMinutes { get; set; } = 60;
+
+ ///
+ /// Gets or sets rate limits per user role.
+ ///
+ public Dictionary RoleLimits { get; set; } = new()
+ {
+ { "guest", 10 },
+ { "user", 100 },
+ { "premium", 500 },
+ { "admin", 1000 },
+ { "super_admin", -1 } // -1 means unlimited
+ };
+
+ ///
+ /// Gets or sets per-tool rate limits that override role limits.
+ ///
+ public Dictionary ToolLimits { get; set; } = new()
+ {
+ { "premium_performance_benchmark", 5 },
+ { "admin_*", 50 }
+ };
+
+ ///
+ /// Gets or sets whether to use sliding window (true) or fixed window (false).
+ ///
+ public bool UseSlidingWindow { get; set; } = true;
+}
+
+///
+/// Configuration for tenant isolation.
+///
+public class TenantIsolationOptions
+{
+ ///
+ /// Gets or sets whether tenant isolation is enabled.
+ ///
+ public bool Enabled { get; set; } = false;
+
+ ///
+ /// Gets or sets the priority of the tenant isolation filter.
+ ///
+ public int Priority { get; set; } = 75;
+
+ ///
+ /// Gets or sets the claim type that contains tenant ID.
+ ///
+ public string TenantClaimType { get; set; } = "tenant_id";
+
+ ///
+ /// Gets or sets the header name for tenant ID (alternative to claims).
+ ///
+ public string TenantHeaderName { get; set; } = "X-Tenant-ID";
+
+ ///
+ /// Gets or sets tenant-specific tool access configuration.
+ ///
+ public Dictionary TenantConfigurations { get; set; } = new();
+}
+
+///
+/// Configuration for a specific tenant.
+///
+public class TenantConfiguration
+{
+ ///
+ /// Gets or sets the tenant name.
+ ///
+ public string Name { get; set; } = "";
+
+ ///
+ /// Gets or sets whether this tenant is active.
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Gets or sets tool patterns allowed for this tenant.
+ ///
+ public string[] AllowedTools { get; set; } = { "*" };
+
+ ///
+ /// Gets or sets tool patterns explicitly denied for this tenant.
+ ///
+ public string[] DeniedTools { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets custom rate limits for this tenant.
+ ///
+ public Dictionary CustomRateLimits { get; set; } = new();
+}
+
+///
+/// Configuration for business logic filtering.
+///
+public class BusinessLogicFilteringOptions
+{
+ ///
+ /// Gets or sets whether business logic filtering is enabled.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the priority of the business logic filter.
+ ///
+ public int Priority { get; set; } = 300;
+
+ ///
+ /// Gets or sets feature flag configuration.
+ ///
+ public FeatureFlagOptions FeatureFlags { get; set; } = new();
+
+ ///
+ /// Gets or sets quota management configuration.
+ ///
+ public QuotaManagementOptions QuotaManagement { get; set; } = new();
+
+ ///
+ /// Gets or sets environment-based restrictions.
+ ///
+ public EnvironmentRestrictionOptions EnvironmentRestrictions { get; set; } = new();
+}
+
+///
+/// Configuration for feature flags.
+///
+public class FeatureFlagOptions
+{
+ ///
+ /// Gets or sets whether feature flag filtering is enabled.
+ ///
+ public bool Enabled { get; set; } = false;
+
+ ///
+ /// Gets or sets feature flag mappings for tools.
+ ///
+ public Dictionary ToolFeatureMapping { get; set; } = new()
+ {
+ { "premium_*", "premium_features" },
+ { "admin_performance_*", "admin_performance_tools" }
+ };
+
+ ///
+ /// Gets or sets the default state for unknown feature flags.
+ ///
+ public bool DefaultFeatureFlagState { get; set; } = false;
+}
+
+///
+/// Configuration for quota management.
+///
+public class QuotaManagementOptions
+{
+ ///
+ /// Gets or sets whether quota management is enabled.
+ ///
+ public bool Enabled { get; set; } = false;
+
+ ///
+ /// Gets or sets the quota period in days.
+ ///
+ public int QuotaPeriodDays { get; set; } = 30;
+
+ ///
+ /// Gets or sets quota limits per user role.
+ ///
+ public Dictionary RoleQuotas { get; set; } = new()
+ {
+ { "user", 1000 },
+ { "premium", 10000 },
+ { "admin", -1 } // -1 means unlimited
+ };
+
+ ///
+ /// Gets or sets quota costs per tool pattern.
+ ///
+ public Dictionary ToolQuotaCosts { get; set; } = new()
+ {
+ { "premium_performance_benchmark", 10 },
+ { "premium_*", 2 },
+ { "*", 1 }
+ };
+}
+
+///
+/// Configuration for environment-based restrictions.
+///
+public class EnvironmentRestrictionOptions
+{
+ ///
+ /// Gets or sets whether environment restrictions are enabled.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets tool patterns restricted in production.
+ ///
+ public string[] ProductionRestrictedTools { get; set; } =
+ {
+ "admin_force_gc",
+ "admin_list_processes"
+ };
+
+ ///
+ /// Gets or sets tool patterns only available in development.
+ ///
+ public string[] DevelopmentOnlyTools { get; set; } = Array.Empty();
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Dockerfile b/samples/DynamicToolFiltering/Dockerfile
new file mode 100644
index 00000000..d86fc7c9
--- /dev/null
+++ b/samples/DynamicToolFiltering/Dockerfile
@@ -0,0 +1,66 @@
+# Dynamic Tool Filtering MCP Server - Docker Configuration
+# Multi-stage build for optimized production image
+
+# Build stage
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+WORKDIR /src
+
+# Copy project file and restore dependencies
+# This layer is cached unless project file changes
+COPY DynamicToolFiltering.csproj .
+RUN dotnet restore
+
+# Copy source code and build
+COPY . .
+RUN dotnet build -c Release -o /app/build --no-restore
+
+# Publish stage
+FROM build AS publish
+RUN dotnet publish -c Release -o /app/publish --no-restore --no-build
+
+# Runtime stage - minimal ASP.NET Core runtime image
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
+
+# Install dependencies for health checks and debugging
+RUN apt-get update && apt-get install -y \
+ curl \
+ jq \
+ && rm -rf /var/lib/apt/lists/*
+
+# Create non-root user for security
+RUN groupadd -r appgroup && useradd -r -g appgroup appuser
+
+# Set working directory
+WORKDIR /app
+
+# Copy published application
+COPY --from=publish /app/publish .
+
+# Create necessary directories and set permissions
+RUN mkdir -p logs data && \
+ chown -R appuser:appgroup /app
+
+# Switch to non-root user
+USER appuser
+
+# Expose port
+EXPOSE 8080
+
+# Add metadata labels
+LABEL org.opencontainers.image.title="Dynamic Tool Filtering MCP Server" \
+ org.opencontainers.image.description="Advanced MCP server demonstrating tool filtering and authorization" \
+ org.opencontainers.image.source="https://github.com/microsoft/mcp-csharp-sdk" \
+ org.opencontainers.image.documentation="https://github.com/microsoft/mcp-csharp-sdk/tree/main/samples/DynamicToolFiltering"
+
+# Health check - verify the application is responding
+HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
+ CMD curl -f http://localhost:8080/health || exit 1
+
+# Set environment variables for container
+ENV ASPNETCORE_URLS=http://+:8080 \
+ ASPNETCORE_ENVIRONMENT=Production \
+ DOTNET_RUNNING_IN_CONTAINER=true \
+ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
+
+# Start application
+ENTRYPOINT ["dotnet", "DynamicToolFiltering.dll"]
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/DynamicToolFiltering.csproj b/samples/DynamicToolFiltering/DynamicToolFiltering.csproj
new file mode 100644
index 00000000..5eb656e2
--- /dev/null
+++ b/samples/DynamicToolFiltering/DynamicToolFiltering.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+ DynamicToolFiltering-sample
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/INTEGRATION_EXAMPLES.md b/samples/DynamicToolFiltering/INTEGRATION_EXAMPLES.md
new file mode 100644
index 00000000..04540985
--- /dev/null
+++ b/samples/DynamicToolFiltering/INTEGRATION_EXAMPLES.md
@@ -0,0 +1,1011 @@
+# Integration Examples for Dynamic Tool Filtering
+
+This document provides practical examples for integrating the Dynamic Tool Filtering system with external services and real-world scenarios.
+
+## Table of Contents
+
+1. [JWT Integration with Identity Providers](#jwt-integration-with-identity-providers)
+2. [Redis Integration for Rate Limiting](#redis-integration-for-rate-limiting)
+3. [Database Integration for Quotas](#database-integration-for-quotas)
+4. [External Feature Flag Services](#external-feature-flag-services)
+5. [Multi-Tenant SaaS Integration](#multi-tenant-saas-integration)
+6. [Monitoring and Observability](#monitoring-and-observability)
+7. [Custom Filter Development](#custom-filter-development)
+
+## JWT Integration with Identity Providers
+
+### Auth0 Integration
+
+```csharp
+// Program.cs - Configure Auth0 JWT authentication
+builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(options =>
+ {
+ options.Authority = "https://your-tenant.auth0.com/";
+ options.Audience = "your-api-identifier";
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ClockSkew = TimeSpan.FromMinutes(5)
+ };
+ });
+
+// Custom claims transformation for Auth0
+builder.Services.AddSingleton();
+
+public class Auth0ClaimsTransformation : IClaimsTransformation
+{
+ public Task TransformAsync(ClaimsPrincipal principal)
+ {
+ var claimsIdentity = (ClaimsIdentity)principal.Identity!;
+
+ // Map Auth0 custom claims to standard claims
+ var permissions = principal.FindFirst("permissions")?.Value;
+ if (!string.IsNullOrEmpty(permissions))
+ {
+ var permissionList = JsonSerializer.Deserialize(permissions);
+ foreach (var permission in permissionList)
+ {
+ claimsIdentity.AddClaim(new Claim("scope", permission));
+ }
+ }
+
+ // Map Auth0 roles
+ var roles = principal.FindFirst("https://myapp.com/roles")?.Value;
+ if (!string.IsNullOrEmpty(roles))
+ {
+ var roleList = JsonSerializer.Deserialize(roles);
+ foreach (var role in roleList)
+ {
+ claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
+ }
+ }
+
+ return Task.FromResult(principal);
+ }
+}
+```
+
+### Azure AD B2C Integration
+
+```csharp
+// Program.cs - Configure Azure AD B2C
+builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"));
+
+// appsettings.json
+{
+ "AzureAdB2C": {
+ "Instance": "https://yourtenant.b2clogin.com",
+ "ClientId": "your-client-id",
+ "Domain": "yourtenant.onmicrosoft.com",
+ "SignUpSignInPolicyId": "B2C_1_signupsignin1"
+ }
+}
+```
+
+## Redis Integration for Rate Limiting
+
+### Production-Ready Rate Limiting Service
+
+```csharp
+// Services/RedisRateLimitingService.cs
+public class RedisRateLimitingService : IRateLimitingService
+{
+ private readonly IDatabase _database;
+ private readonly ILogger _logger;
+ private const string USAGE_KEY_PREFIX = "rate_limit:usage:";
+ private const string STATISTICS_KEY_PREFIX = "rate_limit:stats:";
+
+ public RedisRateLimitingService(IConnectionMultiplexer redis, ILogger logger)
+ {
+ _database = redis.GetDatabase();
+ _logger = logger;
+ }
+
+ public async Task GetUsageCountAsync(string userId, string toolName, DateTime windowStart, CancellationToken cancellationToken = default)
+ {
+ var key = GetUsageKey(userId, toolName);
+ var windowEnd = windowStart.AddHours(1); // 1-hour window
+
+ var count = await _database.SortedSetCountAsync(key,
+ windowStart.Ticks, windowEnd.Ticks);
+
+ return (int)count;
+ }
+
+ public async Task RecordUsageAsync(string userId, string toolName, DateTime timestamp, CancellationToken cancellationToken = default)
+ {
+ var key = GetUsageKey(userId, toolName);
+ var score = timestamp.Ticks;
+
+ // Add usage record
+ await _database.SortedSetAddAsync(key, Guid.NewGuid().ToString(), score);
+
+ // Set expiration for cleanup
+ await _database.KeyExpireAsync(key, TimeSpan.FromDays(1));
+
+ // Update statistics
+ await UpdateStatisticsAsync(userId, toolName, timestamp);
+
+ _logger.LogDebug("Recorded usage for {UserId}, {ToolName} at {Timestamp}",
+ userId, toolName, timestamp);
+ }
+
+ public async Task CleanupOldRecordsAsync(CancellationToken cancellationToken = default)
+ {
+ var cutoffTime = DateTime.UtcNow.AddHours(-24);
+ var pattern = $"{USAGE_KEY_PREFIX}*";
+
+ await foreach (var key in _database.Multiplexer.GetServer().KeysAsync(pattern: pattern))
+ {
+ await _database.SortedSetRemoveRangeByScoreAsync(key,
+ double.NegativeInfinity, cutoffTime.Ticks);
+ }
+ }
+
+ public async Task> GetUsageStatisticsAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var statsKey = $"{STATISTICS_KEY_PREFIX}{userId}";
+ var hash = await _database.HashGetAllAsync(statsKey);
+
+ return hash.ToDictionary(
+ h => h.Name.ToString(),
+ h => (int)h.Value
+ );
+ }
+
+ private async Task UpdateStatisticsAsync(string userId, string toolName, DateTime timestamp)
+ {
+ var statsKey = $"{STATISTICS_KEY_PREFIX}{userId}";
+ var hourKey = $"{toolName}:{timestamp:yyyy-MM-dd:HH}";
+
+ await _database.HashIncrementAsync(statsKey, hourKey);
+ await _database.KeyExpireAsync(statsKey, TimeSpan.FromDays(30));
+ }
+
+ private string GetUsageKey(string userId, string toolName) =>
+ $"{USAGE_KEY_PREFIX}{userId}:{toolName}";
+}
+
+// Program.cs - Register Redis services
+builder.Services.AddStackExchangeRedisCache(options =>
+{
+ options.Configuration = builder.Configuration.GetConnectionString("Redis");
+});
+
+builder.Services.AddSingleton(provider =>
+ ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")));
+
+builder.Services.AddSingleton();
+```
+
+## Database Integration for Quotas
+
+### Entity Framework Quota Service
+
+```csharp
+// Models/QuotaDbContext.cs
+public class QuotaDbContext : DbContext
+{
+ public QuotaDbContext(DbContextOptions options) : base(options) { }
+
+ public DbSet UserQuotas { get; set; }
+ public DbSet QuotaUsages { get; set; }
+ public DbSet QuotaResets { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.UserId);
+ entity.Property(e => e.UserId).HasMaxLength(100);
+ entity.Property(e => e.Role).HasMaxLength(50);
+ entity.HasIndex(e => e.Role);
+ entity.HasIndex(e => e.NextResetDate);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.UserId).HasMaxLength(100);
+ entity.Property(e => e.ToolName).HasMaxLength(100);
+ entity.HasIndex(e => new { e.UserId, e.ToolName });
+ entity.HasIndex(e => e.UsageDate);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.UserId).HasMaxLength(100);
+ entity.HasIndex(e => e.UserId);
+ entity.HasIndex(e => e.ResetDate);
+ });
+ }
+}
+
+// Models/QuotaEntities.cs
+public class UserQuota
+{
+ public string UserId { get; set; } = "";
+ public string Role { get; set; } = "";
+ public int CurrentUsage { get; set; }
+ public int QuotaLimit { get; set; }
+ public DateTime NextResetDate { get; set; }
+ public DateTime LastUsage { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+}
+
+public class QuotaUsage
+{
+ public int Id { get; set; }
+ public string UserId { get; set; } = "";
+ public string ToolName { get; set; } = "";
+ public int Cost { get; set; }
+ public DateTime UsageDate { get; set; }
+ public string? RequestId { get; set; }
+}
+
+public class QuotaReset
+{
+ public int Id { get; set; }
+ public string UserId { get; set; } = "";
+ public DateTime ResetDate { get; set; }
+ public int PreviousUsage { get; set; }
+ public string ResetReason { get; set; } = "";
+}
+
+// Services/DatabaseQuotaService.cs
+public class DatabaseQuotaService : IQuotaService
+{
+ private readonly QuotaDbContext _context;
+ private readonly QuotaManagementOptions _options;
+ private readonly ILogger _logger;
+
+ public DatabaseQuotaService(
+ QuotaDbContext context,
+ IOptions options,
+ ILogger logger)
+ {
+ _context = context;
+ _options = options.Value.BusinessLogic.QuotaManagement;
+ _logger = logger;
+ }
+
+ public async Task HasAvailableQuotaAsync(string userId, string userRole, string toolName, CancellationToken cancellationToken = default)
+ {
+ var userQuota = await GetOrCreateUserQuotaAsync(userId, userRole);
+ var quotaCost = GetQuotaCost(toolName);
+
+ return userQuota.QuotaLimit == -1 || // Unlimited
+ userQuota.CurrentUsage + quotaCost <= userQuota.QuotaLimit;
+ }
+
+ public async Task ConsumeQuotaAsync(string userId, string toolName, int cost, CancellationToken cancellationToken = default)
+ {
+ using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var userQuota = await _context.UserQuotas
+ .FirstOrDefaultAsync(q => q.UserId == userId, cancellationToken);
+
+ if (userQuota != null)
+ {
+ userQuota.CurrentUsage += cost;
+ userQuota.LastUsage = DateTime.UtcNow;
+ userQuota.UpdatedAt = DateTime.UtcNow;
+ }
+
+ var usage = new QuotaUsage
+ {
+ UserId = userId,
+ ToolName = toolName,
+ Cost = cost,
+ UsageDate = DateTime.UtcNow,
+ RequestId = Guid.NewGuid().ToString()
+ };
+
+ _context.QuotaUsages.Add(usage);
+ await _context.SaveChangesAsync(cancellationToken);
+ await transaction.CommitAsync(cancellationToken);
+
+ _logger.LogDebug("Consumed {Cost} quota for user {UserId}, tool {ToolName}",
+ cost, userId, toolName);
+ }
+ catch
+ {
+ await transaction.RollbackAsync(cancellationToken);
+ throw;
+ }
+ }
+
+ public async Task GetCurrentUsageAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var userQuota = await _context.UserQuotas
+ .FirstOrDefaultAsync(q => q.UserId == userId, cancellationToken);
+
+ return userQuota?.CurrentUsage ?? 0;
+ }
+
+ private async Task GetOrCreateUserQuotaAsync(string userId, string userRole)
+ {
+ var userQuota = await _context.UserQuotas
+ .FirstOrDefaultAsync(q => q.UserId == userId);
+
+ if (userQuota == null)
+ {
+ var quotaLimit = GetQuotaLimitForRole(userRole);
+ userQuota = new UserQuota
+ {
+ UserId = userId,
+ Role = userRole,
+ CurrentUsage = 0,
+ QuotaLimit = quotaLimit,
+ NextResetDate = CalculateNextResetDate(DateTime.UtcNow),
+ CreatedAt = DateTime.UtcNow,
+ UpdatedAt = DateTime.UtcNow
+ };
+
+ _context.UserQuotas.Add(userQuota);
+ await _context.SaveChangesAsync();
+ }
+
+ return userQuota;
+ }
+
+ private int GetQuotaLimitForRole(string userRole)
+ {
+ return _options.RoleQuotas.TryGetValue(userRole, out var limit) ? limit : 1000;
+ }
+
+ private int GetQuotaCost(string toolName)
+ {
+ foreach (var mapping in _options.ToolQuotaCosts)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value;
+ }
+ }
+ return 1;
+ }
+
+ private DateTime CalculateNextResetDate(DateTime fromDate)
+ {
+ return fromDate.AddDays(_options.QuotaPeriodDays);
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*") return true;
+ if (pattern.EndsWith("*"))
+ {
+ var prefix = pattern[..^1];
+ return toolName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
+ }
+ return string.Equals(pattern, toolName, StringComparison.OrdinalIgnoreCase);
+ }
+}
+
+// Program.cs - Register database services
+builder.Services.AddDbContext(options =>
+ options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
+
+builder.Services.AddScoped();
+```
+
+## External Feature Flag Services
+
+### LaunchDarkly Integration
+
+```csharp
+// Services/LaunchDarklyFeatureFlagService.cs
+public class LaunchDarklyFeatureFlagService : IFeatureFlagService
+{
+ private readonly LdClient _client;
+ private readonly ILogger _logger;
+
+ public LaunchDarklyFeatureFlagService(IConfiguration configuration, ILogger logger)
+ {
+ var sdkKey = configuration["LaunchDarkly:SdkKey"];
+ var config = Configuration.Default(sdkKey);
+ _client = new LdClient(config);
+ _logger = logger;
+ }
+
+ public Task IsEnabledAsync(string flagName, string userId, CancellationToken cancellationToken = default)
+ {
+ var user = User.WithKey(userId);
+ var isEnabled = _client.BoolVariation(flagName, user, false);
+
+ _logger.LogDebug("Feature flag {FlagName} for user {UserId}: {Enabled}", flagName, userId, isEnabled);
+
+ return Task.FromResult(isEnabled);
+ }
+
+ public async Task> GetAllFlagsAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var user = User.WithKey(userId);
+ var allFlags = _client.AllFlagsState(user);
+
+ var result = new Dictionary();
+ foreach (var flag in allFlags.ToValuesMap())
+ {
+ if (flag.Value is bool boolValue)
+ {
+ result[flag.Key] = boolValue;
+ }
+ }
+
+ return result;
+ }
+
+ public Task SetFlagAsync(string flagName, bool enabled, string? userId = null, CancellationToken cancellationToken = default)
+ {
+ // LaunchDarkly doesn't support programmatic flag setting from SDK
+ // This would typically be done through their REST API or dashboard
+ _logger.LogWarning("Cannot set flag {FlagName} programmatically with LaunchDarkly SDK", flagName);
+ return Task.CompletedTask;
+ }
+
+ public Task GetRolloutPercentageAsync(string flagName, CancellationToken cancellationToken = default)
+ {
+ // This would require calling LaunchDarkly's REST API to get flag configuration
+ _logger.LogWarning("Cannot get rollout percentage for flag {FlagName} with current implementation", flagName);
+ return Task.FromResult(100);
+ }
+}
+
+// Program.cs
+builder.Services.AddSingleton();
+```
+
+### Azure App Configuration Integration
+
+```csharp
+// Services/AzureFeatureFlagService.cs
+public class AzureFeatureFlagService : IFeatureFlagService
+{
+ private readonly IFeatureManager _featureManager;
+ private readonly ILogger _logger;
+
+ public AzureFeatureFlagService(IFeatureManager featureManager, ILogger logger)
+ {
+ _featureManager = featureManager;
+ _logger = logger;
+ }
+
+ public async Task IsEnabledAsync(string flagName, string userId, CancellationToken cancellationToken = default)
+ {
+ var context = new TargetingContext
+ {
+ UserId = userId
+ };
+
+ var isEnabled = await _featureManager.IsEnabledAsync(flagName, context);
+
+ _logger.LogDebug("Feature flag {FlagName} for user {UserId}: {Enabled}", flagName, userId, isEnabled);
+
+ return isEnabled;
+ }
+
+ public async Task> GetAllFlagsAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var context = new TargetingContext { UserId = userId };
+ var result = new Dictionary();
+
+ var flagNames = new[] { "premium_features", "admin_performance_tools", "experimental_tools", "beta_features" };
+
+ foreach (var flagName in flagNames)
+ {
+ result[flagName] = await _featureManager.IsEnabledAsync(flagName, context);
+ }
+
+ return result;
+ }
+
+ // Other methods would interact with Azure App Configuration REST API
+}
+
+// Program.cs
+builder.Configuration.AddAzureAppConfiguration(options =>
+{
+ options.Connect(builder.Configuration.GetConnectionString("AzureAppConfiguration"))
+ .UseFeatureFlags();
+});
+
+builder.Services.AddFeatureManagement();
+builder.Services.AddSingleton();
+```
+
+## Multi-Tenant SaaS Integration
+
+### Advanced Tenant Isolation Filter
+
+```csharp
+// Services/TenantManagementService.cs
+public interface ITenantManagementService
+{
+ Task GetTenantAsync(string tenantId);
+ Task> GetTenantToolsAsync(string tenantId);
+ Task> GetTenantRateLimitsAsync(string tenantId);
+ Task IsTenantActiveAsync(string tenantId);
+}
+
+public class TenantManagementService : ITenantManagementService
+{
+ private readonly HttpClient _httpClient;
+ private readonly IMemoryCache _cache;
+ private readonly ILogger _logger;
+
+ public TenantManagementService(HttpClient httpClient, IMemoryCache cache, ILogger logger)
+ {
+ _httpClient = httpClient;
+ _cache = cache;
+ _logger = logger;
+ }
+
+ public async Task GetTenantAsync(string tenantId)
+ {
+ var cacheKey = $"tenant:{tenantId}";
+
+ if (_cache.TryGetValue(cacheKey, out TenantInfo? cachedTenant))
+ {
+ return cachedTenant;
+ }
+
+ try
+ {
+ var response = await _httpClient.GetAsync($"/api/tenants/{tenantId}");
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync();
+ var tenant = JsonSerializer.Deserialize(json);
+
+ _cache.Set(cacheKey, tenant, TimeSpan.FromMinutes(15));
+ return tenant;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to fetch tenant {TenantId}", tenantId);
+ }
+
+ return null;
+ }
+
+ public async Task> GetTenantToolsAsync(string tenantId)
+ {
+ var tenant = await GetTenantAsync(tenantId);
+ return tenant?.AllowedTools?.ToList() ?? new List();
+ }
+
+ public async Task> GetTenantRateLimitsAsync(string tenantId)
+ {
+ var tenant = await GetTenantAsync(tenantId);
+ return tenant?.CustomRateLimits ?? new Dictionary();
+ }
+
+ public async Task IsTenantActiveAsync(string tenantId)
+ {
+ var tenant = await GetTenantAsync(tenantId);
+ return tenant?.IsActive ?? false;
+ }
+}
+
+// Enhanced Tenant Isolation Filter
+public class EnhancedTenantIsolationFilter : IToolFilter
+{
+ private readonly TenantIsolationOptions _options;
+ private readonly ITenantManagementService _tenantService;
+ private readonly ILogger _logger;
+
+ public EnhancedTenantIsolationFilter(
+ IOptions options,
+ ITenantManagementService tenantService,
+ ILogger logger)
+ {
+ _options = options.Value.TenantIsolation;
+ _tenantService = tenantService;
+ _logger = logger;
+ }
+
+ public int Priority => _options.Priority;
+
+ public async Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled) return true;
+
+ var tenantId = GetTenantId(context);
+ if (string.IsNullOrEmpty(tenantId)) return true;
+
+ var allowedTools = await _tenantService.GetTenantToolsAsync(tenantId);
+ return IsToolAllowed(tool.Name, allowedTools);
+ }
+
+ public async Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ return AuthorizationResult.Allow("Tenant isolation disabled");
+
+ var tenantId = GetTenantId(context);
+ if (string.IsNullOrEmpty(tenantId))
+ return AuthorizationResult.Allow("No tenant context");
+
+ // Check if tenant is active
+ if (!await _tenantService.IsTenantActiveAsync(tenantId))
+ {
+ var reason = $"Tenant '{tenantId}' is not active";
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("tenant_id", tenantId),
+ ("status", "inactive"));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ // Check tool access
+ var allowedTools = await _tenantService.GetTenantToolsAsync(tenantId);
+ if (!IsToolAllowed(toolName, allowedTools))
+ {
+ var reason = $"Tool '{toolName}' is not allowed for tenant '{tenantId}'";
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Tenant",
+ ("realm", "mcp-api"),
+ ("tenant_id", tenantId),
+ ("tool_name", toolName),
+ ("restriction", "tool_not_allowed"));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ return AuthorizationResult.Allow($"Tool '{toolName}' allowed for tenant '{tenantId}'");
+ }
+
+ private string? GetTenantId(ToolAuthorizationContext context)
+ {
+ // Try to get tenant ID from claims first
+ var tenantClaim = context.User?.FindFirst(_options.TenantClaimType)?.Value;
+ if (!string.IsNullOrEmpty(tenantClaim))
+ return tenantClaim;
+
+ // Fall back to header if available in the context
+ // This would need to be passed through from the HTTP context
+ return context.AdditionalData?.TryGetValue("TenantId", out var tenantId) == true
+ ? tenantId?.ToString()
+ : null;
+ }
+
+ private bool IsToolAllowed(string toolName, List allowedTools)
+ {
+ return allowedTools.Any(pattern => IsPatternMatch(pattern, toolName));
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*") return true;
+ if (pattern.EndsWith("*"))
+ {
+ var prefix = pattern[..^1];
+ return toolName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
+ }
+ return string.Equals(pattern, toolName, StringComparison.OrdinalIgnoreCase);
+ }
+}
+
+// Models/TenantInfo.cs
+public class TenantInfo
+{
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ public bool IsActive { get; set; }
+ public string SubscriptionTier { get; set; } = "";
+ public string[] AllowedTools { get; set; } = Array.Empty();
+ public string[] DeniedTools { get; set; } = Array.Empty();
+ public Dictionary CustomRateLimits { get; set; } = new();
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+}
+```
+
+## Monitoring and Observability
+
+### Application Insights Integration
+
+```csharp
+// Services/TelemetryFilterWrapper.cs
+public class TelemetryFilterWrapper : IToolFilter
+{
+ private readonly IToolFilter _innerFilter;
+ private readonly TelemetryClient _telemetryClient;
+ private readonly ILogger _logger;
+
+ public TelemetryFilterWrapper(IToolFilter innerFilter, TelemetryClient telemetryClient, ILogger logger)
+ {
+ _innerFilter = innerFilter;
+ _telemetryClient = telemetryClient;
+ _logger = logger;
+ }
+
+ public int Priority => _innerFilter.Priority;
+
+ public async Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var filterName = _innerFilter.GetType().Name;
+
+ try
+ {
+ var result = await _innerFilter.ShouldIncludeToolAsync(tool, context, cancellationToken);
+
+ stopwatch.Stop();
+
+ _telemetryClient.TrackDependency("Filter", filterName, $"ShouldInclude:{tool.Name}",
+ DateTime.UtcNow.Subtract(stopwatch.Elapsed), stopwatch.Elapsed, result.ToString());
+
+ _telemetryClient.TrackMetric($"Filter.{filterName}.ShouldInclude.Duration", stopwatch.ElapsedMilliseconds);
+ _telemetryClient.TrackMetric($"Filter.{filterName}.ShouldInclude.{(result ? "Allow" : "Deny")}", 1);
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+
+ _telemetryClient.TrackException(ex, new Dictionary
+ {
+ ["FilterName"] = filterName,
+ ["ToolName"] = tool.Name,
+ ["Operation"] = "ShouldIncludeToolAsync"
+ });
+
+ throw;
+ }
+ }
+
+ public async Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var filterName = _innerFilter.GetType().Name;
+
+ try
+ {
+ var result = await _innerFilter.CanExecuteToolAsync(toolName, context, cancellationToken);
+
+ stopwatch.Stop();
+
+ _telemetryClient.TrackDependency("Filter", filterName, $"CanExecute:{toolName}",
+ DateTime.UtcNow.Subtract(stopwatch.Elapsed), stopwatch.Elapsed, result.IsAuthorized.ToString());
+
+ _telemetryClient.TrackMetric($"Filter.{filterName}.CanExecute.Duration", stopwatch.ElapsedMilliseconds);
+ _telemetryClient.TrackMetric($"Filter.{filterName}.CanExecute.{(result.IsAuthorized ? "Allow" : "Deny")}", 1);
+
+ if (!result.IsAuthorized)
+ {
+ _telemetryClient.TrackEvent("FilterDenied", new Dictionary
+ {
+ ["FilterName"] = filterName,
+ ["ToolName"] = toolName,
+ ["Reason"] = result.Reason,
+ ["UserId"] = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "Anonymous"
+ });
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+
+ _telemetryClient.TrackException(ex, new Dictionary
+ {
+ ["FilterName"] = filterName,
+ ["ToolName"] = toolName,
+ ["Operation"] = "CanExecuteToolAsync"
+ });
+
+ throw;
+ }
+ }
+}
+
+// Program.cs - Wrap filters with telemetry
+builder.Services.AddApplicationInsightsTelemetry();
+
+builder.Services.Decorate();
+```
+
+### Prometheus Metrics Integration
+
+```csharp
+// Services/MetricsCollectionService.cs
+public class MetricsCollectionService : IHostedService
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly Counter _filterExecutionCounter;
+ private readonly Histogram _filterExecutionDuration;
+ private readonly Gauge _activeFiltersGauge;
+
+ public MetricsCollectionService(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+
+ _filterExecutionCounter = Metrics.CreateCounter(
+ "filter_executions_total",
+ "Total number of filter executions",
+ new[] { "filter_name", "operation", "result" });
+
+ _filterExecutionDuration = Metrics.CreateHistogram(
+ "filter_execution_duration_seconds",
+ "Duration of filter executions",
+ new[] { "filter_name", "operation" });
+
+ _activeFiltersGauge = Metrics.CreateGauge(
+ "active_filters_count",
+ "Number of active filters");
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ // Initialize metrics collection
+ using var scope = _serviceProvider.CreateScope();
+ var filters = scope.ServiceProvider.GetServices();
+ _activeFiltersGauge.Set(filters.Count());
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public void RecordFilterExecution(string filterName, string operation, string result, double durationSeconds)
+ {
+ _filterExecutionCounter.WithLabels(filterName, operation, result).Inc();
+ _filterExecutionDuration.WithLabels(filterName, operation).Observe(durationSeconds);
+ }
+}
+
+// Program.cs - Add Prometheus
+builder.Services.AddSingleton();
+builder.Services.AddHostedService();
+
+// In the request pipeline
+app.UseMetricServer(); // Expose /metrics endpoint
+```
+
+## Custom Filter Development
+
+### Custom Business Logic Filter Example
+
+```csharp
+// Filters/GeographicRestrictionFilter.cs
+public class GeographicRestrictionFilter : IToolFilter
+{
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+ private readonly Dictionary _toolRegionMapping;
+
+ public GeographicRestrictionFilter(IConfiguration configuration, ILogger logger)
+ {
+ _configuration = configuration;
+ _logger = logger;
+
+ // Load region mappings from configuration
+ _toolRegionMapping = configuration.GetSection("GeographicRestrictions:ToolRegionMapping")
+ .Get>() ?? new Dictionary();
+ }
+
+ public int Priority => 125; // Between role-based and scope-based
+
+ public Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ // Geographic restrictions don't affect tool visibility
+ return Task.FromResult(true);
+ }
+
+ public async Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken = default)
+ {
+ var userRegion = GetUserRegion(context);
+ var allowedRegions = GetAllowedRegions(toolName);
+
+ if (allowedRegions.Length == 0)
+ {
+ // No geographic restrictions for this tool
+ return AuthorizationResult.Allow("No geographic restrictions");
+ }
+
+ if (string.IsNullOrEmpty(userRegion))
+ {
+ var reason = $"Tool '{toolName}' has geographic restrictions but user region could not be determined";
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Geographic",
+ ("realm", "mcp-api"),
+ ("tool_name", toolName),
+ ("allowed_regions", string.Join(",", allowedRegions)));
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ if (!allowedRegions.Contains(userRegion, StringComparer.OrdinalIgnoreCase))
+ {
+ var reason = $"Tool '{toolName}' is not available in region '{userRegion}'. Allowed regions: {string.Join(", ", allowedRegions)}";
+ var challenge = AuthorizationChallenge.CreateCustomChallenge(
+ "Geographic",
+ ("realm", "mcp-api"),
+ ("tool_name", toolName),
+ ("user_region", userRegion),
+ ("allowed_regions", string.Join(",", allowedRegions)));
+
+ _logger.LogWarning("Geographic restriction denied: {ToolName} for region {UserRegion}", toolName, userRegion);
+
+ return AuthorizationResult.DenyWithChallenge(reason, challenge);
+ }
+
+ return AuthorizationResult.Allow($"Tool '{toolName}' allowed in region '{userRegion}'");
+ }
+
+ private string? GetUserRegion(ToolAuthorizationContext context)
+ {
+ // Try to get region from claims
+ var regionClaim = context.User?.FindFirst("region")?.Value
+ ?? context.User?.FindFirst("geo_region")?.Value;
+
+ if (!string.IsNullOrEmpty(regionClaim))
+ return regionClaim;
+
+ // Could also determine region from IP address using a geolocation service
+ // This would require additional context data to be passed through
+
+ return context.AdditionalData?.TryGetValue("UserRegion", out var region) == true
+ ? region?.ToString()
+ : null;
+ }
+
+ private string[] GetAllowedRegions(string toolName)
+ {
+ foreach (var mapping in _toolRegionMapping)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value;
+ }
+ }
+
+ return Array.Empty();
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*") return true;
+ if (pattern.EndsWith("*"))
+ {
+ var prefix = pattern[..^1];
+ return toolName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
+ }
+ return string.Equals(pattern, toolName, StringComparison.OrdinalIgnoreCase);
+ }
+}
+
+// Configuration example
+// appsettings.json
+{
+ "GeographicRestrictions": {
+ "Enabled": true,
+ "ToolRegionMapping": {
+ "admin_*": ["US", "CA", "EU"],
+ "premium_financial_*": ["US", "UK", "EU"],
+ "compliance_*": ["US"]
+ }
+ }
+}
+
+// Register the filter
+builder.Services.AddSingleton();
+```
+
+This comprehensive integration guide shows how the Dynamic Tool Filtering system can be extended and integrated with real-world services and infrastructure. Each example demonstrates production-ready patterns and best practices for building scalable, secure MCP applications.
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Models/FilterResult.cs b/samples/DynamicToolFiltering/Models/FilterResult.cs
new file mode 100644
index 00000000..4bd773fa
--- /dev/null
+++ b/samples/DynamicToolFiltering/Models/FilterResult.cs
@@ -0,0 +1,135 @@
+namespace DynamicToolFiltering.Models;
+
+///
+/// Represents the result of a filter operation.
+///
+public class FilterResult
+{
+ ///
+ /// Gets or sets whether the filter passed.
+ ///
+ public bool Passed { get; set; }
+
+ ///
+ /// Gets or sets the filter name that produced this result.
+ ///
+ public string FilterName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the priority of the filter that produced this result.
+ ///
+ public int Priority { get; set; }
+
+ ///
+ /// Gets or sets the reason for the filter result.
+ ///
+ public string Reason { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets additional data from the filter.
+ ///
+ public Dictionary Data { get; set; } = new();
+
+ ///
+ /// Gets or sets the timestamp when the filter was evaluated.
+ ///
+ public DateTime EvaluatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Gets or sets the execution time of the filter in milliseconds.
+ ///
+ public double ExecutionTimeMs { get; set; }
+
+ ///
+ /// Creates a successful filter result.
+ ///
+ /// The name of the filter.
+ /// The reason for success.
+ /// The filter priority.
+ /// A successful filter result.
+ public static FilterResult Success(string filterName, string reason, int priority)
+ {
+ return new FilterResult
+ {
+ Passed = true,
+ FilterName = filterName,
+ Reason = reason,
+ Priority = priority
+ };
+ }
+
+ ///
+ /// Creates a failed filter result.
+ ///
+ /// The name of the filter.
+ /// The reason for failure.
+ /// The filter priority.
+ /// A failed filter result.
+ public static FilterResult Failure(string filterName, string reason, int priority)
+ {
+ return new FilterResult
+ {
+ Passed = false,
+ FilterName = filterName,
+ Reason = reason,
+ Priority = priority
+ };
+ }
+}
+
+///
+/// Represents a collection of filter results from multiple filters.
+///
+public class FilterResultCollection
+{
+ ///
+ /// Gets or sets the list of individual filter results.
+ ///
+ public List Results { get; set; } = new();
+
+ ///
+ /// Gets or sets the overall result (all filters must pass).
+ ///
+ public bool OverallResult => Results.All(r => r.Passed);
+
+ ///
+ /// Gets or sets the first failed filter result, if any.
+ ///
+ public FilterResult? FirstFailure => Results.FirstOrDefault(r => !r.Passed);
+
+ ///
+ /// Gets or sets the total execution time for all filters.
+ ///
+ public double TotalExecutionTimeMs => Results.Sum(r => r.ExecutionTimeMs);
+
+ ///
+ /// Gets or sets the number of filters that were evaluated.
+ ///
+ public int FilterCount => Results.Count;
+
+ ///
+ /// Adds a filter result to the collection.
+ ///
+ /// The filter result to add.
+ public void AddResult(FilterResult result)
+ {
+ Results.Add(result);
+ }
+
+ ///
+ /// Gets a summary of the filter results.
+ ///
+ /// A summary string of the filter results.
+ public string GetSummary()
+ {
+ if (OverallResult)
+ {
+ return $"All {FilterCount} filters passed in {TotalExecutionTimeMs:F2}ms";
+ }
+ else
+ {
+ var firstFailure = FirstFailure!;
+ return $"Filter '{firstFailure.FilterName}' failed: {firstFailure.Reason}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Models/ToolExecutionContext.cs b/samples/DynamicToolFiltering/Models/ToolExecutionContext.cs
new file mode 100644
index 00000000..5d690464
--- /dev/null
+++ b/samples/DynamicToolFiltering/Models/ToolExecutionContext.cs
@@ -0,0 +1,90 @@
+using System.Security.Claims;
+
+namespace DynamicToolFiltering.Models;
+
+///
+/// Represents the execution context for a tool call with relevant filtering information.
+///
+public class ToolExecutionContext
+{
+ ///
+ /// Gets or sets the name of the tool being executed.
+ ///
+ public string ToolName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the user executing the tool.
+ ///
+ public UserInfo? User { get; set; }
+
+ ///
+ /// Gets or sets the claims principal for the current user.
+ ///
+ public ClaimsPrincipal? ClaimsPrincipal { get; set; }
+
+ ///
+ /// Gets or sets the session ID for the current session.
+ ///
+ public string? SessionId { get; set; }
+
+ ///
+ /// Gets or sets the client information.
+ ///
+ public string? ClientInfo { get; set; }
+
+ ///
+ /// Gets or sets the IP address of the client.
+ ///
+ public string? ClientIpAddress { get; set; }
+
+ ///
+ /// Gets or sets the timestamp when the execution was requested.
+ ///
+ public DateTime RequestedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Gets or sets the tenant context if applicable.
+ ///
+ public TenantContext? TenantContext { get; set; }
+
+ ///
+ /// Gets or sets the execution environment.
+ ///
+ public string Environment { get; set; } = "Development";
+
+ ///
+ /// Gets or sets additional context data for filters.
+ ///
+ public Dictionary AdditionalData { get; set; } = new();
+}
+
+///
+/// Represents tenant context information.
+///
+public class TenantContext
+{
+ ///
+ /// Gets or sets the tenant identifier.
+ ///
+ public string TenantId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the tenant name.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets whether the tenant is active.
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Gets or sets the tenant's subscription tier.
+ ///
+ public string SubscriptionTier { get; set; } = "Basic";
+
+ ///
+ /// Gets or sets tenant-specific settings.
+ ///
+ public Dictionary Settings { get; set; } = new();
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Models/UsageStatistics.cs b/samples/DynamicToolFiltering/Models/UsageStatistics.cs
new file mode 100644
index 00000000..35efe025
--- /dev/null
+++ b/samples/DynamicToolFiltering/Models/UsageStatistics.cs
@@ -0,0 +1,168 @@
+namespace DynamicToolFiltering.Models;
+
+///
+/// Represents usage statistics for a user or tool.
+///
+public class UsageStatistics
+{
+ ///
+ /// Gets or sets the user ID.
+ ///
+ public string UserId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the total number of tool executions.
+ ///
+ public int TotalExecutions { get; set; }
+
+ ///
+ /// Gets or sets the number of successful executions.
+ ///
+ public int SuccessfulExecutions { get; set; }
+
+ ///
+ /// Gets or sets the number of failed executions.
+ ///
+ public int FailedExecutions { get; set; }
+
+ ///
+ /// Gets or sets the number of executions blocked by filters.
+ ///
+ public int BlockedExecutions { get; set; }
+
+ ///
+ /// Gets or sets per-tool usage counts.
+ ///
+ public Dictionary ToolUsageCounts { get; set; } = new();
+
+ ///
+ /// Gets or sets per-filter block counts.
+ ///
+ public Dictionary FilterBlockCounts { get; set; } = new();
+
+ ///
+ /// Gets or sets the first execution timestamp.
+ ///
+ public DateTime? FirstExecutionAt { get; set; }
+
+ ///
+ /// Gets or sets the last execution timestamp.
+ ///
+ public DateTime? LastExecutionAt { get; set; }
+
+ ///
+ /// Gets or sets the current quota usage.
+ ///
+ public int QuotaUsed { get; set; }
+
+ ///
+ /// Gets or sets the quota limit.
+ ///
+ public int QuotaLimit { get; set; }
+
+ ///
+ /// Gets or sets when the quota period resets.
+ ///
+ public DateTime? QuotaResetAt { get; set; }
+
+ ///
+ /// Gets the success rate as a percentage.
+ ///
+ public double SuccessRate => TotalExecutions > 0 ? (double)SuccessfulExecutions / TotalExecutions * 100 : 0;
+
+ ///
+ /// Gets the block rate as a percentage.
+ ///
+ public double BlockRate => TotalExecutions > 0 ? (double)BlockedExecutions / TotalExecutions * 100 : 0;
+
+ ///
+ /// Gets the remaining quota.
+ ///
+ public int RemainingQuota => Math.Max(0, QuotaLimit - QuotaUsed);
+
+ ///
+ /// Gets whether the quota is unlimited.
+ ///
+ public bool IsUnlimitedQuota => QuotaLimit == -1;
+}
+
+///
+/// Represents aggregated usage statistics across multiple users or time periods.
+///
+public class AggregatedUsageStatistics
+{
+ ///
+ /// Gets or sets the time period for these statistics.
+ ///
+ public TimeSpan Period { get; set; }
+
+ ///
+ /// Gets or sets the start time of the statistics period.
+ ///
+ public DateTime PeriodStart { get; set; }
+
+ ///
+ /// Gets or sets the end time of the statistics period.
+ ///
+ public DateTime PeriodEnd { get; set; }
+
+ ///
+ /// Gets or sets the total number of unique users.
+ ///
+ public int UniqueUsers { get; set; }
+
+ ///
+ /// Gets or sets the total number of tool executions.
+ ///
+ public int TotalExecutions { get; set; }
+
+ ///
+ /// Gets or sets the total number of successful executions.
+ ///
+ public int SuccessfulExecutions { get; set; }
+
+ ///
+ /// Gets or sets the total number of failed executions.
+ ///
+ public int FailedExecutions { get; set; }
+
+ ///
+ /// Gets or sets the total number of blocked executions.
+ ///
+ public int BlockedExecutions { get; set; }
+
+ ///
+ /// Gets or sets the most popular tools by execution count.
+ ///
+ public Dictionary PopularTools { get; set; } = new();
+
+ ///
+ /// Gets or sets the most active users by execution count.
+ ///
+ public Dictionary ActiveUsers { get; set; } = new();
+
+ ///
+ /// Gets or sets filter blocking statistics.
+ ///
+ public Dictionary FilterBlockStats { get; set; } = new();
+
+ ///
+ /// Gets or sets peak usage hours.
+ ///
+ public Dictionary HourlyUsage { get; set; } = new();
+
+ ///
+ /// Gets the overall success rate as a percentage.
+ ///
+ public double OverallSuccessRate => TotalExecutions > 0 ? (double)SuccessfulExecutions / TotalExecutions * 100 : 0;
+
+ ///
+ /// Gets the overall block rate as a percentage.
+ ///
+ public double OverallBlockRate => TotalExecutions > 0 ? (double)BlockedExecutions / TotalExecutions * 100 : 0;
+
+ ///
+ /// Gets the average executions per user.
+ ///
+ public double AverageExecutionsPerUser => UniqueUsers > 0 ? (double)TotalExecutions / UniqueUsers : 0;
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Models/UserInfo.cs b/samples/DynamicToolFiltering/Models/UserInfo.cs
new file mode 100644
index 00000000..3d667437
--- /dev/null
+++ b/samples/DynamicToolFiltering/Models/UserInfo.cs
@@ -0,0 +1,57 @@
+namespace DynamicToolFiltering.Models;
+
+///
+/// Represents user information for the filtering system.
+///
+public class UserInfo
+{
+ ///
+ /// Gets or sets the unique user identifier.
+ ///
+ public string UserId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the user's display name.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the user's email address.
+ ///
+ public string Email { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the user's primary role.
+ ///
+ public string Role { get; set; } = "guest";
+
+ ///
+ /// Gets or sets the list of scopes assigned to the user.
+ ///
+ public List Scopes { get; set; } = new();
+
+ ///
+ /// Gets or sets the tenant ID associated with the user.
+ ///
+ public string? TenantId { get; set; }
+
+ ///
+ /// Gets or sets whether the user is currently active.
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Gets or sets when the user was created.
+ ///
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Gets or sets when the user last authenticated.
+ ///
+ public DateTime? LastLoginAt { get; set; }
+
+ ///
+ /// Gets or sets custom user properties.
+ ///
+ public Dictionary Properties { get; set; } = new();
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Program.cs b/samples/DynamicToolFiltering/Program.cs
new file mode 100644
index 00000000..25a4087d
--- /dev/null
+++ b/samples/DynamicToolFiltering/Program.cs
@@ -0,0 +1,322 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using Serilog;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+
+using DynamicToolFiltering.Authorization.Filters;
+using DynamicToolFiltering.Configuration;
+using DynamicToolFiltering.Services;
+using DynamicToolFiltering.Tools;
+using ModelContextProtocol.Server.Authorization;
+
+// Configure Serilog for comprehensive logging
+Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .WriteTo.File("logs/dynamic-tool-filtering-.txt", rollingInterval: RollingInterval.Day)
+ .CreateLogger();
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Replace default logging with Serilog
+builder.Host.UseSerilog();
+
+// Configure filtering options from configuration
+builder.Services.Configure(builder.Configuration.GetSection(FilteringOptions.SectionName));
+
+// Register core services
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+// Configure multiple authentication schemes
+ConfigureAuthentication(builder);
+
+// Configure authorization and filtering
+ConfigureFiltering(builder);
+
+// Configure MCP server with tools
+builder.Services.AddMcpServer()
+ .WithHttpTransport()
+ .WithTools()
+ .WithTools()
+ .WithTools()
+ .WithTools();
+
+// Add telemetry for monitoring
+builder.Services.AddOpenTelemetry()
+ .WithTracing(b => b
+ .AddSource("*")
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation())
+ .WithMetrics(b => b
+ .AddMeter("*")
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation())
+ .WithLogging()
+ .UseOtlpExporter();
+
+// Add CORS for web clients
+builder.Services.AddCors(options =>
+{
+ options.AddDefaultPolicy(policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader()
+ .WithExposedHeaders("WWW-Authenticate");
+ });
+});
+
+var app = builder.Build();
+
+// Configure request pipeline
+if (app.Environment.IsDevelopment())
+{
+ app.UseDeveloperExceptionPage();
+}
+
+app.UseSerilogRequestLogging();
+app.UseCors();
+
+// Authentication must come before authorization
+app.UseAuthentication();
+app.UseAuthorization();
+
+// Map MCP endpoints
+app.MapMcp();
+
+// Add health check endpoint
+app.MapGet("/health", () => new {
+ Status = "healthy",
+ Timestamp = DateTime.UtcNow,
+ Environment = app.Environment.EnvironmentName,
+ Version = "1.0.0"
+});
+
+// Add filter management endpoints (for demo purposes)
+app.MapGet("/admin/filters/status", async (IServiceProvider services) =>
+{
+ var toolAuthService = services.GetRequiredService();
+
+ return new
+ {
+ Message = "Filter management endpoints would be implemented here",
+ Timestamp = DateTime.UtcNow,
+ FiltersRegistered = "Multiple filters active (see configuration)"
+ };
+}).RequireAuthorization("AdminPolicy");
+
+// Add feature flag management endpoints
+app.MapGet("/admin/feature-flags", async (IFeatureFlagService featureFlagService) =>
+{
+ var flags = await featureFlagService.GetAllFlagsAsync("admin");
+ return new { FeatureFlags = flags, Timestamp = DateTime.UtcNow };
+}).RequireAuthorization("AdminPolicy");
+
+app.MapPost("/admin/feature-flags/{flagName}", async (
+ string flagName,
+ bool enabled,
+ IFeatureFlagService featureFlagService) =>
+{
+ await featureFlagService.SetFlagAsync(flagName, enabled);
+ return new { FlagName = flagName, Enabled = enabled, UpdatedAt = DateTime.UtcNow };
+}).RequireAuthorization("AdminPolicy");
+
+Log.Information("Starting Dynamic Tool Filtering MCP Server on {Environment}", app.Environment.EnvironmentName);
+
+app.Run();
+
+static void ConfigureAuthentication(WebApplicationBuilder builder)
+{
+ var authBuilder = builder.Services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ });
+
+ // JWT Bearer authentication
+ authBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
+ {
+ var jwtSettings = builder.Configuration.GetSection("Jwt");
+ var secretKey = jwtSettings["SecretKey"] ?? "your-256-bit-secret-key-here-make-it-secure";
+ var issuer = jwtSettings["Issuer"] ?? "dynamic-tool-filtering";
+ var audience = jwtSettings["Audience"] ?? "mcp-api";
+
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = issuer,
+ ValidAudience = audience,
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
+ ClockSkew = TimeSpan.FromMinutes(5)
+ };
+
+ options.Events = new JwtBearerEvents
+ {
+ OnAuthenticationFailed = context =>
+ {
+ Log.Warning("JWT authentication failed: {Error}", context.Exception.Message);
+ return Task.CompletedTask;
+ },
+ OnTokenValidated = context =>
+ {
+ var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ Log.Debug("JWT token validated for user: {UserId}", userId);
+ return Task.CompletedTask;
+ }
+ };
+ });
+
+ // API Key authentication (custom scheme)
+ authBuilder.AddScheme(
+ "ApiKey", options =>
+ {
+ options.HeaderName = "X-API-Key";
+ options.QueryStringKey = "apikey";
+ });
+
+ // Configure authorization policies
+ builder.Services.AddAuthorization(options =>
+ {
+ options.AddPolicy("AdminPolicy", policy =>
+ {
+ policy.RequireAuthenticatedUser();
+ policy.RequireClaim(ClaimTypes.Role, "admin", "super_admin");
+ });
+
+ options.AddPolicy("PremiumPolicy", policy =>
+ {
+ policy.RequireAuthenticatedUser();
+ policy.RequireClaim(ClaimTypes.Role, "premium", "admin", "super_admin");
+ });
+
+ options.AddPolicy("UserPolicy", policy =>
+ {
+ policy.RequireAuthenticatedUser();
+ policy.RequireClaim(ClaimTypes.Role, "user", "premium", "admin", "super_admin");
+ });
+ });
+}
+
+static void ConfigureFiltering(WebApplicationBuilder builder)
+{
+ // Register the tool authorization service
+ builder.Services.AddSingleton();
+
+ // Register all filter implementations with proper ordering (priority-based)
+ builder.Services.AddSingleton(); // Priority 50 - highest
+ builder.Services.AddSingleton(); // Priority 75
+ builder.Services.AddSingleton(); // Priority 100
+ builder.Services.AddSingleton(); // Priority 150
+ builder.Services.AddSingleton(); // Priority 200
+ builder.Services.AddSingleton(); // Priority 300 - lowest
+
+ // Configure the tool authorization service with all filters
+ builder.Services.AddSingleton(serviceProvider =>
+ {
+ var authService = new ToolAuthorizationService();
+ var filters = serviceProvider.GetServices();
+
+ // Register filters in priority order
+ foreach (var filter in filters.OrderBy(f => f.Priority))
+ {
+ authService.RegisterFilter(filter);
+ Log.Information("Registered tool filter: {FilterType} with priority {Priority}",
+ filter.GetType().Name, filter.Priority);
+ }
+
+ return authService;
+ });
+}
+
+// Custom API Key authentication handler
+public class ApiKeyAuthenticationHandler : AuthenticationHandler
+{
+ private readonly ILogger _logger;
+
+ public ApiKeyAuthenticationHandler(
+ IOptionsMonitor options,
+ ILoggerFactory loggerFactory,
+ UrlEncoder encoder)
+ : base(options, loggerFactory, encoder)
+ {
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ protected override Task HandleAuthenticateAsync()
+ {
+ // Try to get API key from header
+ var apiKey = Request.Headers[Options.HeaderName].FirstOrDefault();
+
+ // If not in header, try query string
+ if (string.IsNullOrEmpty(apiKey))
+ {
+ apiKey = Request.Query[Options.QueryStringKey].FirstOrDefault();
+ }
+
+ if (string.IsNullOrEmpty(apiKey))
+ {
+ return Task.FromResult(AuthenticateResult.NoResult());
+ }
+
+ // Validate API key (in production, use secure storage and proper validation)
+ var validApiKeys = new Dictionary
+ {
+ { "demo-guest-key", ("guest-user", "guest", new[] { "basic:tools" }) },
+ { "demo-user-key", ("demo-user", "user", new[] { "user:tools", "read:tools", "basic:tools" }) },
+ { "demo-premium-key", ("premium-user", "premium", new[] { "premium:tools", "user:tools", "read:tools", "basic:tools" }) },
+ { "demo-admin-key", ("admin-user", "admin", new[] { "admin:tools", "premium:tools", "user:tools", "read:tools", "basic:tools" }) }
+ };
+
+ if (!validApiKeys.TryGetValue(apiKey, out var keyInfo))
+ {
+ _logger.LogWarning("Invalid API key attempted: {ApiKey}", apiKey[..Math.Min(8, apiKey.Length)] + "...");
+ return Task.FromResult(AuthenticateResult.Fail("Invalid API key"));
+ }
+
+ // Create claims for the authenticated user
+ var claims = new List
+ {
+ new(ClaimTypes.NameIdentifier, keyInfo.UserId),
+ new(ClaimTypes.Name, keyInfo.UserId),
+ new(ClaimTypes.Role, keyInfo.Role),
+ new(ClaimTypes.AuthenticationMethod, "ApiKey")
+ };
+
+ // Add scope claims
+ foreach (var scope in keyInfo.Scopes)
+ {
+ claims.Add(new Claim("scope", scope));
+ }
+
+ var identity = new ClaimsIdentity(claims, Scheme.Name);
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, Scheme.Name);
+
+ _logger.LogDebug("API key authentication successful for user: {UserId}, Role: {Role}", keyInfo.UserId, keyInfo.Role);
+
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+
+ protected override Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ Response.Headers.Add("WWW-Authenticate", $"ApiKey realm=\"mcp-api\", parameter=\"{Options.HeaderName}\"");
+ return base.HandleChallengeAsync(properties);
+ }
+}
+
+public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
+{
+ public string HeaderName { get; set; } = "X-API-Key";
+ public string QueryStringKey { get; set; } = "apikey";
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Properties/launchSettings.json b/samples/DynamicToolFiltering/Properties/launchSettings.json
new file mode 100644
index 00000000..2cf9ec42
--- /dev/null
+++ b/samples/DynamicToolFiltering/Properties/launchSettings.json
@@ -0,0 +1,151 @@
+{
+ "profiles": {
+ "DevelopmentMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "true",
+ "Filtering__TimeBased__Enabled": "false",
+ "Filtering__ScopeBased__Enabled": "true",
+ "Filtering__RateLimiting__Enabled": "true",
+ "Filtering__TenantIsolation__Enabled": "false",
+ "Filtering__BusinessLogic__Enabled": "true",
+ "Filtering__BusinessLogic__FeatureFlags__Enabled": "true",
+ "Filtering__BusinessLogic__QuotaManagement__Enabled": "false",
+ "Filtering__BusinessLogic__EnvironmentRestrictions__Enabled": "true"
+ }
+ },
+ "StagingMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Staging",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "true",
+ "Filtering__TimeBased__Enabled": "true",
+ "Filtering__ScopeBased__Enabled": "true",
+ "Filtering__RateLimiting__Enabled": "true",
+ "Filtering__TenantIsolation__Enabled": "true",
+ "Filtering__BusinessLogic__Enabled": "true",
+ "Filtering__BusinessLogic__FeatureFlags__Enabled": "true",
+ "Filtering__BusinessLogic__QuotaManagement__Enabled": "true",
+ "Filtering__BusinessLogic__EnvironmentRestrictions__Enabled": "true"
+ }
+ },
+ "ProductionMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Production",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "true",
+ "Filtering__TimeBased__Enabled": "true",
+ "Filtering__ScopeBased__Enabled": "true",
+ "Filtering__RateLimiting__Enabled": "true",
+ "Filtering__TenantIsolation__Enabled": "true",
+ "Filtering__BusinessLogic__Enabled": "true",
+ "Filtering__BusinessLogic__FeatureFlags__Enabled": "true",
+ "Filtering__BusinessLogic__QuotaManagement__Enabled": "true",
+ "Filtering__BusinessLogic__EnvironmentRestrictions__Enabled": "true"
+ }
+ },
+ "MinimalFilteringMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "true",
+ "Filtering__TimeBased__Enabled": "false",
+ "Filtering__ScopeBased__Enabled": "false",
+ "Filtering__RateLimiting__Enabled": "false",
+ "Filtering__TenantIsolation__Enabled": "false",
+ "Filtering__BusinessLogic__Enabled": "false"
+ }
+ },
+ "NoFilteringMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "false"
+ }
+ },
+ "TenantDemoMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "false",
+ "Filtering__TimeBased__Enabled": "false",
+ "Filtering__ScopeBased__Enabled": "false",
+ "Filtering__RateLimiting__Enabled": "false",
+ "Filtering__TenantIsolation__Enabled": "true",
+ "Filtering__BusinessLogic__Enabled": "false"
+ }
+ },
+ "RateLimitingDemoMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "true",
+ "Filtering__TimeBased__Enabled": "false",
+ "Filtering__ScopeBased__Enabled": "false",
+ "Filtering__RateLimiting__Enabled": "true",
+ "Filtering__RateLimiting__WindowMinutes": "1",
+ "Filtering__RateLimiting__RoleLimits__guest": "3",
+ "Filtering__RateLimiting__RoleLimits__user": "10",
+ "Filtering__RateLimiting__RoleLimits__premium": "25",
+ "Filtering__RateLimiting__RoleLimits__admin": "100",
+ "Filtering__TenantIsolation__Enabled": "false",
+ "Filtering__BusinessLogic__Enabled": "false"
+ }
+ },
+ "BusinessHoursDemoMode": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_URLS": "http://localhost:8080",
+ "Filtering__Enabled": "true",
+ "Filtering__RoleBased__Enabled": "false",
+ "Filtering__TimeBased__Enabled": "true",
+ "Filtering__TimeBased__BusinessHours__Enabled": "true",
+ "Filtering__TimeBased__BusinessHours__StartTime": "09:00",
+ "Filtering__TimeBased__BusinessHours__EndTime": "17:00",
+ "Filtering__ScopeBased__Enabled": "false",
+ "Filtering__RateLimiting__Enabled": "false",
+ "Filtering__TenantIsolation__Enabled": "false",
+ "Filtering__BusinessLogic__Enabled": "false"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/README.md b/samples/DynamicToolFiltering/README.md
new file mode 100644
index 00000000..e70438b7
--- /dev/null
+++ b/samples/DynamicToolFiltering/README.md
@@ -0,0 +1,670 @@
+# Dynamic Tool Filtering MCP Server Sample
+
+This comprehensive sample demonstrates advanced tool filtering and authorization capabilities in the MCP (Model Context Protocol) C# SDK. It showcases how to implement sophisticated access control systems with multiple filter types, authentication schemes, and business logic constraints.
+
+## Overview
+
+The Dynamic Tool Filtering sample illustrates real-world scenarios where different users need different levels of access to tools based on roles, time constraints, quotas, feature flags, and business rules. It's designed to be educational while demonstrating production-ready patterns.
+
+## Features
+
+### 🔐 Multiple Filter Types
+
+- **Role-Based Filtering**: Hierarchical role system (guest → user → premium → admin → super_admin)
+- **Time-Based Filtering**: Business hours restrictions and maintenance windows
+- **Scope-Based Filtering**: OAuth2-style scope checking for fine-grained permissions
+- **Rate Limiting**: Per-user and per-tool rate limits with sliding/fixed windows
+- **Tenant Isolation**: Multi-tenant tool access with tenant-specific configurations
+- **Business Logic Filtering**: Feature flags, quota management, and environment restrictions
+
+### 🛠️ Tool Categories
+
+The sample includes four categories of tools representing different security levels:
+
+1. **Public Tools** (`PublicTools.cs`): Available to all users without authentication
+2. **User Tools** (`UserTools.cs`): Require basic authentication and user role
+3. **Admin Tools** (`AdminTools.cs`): Require administrative privileges
+4. **Premium Tools** (`PremiumTools.cs`): Advanced functionality requiring premium access
+
+### 🔑 Authentication Methods
+
+- **JWT Bearer Tokens**: Standard OAuth2/OIDC authentication with claims
+- **API Key Authentication**: Simple header or query-based authentication
+- **Role-based Claims**: Hierarchical role system with inheritance
+- **Scope Claims**: Granular permission scopes for different operations
+
+## Architecture
+
+### Filter Priority System
+
+Filters execute in priority order (lowest number = highest priority):
+
+1. **Rate Limiting** (Priority 50): Enforces usage quotas first
+2. **Tenant Isolation** (Priority 75): Multi-tenant access control
+3. **Role-Based** (Priority 100): User role verification
+4. **Scope-Based** (Priority 150): OAuth2 scope checking
+5. **Time-Based** (Priority 200): Business hours and maintenance
+6. **Business Logic** (Priority 300): Feature flags and environment rules
+
+### Filter Flow Architecture
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ MCP Client │────│ Authentication │────│ Authorization │
+│ │ │ & Identity │ │ Filters │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+ │
+ ▼
+┌───────────────────────────────────────────────────────────────────┐
+│ FILTER CHAIN EXECUTION │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ 1. Rate Limiting Filter (Priority 50) │ │
+│ │ ├─ Check per-user rate limits │ │
+│ │ ├─ Validate time windows │ │
+│ │ └─ Record usage statistics │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ 2. Tenant Isolation Filter (Priority 75) │ │
+│ │ ├─ Validate tenant status │ │
+│ │ ├─ Check tenant tool allowlist │ │
+│ │ └─ Apply tenant-specific rate limits │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ 3. Role-Based Filter (Priority 100) │ │
+│ │ ├─ Extract user roles from claims │ │
+│ │ ├─ Check hierarchical permissions │ │
+│ │ └─ Validate tool access patterns │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ 4. Scope-Based Filter (Priority 150) │ │
+│ │ ├─ Parse OAuth2 scopes │ │
+│ │ ├─ Match required tool scopes │ │
+│ │ └─ Validate scope hierarchy │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ 5. Time-Based Filter (Priority 200) │ │
+│ │ ├─ Check business hours │ │
+│ │ ├─ Validate maintenance windows │ │
+│ │ └─ Apply timezone calculations │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ 6. Business Logic Filter (Priority 300) │ │
+│ │ ├─ Check feature flags │ │
+│ │ ├─ Validate quotas │ │
+│ │ └─ Apply environment restrictions │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+└───────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+ ┌─────────────────┐
+ │ Tool Execution │
+ │ or Rejection │
+ └─────────────────┘
+```
+
+### Component Interaction Diagram
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ MCP SERVER │
+│ │
+│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
+│ │ Public Tools │ │ User Tools │ │ Premium Tools │ │
+│ │ │ │ │ │ │ │
+│ │ • echo │ │ • get_profile │ │ • secure_random │ │
+│ │ • system_info │ │ • hash_calc │ │ • text_analysis │ │
+│ │ • utc_time │ │ • uuid_gen │ │ • password_gen │ │
+│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
+│ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ TOOL AUTHORIZATION SERVICE │ │
+│ │ │ │
+│ │ • Filter registration & management │ │
+│ │ • Priority-based execution │ │
+│ │ • Result aggregation & challenge generation │ │
+│ │ • Context enrichment │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+│ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────┐ │
+│ │ EXTERNAL SERVICES │ │
+│ │ │ │
+│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
+│ │ │ Rate Limiting │ │ Feature Flags │ │ Quota Service │ │ │
+│ │ │ Service │ │ Service │ │ │ │ │
+│ │ │ │ │ │ │ │ │ │
+│ │ │ • Usage cache │ │ • Flag state │ │ • Usage track │ │ │
+│ │ │ • Time windows│ │ • A/B testing │ │ • Limits mgmt │ │ │
+│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
+│ └─────────────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+### Project Structure
+
+```
+DynamicToolFiltering/
+├── Authorization/
+│ └── Filters/ # Filter implementations
+│ ├── BusinessLogicFilter.cs
+│ ├── RateLimitingToolFilter.cs
+│ ├── RoleBasedToolFilter.cs
+│ ├── ScopeBasedToolFilter.cs
+│ ├── TenantIsolationFilter.cs
+│ └── TimeBasedToolFilter.cs
+├── Configuration/ # Configuration models
+│ └── FilteringOptions.cs
+├── Models/ # Data models
+│ ├── FilterResult.cs
+│ ├── ToolExecutionContext.cs
+│ ├── UsageStatistics.cs
+│ └── UserInfo.cs
+├── Services/ # Supporting services
+│ ├── IFeatureFlagService.cs
+│ ├── IQuotaService.cs
+│ ├── IRateLimitingService.cs
+│ ├── InMemoryFeatureFlagService.cs
+│ ├── InMemoryQuotaService.cs
+│ └── InMemoryRateLimitingService.cs
+├── Tools/ # Tool implementations
+│ ├── AdminTools.cs
+│ ├── PremiumTools.cs
+│ ├── PublicTools.cs
+│ └── UserTools.cs
+├── Properties/ # Launch profiles
+│ └── launchSettings.json
+├── docs/ # Enhanced documentation
+│ ├── ARCHITECTURE.md
+│ ├── DEPLOYMENT.md
+│ ├── PERFORMANCE.md
+│ └── TROUBLESHOOTING.md
+├── scripts/ # Automation scripts
+│ ├── test-all.sh
+│ ├── test-all.ps1
+│ └── setup-dev.sh
+├── .vscode/ # VS Code configuration
+│ ├── launch.json
+│ ├── settings.json
+│ └── tasks.json
+├── appsettings.*.json # Configuration files
+├── Dockerfile # Docker configuration
+├── docker-compose.yml # Multi-service setup
+├── Program.cs
+├── README.md
+├── TESTING_GUIDE.md
+└── INTEGRATION_EXAMPLES.md
+```
+
+## Quick Start
+
+### 1. Prerequisites
+
+- .NET 9.0 SDK or later
+- Git (for cloning the repository)
+- curl or Postman (for testing)
+- Optional: Docker Desktop (for containerized deployment)
+- Optional: Visual Studio Code with C# extension
+
+### 2. One-Line Setup
+
+```bash
+# Clone, build, and run in development mode
+git clone https://github.com/microsoft/mcp-csharp-sdk.git && \
+cd mcp-csharp-sdk/samples/DynamicToolFiltering && \
+dotnet run --launch-profile DevelopmentMode
+```
+
+### 3. Alternative Setup Methods
+
+#### Option A: Manual Setup
+
+```bash
+# Navigate to the sample directory
+cd samples/DynamicToolFiltering
+
+# Restore dependencies
+dotnet restore
+
+# Build the project
+dotnet build
+
+# Run with development profile
+dotnet run --launch-profile DevelopmentMode
+```
+
+#### Option B: Docker Setup
+
+```bash
+# Build Docker image
+docker build -t dynamic-tool-filtering .
+
+# Run container
+docker run -p 8080:8080 dynamic-tool-filtering
+```
+
+#### Option C: Development Environment Setup
+
+```bash
+# Run the setup script (creates .vscode config, installs tools)
+./scripts/setup-dev.sh
+
+# Open in VS Code with debugging ready
+code .
+```
+
+### 4. Verify Installation
+
+```bash
+# Check server health
+curl http://localhost:8080/health
+
+# Expected response:
+# {
+# "Status": "healthy",
+# "Timestamp": "2024-01-01T12:00:00.000Z",
+# "Environment": "Development",
+# "Version": "1.0.0"
+# }
+```
+
+### 5. Quick API Test Suite
+
+The sample includes predefined API keys for testing different user roles:
+
+| Role | API Key | Available Tools | Rate Limit |
+|------|---------|-----------------|------------|
+| Guest | `demo-guest-key` | Public tools only | 20/hour |
+| User | `demo-user-key` | Public + User tools | 100/hour |
+| Premium | `demo-premium-key` | Public + User + Premium tools | 500/hour |
+| Admin | `demo-admin-key` | All tools | 1000/hour |
+
+#### Test Tool Visibility by Role
+
+```bash
+# Guest user - should see only basic tools
+curl -H "X-API-Key: demo-guest-key" \
+ http://localhost:8080/mcp/v1/tools
+
+# User role - should see user-level tools
+curl -H "X-API-Key: demo-user-key" \
+ http://localhost:8080/mcp/v1/tools
+
+# Premium user - should see premium tools
+curl -H "X-API-Key: demo-premium-key" \
+ http://localhost:8080/mcp/v1/tools
+
+# Admin user - should see all tools
+curl -H "X-API-Key: demo-admin-key" \
+ http://localhost:8080/mcp/v1/tools
+```
+
+#### Test Tool Execution
+
+```bash
+# Test 1: Public tool (no authentication required)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "echo",
+ "arguments": {
+ "message": "Hello Dynamic Filtering!"
+ }
+ }'
+
+# Test 2: User tool (requires authentication)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "get_user_profile",
+ "arguments": {}
+ }'
+
+# Test 3: Premium tool (requires premium role)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-premium-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_generate_secure_random",
+ "arguments": {
+ "byteCount": 32,
+ "format": "hex"
+ }
+ }'
+
+# Test 4: Admin tool (requires admin role)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-admin-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_get_system_diagnostics",
+ "arguments": {}
+ }'
+
+# Test 5: Authorization failure (user trying admin tool)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_get_system_diagnostics",
+ "arguments": {}
+ }'
+# Expected: HTTP 401 with authorization error
+```
+
+### 6. Run Automated Test Suite
+
+```bash
+# Run comprehensive test suite (bash)
+./scripts/test-all.sh
+
+# Or PowerShell version
+.\scripts\test-all.ps1
+
+# Run specific test categories
+./scripts/test-all.sh --category authentication
+./scripts/test-all.sh --category rate-limiting
+./scripts/test-all.sh --category performance
+```
+
+## Launch Profiles
+
+The sample includes multiple launch profiles for different scenarios:
+
+### Development Profiles
+
+- **DevelopmentMode**: Basic filtering with relaxed rate limits
+- **NoFilteringMode**: All filtering disabled for testing
+- **MinimalFilteringMode**: Only role-based filtering enabled
+
+### Feature-Specific Profiles
+
+- **TenantDemoMode**: Demonstrates multi-tenant access control
+- **RateLimitingDemoMode**: Shows rate limiting with strict limits (1-minute windows)
+- **BusinessHoursDemoMode**: Time-based restrictions (9 AM - 5 PM weekdays)
+
+### Environment Profiles
+
+- **StagingMode**: All filters enabled with moderate settings
+- **ProductionMode**: Strict security with all filters active
+
+## Configuration
+
+### Environment Variables
+
+Override any configuration using environment variables:
+
+```bash
+# Enable/disable specific filters
+export Filtering__RoleBased__Enabled=true
+export Filtering__RateLimiting__Enabled=true
+export Filtering__TimeBased__Enabled=false
+
+# Customize rate limits
+export Filtering__RateLimiting__WindowMinutes=60
+export Filtering__RateLimiting__RoleLimits__user=100
+
+# Business hours (UTC times)
+export Filtering__TimeBased__BusinessHours__StartTime="09:00"
+export Filtering__TimeBased__BusinessHours__EndTime="17:00"
+```
+
+### JWT Configuration
+
+For JWT authentication, configure the following:
+
+```json
+{
+ "Jwt": {
+ "SecretKey": "your-256-bit-secret-key",
+ "Issuer": "your-issuer",
+ "Audience": "your-audience",
+ "ExpirationMinutes": 60
+ }
+}
+```
+
+## Filter Implementations
+
+### Role-Based Filter
+
+Implements hierarchical role checking with pattern matching:
+
+- Supports glob patterns for tool names (`admin_*`, `premium_*`)
+- Hierarchical inheritance (admin inherits user permissions)
+- Configurable role mappings
+
+### Rate Limiting Filter
+
+Implements quota management:
+
+- Per-user and per-tool rate limits
+- Sliding or fixed time windows
+- Role-based default limits with tool-specific overrides
+- Automatic cleanup of old usage records
+
+### Scope-Based Filter
+
+OAuth2-style scope checking:
+
+- Space-separated scopes in JWT claims
+- Hierarchical scope inheritance
+- Wildcard scope matching
+- Proper OAuth2 error responses
+
+### Time-Based Filter
+
+Business hours and maintenance windows:
+
+- Configurable business hours per timezone
+- Maintenance window blocking
+- Tool-specific time restrictions
+
+### Tenant Isolation Filter
+
+Multi-tenant access control:
+
+- Tenant-specific tool allowlists/denylists
+- Custom rate limits per tenant
+- Tenant activation status checking
+
+### Business Logic Filter
+
+Advanced business rules:
+
+- Feature flag integration
+- Quota management with periodic resets
+- Environment-specific restrictions (dev/staging/prod)
+
+## Error Handling
+
+The sample demonstrates proper HTTP error responses with WWW-Authenticate headers:
+
+### 401 Unauthorized Responses
+
+```http
+HTTP/1.1 401 Unauthorized
+WWW-Authenticate: Bearer realm="mcp-api", scope="admin:tools", error="insufficient_scope"
+Content-Type: application/json
+
+{
+ "error": {
+ "code": -32002,
+ "message": "Access denied for tool 'admin_tool': Insufficient scope",
+ "data": {
+ "ToolName": "admin_tool",
+ "Reason": "Insufficient scope",
+ "HttpStatusCode": 401,
+ "RequiresAuthentication": true
+ }
+ }
+}
+```
+
+### Custom Challenge Headers
+
+Different filter types generate appropriate challenge headers:
+
+- **Bearer**: OAuth2 Bearer token challenges with scope information
+- **Basic**: Basic authentication challenges
+- **ApiKey**: Custom API key challenges
+- **Role**: Role-based access challenges
+- **Tenant**: Tenant-specific challenges
+
+## Production Considerations
+
+### Security
+
+- Use secure JWT secret keys (256-bit minimum)
+- Implement proper token storage and rotation
+- Use HTTPS in production
+- Consider rate limiting at the infrastructure level
+- Implement proper audit logging
+
+### Performance
+
+- Use distributed caches (Redis) for rate limiting in production
+- Implement proper database storage for quotas and usage tracking
+- Consider caching authorization decisions
+- Monitor filter performance and adjust priorities
+
+### Scalability
+
+- Use external feature flag services (LaunchDarkly, Azure App Configuration)
+- Implement distributed quota management
+- Consider eventual consistency for rate limiting
+- Use proper database indexing for usage queries
+
+## Advanced Usage
+
+### Custom Filter Implementation
+
+Create custom filters by implementing `IToolFilter`:
+
+```csharp
+public class CustomBusinessFilter : IToolFilter
+{
+ public int Priority => 250; // Between time-based and business logic
+
+ public async Task ShouldIncludeToolAsync(Tool tool, ToolAuthorizationContext context, CancellationToken cancellationToken)
+ {
+ // Custom visibility logic
+ return true;
+ }
+
+ public async Task CanExecuteToolAsync(string toolName, ToolAuthorizationContext context, CancellationToken cancellationToken)
+ {
+ // Custom authorization logic
+ return AuthorizationResult.Allow("Custom filter passed");
+ }
+}
+```
+
+### Dynamic Filter Registration
+
+Filters can be registered and unregistered at runtime:
+
+```csharp
+var authService = serviceProvider.GetRequiredService();
+var customFilter = new CustomBusinessFilter();
+authService.RegisterFilter(customFilter);
+```
+
+### Integration with External Services
+
+The sample demonstrates integration patterns for:
+
+- Feature flag services
+- Rate limiting backends
+- Quota management systems
+- Tenant management APIs
+
+## Testing
+
+The sample includes comprehensive testing scenarios:
+
+1. **Authentication Testing**: Test different API keys and JWT tokens
+2. **Authorization Testing**: Verify role and scope-based access
+3. **Rate Limiting Testing**: Exceed limits and verify blocking
+4. **Time-Based Testing**: Test business hours restrictions
+5. **Feature Flag Testing**: Toggle features and verify access
+6. **Error Handling Testing**: Verify proper error responses
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Tools not visible**: Check filter configurations and user roles
+2. **Rate limit errors**: Verify rate limiting settings and time windows
+3. **Authentication failures**: Check API keys and JWT configuration
+4. **Time-based restrictions**: Verify timezone settings and business hours
+
+### Debugging
+
+Enable debug logging to see filter execution:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "DynamicToolFiltering": "Debug"
+ }
+ }
+}
+```
+
+### Health Checks
+
+Use the health endpoint to verify service status:
+
+```bash
+curl http://localhost:8080/health
+```
+
+## Learning Objectives
+
+This sample demonstrates:
+
+1. **Filter Architecture**: How to design and implement filter chains
+2. **Authorization Patterns**: Multiple authentication and authorization strategies
+3. **Configuration Management**: Environment-specific configurations
+4. **Error Handling**: Proper HTTP error responses and challenges
+5. **Performance Considerations**: Efficient filter design and caching
+6. **Security Best Practices**: Secure authentication and authorization
+7. **Testing Strategies**: Comprehensive testing of authorization systems
+
+## Next Steps
+
+- Implement persistent storage for rate limiting and quotas
+- Add integration with external identity providers
+- Implement audit logging for all authorization decisions
+- Add metrics and monitoring for filter performance
+- Create admin APIs for dynamic filter management
+- Implement filter testing framework
+
+## Contributing
+
+This sample is designed to be educational and demonstrative. Feel free to:
+
+- Extend with additional filter types
+- Add integration with real external services
+- Improve error handling and edge cases
+- Add more comprehensive testing
+- Enhance documentation and examples
+
+## License
+
+This sample is part of the MCP C# SDK and follows the same license terms.
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Services/IFeatureFlagService.cs b/samples/DynamicToolFiltering/Services/IFeatureFlagService.cs
new file mode 100644
index 00000000..b045d29c
--- /dev/null
+++ b/samples/DynamicToolFiltering/Services/IFeatureFlagService.cs
@@ -0,0 +1,41 @@
+namespace DynamicToolFiltering.Services;
+
+///
+/// Service for managing feature flags.
+///
+public interface IFeatureFlagService
+{
+ ///
+ /// Checks if a feature flag is enabled for a specific user.
+ ///
+ /// The feature flag name.
+ /// The user ID.
+ /// Cancellation token.
+ /// True if the feature is enabled, false otherwise.
+ Task IsEnabledAsync(string flagName, string userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets all feature flags and their states for a user.
+ ///
+ /// The user ID.
+ /// Cancellation token.
+ /// Dictionary of feature flag names and their enabled states.
+ Task> GetAllFlagsAsync(string userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets the state of a feature flag (for testing/admin purposes).
+ ///
+ /// The feature flag name.
+ /// Whether the flag should be enabled.
+ /// Optional user ID for user-specific flags.
+ /// Cancellation token.
+ Task SetFlagAsync(string flagName, bool enabled, string? userId = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the rollout percentage for a feature flag.
+ ///
+ /// The feature flag name.
+ /// Cancellation token.
+ /// The rollout percentage (0-100).
+ Task GetRolloutPercentageAsync(string flagName, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Services/IQuotaService.cs b/samples/DynamicToolFiltering/Services/IQuotaService.cs
new file mode 100644
index 00000000..14ba06a8
--- /dev/null
+++ b/samples/DynamicToolFiltering/Services/IQuotaService.cs
@@ -0,0 +1,75 @@
+namespace DynamicToolFiltering.Services;
+
+///
+/// Service for managing user quotas and usage tracking.
+///
+public interface IQuotaService
+{
+ ///
+ /// Checks if a user has available quota for a specific tool.
+ ///
+ /// The user ID.
+ /// The user role.
+ /// The tool name.
+ /// Cancellation token.
+ /// True if quota is available, false otherwise.
+ Task HasAvailableQuotaAsync(string userId, string userRole, string toolName, CancellationToken cancellationToken = default);
+
+ ///
+ /// Consumes quota for a tool usage.
+ ///
+ /// The user ID.
+ /// The tool name.
+ /// The quota cost to consume.
+ /// Cancellation token.
+ Task ConsumeQuotaAsync(string userId, string toolName, int cost, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the current quota usage for a user.
+ ///
+ /// The user ID.
+ /// Cancellation token.
+ /// The current usage amount.
+ Task GetCurrentUsageAsync(string userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the quota limit for a user based on their role.
+ ///
+ /// The user ID.
+ /// The user role.
+ /// Cancellation token.
+ /// The quota limit (-1 for unlimited).
+ Task GetQuotaLimitAsync(string userId, string userRole, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the remaining quota for a user.
+ ///
+ /// The user ID.
+ /// The user role.
+ /// Cancellation token.
+ /// The remaining quota amount.
+ Task GetRemainingQuotaAsync(string userId, string userRole, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the date when the user's quota will reset.
+ ///
+ /// The user ID.
+ /// Cancellation token.
+ /// The quota reset date.
+ Task GetQuotaResetDateAsync(string userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resets quota for a user (for admin purposes or period rollover).
+ ///
+ /// The user ID.
+ /// Cancellation token.
+ Task ResetQuotaAsync(string userId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets detailed quota usage breakdown by tool for a user.
+ ///
+ /// The user ID.
+ /// Cancellation token.
+ /// Dictionary of tool names and their usage amounts.
+ Task> GetUsageBreakdownAsync(string userId, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Services/IRateLimitingService.cs b/samples/DynamicToolFiltering/Services/IRateLimitingService.cs
new file mode 100644
index 00000000..b2a83fa3
--- /dev/null
+++ b/samples/DynamicToolFiltering/Services/IRateLimitingService.cs
@@ -0,0 +1,40 @@
+namespace DynamicToolFiltering.Services;
+
+///
+/// Service for managing rate limiting and usage tracking.
+///
+public interface IRateLimitingService
+{
+ ///
+ /// Gets the current usage count for a user/tool combination within the specified time window.
+ ///
+ /// The user ID.
+ /// The tool name.
+ /// The start of the time window.
+ /// Cancellation token.
+ /// The current usage count.
+ Task GetUsageCountAsync(string userId, string toolName, DateTime windowStart, CancellationToken cancellationToken = default);
+
+ ///
+ /// Records a tool usage for rate limiting tracking.
+ ///
+ /// The user ID.
+ /// The tool name.
+ /// The timestamp of the usage.
+ /// Cancellation token.
+ Task RecordUsageAsync(string userId, string toolName, DateTime timestamp, CancellationToken cancellationToken = default);
+
+ ///
+ /// Cleans up old usage records that are outside the retention window.
+ ///
+ /// Cancellation token.
+ Task CleanupOldRecordsAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets usage statistics for a user.
+ ///
+ /// The user ID.
+ /// Cancellation token.
+ /// Usage statistics.
+ Task> GetUsageStatisticsAsync(string userId, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Services/InMemoryFeatureFlagService.cs b/samples/DynamicToolFiltering/Services/InMemoryFeatureFlagService.cs
new file mode 100644
index 00000000..a9594e1c
--- /dev/null
+++ b/samples/DynamicToolFiltering/Services/InMemoryFeatureFlagService.cs
@@ -0,0 +1,164 @@
+using System.Collections.Concurrent;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace DynamicToolFiltering.Services;
+
+///
+/// In-memory implementation of feature flag service with percentage rollout support.
+/// Note: This is for demonstration purposes. In production, use a dedicated feature flag service.
+///
+public class InMemoryFeatureFlagService : IFeatureFlagService
+{
+ private readonly ConcurrentDictionary _flags = new();
+ private readonly ILogger _logger;
+
+ public InMemoryFeatureFlagService(ILogger logger)
+ {
+ _logger = logger;
+ InitializeDefaultFlags();
+ }
+
+ public Task IsEnabledAsync(string flagName, string userId, CancellationToken cancellationToken = default)
+ {
+ if (!_flags.TryGetValue(flagName, out var flag))
+ {
+ _logger.LogDebug("Feature flag {FlagName} not found, returning false", flagName);
+ return Task.FromResult(false);
+ }
+
+ // Check if globally enabled/disabled
+ if (!flag.Enabled)
+ {
+ return Task.FromResult(false);
+ }
+
+ // Check user-specific overrides
+ if (flag.UserOverrides.TryGetValue(userId, out var userOverride))
+ {
+ _logger.LogDebug("Feature flag {FlagName} has user override for {UserId}: {Enabled}", flagName, userId, userOverride);
+ return Task.FromResult(userOverride);
+ }
+
+ // Check percentage rollout
+ if (flag.RolloutPercentage < 100)
+ {
+ var userHash = GetUserHash(userId, flagName);
+ var enabled = userHash < flag.RolloutPercentage;
+ _logger.LogDebug("Feature flag {FlagName} percentage rollout for {UserId}: {Percentage}% -> {Enabled}",
+ flagName, userId, flag.RolloutPercentage, enabled);
+ return Task.FromResult(enabled);
+ }
+
+ return Task.FromResult(true);
+ }
+
+ public Task> GetAllFlagsAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var result = new Dictionary();
+
+ foreach (var kvp in _flags)
+ {
+ var flagName = kvp.Key;
+ var isEnabled = IsEnabledAsync(flagName, userId, cancellationToken).Result;
+ result[flagName] = isEnabled;
+ }
+
+ return Task.FromResult(result);
+ }
+
+ public Task SetFlagAsync(string flagName, bool enabled, string? userId = null, CancellationToken cancellationToken = default)
+ {
+ _flags.AddOrUpdate(flagName,
+ new FeatureFlag { Name = flagName, Enabled = enabled },
+ (_, existingFlag) =>
+ {
+ if (userId != null)
+ {
+ existingFlag.UserOverrides[userId] = enabled;
+ _logger.LogInformation("Set user override for feature flag {FlagName}, User: {UserId}, Enabled: {Enabled}",
+ flagName, userId, enabled);
+ }
+ else
+ {
+ existingFlag.Enabled = enabled;
+ _logger.LogInformation("Set global state for feature flag {FlagName}, Enabled: {Enabled}", flagName, enabled);
+ }
+ return existingFlag;
+ });
+
+ return Task.CompletedTask;
+ }
+
+ public Task GetRolloutPercentageAsync(string flagName, CancellationToken cancellationToken = default)
+ {
+ if (_flags.TryGetValue(flagName, out var flag))
+ {
+ return Task.FromResult(flag.RolloutPercentage);
+ }
+
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// Sets the rollout percentage for a feature flag.
+ ///
+ public Task SetRolloutPercentageAsync(string flagName, int percentage, CancellationToken cancellationToken = default)
+ {
+ if (percentage < 0 || percentage > 100)
+ {
+ throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be between 0 and 100");
+ }
+
+ _flags.AddOrUpdate(flagName,
+ new FeatureFlag { Name = flagName, Enabled = true, RolloutPercentage = percentage },
+ (_, existingFlag) =>
+ {
+ existingFlag.RolloutPercentage = percentage;
+ return existingFlag;
+ });
+
+ _logger.LogInformation("Set rollout percentage for feature flag {FlagName}: {Percentage}%", flagName, percentage);
+
+ return Task.CompletedTask;
+ }
+
+ private void InitializeDefaultFlags()
+ {
+ // Initialize some example feature flags
+ var defaultFlags = new[]
+ {
+ new FeatureFlag { Name = "premium_features", Enabled = true, RolloutPercentage = 50 },
+ new FeatureFlag { Name = "admin_performance_tools", Enabled = true, RolloutPercentage = 25 },
+ new FeatureFlag { Name = "experimental_tools", Enabled = false, RolloutPercentage = 5 },
+ new FeatureFlag { Name = "beta_features", Enabled = true, RolloutPercentage = 75 }
+ };
+
+ foreach (var flag in defaultFlags)
+ {
+ _flags.TryAdd(flag.Name, flag);
+ }
+
+ _logger.LogInformation("Initialized {Count} default feature flags", defaultFlags.Length);
+ }
+
+ private static int GetUserHash(string userId, string flagName)
+ {
+ // Create a consistent hash for the user/flag combination
+ // This ensures the same user always gets the same result for a flag
+ var input = $"{userId}:{flagName}";
+ var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
+
+ // Convert first 4 bytes to int and get percentage (0-99)
+ var hashInt = BitConverter.ToInt32(hash, 0);
+ return Math.Abs(hashInt) % 100;
+ }
+
+ private class FeatureFlag
+ {
+ public string Name { get; set; } = "";
+ public bool Enabled { get; set; } = true;
+ public int RolloutPercentage { get; set; } = 100;
+ public ConcurrentDictionary UserOverrides { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Services/InMemoryQuotaService.cs b/samples/DynamicToolFiltering/Services/InMemoryQuotaService.cs
new file mode 100644
index 00000000..852d41dd
--- /dev/null
+++ b/samples/DynamicToolFiltering/Services/InMemoryQuotaService.cs
@@ -0,0 +1,258 @@
+using System.Collections.Concurrent;
+using DynamicToolFiltering.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace DynamicToolFiltering.Services;
+
+///
+/// In-memory implementation of quota service with period-based quotas.
+/// Note: This is for demonstration purposes. In production, use a persistent store.
+///
+public class InMemoryQuotaService : IQuotaService
+{
+ private readonly QuotaManagementOptions _options;
+ private readonly ConcurrentDictionary _userQuotas = new();
+ private readonly ILogger _logger;
+ private readonly Timer _resetTimer;
+
+ public InMemoryQuotaService(IOptions options, ILogger logger)
+ {
+ _options = options.Value.BusinessLogic.QuotaManagement;
+ _logger = logger;
+
+ // Check for quota resets daily
+ _resetTimer = new Timer(async _ => await ProcessQuotaResetsAsync(), null, TimeSpan.FromHours(1), TimeSpan.FromHours(1));
+ }
+
+ public Task HasAvailableQuotaAsync(string userId, string userRole, string toolName, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(true);
+ }
+
+ var quotaLimit = GetQuotaLimitForRole(userRole);
+ if (quotaLimit == -1)
+ {
+ return Task.FromResult(true); // Unlimited
+ }
+
+ var userQuota = GetOrCreateUserQuota(userId);
+ var quotaCost = GetQuotaCost(toolName);
+
+ lock (userQuota)
+ {
+ var hasQuota = userQuota.CurrentUsage + quotaCost <= quotaLimit;
+ return Task.FromResult(hasQuota);
+ }
+ }
+
+ public Task ConsumeQuotaAsync(string userId, string toolName, int cost, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.CompletedTask;
+ }
+
+ var userQuota = GetOrCreateUserQuota(userId);
+
+ lock (userQuota)
+ {
+ userQuota.CurrentUsage += cost;
+ userQuota.LastUsage = DateTime.UtcNow;
+
+ // Track tool-specific usage
+ userQuota.ToolUsage.TryGetValue(toolName, out var currentToolUsage);
+ userQuota.ToolUsage[toolName] = currentToolUsage + cost;
+
+ _logger.LogDebug("Consumed {Cost} quota for user {UserId}, tool {ToolName}. Total usage: {Usage}",
+ cost, userId, toolName, userQuota.CurrentUsage);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task GetCurrentUsageAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(0);
+ }
+
+ var userQuota = GetOrCreateUserQuota(userId);
+
+ lock (userQuota)
+ {
+ return Task.FromResult(userQuota.CurrentUsage);
+ }
+ }
+
+ public Task GetQuotaLimitAsync(string userId, string userRole, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(-1); // Unlimited when disabled
+ }
+
+ return Task.FromResult(GetQuotaLimitForRole(userRole));
+ }
+
+ public Task GetRemainingQuotaAsync(string userId, string userRole, CancellationToken cancellationToken = default)
+ {
+ if (!_options.Enabled)
+ {
+ return Task.FromResult(-1); // Unlimited when disabled
+ }
+
+ var quotaLimit = GetQuotaLimitForRole(userRole);
+ if (quotaLimit == -1)
+ {
+ return Task.FromResult(-1); // Unlimited
+ }
+
+ var userQuota = GetOrCreateUserQuota(userId);
+
+ lock (userQuota)
+ {
+ var remaining = Math.Max(0, quotaLimit - userQuota.CurrentUsage);
+ return Task.FromResult(remaining);
+ }
+ }
+
+ public Task GetQuotaResetDateAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var userQuota = GetOrCreateUserQuota(userId);
+
+ lock (userQuota)
+ {
+ return Task.FromResult(userQuota.NextResetDate);
+ }
+ }
+
+ public Task ResetQuotaAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var userQuota = GetOrCreateUserQuota(userId);
+
+ lock (userQuota)
+ {
+ userQuota.CurrentUsage = 0;
+ userQuota.ToolUsage.Clear();
+ userQuota.NextResetDate = CalculateNextResetDate(DateTime.UtcNow);
+
+ _logger.LogInformation("Reset quota for user {UserId}. Next reset: {NextReset}", userId, userQuota.NextResetDate);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task> GetUsageBreakdownAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var userQuota = GetOrCreateUserQuota(userId);
+
+ lock (userQuota)
+ {
+ return Task.FromResult(new Dictionary(userQuota.ToolUsage));
+ }
+ }
+
+ private UserQuotaInfo GetOrCreateUserQuota(string userId)
+ {
+ return _userQuotas.GetOrAdd(userId, _ => new UserQuotaInfo
+ {
+ UserId = userId,
+ CurrentUsage = 0,
+ NextResetDate = CalculateNextResetDate(DateTime.UtcNow),
+ LastUsage = DateTime.UtcNow,
+ ToolUsage = new ConcurrentDictionary()
+ });
+ }
+
+ private int GetQuotaLimitForRole(string userRole)
+ {
+ if (_options.RoleQuotas.TryGetValue(userRole, out var limit))
+ {
+ return limit;
+ }
+
+ // Default to user quota if role not found
+ return _options.RoleQuotas.TryGetValue("user", out var userLimit) ? userLimit : 1000;
+ }
+
+ private int GetQuotaCost(string toolName)
+ {
+ foreach (var mapping in _options.ToolQuotaCosts)
+ {
+ if (IsPatternMatch(mapping.Key, toolName))
+ {
+ return mapping.Value;
+ }
+ }
+
+ return 1; // Default cost
+ }
+
+ private DateTime CalculateNextResetDate(DateTime fromDate)
+ {
+ return fromDate.AddDays(_options.QuotaPeriodDays);
+ }
+
+ private async Task ProcessQuotaResetsAsync()
+ {
+ var now = DateTime.UtcNow;
+ var usersToReset = new List();
+
+ foreach (var kvp in _userQuotas)
+ {
+ var userQuota = kvp.Value;
+
+ lock (userQuota)
+ {
+ if (now >= userQuota.NextResetDate)
+ {
+ usersToReset.Add(kvp.Key);
+ }
+ }
+ }
+
+ foreach (var userId in usersToReset)
+ {
+ await ResetQuotaAsync(userId);
+ }
+
+ if (usersToReset.Count > 0)
+ {
+ _logger.LogInformation("Reset quotas for {Count} users", usersToReset.Count);
+ }
+ }
+
+ private static bool IsPatternMatch(string pattern, string toolName)
+ {
+ if (pattern == "*")
+ {
+ return true;
+ }
+
+ // Simple glob pattern matching
+ if (pattern.EndsWith("*"))
+ {
+ var prefix = pattern[..^1];
+ return toolName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return string.Equals(pattern, toolName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public void Dispose()
+ {
+ _resetTimer?.Dispose();
+ }
+
+ private class UserQuotaInfo
+ {
+ public string UserId { get; set; } = "";
+ public int CurrentUsage { get; set; }
+ public DateTime NextResetDate { get; set; }
+ public DateTime LastUsage { get; set; }
+ public ConcurrentDictionary ToolUsage { get; set; } = new();
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Services/InMemoryRateLimitingService.cs b/samples/DynamicToolFiltering/Services/InMemoryRateLimitingService.cs
new file mode 100644
index 00000000..ebd3c2d9
--- /dev/null
+++ b/samples/DynamicToolFiltering/Services/InMemoryRateLimitingService.cs
@@ -0,0 +1,128 @@
+using System.Collections.Concurrent;
+
+namespace DynamicToolFiltering.Services;
+
+///
+/// In-memory implementation of rate limiting service.
+/// Note: This is for demonstration purposes. In production, use a distributed cache like Redis.
+///
+public class InMemoryRateLimitingService : IRateLimitingService
+{
+ private readonly ConcurrentDictionary> _usageRecords = new();
+ private readonly ILogger _logger;
+ private readonly Timer _cleanupTimer;
+
+ public InMemoryRateLimitingService(ILogger logger)
+ {
+ _logger = logger;
+
+ // Run cleanup every 10 minutes
+ _cleanupTimer = new Timer(async _ => await CleanupOldRecordsAsync(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+ }
+
+ public Task GetUsageCountAsync(string userId, string toolName, DateTime windowStart, CancellationToken cancellationToken = default)
+ {
+ var key = GetKey(userId, toolName);
+
+ if (!_usageRecords.TryGetValue(key, out var records))
+ {
+ return Task.FromResult(0);
+ }
+
+ lock (records)
+ {
+ var count = records.Count(r => r.Timestamp >= windowStart);
+ return Task.FromResult(count);
+ }
+ }
+
+ public Task RecordUsageAsync(string userId, string toolName, DateTime timestamp, CancellationToken cancellationToken = default)
+ {
+ var key = GetKey(userId, toolName);
+ var record = new UsageRecord(timestamp);
+
+ _usageRecords.AddOrUpdate(key,
+ new List { record },
+ (_, existingRecords) =>
+ {
+ lock (existingRecords)
+ {
+ existingRecords.Add(record);
+ return existingRecords;
+ }
+ });
+
+ _logger.LogDebug("Recorded usage for {UserId}, {ToolName} at {Timestamp}", userId, toolName, timestamp);
+
+ return Task.CompletedTask;
+ }
+
+ public Task CleanupOldRecordsAsync(CancellationToken cancellationToken = default)
+ {
+ var cutoffTime = DateTime.UtcNow.AddHours(-24); // Keep records for 24 hours
+ var keysToRemove = new List();
+
+ foreach (var kvp in _usageRecords)
+ {
+ var records = kvp.Value;
+
+ lock (records)
+ {
+ records.RemoveAll(r => r.Timestamp < cutoffTime);
+
+ if (records.Count == 0)
+ {
+ keysToRemove.Add(kvp.Key);
+ }
+ }
+ }
+
+ foreach (var key in keysToRemove)
+ {
+ _usageRecords.TryRemove(key, out _);
+ }
+
+ if (keysToRemove.Count > 0)
+ {
+ _logger.LogDebug("Cleaned up {Count} empty usage record collections", keysToRemove.Count);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task> GetUsageStatisticsAsync(string userId, CancellationToken cancellationToken = default)
+ {
+ var statistics = new Dictionary();
+ var userPrefix = $"{userId}:";
+ var windowStart = DateTime.UtcNow.AddHours(-1); // Last hour
+
+ foreach (var kvp in _usageRecords)
+ {
+ if (kvp.Key.StartsWith(userPrefix))
+ {
+ var toolName = kvp.Key[userPrefix.Length..];
+ var records = kvp.Value;
+
+ lock (records)
+ {
+ var count = records.Count(r => r.Timestamp >= windowStart);
+ if (count > 0)
+ {
+ statistics[toolName] = count;
+ }
+ }
+ }
+ }
+
+ return Task.FromResult(statistics);
+ }
+
+ private static string GetKey(string userId, string toolName) => $"{userId}:{toolName}";
+
+ public void Dispose()
+ {
+ _cleanupTimer?.Dispose();
+ }
+
+ private record UsageRecord(DateTime Timestamp);
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/TESTING_GUIDE.md b/samples/DynamicToolFiltering/TESTING_GUIDE.md
new file mode 100644
index 00000000..7b20ff1a
--- /dev/null
+++ b/samples/DynamicToolFiltering/TESTING_GUIDE.md
@@ -0,0 +1,597 @@
+# Testing Guide for Dynamic Tool Filtering Sample
+
+This guide provides comprehensive testing scenarios and examples for the Dynamic Tool Filtering MCP server sample.
+
+## Quick Start Testing
+
+### 1. Start the Server
+
+```bash
+cd samples/DynamicToolFiltering
+dotnet run --launch-profile DevelopmentMode
+```
+
+The server will start on `http://localhost:8080` with development-friendly settings.
+
+### 2. Basic Health Check
+
+```bash
+curl http://localhost:8080/health
+```
+
+Expected response:
+```json
+{
+ "Status": "healthy",
+ "Timestamp": "2024-01-01T12:00:00.000Z",
+ "Environment": "Development",
+ "Version": "1.0.0"
+}
+```
+
+## API Key Testing
+
+The sample includes predefined API keys for different user roles:
+
+| API Key | Role | Scopes | Description |
+|---------|------|--------|-------------|
+| `demo-guest-key` | guest | basic:tools | Limited access to public tools |
+| `demo-user-key` | user | user:tools, read:tools, basic:tools | Standard user access |
+| `demo-premium-key` | premium | premium:tools, user:tools, read:tools, basic:tools | Premium features |
+| `demo-admin-key` | admin | admin:tools, premium:tools, user:tools, read:tools, basic:tools | Full administrative access |
+
+## Test Scenarios
+
+### 1. Tool Visibility Testing
+
+Test which tools are visible to different user roles:
+
+```bash
+# Guest user - should see only basic tools
+curl -H "X-API-Key: demo-guest-key" \
+ http://localhost:8080/mcp/v1/tools
+
+# User role - should see user-level tools
+curl -H "X-API-Key: demo-user-key" \
+ http://localhost:8080/mcp/v1/tools
+
+# Premium user - should see premium tools
+curl -H "X-API-Key: demo-premium-key" \
+ http://localhost:8080/mcp/v1/tools
+
+# Admin user - should see all tools
+curl -H "X-API-Key: demo-admin-key" \
+ http://localhost:8080/mcp/v1/tools
+```
+
+### 2. Tool Execution Testing
+
+#### Public Tools (No Authentication Required)
+
+```bash
+# Echo tool - available to everyone
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "echo",
+ "arguments": {
+ "message": "Hello Dynamic Filtering!"
+ }
+ }'
+
+# System info - public tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "get_system_info",
+ "arguments": {}
+ }'
+
+# UTC time - public tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "get_utc_time",
+ "arguments": {}
+ }'
+```
+
+#### User Tools (Requires Authentication)
+
+```bash
+# User profile - requires user role or higher
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "get_user_profile",
+ "arguments": {}
+ }'
+
+# Hash calculation - user tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "calculate_hash",
+ "arguments": {
+ "text": "Hello World",
+ "algorithm": "sha256"
+ }
+ }'
+
+# UUID generation - user tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "generate_uuid",
+ "arguments": {
+ "count": 3
+ }
+ }'
+
+# Email validation - user tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "validate_email",
+ "arguments": {
+ "email": "test@example.com"
+ }
+ }'
+```
+
+#### Premium Tools (Requires Premium Role)
+
+```bash
+# Secure random generation - premium tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-premium-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_generate_secure_random",
+ "arguments": {
+ "byteCount": 32,
+ "format": "hex"
+ }
+ }'
+
+# Text analysis - premium tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-premium-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_analyze_text",
+ "arguments": {
+ "text": "This is a sample text for comprehensive analysis. It contains multiple sentences and various words to demonstrate the text analysis capabilities.",
+ "depth": "comprehensive"
+ }
+ }'
+
+# Password generation - premium tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-premium-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_generate_password",
+ "arguments": {
+ "length": 16,
+ "includeUppercase": true,
+ "includeLowercase": true,
+ "includeNumbers": true,
+ "includeSpecial": true,
+ "excludeAmbiguous": true
+ }
+ }'
+
+# Performance benchmark - premium tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-premium-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_performance_benchmark",
+ "arguments": {
+ "benchmarkType": "cpu",
+ "durationSeconds": 3
+ }
+ }'
+```
+
+#### Admin Tools (Requires Admin Role)
+
+```bash
+# System diagnostics - admin tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-admin-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_get_system_diagnostics",
+ "arguments": {}
+ }'
+
+# Force garbage collection - admin tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-admin-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_force_gc",
+ "arguments": {
+ "generation": -1
+ }
+ }'
+
+# List processes - admin tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-admin-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_list_processes",
+ "arguments": {
+ "limit": 10
+ }
+ }'
+
+# Reload configuration - admin tool
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-admin-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_reload_config",
+ "arguments": {
+ "section": "filtering"
+ }
+ }'
+```
+
+### 3. Authorization Failure Testing
+
+Test that users cannot access tools above their privilege level:
+
+```bash
+# Try to access admin tool with user key (should fail)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "admin_get_system_diagnostics",
+ "arguments": {}
+ }'
+
+# Try to access premium tool with guest key (should fail)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-guest-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_generate_secure_random",
+ "arguments": {
+ "byteCount": 32
+ }
+ }'
+
+# Try to access user tool without authentication (should fail)
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "get_user_profile",
+ "arguments": {}
+ }'
+```
+
+Expected response for unauthorized access:
+```json
+{
+ "error": {
+ "code": -32002,
+ "message": "Access denied for tool 'admin_get_system_diagnostics': Tool requires role(s): admin or super_admin. User has role(s): user",
+ "data": {
+ "ToolName": "admin_get_system_diagnostics",
+ "Reason": "Tool requires role(s): admin or super_admin. User has role(s): user",
+ "HttpStatusCode": 401,
+ "RequiresAuthentication": true
+ }
+ }
+}
+```
+
+### 4. Rate Limiting Testing
+
+Test rate limiting by making multiple rapid requests:
+
+```bash
+# Test rate limiting with guest account (limited to 20 requests in development)
+for i in {1..25}; do
+ echo "Request $i:"
+ curl -H "X-API-Key: demo-guest-key" \
+ -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{"name": "echo", "arguments": {"message": "Test '$i'"}}' \
+ -w "Status: %{http_code}\n" -s -o /dev/null
+ sleep 0.1
+done
+```
+
+After hitting the limit, you should see HTTP 429 responses with a rate limit error.
+
+### 5. Feature Flag Testing
+
+Test feature flag functionality:
+
+```bash
+# Check current feature flags (admin only)
+curl -H "X-API-Key: demo-admin-key" \
+ http://localhost:8080/admin/feature-flags
+
+# Toggle a feature flag (admin only)
+curl -X POST \
+ -H "X-API-Key: demo-admin-key" \
+ "http://localhost:8080/admin/feature-flags/premium_features?enabled=false"
+
+# Try to use a premium tool after disabling the feature flag
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-premium-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "premium_generate_secure_random",
+ "arguments": {
+ "byteCount": 32
+ }
+ }'
+
+# Re-enable the feature flag
+curl -X POST \
+ -H "X-API-Key: demo-admin-key" \
+ "http://localhost:8080/admin/feature-flags/premium_features?enabled=true"
+```
+
+## Launch Profile Testing
+
+Test different launch profiles to verify filter configurations:
+
+### 1. No Filtering Mode
+
+```bash
+dotnet run --launch-profile NoFilteringMode
+```
+
+In this mode, all tools should be accessible regardless of authentication.
+
+### 2. Rate Limiting Demo Mode
+
+```bash
+dotnet run --launch-profile RateLimitingDemoMode
+```
+
+This mode has very strict rate limits (1-minute windows with low limits):
+- Guest: 3 requests per minute
+- User: 10 requests per minute
+- Premium: 25 requests per minute
+- Admin: 100 requests per minute
+
+### 3. Business Hours Demo Mode
+
+```bash
+dotnet run --launch-profile BusinessHoursDemoMode
+```
+
+This mode restricts admin tools to business hours (9 AM - 5 PM weekdays UTC).
+
+### 4. Tenant Demo Mode
+
+```bash
+dotnet run --launch-profile TenantDemoMode
+```
+
+This mode enables tenant isolation. Add `X-Tenant-ID` header to test:
+
+```bash
+curl -H "X-API-Key: demo-user-key" \
+ -H "X-Tenant-ID: tenant-a" \
+ -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{"name": "echo", "arguments": {"message": "Tenant A"}}'
+```
+
+## Error Response Testing
+
+### 1. Invalid API Key
+
+```bash
+curl -H "X-API-Key: invalid-key" \
+ http://localhost:8080/mcp/v1/tools
+```
+
+### 2. Malformed Request
+
+```bash
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{"invalid": "json"}'
+```
+
+### 3. Tool Not Found
+
+```bash
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "nonexistent_tool",
+ "arguments": {}
+ }'
+```
+
+### 4. Invalid Arguments
+
+```bash
+curl -X POST http://localhost:8080/mcp/v1/tools/call \
+ -H "X-API-Key: demo-user-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "calculate_hash",
+ "arguments": {
+ "text": "test",
+ "algorithm": "invalid_algorithm"
+ }
+ }'
+```
+
+## Performance Testing
+
+### 1. Concurrent Request Testing
+
+Use a tool like Apache Bench to test concurrent requests:
+
+```bash
+# Install apache2-utils if not installed
+# Ubuntu/Debian: sudo apt-get install apache2-utils
+# macOS: brew install httpd
+
+# Test with 10 concurrent connections, 100 total requests
+ab -n 100 -c 10 -H "X-API-Key: demo-user-key" \
+ -p echo_data.json -T application/json \
+ http://localhost:8080/mcp/v1/tools/call
+```
+
+Create `echo_data.json`:
+```json
+{
+ "name": "echo",
+ "arguments": {
+ "message": "Performance test"
+ }
+}
+```
+
+### 2. Memory Usage Testing
+
+Monitor memory usage during heavy load:
+
+```bash
+# Run server in background
+dotnet run --launch-profile DevelopmentMode &
+SERVER_PID=$!
+
+# Monitor memory usage
+while true; do
+ ps -o pid,vsz,rss,comm -p $SERVER_PID
+ sleep 5
+done
+
+# Kill server when done
+kill $SERVER_PID
+```
+
+## JWT Token Testing
+
+For JWT testing, you need to generate valid tokens. Here's a simple example using a script or online JWT generator:
+
+### JWT Payload Example
+
+```json
+{
+ "sub": "user123",
+ "name": "Test User",
+ "role": "premium",
+ "scope": "premium:tools user:tools read:tools basic:tools",
+ "iat": 1609459200,
+ "exp": 1609462800,
+ "iss": "dynamic-tool-filtering-demo",
+ "aud": "mcp-api-clients"
+}
+```
+
+Use the secret key from configuration: `your-256-bit-secret-key-here-make-it-secure-and-change-in-production`
+
+Test with JWT:
+
+```bash
+curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
+ http://localhost:8080/mcp/v1/tools
+```
+
+## Automated Testing Script
+
+Create a comprehensive test script:
+
+```bash
+#!/bin/bash
+
+BASE_URL="http://localhost:8080"
+GUEST_KEY="demo-guest-key"
+USER_KEY="demo-user-key"
+PREMIUM_KEY="demo-premium-key"
+ADMIN_KEY="demo-admin-key"
+
+echo "=== Dynamic Tool Filtering Test Suite ==="
+
+# Test 1: Health Check
+echo "Test 1: Health Check"
+curl -s "$BASE_URL/health" | jq .
+echo ""
+
+# Test 2: Tool Visibility by Role
+echo "Test 2: Tool Visibility"
+echo "Guest tools:"
+curl -s -H "X-API-Key: $GUEST_KEY" "$BASE_URL/mcp/v1/tools" | jq '.result.tools[].name'
+echo "User tools:"
+curl -s -H "X-API-Key: $USER_KEY" "$BASE_URL/mcp/v1/tools" | jq '.result.tools[].name'
+echo "Premium tools:"
+curl -s -H "X-API-Key: $PREMIUM_KEY" "$BASE_URL/mcp/v1/tools" | jq '.result.tools[].name'
+echo "Admin tools:"
+curl -s -H "X-API-Key: $ADMIN_KEY" "$BASE_URL/mcp/v1/tools" | jq '.result.tools[].name'
+echo ""
+
+# Test 3: Successful Tool Execution
+echo "Test 3: Successful Tool Execution"
+curl -s -X POST "$BASE_URL/mcp/v1/tools/call" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "echo", "arguments": {"message": "Test"}}' | jq .
+echo ""
+
+# Test 4: Authorization Failure
+echo "Test 4: Authorization Failure"
+curl -s -X POST "$BASE_URL/mcp/v1/tools/call" \
+ -H "X-API-Key: $GUEST_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"name": "admin_get_system_diagnostics", "arguments": {}}' | jq .
+echo ""
+
+echo "=== Test Suite Complete ==="
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Server not starting**: Check that port 8080 is available
+2. **Tools not visible**: Verify API key and user role configuration
+3. **Rate limit errors**: Wait for rate limit window to reset or use a different launch profile
+4. **Feature flag errors**: Check feature flag configuration and current state
+
+### Debug Logging
+
+Enable debug logging in `appsettings.Development.json`:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "DynamicToolFiltering": "Debug",
+ "ModelContextProtocol": "Debug"
+ }
+ }
+}
+```
+
+### Verbose Filter Logging
+
+Check the console output for detailed filter execution logs:
+
+```
+[Debug] Tool inclusion check for echo: User roles [guest], Required roles [guest, user, premium, admin, super_admin], HasAccess: True
+[Debug] Tool execution authorized: echo for user anonymous_12345678. Remaining: 19/20
+```
+
+This guide provides comprehensive testing coverage for all aspects of the Dynamic Tool Filtering sample, from basic functionality to advanced scenarios and edge cases.
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Tools/AdminTools.cs b/samples/DynamicToolFiltering/Tools/AdminTools.cs
new file mode 100644
index 00000000..0e78d500
--- /dev/null
+++ b/samples/DynamicToolFiltering/Tools/AdminTools.cs
@@ -0,0 +1,210 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+namespace DynamicToolFiltering.Tools;
+
+///
+/// Administrative tools that require elevated permissions.
+/// These tools are only available to users with admin roles.
+///
+public class AdminTools
+{
+ ///
+ /// Get detailed system diagnostics and performance metrics.
+ ///
+ [McpServerTool(Name = "admin_get_system_diagnostics", Description = "Get detailed system diagnostics and performance metrics")]
+ public static async Task GetSystemDiagnosticsAsync(CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(200, cancellationToken);
+
+ var process = System.Diagnostics.Process.GetCurrentProcess();
+ var gc = GC.GetTotalMemory(false);
+
+ var diagnostics = new
+ {
+ ProcessInfo = new
+ {
+ ProcessId = process.Id,
+ StartTime = process.StartTime.ToString("O"),
+ TotalProcessorTime = process.TotalProcessorTime.ToString(),
+ WorkingSet = process.WorkingSet64,
+ PrivateMemorySize = process.PrivateMemorySize64,
+ VirtualMemorySize = process.VirtualMemorySize64
+ },
+ MemoryInfo = new
+ {
+ GCTotalMemory = gc,
+ Gen0Collections = GC.CollectionCount(0),
+ Gen1Collections = GC.CollectionCount(1),
+ Gen2Collections = GC.CollectionCount(2)
+ },
+ EnvironmentInfo = new
+ {
+ OSVersion = Environment.OSVersion.ToString(),
+ CLRVersion = Environment.Version.ToString(),
+ MachineName = Environment.MachineName,
+ UserName = Environment.UserName,
+ ProcessorCount = Environment.ProcessorCount,
+ SystemDirectory = Environment.SystemDirectory,
+ TickCount = Environment.TickCount64
+ },
+ Timestamp = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"System Diagnostics: {System.Text.Json.JsonSerializer.Serialize(diagnostics, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Force garbage collection (admin operation).
+ ///
+ [McpServerTool(Name = "admin_force_gc", Description = "Force garbage collection (administrative operation)")]
+ public static async Task ForceGarbageCollectionAsync(
+ [Description("GC generation (0, 1, 2, or -1 for all)")] int generation = -1,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(50, cancellationToken);
+
+ var beforeMemory = GC.GetTotalMemory(false);
+ var beforeCollections = new
+ {
+ Gen0 = GC.CollectionCount(0),
+ Gen1 = GC.CollectionCount(1),
+ Gen2 = GC.CollectionCount(2)
+ };
+
+ if (generation == -1)
+ {
+ GC.Collect();
+ }
+ else if (generation >= 0 && generation <= 2)
+ {
+ GC.Collect(generation);
+ }
+ else
+ {
+ return CallToolResult.FromError("Invalid generation. Must be 0, 1, 2, or -1 for all generations");
+ }
+
+ GC.WaitForPendingFinalizers();
+
+ var afterMemory = GC.GetTotalMemory(false);
+ var afterCollections = new
+ {
+ Gen0 = GC.CollectionCount(0),
+ Gen1 = GC.CollectionCount(1),
+ Gen2 = GC.CollectionCount(2)
+ };
+
+ var result = new
+ {
+ Generation = generation,
+ MemoryBefore = beforeMemory,
+ MemoryAfter = afterMemory,
+ MemoryReclaimed = beforeMemory - afterMemory,
+ CollectionsBefore = beforeCollections,
+ CollectionsAfter = afterCollections,
+ ExecutedAt = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Garbage Collection Result: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Get list of all running processes (admin operation).
+ ///
+ [McpServerTool(Name = "admin_list_processes", Description = "Get list of all running processes")]
+ public static async Task ListProcessesAsync(
+ [Description("Maximum number of processes to return")] int limit = 20,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(300, cancellationToken);
+
+ if (limit < 1 || limit > 100)
+ {
+ return CallToolResult.FromError("Limit must be between 1 and 100");
+ }
+
+ var processes = System.Diagnostics.Process.GetProcesses()
+ .Take(limit)
+ .Select(p => new
+ {
+ ProcessId = p.Id,
+ ProcessName = p.ProcessName,
+ StartTime = TryGetStartTime(p),
+ WorkingSet = TryGetWorkingSet(p),
+ HasExited = TryGetHasExited(p)
+ })
+ .ToArray();
+
+ var result = new
+ {
+ ProcessCount = processes.Length,
+ Processes = processes,
+ RetrievedAt = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Process List: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Simulate configuration reload (admin operation).
+ ///
+ [McpServerTool(Name = "admin_reload_config", Description = "Simulate configuration reload")]
+ public static async Task ReloadConfigurationAsync(
+ [Description("Configuration section to reload")] string section = "all",
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(500, cancellationToken); // Simulate configuration reload time
+
+ var result = new
+ {
+ Section = section,
+ Status = "success",
+ ReloadedAt = DateTime.UtcNow.ToString("O"),
+ Message = $"Configuration section '{section}' has been reloaded successfully",
+ Version = Guid.NewGuid().ToString("N")[..8] // Simulate new config version
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Configuration Reload: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ private static string TryGetStartTime(System.Diagnostics.Process process)
+ {
+ try
+ {
+ return process.StartTime.ToString("O");
+ }
+ catch
+ {
+ return "N/A";
+ }
+ }
+
+ private static long TryGetWorkingSet(System.Diagnostics.Process process)
+ {
+ try
+ {
+ return process.WorkingSet64;
+ }
+ catch
+ {
+ return -1;
+ }
+ }
+
+ private static bool TryGetHasExited(System.Diagnostics.Process process)
+ {
+ try
+ {
+ return process.HasExited;
+ }
+ catch
+ {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Tools/PremiumTools.cs b/samples/DynamicToolFiltering/Tools/PremiumTools.cs
new file mode 100644
index 00000000..38aa3a6e
--- /dev/null
+++ b/samples/DynamicToolFiltering/Tools/PremiumTools.cs
@@ -0,0 +1,382 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace DynamicToolFiltering.Tools;
+
+///
+/// Premium tools that require subscription or premium access.
+/// These tools provide advanced functionality with higher resource usage.
+///
+public class PremiumTools
+{
+ ///
+ /// Generate cryptographically secure random bytes.
+ ///
+ [McpServerTool(Name = "premium_generate_secure_random", Description = "Generate cryptographically secure random bytes")]
+ public static async Task GenerateSecureRandomAsync(
+ [Description("Number of bytes to generate (1-1024)")] int byteCount = 32,
+ [Description("Output format: hex, base64, or bytes")] string format = "hex",
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(100, cancellationToken);
+
+ if (byteCount < 1 || byteCount > 1024)
+ {
+ return CallToolResult.FromError("Byte count must be between 1 and 1024");
+ }
+
+ using var rng = RandomNumberGenerator.Create();
+ var randomBytes = new byte[byteCount];
+ rng.GetBytes(randomBytes);
+
+ var output = format.ToLowerInvariant() switch
+ {
+ "hex" => Convert.ToHexString(randomBytes).ToLowerInvariant(),
+ "base64" => Convert.ToBase64String(randomBytes),
+ "bytes" => string.Join(",", randomBytes),
+ _ => Convert.ToHexString(randomBytes).ToLowerInvariant()
+ };
+
+ var result = new
+ {
+ ByteCount = byteCount,
+ Format = format,
+ RandomData = output,
+ GeneratedAt = DateTime.UtcNow.ToString("O"),
+ Entropy = randomBytes.Length * 8 // bits of entropy
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Secure Random Data: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Perform advanced text analysis with metrics.
+ ///
+ [McpServerTool(Name = "premium_analyze_text", Description = "Perform advanced text analysis with detailed metrics")]
+ public static async Task AnalyzeTextAsync(
+ [Description("Text to analyze")] string text,
+ [Description("Analysis depth: basic, standard, comprehensive")] string depth = "standard",
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(200, cancellationToken); // Simulate analysis time
+
+ if (string.IsNullOrEmpty(text))
+ {
+ return CallToolResult.FromError("Text cannot be empty");
+ }
+
+ var words = text.Split(new[] { ' ', '\t', '\n', '\r', '.', ',', ';', ':', '!', '?' },
+ StringSplitOptions.RemoveEmptyEntries);
+
+ var sentences = text.Split(new[] { '.', '!', '?' },
+ StringSplitOptions.RemoveEmptyEntries)
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .ToArray();
+
+ var basicMetrics = new
+ {
+ CharacterCount = text.Length,
+ WordCount = words.Length,
+ SentenceCount = sentences.Length,
+ ParagraphCount = text.Split(new[] { "\n\n", "\r\n\r\n" }, StringSplitOptions.RemoveEmptyEntries).Length,
+ AverageWordsPerSentence = sentences.Length > 0 ? (double)words.Length / sentences.Length : 0,
+ AverageCharactersPerWord = words.Length > 0 ? words.Average(w => w.Length) : 0
+ };
+
+ var analysis = new Dictionary
+ {
+ ["BasicMetrics"] = basicMetrics
+ };
+
+ if (depth is "standard" or "comprehensive")
+ {
+ var wordFrequency = words
+ .GroupBy(w => w.ToLowerInvariant())
+ .OrderByDescending(g => g.Count())
+ .Take(10)
+ .ToDictionary(g => g.Key, g => g.Count());
+
+ var readabilityMetrics = new
+ {
+ LongestWord = words.OrderByDescending(w => w.Length).FirstOrDefault()?.Length ?? 0,
+ ShortestWord = words.OrderBy(w => w.Length).FirstOrDefault()?.Length ?? 0,
+ UniqueWords = words.Distinct(StringComparer.OrdinalIgnoreCase).Count(),
+ LexicalDiversity = words.Length > 0 ? (double)words.Distinct(StringComparer.OrdinalIgnoreCase).Count() / words.Length : 0
+ };
+
+ analysis["WordFrequency"] = wordFrequency;
+ analysis["ReadabilityMetrics"] = readabilityMetrics;
+ }
+
+ if (depth == "comprehensive")
+ {
+ var advancedMetrics = new
+ {
+ CapitalLetters = text.Count(char.IsUpper),
+ LowercaseLetters = text.Count(char.IsLower),
+ Digits = text.Count(char.IsDigit),
+ Punctuation = text.Count(char.IsPunctuation),
+ Whitespace = text.Count(char.IsWhiteSpace),
+ VowelCount = text.Count(c => "aeiouAEIOU".Contains(c)),
+ ConsonantCount = text.Count(c => char.IsLetter(c) && !"aeiouAEIOU".Contains(c))
+ };
+
+ analysis["AdvancedMetrics"] = advancedMetrics;
+ }
+
+ analysis["AnalysisDepth"] = depth;
+ analysis["AnalyzedAt"] = DateTime.UtcNow.ToString("O");
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Text Analysis: {System.Text.Json.JsonSerializer.Serialize(analysis, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Generate secure password with customizable complexity.
+ ///
+ [McpServerTool(Name = "premium_generate_password", Description = "Generate secure password with customizable complexity")]
+ public static async Task GeneratePasswordAsync(
+ [Description("Password length (8-128)")] int length = 16,
+ [Description("Include uppercase letters")] bool includeUppercase = true,
+ [Description("Include lowercase letters")] bool includeLowercase = true,
+ [Description("Include numbers")] bool includeNumbers = true,
+ [Description("Include special characters")] bool includeSpecial = true,
+ [Description("Exclude ambiguous characters (0, O, l, I, etc.)")] bool excludeAmbiguous = false,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(150, cancellationToken);
+
+ if (length < 8 || length > 128)
+ {
+ return CallToolResult.FromError("Password length must be between 8 and 128 characters");
+ }
+
+ if (!includeUppercase && !includeLowercase && !includeNumbers && !includeSpecial)
+ {
+ return CallToolResult.FromError("At least one character type must be enabled");
+ }
+
+ var characterSets = new List();
+ var guaranteedChars = new List();
+
+ if (includeUppercase)
+ {
+ var upperChars = excludeAmbiguous ? "ABCDEFGHJKLMNPQRSTUVWXYZ" : "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ characterSets.Add(upperChars);
+ guaranteedChars.Add(upperChars[RandomNumberGenerator.GetInt32(upperChars.Length)]);
+ }
+
+ if (includeLowercase)
+ {
+ var lowerChars = excludeAmbiguous ? "abcdefghjkmnpqrstuvwxyz" : "abcdefghijklmnopqrstuvwxyz";
+ characterSets.Add(lowerChars);
+ guaranteedChars.Add(lowerChars[RandomNumberGenerator.GetInt32(lowerChars.Length)]);
+ }
+
+ if (includeNumbers)
+ {
+ var numberChars = excludeAmbiguous ? "23456789" : "0123456789";
+ characterSets.Add(numberChars);
+ guaranteedChars.Add(numberChars[RandomNumberGenerator.GetInt32(numberChars.Length)]);
+ }
+
+ if (includeSpecial)
+ {
+ var specialChars = excludeAmbiguous ? "!@#$%^&*+-=" : "!@#$%^&*()_+-=[]{}|;:,.<>?";
+ characterSets.Add(specialChars);
+ guaranteedChars.Add(specialChars[RandomNumberGenerator.GetInt32(specialChars.Length)]);
+ }
+
+ var allChars = string.Join("", characterSets);
+ var password = new StringBuilder();
+
+ // Add guaranteed characters first
+ foreach (var c in guaranteedChars)
+ {
+ password.Append(c);
+ }
+
+ // Fill remaining positions
+ for (int i = guaranteedChars.Count; i < length; i++)
+ {
+ password.Append(allChars[RandomNumberGenerator.GetInt32(allChars.Length)]);
+ }
+
+ // Shuffle the password
+ var passwordArray = password.ToString().ToCharArray();
+ for (int i = passwordArray.Length - 1; i > 0; i--)
+ {
+ int j = RandomNumberGenerator.GetInt32(i + 1);
+ (passwordArray[i], passwordArray[j]) = (passwordArray[j], passwordArray[i]);
+ }
+
+ var finalPassword = new string(passwordArray);
+
+ // Calculate password strength
+ var entropy = Math.Log2(allChars.Length) * length;
+ var strengthRating = entropy switch
+ {
+ < 50 => "Weak",
+ < 75 => "Moderate",
+ < 100 => "Strong",
+ _ => "Very Strong"
+ };
+
+ var result = new
+ {
+ Password = finalPassword,
+ Length = length,
+ Entropy = Math.Round(entropy, 2),
+ StrengthRating = strengthRating,
+ CharacterTypes = new
+ {
+ Uppercase = includeUppercase,
+ Lowercase = includeLowercase,
+ Numbers = includeNumbers,
+ Special = includeSpecial,
+ ExcludeAmbiguous = excludeAmbiguous
+ },
+ GeneratedAt = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Password Generation: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Perform benchmark test to measure system performance.
+ ///
+ [McpServerTool(Name = "premium_performance_benchmark", Description = "Perform system performance benchmark")]
+ public static async Task PerformanceBenchmarkAsync(
+ [Description("Benchmark type: cpu, memory, disk, network")] string benchmarkType = "cpu",
+ [Description("Test duration in seconds (1-30)")] int durationSeconds = 5,
+ CancellationToken cancellationToken = default)
+ {
+ if (durationSeconds < 1 || durationSeconds > 30)
+ {
+ return CallToolResult.FromError("Duration must be between 1 and 30 seconds");
+ }
+
+ var startTime = DateTime.UtcNow;
+ var results = new Dictionary();
+
+ switch (benchmarkType.ToLowerInvariant())
+ {
+ case "cpu":
+ results = await BenchmarkCpuAsync(durationSeconds, cancellationToken);
+ break;
+ case "memory":
+ results = await BenchmarkMemoryAsync(durationSeconds, cancellationToken);
+ break;
+ case "disk":
+ results = await BenchmarkDiskAsync(durationSeconds, cancellationToken);
+ break;
+ default:
+ return CallToolResult.FromError($"Unknown benchmark type: {benchmarkType}. Supported types: cpu, memory, disk");
+ }
+
+ results["BenchmarkType"] = benchmarkType;
+ results["DurationSeconds"] = durationSeconds;
+ results["StartTime"] = startTime.ToString("O");
+ results["EndTime"] = DateTime.UtcNow.ToString("O");
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Performance Benchmark: {System.Text.Json.JsonSerializer.Serialize(results, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ private static async Task> BenchmarkCpuAsync(int durationSeconds, CancellationToken cancellationToken)
+ {
+ var operations = 0L;
+ var endTime = DateTime.UtcNow.AddSeconds(durationSeconds);
+
+ while (DateTime.UtcNow < endTime && !cancellationToken.IsCancellationRequested)
+ {
+ // Perform CPU-intensive operation
+ Math.Sqrt(operations);
+ operations++;
+
+ if (operations % 10000 == 0)
+ {
+ await Task.Yield(); // Allow other tasks to run
+ }
+ }
+
+ return new Dictionary
+ {
+ ["OperationsPerformed"] = operations,
+ ["OperationsPerSecond"] = operations / durationSeconds,
+ ["BenchmarkScore"] = operations / 1000000.0 // Normalize to millions of operations
+ };
+ }
+
+ private static async Task> BenchmarkMemoryAsync(int durationSeconds, CancellationToken cancellationToken)
+ {
+ var allocations = 0L;
+ var totalMemoryAllocated = 0L;
+ var endTime = DateTime.UtcNow.AddSeconds(durationSeconds);
+
+ while (DateTime.UtcNow < endTime && !cancellationToken.IsCancellationRequested)
+ {
+ // Allocate and deallocate memory
+ var data = new byte[1024]; // 1KB allocation
+ allocations++;
+ totalMemoryAllocated += data.Length;
+
+ if (allocations % 1000 == 0)
+ {
+ await Task.Yield();
+ GC.Collect(0, GCCollectionMode.Optimized, false);
+ }
+ }
+
+ return new Dictionary
+ {
+ ["AllocationsPerformed"] = allocations,
+ ["TotalMemoryAllocated"] = totalMemoryAllocated,
+ ["AllocationsPerSecond"] = allocations / durationSeconds,
+ ["MemoryThroughputMBps"] = (totalMemoryAllocated / 1024.0 / 1024.0) / durationSeconds
+ };
+ }
+
+ private static async Task> BenchmarkDiskAsync(int durationSeconds, CancellationToken cancellationToken)
+ {
+ var operations = 0L;
+ var tempFile = Path.GetTempFileName();
+ var data = new byte[4096]; // 4KB blocks
+ RandomNumberGenerator.Fill(data);
+
+ try
+ {
+ var endTime = DateTime.UtcNow.AddSeconds(durationSeconds);
+
+ using var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.DeleteOnClose);
+
+ while (DateTime.UtcNow < endTime && !cancellationToken.IsCancellationRequested)
+ {
+ await fileStream.WriteAsync(data, cancellationToken);
+ operations++;
+
+ if (operations % 100 == 0)
+ {
+ await fileStream.FlushAsync(cancellationToken);
+ await Task.Yield();
+ }
+ }
+ }
+ finally
+ {
+ try { File.Delete(tempFile); } catch { /* Ignore cleanup errors */ }
+ }
+
+ return new Dictionary
+ {
+ ["WriteOperations"] = operations,
+ ["TotalBytesWritten"] = operations * data.Length,
+ ["OperationsPerSecond"] = operations / durationSeconds,
+ ["ThroughputMBps"] = (operations * data.Length / 1024.0 / 1024.0) / durationSeconds
+ };
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Tools/PublicTools.cs b/samples/DynamicToolFiltering/Tools/PublicTools.cs
new file mode 100644
index 00000000..14601d82
--- /dev/null
+++ b/samples/DynamicToolFiltering/Tools/PublicTools.cs
@@ -0,0 +1,67 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+namespace DynamicToolFiltering.Tools;
+
+///
+/// Public tools available to all users without authentication.
+/// These represent the most basic functionality that doesn't require any authorization.
+///
+public class PublicTools
+{
+ ///
+ /// Get basic system information - available to all users.
+ ///
+ [McpServerTool(Name = "get_system_info", Description = "Get basic system information and API status")]
+ public static async Task GetSystemInfoAsync(CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(50, cancellationToken); // Simulate some work
+
+ var systemInfo = new
+ {
+ Status = "online",
+ Version = "1.0.0",
+ Timestamp = DateTime.UtcNow.ToString("O"),
+ Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
+ MachineName = Environment.MachineName,
+ ProcessorCount = Environment.ProcessorCount
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"System Information: {System.Text.Json.JsonSerializer.Serialize(systemInfo, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Simple echo service for testing connectivity.
+ ///
+ [McpServerTool(Name = "echo", Description = "Echo back the provided message")]
+ public static async Task EchoAsync(
+ [Description("The message to echo back")] string message,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(10, cancellationToken);
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Echo: {message} (timestamp: {DateTime.UtcNow:O})"));
+ }
+
+ ///
+ /// Get current UTC time - useful for timezone-independent operations.
+ ///
+ [McpServerTool(Name = "get_utc_time", Description = "Get the current UTC timestamp")]
+ public static async Task GetUtcTimeAsync(CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(5, cancellationToken);
+
+ var timeInfo = new
+ {
+ UtcTime = DateTime.UtcNow.ToString("O"),
+ UnixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
+ DayOfWeek = DateTime.UtcNow.DayOfWeek.ToString(),
+ IsWeekend = DateTime.UtcNow.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Time Information: {System.Text.Json.JsonSerializer.Serialize(timeInfo, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/Tools/UserTools.cs b/samples/DynamicToolFiltering/Tools/UserTools.cs
new file mode 100644
index 00000000..8f26eb10
--- /dev/null
+++ b/samples/DynamicToolFiltering/Tools/UserTools.cs
@@ -0,0 +1,138 @@
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+namespace DynamicToolFiltering.Tools;
+
+///
+/// User-level tools that require basic authentication.
+/// These tools are available to authenticated users with basic permissions.
+///
+public class UserTools
+{
+ ///
+ /// Get current user profile information.
+ ///
+ [McpServerTool(Name = "get_user_profile", Description = "Get current user's profile information")]
+ public static async Task GetUserProfileAsync(
+ RequestContext context,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(100, cancellationToken);
+
+ // Extract user information from the request context
+ var userId = context.Session.ClientInfo?.Name ?? "anonymous";
+
+ var userProfile = new
+ {
+ UserId = userId,
+ AuthenticatedAt = DateTime.UtcNow.ToString("O"),
+ SessionId = context.Session.ToString(),
+ ClientInfo = context.Session.ClientInfo?.Name ?? "Unknown Client",
+ Permissions = new[] { "user:read", "user:profile" }
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"User Profile: {System.Text.Json.JsonSerializer.Serialize(userProfile, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Calculate hash of provided text.
+ ///
+ [McpServerTool(Name = "calculate_hash", Description = "Calculate SHA256 hash of provided text")]
+ public static async Task CalculateHashAsync(
+ [Description("Text to hash")] string text,
+ [Description("Hash algorithm (sha256, sha1, md5)")] string algorithm = "sha256",
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(50, cancellationToken);
+
+ using var hashAlgorithm = algorithm.ToLowerInvariant() switch
+ {
+ "sha256" => System.Security.Cryptography.SHA256.Create(),
+ "sha1" => System.Security.Cryptography.SHA1.Create(),
+ "md5" => System.Security.Cryptography.MD5.Create(),
+ _ => System.Security.Cryptography.SHA256.Create()
+ };
+
+ var bytes = System.Text.Encoding.UTF8.GetBytes(text);
+ var hashBytes = hashAlgorithm.ComputeHash(bytes);
+ var hashString = Convert.ToHexString(hashBytes).ToLowerInvariant();
+
+ var result = new
+ {
+ Algorithm = algorithm,
+ Input = text,
+ Hash = hashString,
+ ComputedAt = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Hash Result: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Generate a random UUID.
+ ///
+ [McpServerTool(Name = "generate_uuid", Description = "Generate a random UUID")]
+ public static async Task GenerateUuidAsync(
+ [Description("Number of UUIDs to generate")] int count = 1,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(20, cancellationToken);
+
+ if (count < 1 || count > 10)
+ {
+ return CallToolResult.FromError("Count must be between 1 and 10");
+ }
+
+ var uuids = Enumerable.Range(0, count)
+ .Select(_ => Guid.NewGuid().ToString())
+ .ToArray();
+
+ var result = new
+ {
+ Count = count,
+ UUIDs = uuids,
+ GeneratedAt = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Generated UUIDs: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+
+ ///
+ /// Validate an email address format.
+ ///
+ [McpServerTool(Name = "validate_email", Description = "Validate email address format")]
+ public static async Task ValidateEmailAsync(
+ [Description("Email address to validate")] string email,
+ CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(30, cancellationToken);
+
+ bool isValid;
+ string reason = "Valid email format";
+
+ try
+ {
+ var mail = new System.Net.Mail.MailAddress(email);
+ isValid = mail.Address == email;
+ }
+ catch
+ {
+ isValid = false;
+ reason = "Invalid email format";
+ }
+
+ var result = new
+ {
+ Email = email,
+ IsValid = isValid,
+ Reason = reason,
+ ValidatedAt = DateTime.UtcNow.ToString("O")
+ };
+
+ return CallToolResult.FromContent(
+ TextContent.Create($"Email Validation: {System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })}"));
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/appsettings.Development.json b/samples/DynamicToolFiltering/appsettings.Development.json
new file mode 100644
index 00000000..9d11feb3
--- /dev/null
+++ b/samples/DynamicToolFiltering/appsettings.Development.json
@@ -0,0 +1,35 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Information",
+ "DynamicToolFiltering": "Debug",
+ "ModelContextProtocol": "Debug"
+ }
+ },
+ "Filtering": {
+ "RateLimiting": {
+ "WindowMinutes": 5,
+ "RoleLimits": {
+ "guest": 20,
+ "user": 200,
+ "premium": 1000,
+ "admin": -1
+ }
+ },
+ "TimeBased": {
+ "Enabled": false
+ },
+ "TenantIsolation": {
+ "Enabled": false
+ },
+ "BusinessLogic": {
+ "QuotaManagement": {
+ "Enabled": false
+ },
+ "EnvironmentRestrictions": {
+ "Enabled": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/appsettings.Production.json b/samples/DynamicToolFiltering/appsettings.Production.json
new file mode 100644
index 00000000..52ef2bad
--- /dev/null
+++ b/samples/DynamicToolFiltering/appsettings.Production.json
@@ -0,0 +1,63 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft.AspNetCore": "Error",
+ "DynamicToolFiltering": "Information"
+ }
+ },
+ "Jwt": {
+ "SecretKey": "${JWT_SECRET_KEY}",
+ "Issuer": "${JWT_ISSUER}",
+ "Audience": "${JWT_AUDIENCE}"
+ },
+ "Filtering": {
+ "RateLimiting": {
+ "WindowMinutes": 60,
+ "RoleLimits": {
+ "guest": 5,
+ "user": 50,
+ "premium": 200,
+ "admin": 500,
+ "super_admin": -1
+ },
+ "ToolLimits": {
+ "premium_performance_benchmark": 2,
+ "admin_*": 20
+ }
+ },
+ "TimeBased": {
+ "Enabled": true,
+ "BusinessHours": {
+ "Enabled": true,
+ "StartTime": "08:00",
+ "EndTime": "18:00",
+ "BusinessDays": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ],
+ "RestrictedTools": [ "admin_*", "premium_performance_benchmark" ]
+ }
+ },
+ "TenantIsolation": {
+ "Enabled": true
+ },
+ "BusinessLogic": {
+ "QuotaManagement": {
+ "Enabled": true,
+ "QuotaPeriodDays": 30,
+ "RoleQuotas": {
+ "guest": 100,
+ "user": 1000,
+ "premium": 5000,
+ "admin": -1
+ }
+ },
+ "EnvironmentRestrictions": {
+ "Enabled": true,
+ "ProductionRestrictedTools": [
+ "admin_force_gc",
+ "admin_list_processes",
+ "premium_performance_benchmark"
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/appsettings.json b/samples/DynamicToolFiltering/appsettings.json
new file mode 100644
index 00000000..42b7a5e1
--- /dev/null
+++ b/samples/DynamicToolFiltering/appsettings.json
@@ -0,0 +1,147 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "DynamicToolFiltering": "Debug"
+ }
+ },
+ "AllowedHosts": "*",
+ "Jwt": {
+ "SecretKey": "your-256-bit-secret-key-here-make-it-secure-and-change-in-production",
+ "Issuer": "dynamic-tool-filtering-demo",
+ "Audience": "mcp-api-clients",
+ "ExpirationMinutes": 60
+ },
+ "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": []
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/DynamicToolFiltering/clients/Program.cs b/samples/DynamicToolFiltering/clients/Program.cs
new file mode 100644
index 00000000..bf1d736a
--- /dev/null
+++ b/samples/DynamicToolFiltering/clients/Program.cs
@@ -0,0 +1,399 @@
+using DynamicToolFiltering.TestClient;
+using Microsoft.Extensions.Logging;
+using System.CommandLine;
+
+///
+/// Test client console application for the Dynamic Tool Filtering MCP server.
+///
+/// This application demonstrates how to:
+/// 1. Connect to and authenticate with an MCP server
+/// 2. Discover available tools based on user role
+/// 3. Execute tools with proper error handling
+/// 4. Run comprehensive test suites
+/// 5. Perform load testing and performance analysis
+///
+/// USAGE EXAMPLES:
+///
+/// Basic health check:
+/// dotnet run -- health --url http://localhost:8080
+///
+/// Discover tools with API key:
+/// dotnet run -- discover --url http://localhost:8080 --api-key demo-user-key
+///
+/// Execute a specific tool:
+/// dotnet run -- execute --url http://localhost:8080 --api-key demo-user-key --tool echo --args '{"message":"Hello World"}'
+///
+/// Run comprehensive tests:
+/// dotnet run -- test --url http://localhost:8080
+///
+/// Performance testing:
+/// dotnet run -- perf --url http://localhost:8080 --users 10 --requests 50
+///
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var rootCommand = new RootCommand("Dynamic Tool Filtering MCP Server Test Client");
+
+ // Common options
+ var urlOption = new Option("--url", () => "http://localhost:8080", "Server URL");
+ var apiKeyOption = new Option("--api-key", "API key for authentication");
+ var verboseOption = new Option("--verbose", "Enable verbose logging");
+
+ // Health check command
+ var healthCommand = new Command("health", "Check server health");
+ healthCommand.AddOption(urlOption);
+ healthCommand.AddOption(verboseOption);
+ healthCommand.SetHandler(async (string url, bool verbose) =>
+ {
+ using var client = CreateTestClient(url, verbose);
+ await RunHealthCheckAsync(client);
+ }, urlOption, verboseOption);
+
+ // Discover tools command
+ var discoverCommand = new Command("discover", "Discover available tools");
+ discoverCommand.AddOption(urlOption);
+ discoverCommand.AddOption(apiKeyOption);
+ discoverCommand.AddOption(verboseOption);
+ discoverCommand.SetHandler(async (string url, string? apiKey, bool verbose) =>
+ {
+ using var client = CreateTestClient(url, verbose);
+ await RunDiscoveryAsync(client, apiKey);
+ }, urlOption, apiKeyOption, verboseOption);
+
+ // Execute tool command
+ var executeCommand = new Command("execute", "Execute a specific tool");
+ executeCommand.AddOption(urlOption);
+ executeCommand.AddOption(apiKeyOption);
+ executeCommand.AddOption(verboseOption);
+ var toolOption = new Option("--tool", "Tool name to execute") { IsRequired = true };
+ var argsOption = new Option