.NET 11

.NET 11 builds you an MCP server in one command. Securing it is still your job.

Since 10 November 2026 the MCP server template ships with the .NET SDK. That makes it easy to put your business logic in front of an AI agent, and just as easy to forget that the agent is a client you cannot trust.

01
MCP Client + LLM
Untrusted. The model picks the tool and the arguments.
↓
02
ASP.NET Core
Validates token, audience and scope.
↓
03
MCP Tool
Acts as the signed-in user, nothing more.
The template

dotnet new mcpserver, no extra install

.NET 11 is out since 10 November 2026, a Standard Term Support release with two years of support. Besides union types in C# 15 and a faster CLI it brings one small change with a large effect: the mcpserver template is now part of the SDK. Up to .NET 10 you had to install Microsoft.McpServer.ProjectTemplates first.

The template builds on the official MCP C# SDK and creates a runnable server with a sample tool. By default you get a console app that talks stdio, the transport that local clients such as VS Code, Visual Studio or Claude Code start as a child process.

Terminal
dotnet new mcpserver -o OrdersMcp
dotnet new mcpserver -o OrdersMcp --transport remote
Option Effect
-t local Default. Console app using the stdio transport, packed as a .NET tool with PackageType McpServer for NuGet.
-t remote ASP.NET Core app using the HTTP transport via MapMcp(), stateless by default.
--aot Prepares the server for publishing with Native AOT.
--self-contained On by default. Ships the runtime with the server, so the client machine needs no installed .NET.
-f net10.0 Target framework, net11.0 by default, net10.0 for the LTS release.

A tool is a plain method. [McpServerTool] exposes it as get_random_number, and the [Description] texts are what the model reads to decide when and how to call it. Treat them like API documentation for a very literal reader. One detail: the template references ModelContextProtocol 2.1.0, newer SDK versions are already on NuGet.

Tools/RandomNumberTools.cs
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);
    }
}
Building blocks

Where MCP fits in the .NET AI stack

MCP is one of four building blocks Microsoft names for AI in .NET. Each solves its own problem, and they combine.

Microsoft.Extensions.AI

IChatClient and IEmbeddingGenerator as one abstraction across model providers, with DI, caching, logging and OpenTelemetry as middleware.

Microsoft.Extensions.VectorData

Storing embeddings and semantic search behind one abstraction, for RAG over your own data.

Microsoft Agent Framework

Agents with tools, conversation state and multi-step workflows. Agents can use MCP tools directly.

Model Context Protocol

The open protocol between an AI client and your software. Your server exposes tools, resources and prompts, any MCP-capable agent can use them.

Trust

An MCP client is a client like any other

An MCP server is an API. The only difference to a REST API is who is calling: not your own frontend, but a language model that reads text from emails, tickets, web pages or documents and decides which tool to call with which arguments. One prepared sentence in such a text (prompt injection) is enough to make it call something nobody intended.

So the same rule applies as for every browser: the server decides what is allowed, never the client. And the remote template starts open, without any authentication. Anyone who can reach the endpoint can call every tool.

The model chooses the arguments

A parameter customerId is an invitation. Derive identity and tenant from the validated token, not from what the model passes in.

Hidden is not forbidden

Filtering tools/list is convenient, but a client can call any tool name directly. The check has to happen on the server at call time.

The server acts as the user

A tool must never do more than the signed-in user could do in your normal application. A service account with full access turns every prompt injection into a data leak.

Authentication

Securing the remote template with OAuth

The MCP specification uses OAuth 2.1 for HTTP servers. Your server is the protected resource, an existing identity provider such as Entra ID, Keycloak or Auth0 issues the tokens. ModelContextProtocol.AspNetCore brings the missing pieces: AddMcp() publishes the Protected Resource Metadata (RFC 9728) so clients find the login, AddAuthorizationFilters() makes [Authorize] work on individual tools.

Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using ModelContextProtocol.AspNetCore.Authentication;
using ModelContextProtocol.Authentication;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com";
        options.Audience = "https://mcp.example.com";
    })
    .AddMcp(options =>
    {
        options.ResourceMetadata = new ProtectedResourceMetadata
        {
            AuthorizationServers = ["https://login.example.com"],
            ScopesSupported = ["orders.read", "orders.write"]
        };
    });

builder.Services.AddAuthorization(options =>
    options.AddPolicy("orders.write", policy => policy.RequireClaim("scope", "orders.write")));

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options => options.Stateless = true)
    .AddAuthorizationFilters()
    .WithTools<OrderTools>();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp().RequireAuthorization();
app.Run();

Plain ASP.NET Core does the rest. JWT Bearer validates signature, issuer and audience, RequireAuthorization() locks the whole endpoint, and one policy per scope protects individual tools. The tool itself takes a ClaimsPrincipal parameter and knows the user from the token, not from the model.

Tools/OrderTools.cs
internal class OrderTools
{
    [McpServerTool, Description("Lists the open orders of the signed-in customer.")]
    public string[] GetOpenOrders(ClaimsPrincipal user) =>
        [$"orders for {user.FindFirstValue(ClaimTypes.NameIdentifier)}"];

    [McpServerTool, Description("Cancels one order of the signed-in customer.")]
    [Authorize(Policy = "orders.write")]
    public string CancelOrder(ClaimsPrincipal user, [Description("Order number")] string orderId) =>
        $"cancel {orderId} for {user.FindFirstValue(ClaimTypes.NameIdentifier)}";
}

What happens on the wire

  • Without a token the server answers 401 with a WWW-Authenticate header that points to the metadata. That is how an MCP client knows where to sign in.
  • /.well-known/oauth-protected-resource names the authorization server and the supported scopes.
  • With scope orders.read, tools/list only returns get_open_orders. Add orders.write and cancel_order appears as well.
  • If the client calls cancel_order anyway, the server refuses. The filtered list is convenience, the check at call time is the protection.
  • A token issued for another application of the same identity provider gets 401, because the server only accepts its own audience. In our first test run the audience was misconfigured, and the foreign token went straight through.
  • The counter-example: a tool with a customerId parameter. Signed in as alice, the call with bob returns bob's orders. Authentication was correct, the tool was not.
Responses from the sample server
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="http://localhost:6182/.well-known/oauth-protected-resource/"

GET /.well-known/oauth-protected-resource
{"resource":"http://localhost:6182","authorization_servers":["https://login.example.com"],
 "bearer_methods_supported":["header"],"scopes_supported":["orders.read","orders.write"]}

tools/call cancel_order (scope orders.read)
{"error":{"code":-32600,"message":"Access forbidden: This tool requires authorization."}}

tools/list (token for https://other-app.example.com)
HTTP/1.1 401 Unauthorized

tools/call get_orders_for {"customerId":"bob"} (signed in as alice)
{"result":{"content":[{"type":"text","text":"[\"orders for bob\"]"}]}}

Tested on 12 November 2026 with .NET SDK 11.0.100, the template's ModelContextProtocol.AspNetCore 2.1.0 and tokens from dotnet user-jwts.

Checklist

Before your MCP server goes into production

Validate the audience

Only accept tokens issued for this server. Without audience validation, a token for another application of the same identity provider works on your MCP server too.

No token passthrough

The MCP specification forbids passing the client's token on to downstream APIs. Get a token of your own for those calls, for example via On-Behalf-Of, so every system checks its own permissions.

Small scopes per tool

Reading and writing belong in separate scopes. Clients then request only what the task needs, and a stolen token does less damage.

stdio is not a sandbox

A local server runs with the rights of the user who starts it, and the specification defines no OAuth flow for stdio. Pass secrets via environment variables, never as tool arguments, and give the server only the access it needs.

Confirmation for irreversible actions

Cancelling, deleting, paying: let a human confirm before the tool acts, in the client or via elicitation from the server, which needs the stateful HTTP mode. The model may suggest, it should not decide alone.

Log every tool call

Who called which tool with which arguments, and what came back? Logging and OpenTelemetry in ASP.NET Core make this traceable without extra infrastructure.

FAQ

Frequently asked questions about MCP servers in .NET 11

Is the MCP server template included in the .NET 11 SDK?

Yes. Since .NET 11, released on 10 November 2026, dotnet new mcpserver works without installing anything. Up to .NET 10 the template came from the separate Microsoft.McpServer.ProjectTemplates package.

Does the .NET MCP server template include authentication?

No. The remote template with HTTP transport starts without authentication, so anyone who can reach the endpoint can call every tool. You add it with ASP.NET Core authentication, JWT Bearer and AddMcp() from ModelContextProtocol.AspNetCore.

How do I restrict a single MCP tool to certain users in .NET?

Call AddAuthorizationFilters() on the MCP server builder and put [Authorize] with a policy or role on the tool method or class. Users without permission do not see the tool in tools/list, and a direct call is rejected.

Why can't an MCP server trust its client?

The client is driven by a language model that reads untrusted text and chooses tools and arguments itself. A prompt injection can make it call any tool with any value, so the server has to check identity and permissions on every call, exactly as for a browser.

Do stdio MCP servers need OAuth?

No. The authorization part of the MCP specification applies to HTTP transports. A stdio server runs locally with the rights of the user who starts it and reads its credentials from the environment.

May an MCP server forward the client's access token to other APIs?

No. The MCP specification forbids token passthrough. The server only accepts tokens issued for itself and gets its own token for downstream calls, for example with the On-Behalf-Of flow.

Your software

Making your .NET software agent-ready, safely

We build MCP servers for existing .NET applications, connected to your identity provider and with exactly the permissions your users already have. No service account with full access, no tool call nobody can trace.

More on AI agents for your software

Get In Touch

Tell us what you're planning.

You talk directly to the people who will write your code.

  1. You get in touch: We do our best to reply within 24 hours.
  2. We get to know each other: 30 minutes. You talk, we ask. No pitch.
  3. We decide together: How to move forward, or honestly, that we're not the right fit.
0 / 2000

A few sentences are enough. What's holding you back today, is there a deadline?

We only use your details to reply to you.

Rather talk directly? 30 minutes on Google Meet with Steven Giesel.

Pick a time
Or email us at contact@bitspire.ch