Skip to content

Let AI Agents Use Your .NET Modules

Your API already exposes endpoints for humans. What if AI agents could discover and invoke them too — with the same permissions, the same multi-tenancy isolation, and automatic PII redaction?

That’s what the Model Context Protocol (MCP) enables. And with the new Granit.Mcp module family, any Granit module can become an MCP server in 3 steps.

MCP is an open protocol (created by Anthropic, backed by Microsoft) that lets AI agents — Claude Desktop, GitHub Copilot, Cursor, or your own custom agents — discover and invoke tools exposed by your application over HTTP.

Think of it as OpenAPI for AI agents, but bidirectional: the agent lists available tools, picks the right one based on the user’s intent, passes typed parameters, and gets structured results back.

sequenceDiagram
    participant User
    participant Agent as AI Agent (Claude)
    participant App as Your Granit App

    User->>Agent: "Show me unpaid invoices from last week"
    Agent->>App: tools/list
    App-->>Agent: [SearchInvoices, GetInvoiceDetails, ...]
    Agent->>App: tools/call SearchInvoices {status: "unpaid", from: "2026-03-19"}
    App-->>Agent: [{id: 42, amount: 1250, ...}, ...]
    Agent->>User: "Found 3 unpaid invoices totaling EUR 4,200..."

The agent doesn’t need custom integrations, API documentation, or prompt engineering. It reads tool descriptions, decides which ones to call, and handles the conversation.

The official MCP C# SDK handles the protocol. Granit adds what enterprise apps need:

ConcernMCP SDK aloneWith Granit.Mcp
Tool discoveryManual registrationAuto-discovery from module assemblies
AuthorizationRoll your own[Authorize(Policy = "...")] with Granit RBAC
PII in responsesYou handle it[SensitiveData] + sanitization pipeline (GDPR)
Multi-tenancyNot supported[McpTenantScope] + ICurrentTenant injection
Error leakageStack traces exposedErrorSanitizer strips connection strings and traces
ObservabilityBasicOpenTelemetry metrics + activity source
Module scopingAll tools alwaysEnabledModules config limits exposure

Four new packages, zero new abstractions. Everything plugs into the SDK’s native filter pipeline.

flowchart LR
    subgraph app["Your Granit Application"]
        direction TB
        T1["Invoice tools"] --> MCP["Granit.Mcp\nAuto-discovery\n+ Filters"]
        T2["Workflow tools"] --> MCP
        T3["BlobStorage tools"] --> MCP
        MCP --> SRV["Granit.Mcp.Server\nHTTP + Auth"]
    end

    AI["AI Agent"] -- "/mcp" --> SRV

    subgraph ext["External"]
        ERP["ERP Server"]
    end

    CLI["Granit.Mcp.Client"] -- "HTTP" --> ERP
    CLI --> AIM["Granit.AI.Mcp\n→ IChatClient"]

    style app fill:#1a1a2e,stroke:#e94560,color:#eee
    style ext fill:#1a1a2e,stroke:#0f3460,color:#eee
    style AI fill:#e94560,stroke:#e94560,color:#fff
PackagePurpose
Granit.McpAuto-discovery, sanitization pipeline, metrics
Granit.Mcp.ServerASP.NET Core HTTP transport, [Authorize], permissions
Granit.Mcp.ClientConnect to external MCP servers (HTTP + stdio)
Granit.AI.McpInject MCP tools into AI workspaces, sampling guard
Terminal window
dotnet add package Granit.Mcp.Server
Program.cs
builder.AddGranit(granit =>
{
granit.AddModule<GranitMcpServerModule>();
});
app.MapGranitMcpServer();

That’s it for infrastructure. The MCP endpoint is live at /mcp, protected by Mcp.Server.Access permission.

Add a class in your module with [McpServerToolType] and [McpExposed]:

InvoiceMcpTools.cs
[McpServerToolType, McpExposed]
[Authorize(Policy = "Invoicing.Invoices.Read")]
public sealed class InvoiceMcpTools(IInvoiceReader reader)
{
[McpServerTool, Description("Search invoices by status and date range.")]
[McpToolOptions(ReadOnly = true)]
public async Task<string> SearchInvoicesAsync(
[Description("Invoice status: draft, sent, paid, overdue")] string status,
[Description("Start date (ISO 8601)")] DateOnly from,
ICurrentTenant tenant, // DI-injected, invisible to agent
CancellationToken ct)
{
var invoices = await reader.SearchAsync(status, from, ct);
return JsonSerializer.Serialize(invoices);
}
[McpServerTool, Description("Permanently voids an invoice.")]
[Authorize(Policy = "Invoicing.Invoices.Manage")]
[McpToolOptions(Destructive = true)]
public async Task<string> VoidInvoiceAsync(
[Description("Invoice ID")] Guid invoiceId,
[Description("Reason for voiding")] string reason,
CancellationToken ct)
{
await reader.VoidAsync(invoiceId, reason, ct);
return $"Invoice {invoiceId} voided.";
}
}

GranitMcpModule auto-discovers your tool class from the module assembly. The AI agent sees SearchInvoices and VoidInvoice with their descriptions and parameter schemas.

The ICurrentTenant and CancellationToken parameters are invisible to the agent — the SDK resolves them from DI automatically.

You might expect a [RequiresMcpPermission] attribute. We built one, then deleted it.

The SDK supports standard [Authorize] via .AddAuthorizationFilters(). Granit’s DynamicPermissionPolicyProvider already maps policy names to RBAC permissions. So [Authorize(Policy = "Invoicing.Invoices.Read")] just works — same attribute you use on Minimal API endpoints.

  • Class-level: all tools in the class require the permission
  • Method-level: override per tool for fine-grained control
  • ClaimsPrincipal: injectable into any tool method for runtime checks

MCP tool responses are sanitized before they leave your app. This happens in the SDK’s AddCallToolFilter pipeline — not a parallel abstraction.

PatientResponse.cs
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.Restricted, Mode = SensitiveDataMode.Hash)]
public string NationalId { get; init; }
}

One attribute, three sensitivity levels (ISO 27001 A.8.2), three protection modes:

LevelModeBehaviorWhen to use
InternalMask"***"Names, usernames
ConfidentialMask"***"Email, phone, IP
RestrictedOmitRemove entirelyPasswords, tokens
RestrictedHashStable SHA-256Correlation IDs (SSN)

The ErrorSanitizer also strips stack traces and connection strings from error responses — no accidental Server=db.prod;Password=... leaking to an AI agent.

Three visibility filters compose independently:

  1. [McpExposed] — required in production (Explicit mode). Prevents accidental exposure of internal services
  2. [McpTenantScope(RequireTenant = true)] — hides tools when no tenant context is active
  3. EnabledModules — configuration-driven whitelist to reduce context window saturation (AI agents work better with fewer, well-described tools)
appsettings.json
{
"Mcp": {
"Server": {
"EnabledModules": ["Invoicing", "Scheduling", "BlobStorage"]
}
}
}

The flow works both ways. Granit.Mcp.Client provides a named factory for connecting to MCP servers exposed by other services:

Program.cs
builder.AddGranitMcpClient();
appsettings.json
{
"Mcp": {
"Client": {
"Connections": {
"erp": { "Url": "https://erp.internal/mcp" },
"local-tools": {
"Transport": "stdio",
"Command": "python",
"Arguments": ["-m", "data_analyzer_mcp"]
}
}
}
}
}

Granit.AI.Mcp bridges these external tools into AI workspaces — MCP tools become AITool instances in your IChatClient pipeline. The SamplingGuard prevents external servers from abusing your LLM budget (disabled by default, rate-limited when enabled).

  • MCP lets AI agents discover and invoke your application tools — no custom integrations, no prompt engineering per endpoint
  • Granit.Mcp wraps the official SDK with auto-discovery, RBAC authorization, GDPR sanitization, multi-tenancy, and observability
  • 3 lines to expose your first tool: install, register, annotate
  • Standard [Authorize] — no custom auth attributes, uses your existing permission definitions
  • PII is protected by default[SensitiveData] is a single cross-cutting attribute consumed by auditing, MCP, logging, and GDPR exports
  • 4 packages, 31 tests, zero circular dependencies — ready for production