Build and Secure a C# MCP Server with .NET 11 and Duende IdentityServer

Khalid Abuhakmeh Senior Customer Success Engineer

For me, the Model Context Protocol (MCP) is a must-have for anyone building applications. MCP lets users access any feature you painstakingly built, whether explicitly or implicitly. Once my imagination comes back to earth, I ask the obvious question: who, exactly, is allowed to call this thing? On my laptop, where the AI harness launches the MCP server as a local subprocess, the answer is the machine boundary. The moment I host the same server over HTTP so that others can reach it, that answer becomes more complicated, and anyone can call my tools, ANYONE (yikes).

This post covers two parts. First, building the server: the mcpserver template now ships in the .NET 11 SDK, so a C# MCP server is a single dotnet new away. Second, securing it: putting OAuth 2.0 authentication in front of it with Duende IdentityServer, so only clients you trust can invoke your tools.

If you already know what MCP is, skip ahead to Scaffold the MCP server.

What is an MCP server?

The Model Context Protocol is an open standard that AI applications and agents use to discover and call external tools through a consistent interface. An MCP server exposes your functionality. Examples include a database query, a weather lookup, or an internal API, so an AI host like Claude, Visual Studio Code, or GitHub Copilot can invoke it on the user's behalf. If you have written a web API, the mental model is similar to request/response: an MCP tool is an endpoint, and the agent is the client calling it.

That framing matters for security. Once an MCP server is exposed over a network, each tool it hosts becomes a networked capability that an automated caller can invoke. The identity and permissions you already apply to web APIs, who is calling and what they are allowed to do, apply here as well.

What's New in .NET 11 for MCP Development

Before .NET 11, the mcpserver template lived in a separate NuGet package (Microsoft.McpServer.ProjectTemplates) that you had to install first. With the .NET 11 SDK, the template is bundled in. It shows up in dotnet new list with no extra install, and it is maintained alongside the rest of the web stack. The generated project uses the official C# SDK for MCP and includes a working sample tool, so you get a runnable starting point.

Scaffold the MCP server

With the .NET 11 SDK installed, a project is one command away:

Bash

dotnet new mcpserver -n SampleMcpServer
cd SampleMcpServer
dotnet run

The template scaffolds a Program.cs that registers the MCP server and a sample tool in Tools/RandomNumberTools.cs, which exposes a get_random_number tool you can call right away. By default, the server communicates over stdio, the standard input/output transport. That is perfect for a tool an AI host launches as a local subprocess on your own machine. The host starts your process, talks to it over stdin and stdout, and generates no network traffic.

Here is that sample tool. It is an ordinary C# class, and the [McpServerTool] attribute is what exposes the method to MCP clients:

Csharp

using System.ComponentModel;
using ModelContextProtocol.Server;

internal class RandomNumberTools
{
    [McpServerTool]
    [Description("Generates a random number between the specified minimum and maximum values.")]
    public int GetRandomNumber(
        [Description("Minimum value (inclusive)")] int min = 0,
        [Description("Maximum value (exclusive)")] int max = 100)
    {
        return Random.Shared.Next(min, max);
    }
}

Stdio has no authentication story because it does not need one. The process itself is the trust boundary. Whoever can launch the process can use it. The moment you want that server to live somewhere other than the caller's machine, over HTTP, you have introduced a network boundary, and as you may have guessed, a network boundary must be secured and needs identity.

Moving an MCP Server from Stdio to HTTP

Real deployments rarely stay on stdio. You want your MCP server hosted once and reachable by many clients: a team of developers, a fleet of agents, a backend service. That means HTTP transport.

The template can generate an HTTP server for you directly. The --transport option takes local (stdio, the default) or remote (HTTP):

Bash

dotnet new mcpserver -n SampleMcpServer --transport remote
cd SampleMcpServer

That produces an ASP.NET Core web application targeting net11.0: a Program.cs already configured for HTTP transport and mapped with MapMcp(), the same Tools/RandomNumberTools.cs sample tool, and a reference to the ModelContextProtocol.AspNetCore package. If you started from the default stdio project instead, convert it by switching to the Microsoft.NET.Sdk.Web SDK and replacing its stdio Program.cs with the host shown here. Either way, you end up with a web application you host at a known URL, and that is where .AddMcp() and the JWT bearer pieces come in.

An unauthenticated networked MCP server is an open door to your most sensitive subsystems. Any agent that discovers the URL can list your tools and call them. If one of those tools reads customer data or hits an internal system, you have handed that capability to anonymous callers. Very few of us intend to do that with our production resources.

The secure approach follows standard OAuth 2.0. IdentityServer issues access tokens. The MCP server validates them and checks for the right scope. Clients obtain a token before they call. Nothing about MCP changes these mechanics. You are reusing identity infrastructure your .NET stack already understands.

Secure a .NET MCP Server with OAuth 2.0 and Duende IdentityServer

Here is the generated HTTP project's Program.cs with authentication added. It hosts the MCP server over HTTP, registers the scaffolded RandomNumberTools, validates tokens, and requires the mcp:tools scope. The remote template already references ModelContextProtocol.AspNetCore, so you only add the JWT Bearer package:

Bash

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Next, configure the host. The .AddMcp() call publishes the protected resource metadata that MCP clients read to discover which authorization server to use and which scope to request. The JWT bearer handler validates the token on every call. The .WithHttpTransport() call hosts the server over HTTP, and .WithTools<RandomNumberTools>() exposes the scaffolded tool type.

Csharp

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ModelContextProtocol.AspNetCore.Authentication;

var builder = WebApplication.CreateBuilder(args);

// Host the app at the URL you advertise as the MCP resource
var mcpServerUrl = "https://localhost:7141";
var identityServerUrl = "https://localhost:5001";

builder.Services.AddAuthentication(options =>
    {
        options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Authority = identityServerUrl;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidAudience = mcpServerUrl,
            ValidIssuer = identityServerUrl
        };
    })
    .AddMcp(options =>
    {
        options.ResourceMetadata = new()
        {
            Resource = mcpServerUrl,
            AuthorizationServers = { identityServerUrl },
            ScopesSupported = ["mcp:tools"]
        };
    });

// Require the mcp:tools scope, not just any authenticated caller
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("mcp:tools", policy =>
        policy.RequireAuthenticatedUser()
            .RequireClaim("scope", "mcp:tools"));
});

// Host the MCP server over HTTP and expose the scaffolded tool
builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithTools<RandomNumberTools>();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapMcp().RequireAuthorization("mcp:tools");

app.Run();

Make sure the app actually listens at mcpServerUrl, through applicationUrl in launchSettings.json or your Kestrel configuration, so the address matches the resource you advertise in the metadata.

That RequireAuthorization("mcp:tools") call enforces access. An agent with no token, or an invalid one, gets a 401 whose WWW-Authenticate header points to the server's protected resource metadata (/.well-known/oauth-protected-resource), telling the client where to authenticate. An agent that authenticates but presents a token without the mcp:tools scope gets a 403. Only a token carrying the scope gets your tools.

On the IdentityServer side, you define the mcp:tools scope, tie it to an API resource for your MCP server, and decide how clients can register. For agent scenarios, that registration is often Dynamic Client Registration (DCR). DCR is not automatic: you enable a registration endpoint deliberately and protect it according to your policy. Once it is configured, a client you have never seen before can register itself at runtime and obtain a token through the standard authorization code flow with PKCE (pronounced "pixy"), which keeps you from hand-configuring every agent that might ever connect.

You do not have to build the IdentityServer host and client from scratch to see this working. Duende maintains a complete, runnable version, so this post stays focused on the MCP server and its authentication.

Get the complete sample

Duende's Securing an MCP Server with IdentityServer quickstart walks through the complete flow end to end: the IdentityServer host, the MCP server, and a console client that registers through Dynamic Client Registration and signs in. Its reference implementation is the finished sample you can clone and run. That is the natural next step after the server above.

Where to go deeper

This post should get you moving in the right direction. Once your server authenticates, these posts cover the harder questions that come next:

For the broader picture, including proof-of-possession and token lifetimes for agents, see the AI and MCP documentation.

Conclusion: Bringing Production Security to MCP

The SDK template hands you a working MCP server in one command, but doesn't give you access control. HTTP needs access control, and OAuth 2.0 with Duende IdentityServer gives you the same tokens, scopes, and validation you already use for your APIs, now in front of your tools.

Build the server with dotnet new mcpserver. Secure it with the quickstart. Ship something your security team will sign off on.

About the author

Khalid Abuhakmeh Senior Customer Success Engineer

I'm a Senior Customer Success Engineer, where I help teams succeed with Duende IdentityServer. I've worked with .NET for over a decade, including ASP.NET Core, C#, and web development.

Related Articles