MCP Security — Authorization, GDPR Sanitization & DPoP
MCP exposes application logic to external AI agents. Granit ensures every tool invocation is authorized, every response is sanitized, and every action is audited.
Authorization
Section titled “Authorization”Standard [Authorize] — no custom attributes
Section titled “Standard [Authorize] — no custom attributes”The MCP SDK’s .AddAuthorizationFilters() (wired by AddGranitMcpServer()) enables
standard ASP.NET Core [Authorize] on tool classes and methods. Granit’s
DynamicPermissionPolicyProvider maps policy names to RBAC permissions:
[McpServerToolType, McpExposed][Authorize(Policy = McpPermissions.Tools.Execute)] // Class-level defaultpublic sealed class NotificationMcpTools(INotificationSender sender){ [McpServerTool, Description("Sends a notification to a user.")] [Authorize(Policy = "Notifications.Messages.Create")] [McpToolOptions(Destructive = true)] public async Task SendNotificationAsync( [Description("Recipient user ID")] Guid userId, [Description("Message body")] string message, CancellationToken ct) { ... }
[McpServerTool, Description("Lists notification templates.")] [Authorize(Policy = "Notifications.Templates.Read")] [McpToolOptions(ReadOnly = true)] public async Task<string> ListTemplatesAsync(CancellationToken ct) { ... }}Endpoint-level permission
Section titled “Endpoint-level permission”MapGranitMcpServer() applies Mcp.Server.Access on the HTTP endpoint itself.
A user needs this permission to connect — then per-tool permissions apply on
each invocation.
Runtime permission checks
Section titled “Runtime permission checks”Inject ClaimsPrincipal for dynamic authorization:
[McpServerTool, Description("Returns patient medical history.")]public async Task<string> GetMedicalHistoryAsync( ClaimsPrincipal user, IPermissionChecker permissionChecker, [Description("Patient ID")] Guid patientId, CancellationToken ct){ // Runtime check: user must have access to this specific patient if (!await permissionChecker.IsGrantedAsync("Patients.Records.ReadSensitive")) { return "Access denied: insufficient permissions for sensitive records."; }
var history = await reader.GetHistoryAsync(patientId, ct); return JsonSerializer.Serialize(history);}Output sanitization (GDPR)
Section titled “Output sanitization (GDPR)”Tool responses pass through a sanitization pipeline before reaching the AI agent.
This is wired into the SDK’s AddCallToolFilter — not a parallel abstraction.
[McpRedact] attribute
Section titled “[McpRedact] attribute”Mark sensitive properties on tool response DTOs:
public sealed class PatientResponse{ public Guid Id { get; init; } public string DisplayName { get; init; }
[McpRedact(Strategy = RedactionStrategy.Omit)] public string Email { get; init; }
[McpRedact(Strategy = RedactionStrategy.Omit)] public string PhoneNumber { get; init; }
[McpRedact(Strategy = RedactionStrategy.Hash)] public string NationalId { get; init; }}Redaction strategies
Section titled “Redaction strategies”| Strategy | Behavior | When to use |
|---|---|---|
Omit (default) | Remove property entirely | PII fields: email, phone, address |
Hash | Replace with stable SHA-256 | Correlation IDs: national ID, SSN |
Mask | Replace with j***@***.com | UI-facing tools only (not recommended for LLM) |
Built-in sanitizers
Section titled “Built-in sanitizers”| Sanitizer | Registered by | Behavior |
|---|---|---|
PropertyRedactionSanitizer | Granit.Mcp | Redacts well-known sensitive JSON properties (password, secret, apiKey, token, credential, ssn, connectionString) using Omit or Hash strategies |
ResponseSizeLimitSanitizer | Granit.Mcp | Truncates responses > 50 KB (configurable via MaxResponseSizeBytes) |
ErrorSanitizer | Granit.Mcp.Server | Strips stack traces, connection strings, API keys (Bearer ey..., sk-...), internal paths, Vault tokens, and K8s hostnames from error responses |
The PropertyRedactionSanitizer applies to all tool responses (not just errors). It
scans JSON content blocks for property names matching known sensitive patterns and applies
the appropriate RedactionStrategy. This works in addition to the [McpRedact] attribute —
even if a developer forgets to annotate a DTO, well-known sensitive field names are caught.
Custom sanitizer
Section titled “Custom sanitizer”Implement IMcpOutputSanitizer and register via DI:
internal sealed class AuditTrailSanitizer : IMcpOutputSanitizer{ public ValueTask<CallToolResult> SanitizeAsync( CallToolResult result, IServiceProvider services, CancellationToken ct) { // Custom sanitization logic return ValueTask.FromResult(result); }}
// Registration:services.TryAddEnumerable( ServiceDescriptor.Singleton<IMcpOutputSanitizer, AuditTrailSanitizer>());Sanitizers compose in registration order — each receives the result from the previous sanitizer.
DPoP compatibility
Section titled “DPoP compatibility”Granit.Authentication.DPoP validates Demonstration of Proof-of-Possession
tokens via standard ASP.NET Core middleware. Since MapGranitMcpServer() applies
.RequireAuthorization(), DPoP proofs are validated automatically on every MCP
request — no MCP-specific configuration needed.
Human-in-the-loop (Elicitation)
Section titled “Human-in-the-loop (Elicitation)”The MCP SDK supports server.ElicitAsync() for requesting user input directly
from the end user, bypassing the AI agent. Use this for:
- Destructive operations requiring confirmation
- OAuth authorization flows
- Sensitive data input (API keys, credentials)
[McpServerTool, Description("Permanently deletes all patient records for a tenant.")][Authorize(Policy = "Patients.Records.Manage")][McpToolOptions(Destructive = true)]public async Task<string> PurgePatientRecordsAsync( McpServer server, ICurrentTenant tenant, CancellationToken ct){ ElicitResult result = await server.ElicitAsync(new ElicitRequestParams { Message = $"Confirm permanent deletion of all patient records for tenant {tenant.Id}?", RequestedSchema = new ElicitRequestParams.RequestSchema { Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition> { ["confirm"] = new ElicitRequestParams.BooleanSchema { Description = "Type true to confirm irreversible deletion", }, }, }, }, ct);
if (result.Action != ElicitResult.UserAction.Accept) { return "Operation cancelled by user."; }
// Proceed with deletion... return "Records purged successfully.";}Audit trail
Section titled “Audit trail”Every tool invocation is recorded via the SDK’s AddCallToolFilter:
- Metric:
granit.mcp.tools.invokedwithtenant_id,tool_name,status - Duration:
granit.mcp.request.durationhistogram - Activity: spans under
Granit.Mcpactivity source
For ISO 27001 compliance, integrate with Granit.Auditing by adding a custom
IMcpOutputSanitizer that emits McpToolInvokedEvent via ILocalEventBus.
See also
Section titled “See also”- Tool visibility — opt-in discovery, tenant scope, module scope
- Creating tools — attributes, DI injection, return types
- Setup guide — permissions table and configuration
- Granit Authorization — RBAC system
- Granit Privacy — GDPR personal data management