{"slug": "give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually", "title": "Give Your .NET REST API an AI Mouth: Adding MCP So Claude and Gemini Can Actually Use It", "summary": "A developer at T1Tech detailed how to give a .NET REST API an AI interface by adding the Model Context Protocol (MCP), enabling AI clients like Claude and Gemini to discover and call API endpoints as tools. The approach uses the official C# SDK to create a thin MCP server that translates between AI clients and the existing JWT-secured API, with authentication handled via OAuth 2.1 to preserve user identity.", "body_md": "Last quarter a product manager dropped a Slack message that a lot of us are getting now: *\"Can I just ask Claude to pull the open invoices from our system?\"*\n\nWe already had the API. Fully built, JWT-secured, battle-tested in production. The problem was never the data — it was that an LLM has no idea our `/api/invoices?status=open`\n\nendpoint exists, and even if it did, it can't read our OpenAPI spec and authenticate itself.\n\nThe Model Context Protocol (MCP) is the missing adapter. This is how you bolt it onto an API you already own, in an afternoon, and what to do about authentication before security reviews it.\n\nMCP is an open protocol that lets AI clients (Claude Desktop, Gemini, Cursor, and others) **discover and call your capabilities as \"tools.\"** You do not rewrite your API. You stand up a thin MCP server that exposes selected endpoints as tools and forwards the calls.\n\n```\n   AI Chat (Claude / Gemini)\n            │   MCP protocol (JSON-RPC)\n            ▼\n   ┌──────────────────────┐\n   │   MCP Server (.NET)  │  ← [McpServerTool] methods\n   │   - GetOpenInvoices  │\n   │   - CreateTicket     │\n   └─────────┬────────────┘\n             │   HttpClient + Bearer/API key\n             ▼\n   ┌──────────────────────┐\n   │  Existing REST API   │  ← unchanged, still JWT-secured\n   └──────────────────────┘\n```\n\nThe MCP server is a translator: it speaks JSON-RPC to the model and plain HTTP to your existing API. Your business logic never moves.\n\nUse the official C# SDK (maintained together with Microsoft). Add it to a new minimal ASP.NET Core project so you can host a **remote** MCP server over Streamable HTTP:\n\n```\ndotnet add package ModelContextProtocol.AspNetCore\nWebApplicationBuilder builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services\n    .AddMcpServer()\n    .WithHttpTransport()          // Streamable HTTP for remote clients\n    .WithToolsFromAssembly();     // discover [McpServerTool] methods\n\n// Typed client to your EXISTING API\nbuilder.Services.AddHttpClient(\"BackendApi\", static client =>\n{\n    client.BaseAddress = new Uri(\"https://api.internal.t1tech.com/\");\n});\n\nWebApplication app = builder.Build();\napp.MapMcp();                     // exposes the /mcp endpoint\napp.Run();\n```\n\nThat is the entire host. `MapMcp()`\n\nwires up discovery, so any compliant AI client can enumerate your tools.\n\nA tool is just a method. The attributes and XML-style descriptions are not decoration — the model reads them to decide *when* and *how* to call you. Be explicit; vague descriptions cause hallucinated arguments.\n\n```\n[McpServerToolType]\npublic sealed class InvoiceTools\n{\n    private readonly IHttpClientFactory _httpClientFactory;\n\n    public InvoiceTools(IHttpClientFactory httpClientFactory)\n    {\n        _httpClientFactory = httpClientFactory;\n    }\n\n    [McpServerTool]\n    [Description(\"Returns open (unpaid) invoices for a given customer ID.\")]\n    public async Task<string> GetOpenInvoices(\n        [Description(\"The numeric customer identifier.\")] int customerId,\n        CancellationToken cancellationToken)\n    {\n        HttpClient client = _httpClientFactory.CreateClient(\"BackendApi\");\n\n        HttpResponseMessage response = await client.GetAsync(\n            $\"api/invoices?customerId={customerId}&status=open\",\n            cancellationToken);\n\n        response.EnsureSuccessStatusCode();\n        return await response.Content.ReadAsStringAsync(cancellationToken);\n    }\n}\n```\n\nNotice there is no token in that call yet. That is the entire fight, and the next two sections are where production engineering actually happens.\n\nYour API trusts a JWT. The naive instinct is to bake a service account token into the MCP server. **Do not.** That collapses every user into one identity and hands the LLM god-mode over your data.\n\nThe correct model: the MCP server is an **OAuth 2.1 Resource Server.** The AI client authenticates the *human*, receives a token, and passes it through. The MCP Authorization spec standardizes this with Protected Resource Metadata (RFC 9728), so the client can discover where to log in.\n\n```\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(static options =>\n    {\n        options.Authority = \"https://login.t1tech.com/\";\n        options.Audience = \"mcp-invoice-server\";\n    });\n\nbuilder.Services.AddAuthorization();\n\n// ...after Build()\napp.UseAuthentication();\napp.UseAuthorization();\napp.MapMcp().RequireAuthorization();\n```\n\nThen forward the caller's identity instead of a hardcoded secret. Grab the incoming token from `HttpContext`\n\nand attach it downstream:\n\n```\npublic InvoiceTools(\n    IHttpClientFactory httpClientFactory,\n    IHttpContextAccessor httpContextAccessor)\n{\n    _httpClientFactory = httpClientFactory;\n    _httpContextAccessor = httpContextAccessor;\n}\n\nprivate async Task<HttpClient> CreateAuthorizedClientAsync(CancellationToken ct)\n{\n    HttpClient client = _httpClientFactory.CreateClient(\"BackendApi\");\n\n    string? token = await _httpContextAccessor.HttpContext!\n        .GetTokenAsync(\"access_token\");\n\n    if (!string.IsNullOrEmpty(token))\n    {\n        client.DefaultRequestHeaders.Authorization =\n            new AuthenticationHeaderValue(\"Bearer\", token);\n    }\n\n    return client;\n}\n```\n\nNow the existing API sees the *real* user's scopes and roles. Your authorization rules keep working, untouched. The LLM cannot read an invoice the human behind it could not read.\n\nJWT/OAuth is right for interactive chat where a human is present. But not every MCP consumer is a person clicking \"Authorize.\" Choose the scheme by *who* connects:\n\n`stdio`\n\nserver or machine-to-machine automation → API key.\n\n```\n// API key path — for non-interactive / stdio clients\nstring apiKey = builder.Configuration[\"Backend:ApiKey\"]\n    ?? throw new InvalidOperationException(\"Missing Backend:ApiKey.\");\n\nclient.DefaultRequestHeaders.Add(\"X-Api-Key\", apiKey);\n```\n\nMy recommendation for a team shipping this: **default to OAuth 2.1 JWT pass-through for anything remote**, and reserve API keys for headless integrations where you can mint *narrowly scoped, per-integration* keys and rotate them. Treat a static API key like a password — short TTL, per-client, logged, revocable. Never a single shared secret with full API surface.\n\nCode is abstract until you watch a real request move through it. Here is the end-to-end round trip when a user types *\"Show me the open invoices for customer 4821\"* into Claude.\n\nBut first — the question everyone asks: **where does Claude get that JWT?** It is not magic and Claude does not \"have your token.\" The MCP client obtains it through a standard OAuth 2.1 handshake the *first* time it connects, and this is worth seeing on its own.\n\n```\n[0a] Claude → MCP Server:  first call, no token\n     ← 401 Unauthorized\n       WWW-Authenticate: Bearer resource_metadata=\"https://mcp.t1tech.com/.well-known/...\"\n\n[0b] Claude reads Protected Resource Metadata (RFC 9728)\n     → learns which Authorization Server (your IdP) issues tokens\n\n[0c] Claude runs OAuth 2.1 Authorization Code + PKCE:\n     → opens a browser window\n     → USER logs in at login.t1tech.com and clicks \"Allow\"\n     → IdP redirects back with an auth code\n     → Claude exchanges code → access_token (JWT) + refresh_token\n\n[0d] Claude securely stores the token and reuses it.\n     Refresh happens silently; the login prompt does NOT repeat each message.\n```\n\nThe critical point: **the human authenticates directly with your identity provider, not with Claude.** Claude never sees a password. It receives a scoped, expiring JWT through the same OAuth flow your web frontend would use — and your MCP server, as the Resource Server, advertises where that login lives via the 401 challenge in Step 0a.\n\nOn the .NET side, `RequireAuthorization()`\n\nalready returns the 401. The one extra thing you owe the client is the discovery pointer in Step 0b — register the MCP server as a protected resource so the SDK emits the `resource_metadata`\n\nchallenge and serves the metadata document:\n\n```\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddMcp(static options =>\n    {\n        // Advertises this server as an OAuth 2.1 Resource Server\n        options.ResourceMetadata = new ProtectedResourceMetadata\n        {\n            AuthorizationServers = { new Uri(\"https://login.t1tech.com/\") }\n        };\n    });\n```\n\nThat single registration is what turns \"Claude somehow has a token\" into a discoverable, spec-compliant login the client can drive on its own.\n\nOnly *after* that handshake does the everyday request loop below run:\n\n```\n[1] USER  →  Claude Chat\n    \"Show me the open invoices for customer 4821.\"\n\n[2] Claude → MCP Server:  discovery (once, on connect)\n    \"What tools do you have?\"\n    ← [{ name: \"GetOpenInvoices\",\n         description: \"Returns open (unpaid) invoices for a customer ID.\",\n         params: { customerId: int } }]\n\n[3] Claude reasons:\n    intent = list open invoices  →  tool = GetOpenInvoices\n    extracts argument  →  customerId = 4821\n\n[4] Claude → MCP Server:  tools/call (JSON-RPC)\n    Authorization: Bearer <the signed-in user's JWT>\n    { \"name\": \"GetOpenInvoices\", \"arguments\": { \"customerId\": 4821 } }\n\n[5] MCP Server validates the JWT, then forwards downstream:\n    GET /api/invoices?customerId=4821&status=open\n    Authorization: Bearer <same JWT — user identity preserved>\n\n[6] Existing REST API applies the user's scopes → returns JSON\n\n[7] MCP Server → Claude:  tool result (raw JSON)\n    [{ \"id\": 5567, \"amount\": 1200.00, \"due\": \"2026-09-15\" }, ...]\n\n[8] Claude → USER:\n    \"Customer 4821 has 2 open invoices totaling $2,050 —\n     one due Sep 15 ($1,200) and one due Oct 2 ($850).\"\n```\n\nTwo things are worth pausing on.\n\n**Step 2 happens once, not per message.** The client caches your tool catalog on connect. This is why the `[Description]`\n\ntext is load-bearing — the model chooses tools purely from it, long before your code ever runs.\n\n**Step 4 to Step 6 is the whole security story.** The JWT the human logged in with is the same JWT that reaches your API. Claude never sees a service credential, and it can never retrieve an invoice the signed-in user isn't authorized to see. The model orchestrates; your API still decides.\n\nIf you want to see this concretely, the tool result at Step 7 is exactly the string your `GetOpenInvoices`\n\nmethod returned — Claude does the natural-language summarization in Step 8. You return data; the model handles the prose.\n\nThe payoff is not \"AI hype.\" It is that a capability you already built becomes usable by a new class of client with near-zero duplication.\n\nYour team writes one thin tool method per endpoint you want to expose — not a second API. Because identity flows through, your security posture is *unchanged*: the same JWT, the same scopes, the same audit trail. And because tools are just decorated C# methods, they unit-test like any other service, and new endpoints become new tools in minutes.\n\nThe long-term maintainability win is that MCP is a stable seam. When you swap Claude for Gemini, or add a third client, nothing changes on your side. You built the adapter once.\n\n`HttpClient`\n\n. Business logic stays put.`[Description]`\n\ntext. Vague text produces bad calls.`stdio`\n\nor M2M integrations.", "url": "https://wpnews.pro/news/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually", "canonical_source": "https://dev.to/karamkhoury88/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually-use-it-3fi4", "published_at": "2026-09-02 10:18:11+00:00", "updated_at": "2026-09-02 10:53:06.883419+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "artificial-intelligence"], "entities": ["T1Tech", "Claude", "Gemini", "Model Context Protocol", "Microsoft", "ASP.NET Core"], "alternates": {"html": "https://wpnews.pro/news/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually", "markdown": "https://wpnews.pro/news/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually.md", "text": "https://wpnews.pro/news/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually.txt", "jsonld": "https://wpnews.pro/news/give-your-net-rest-api-an-ai-mouth-adding-mcp-so-claude-and-gemini-can-actually.jsonld"}}