How to Build an MCP Server in ASP.NET Core: A Real-World Walkthrough

Every team that ships a .NET API eventually runs into the same question in 2026: how do you let an AI assistant actually use your API, not just read documentation about it, but call it, pass real arguments, and act on the result? That is exactly the problem the Model Context Protocol solves, and in this walkthrough I will show you how to build an MCP server in ASP.NET Core that turns your existing endpoints into tools an AI agent can call safely. By Celin Daniel, Co-founder of Coding Droplets, with 13+ years building .NET in production.
I have wired MCP into a couple of production .NET services over the last few months, and what surprised me was how little new code it actually takes once the mental model clicks - and how easy it is to get the security boundary wrong. The walkthrough here stays conceptual with short snippets; if you want the complete server, with authentication, error handling, and a working test client wired together end to end, the full implementation is on Patreon, ready to run and adapt.
Getting MCP right in production also means handling tool safety, cost, and observability at the same time, not as an afterthought. That is the part most tutorials skip. Chapter 14 of the AI-Powered .NET APIs course builds an MCP server over one real ASP.NET Core support API in C#, with the auth and guardrails already connected, so the context is always clear.
What Is an MCP Server and What Problem Does It Solve?
An MCP server is a small adapter that sits in front of your application and advertises a set of tools (and optionally resources and prompts) that any MCP-aware AI client can discover and invoke over a standard protocol. Instead of hand-writing a custom function-calling integration for every model and every agent, you expose your capabilities once and any compliant client can use them.
In practice it solves three concrete problems:
No more bespoke glue per agent. One MCP server works with Claude, VS Code, GitHub Copilot, and your own
IChatClient-based app without rewrites.A clean trust boundary. The model never touches your database directly. It asks your server to run a named tool, and your server decides what is allowed.
Discoverability. Clients fetch the tool list at runtime, so adding a capability is a server-side change, not a client release.
The Model Context Protocol is an open standard; the official specification and the official C# SDK are both worth bookmarking before you start.
The Business Problem: An Assistant That Can Act, Not Just Chat
The project that pushed me into MCP was an internal support tool. The support team wanted to ask an assistant questions like "what is the status of order ORD-1042 and has the refund cleared?" and get a grounded answer, not a hallucinated one.
We already had a perfectly good ASP.NET Core API with GET /orders/{id} and GET /refunds/{id}. The naive approach is to paste those responses into a prompt by hand. That does not scale, and it puts a human in the loop for every lookup. What we actually wanted was for the assistant to call those endpoints itself, on demand, with the same authorization rules our API already enforced.
This is the natural next step after structured outputs and tool calling: tool calling lets one model invoke your functions, but MCP standardizes that so any client can, and so the tools live in a server you control rather than inside each app.
Design Decisions Before You Write Any Code
Three decisions shaped the whole build, and I would make the same calls again:
Transport: HTTP, not stdio. The SDK supports a stdio transport (great for a local CLI tool the model launches as a child process) and a streamable HTTP transport. For a server that lives alongside a web API and serves multiple clients, HTTP is the right fit.
Read tools auto-registered, write tools gated. Anything that only reads data is safe to expose automatically. Anything that moves money or mutates state gets an explicit allow-list and a human confirmation step. More on this below.
Reuse the existing service layer. The MCP tools call the same application services the controllers already use, so validation and authorization are not duplicated or bypassed.
A quick version note before the snippets: the official C# SDK ships as the ModelContextProtocol package, and the ASP.NET Core HTTP transport lives in ModelContextProtocol.AspNetCore. Both were in active preview at the time of writing on .NET 10, so pin the exact version and read the release notes before you upgrade.
Wiring Up the MCP Server
The server registration is almost anticlimactic. In Program.cs you add the MCP server, choose the HTTP transport, and let it discover your tools:
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly(); // scans for [McpServerToolType] classes
Then map the endpoint after the app is built:
app.MapMcp(); // exposes the MCP HTTP endpoint
That is the entire host-side wiring. WithToolsFromAssembly() scans the current assembly for tool classes so you do not maintain a manual registry. If you prefer to be explicit, WithTools<T>() registers a single tool type, which is exactly what you want for the write-side tools you do not want auto-discovered.
Exposing an Endpoint as a Tool
A tool is just a method decorated with [McpServerTool], inside a class marked [McpServerToolType]. The [Description] attributes are not decoration: the model reads them to decide when and how to call the tool, so treat them as part of the contract.
[McpServerToolType]
public class OrderTools
{
[McpServerTool]
[Description("Get the current status of a customer order by its ID.")]
public static async Task<OrderStatus> GetOrderStatus(
IOrderService orders, // injected from DI
[Description("Order ID, e.g. ORD-1042")] string orderId)
=> await orders.GetStatusAsync(orderId);
}
Two things make this click. First, the SDK injects services from the ASP.NET Core container straight into the tool method, so IOrderService is the same instance your controllers use. Second, the return type is a real C# type; the SDK serializes it for the model, and a strongly typed OrderStatus record gives the model cleaner, more reliable input than a hand-formatted string.
That is genuinely most of the happy path. The interesting engineering is in everything around it.
Where Most MCP Servers Go Wrong: The Security Boundary
Here is the trade-off that bit us early: an MCP tool is remote code execution that an LLM gets to trigger. If you expose a CancelOrder or IssueRefund tool the same way you expose GetOrderStatus, you have handed a probabilistic system the keys to your write path.
The rule we settled on, and the one I would enforce on any team, is least privilege per tool:
Auto-register read-only tools. The blast radius of a wrong read is small.
Never auto-register state-changing tools. Register them explicitly, validate their arguments as untrusted input, and require a human confirmation step before anything irreversible runs.
Authenticate the MCP endpoint itself. It is an API surface like any other; put it behind your existing auth and rate limiting rather than leaving it open.
This matters even more once you connect retrieval, because a poisoned document can try to talk your model into calling a tool it should not. If you have not read it yet, preventing prompt injection in ASP.NET Core AI APIs covers the indirect-injection angle directly, and it applies the moment your tools can change state.
A small but effective pattern is to keep destructive tools in a separate, explicitly registered class so they can never be picked up by assembly scanning:
// Safe: read-only, auto-discovered
.WithToolsFromAssembly()
// Deliberate: write tools opted in one by one, behind confirmation
.WithTools<RefundTools>();
Trade-offs and When Not to Build an MCP Server
MCP is not free complexity, and it is not always the answer:
If only one app will ever call these capabilities, plain function calling inside that app is simpler. MCP earns its keep when multiple clients need the same tools.
If your tools are pure reads over public data, a well-documented REST endpoint the model fetches may be enough.
MCP adds an operational surface. It needs the same monitoring, auth, and cost controls as the rest of your API. Treat token usage and tool latency as first-class metrics from day one.
The honest summary: reach for an MCP server when you want your API to be a reusable capability for many AI clients, with a trust boundary you own. Skip it when a single inline integration would do.
What to Do Next
If you want to take this further, the order I would suggest is: stand up the server with one read-only tool, connect it from a real client (VS Code or Claude both speak MCP), then add a single write tool behind a confirmation step and watch how the model handles it. That last step teaches you more about guardrails than any amount of reading.
From there, the production concerns - authentication, structured logging per tool call, token-cost tracking, and an evaluation suite so quality does not regress - are what separate a demo from something you can ship.
Frequently Asked Questions
What is the difference between MCP and tool calling in .NET?
Tool calling lets a single model invoke functions you define inside one application. MCP standardizes that capability into a protocol so any compliant client can discover and call your tools, and it moves the tools into a server you control. Tool calling is the in-app mechanism; MCP is the cross-client standard built on the same idea.
Do I need a separate project to host the MCP server?
No. You can add AddMcpServer().WithHttpTransport() and MapMcp() to an existing ASP.NET Core API and host the MCP endpoint alongside your controllers. A separate project is only worth it when you want independent deployment or scaling for the MCP surface.
Which transport should I use, stdio or HTTP, for an MCP server in ASP.NET Core?
Use the streamable HTTP transport when the server lives next to a web API and serves multiple remote clients. Use stdio when a single local client launches the server as a child process, such as a developer tool running on the same machine.
Is it safe to let an AI agent call my .NET API through MCP?
It is safe only if you treat every tool as untrusted execution. Auto-expose read-only tools, register state-changing tools explicitly behind validation and a human confirmation step, and put the MCP endpoint behind your existing authentication and rate limiting. Never give a tool more privilege than the action strictly needs.
Does the MCP C# SDK work with Microsoft.Extensions.AI?
Yes. The official C# SDK is designed to sit alongside Microsoft.Extensions.AI, so your IChatClient-based application can act as an MCP client and consume tools from any MCP server, including one you build in ASP.NET Core.
About the Author
I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.
GitHub: codingdroplets
YouTube: Coding Droplets
Website: codingdroplets.com






