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.
[SensitiveData] — unified data protection attribute
Section titled “[SensitiveData] — unified data protection attribute”Granit uses a single cross-cutting attribute [SensitiveData] from Granit.DataProtection
(in the Granit base package) to mark sensitive properties on any entity or DTO. The
same annotation is consumed by auditing, MCP output sanitization, logging, and GDPR exports.
using Granit.DataProtection;
public sealed class PatientResponse{ public Guid Id { get; init; } public string DisplayName { get; init; }
[SensitiveData(Level = Sensitivity.Confidential)] public string Email { get; init; }
[SensitiveData(Level = Sensitivity.Confidential)] public string PhoneNumber { get; init; }
[SensitiveData(Level = Sensitivity.Restricted, Mode = SensitiveDataMode.Hash)] public string NationalId { get; init; }}Sensitivity levels (ISO 27001 A.8.2)
Section titled “Sensitivity levels (ISO 27001 A.8.2)”| Level | Examples | MCP behavior |
|---|---|---|
Internal (default) | First name, last name, username | Not redacted in MCP (below threshold) |
Confidential | Email, phone, IP address, DOB | Redacted — Mask by default |
Restricted | Password, token, SSN, health data | Redacted — use Omit or Hash |
Protection modes (SensitiveDataMode)
Section titled “Protection modes (SensitiveDataMode)”| Mode | Behavior | When to use |
|---|---|---|
Mask (default) | Replace with "***" | PII fields: email, phone, address |
Omit | Remove property entirely | Secrets: passwords, tokens, keys |
Hash | Replace with stable SHA-256 | Correlation IDs: national ID, SSN |
Built-in sanitizers
Section titled “Built-in sanitizers”| Sanitizer | Registered by | Behavior |
|---|---|---|
PropertyRedactionSanitizer | Granit.Mcp | Reads SensitivePropertyRegistry (auto-discovered from [SensitiveData] annotations at startup, threshold: Confidential+) plus well-known secret property names (password, secret, apiKey, token, credential, ssn, connectionString) |
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). At
startup, the SensitivePropertyRegistry scans all loaded Granit assemblies for [SensitiveData]
properties and builds an O(1) lookup map. This means annotating an entity property in one module
(e.g., UserCacheEntry.Email) automatically protects it in every MCP tool response — even if a
developer forgets to design the DTO carefully.
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