This is the first part of Building MCP Servers in .NET for AI Agents. The topics we’ll cover are:
- What is the Model Context Protocol? An Introduction for .NET Developers (This Post)
- Building Your First MCP Server in C#
- MCP Tools, Resources and Prompts in .NET
- Wiring MCP Tools to Azure SQL and External APIs
- Hosting an MCP Server in ASP.NET Core with Streamable HTTP
- Building an MCP Client and AI Agent in .NET
- Securing an MCP Server with Microsoft Entra ID
- Deploying an MCP Server to Azure Container Apps
- Connecting an MCP Server to Microsoft Foundry Agent Service
- Observability, Cost Control and MCP Security
The source code for this tutorial is available on GitHub.
What is the Model Context Protocol? An Introduction for .NET Developers
I haven’t published anything here since early 2023. The reason I’m back is that I kept running into the same situation with the teams I work with. They have a .NET codebase, they have data in Azure SQL, and someone has asked them to “add AI to it”. They start reading about the Model Context Protocol, and almost everything they find is written in Python or TypeScript.
That gap is what this series is about. Over ten parts we build one application and take it all the way to production. It’s called BitOfTech Expenses, and it does what every company does. Employees submit expense reports, managers approve them.
We start with a console application that GitHub Copilot can call, and we finish with a secured service running in Azure.
A word on how I write these. I’ve worked with MCP on customer engagements over the past year, but this is a young protocol and the tooling around it moves quickly. Things that were true six months ago are not true today. I run every line of code in this series before I publish it, and where I’m unsure about something, or where I think an API is likely to change under you, I’ll say so in the post rather than glossing over it.
Note: This series targets .NET 10 (LTS) and version 2.2.0 of the MCP C# SDK. I will state the SDK version at the top of every post, because the jump from version 1 to version 2 was a breaking change and a lot of the MCP content you’ll find online is written against the older API.
What you’ll have at the end
A service running in Azure that answers questions about expense reports.
You ask GitHub Copilot in VS Code “what has Sara submitted this month?” and it answers, because it called a tool you wrote that read your database. You ask Claude the same question and get the same answer. Then you ask an agent running in Azure to approve a report, and it says no, because that report is yours and nobody approves their own expenses.
Three AI applications, made by three different companies. None of them knows the other two exist. One server, and nothing in it mentions any of them by name.
That is why I think MCP is worth learning now. First, what it actually is.
The problem MCP actually solves
A language model is good at language and knows nothing about your business. It has never seen your expense database, it can’t call your internal API, and it has no idea what your approval policy says. On its own it can only produce text.
The fix for that isn’t new. Every major model provider has supported tool calling for a while now. You describe a function to the model as a JSON schema, the model decides when to call it, your code runs it, and you hand the result back. That loop works well.
The problem is that the loop is written from scratch every time. If you build a tool that reads expense reports, that tool lives inside whatever application you built it in. If you then want the same capability in a different application, you write it again against a different SDK. Now multiply that by every integration you own and every AI host your company uses.
Where MCP fits
MCP is a specification that sits in the middle of that repetition. You expose your capability once, behind a standard protocol, and any client that speaks MCP can use it. If you have been doing this long enough, the closest comparison is what ODBC did for databases. Before it, every application wrote a driver per database. After it, applications talked to one interface and drivers handled the rest.
Important note: MCP does not replace tool calling. It’s a layer above it. The model still decides to call a tool the same way it always did. MCP standardises how that tool gets discovered, described and invoked across process and network boundaries. It doesn’t replace your APIs either. Your database still has a connection string, and Stripe still has a REST API. What MCP replaces is the glue code you’d otherwise write between every model and every system.
Why put a server in the middle
There’s a second problem, and it’s the one that catches people out. An API built for a developer returns everything, because the developer will pick out the fields they want. A model reads all of it.
Say get_expense_report hands back forty columns of joined data. You pay for every one of those tokens on every call, and you’ve given the model thirty nine chances to reason about the wrong field. A tool that answers with “three reports pending approval, totalling 1,240 GBP, oldest submitted 9 days ago” is cheaper to run and more likely to be right.
MCP doesn’t do that shaping for you. What it gives you is the place to do it. The server sits between the model and your data, and deciding what comes back is your job. That turns out to be most of the work, and it’s what Part 3 is about.
The other thing you get from a server in the middle is a credential boundary. The model never sees your secrets. It sees a tool name, a description and a schema. When it decides to call approve_expense_report, your server runs the code, using connection strings and tokens the model has no access to.
That boundary is also the right place to enforce policy. Rules like “a manager can only approve reports from their own team” belong in your code, where they’re enforced, rather than in a prompt where you’re asking the model nicely. We lean on that hard in Part 7.
Important note: MCP doesn’t grant capabilities. If the account your server connects with can only read from the database, wrapping it in MCP will not let a model write to it. The server is a translation layer, not an escalation.
Host, client and server
MCP has three roles, and the naming trips people up because “client” doesn’t mean what you might expect.
- Host. The application the user is actually talking to. VS Code with GitHub Copilot is a host. Claude Desktop is a host. The .NET agent we build in Part 6 is a host.
- Client. A connector that lives inside the host and maintains one connection to one server. If a host is connected to four servers, it’s running four clients.
- Server. The thing that exposes capabilities. This is what we are building for the rest of the series.
The important consequence is that a host can connect to many servers at once, and your server doesn’t need to know or care which host is calling it. You write it once and it works in VS Code, in Claude Desktop, in Microsoft Foundry, and in anything else that implements the protocol. The handshake does carry the client’s name and version, so you can log it if you find that useful, but nothing in your tool code should be branching on it.
What a server can expose
A server can offer three kinds of capability. The difference between them is not what they contain, it’s who decides to use them.
| Primitive | What it is | Who decides to use it |
|---|---|---|
| Tools | Actions the server can perform, described with a JSON schema | The model |
| Resources | Data the server can hand over, identified by a URI | The application or the user |
| Prompts | Reusable prompt templates, often with parameters | The user |
That last column is the part people get wrong, so it’s worth slowing down on. A tool is chosen by the model. A resource is attached by the application or picked by the user, which means it enters the context without the model deciding anything. A prompt is something the user explicitly invokes, and in VS Code these show up as slash commands.
None of this is configured on the client side. When a host connects, it asks the server what it has and the server answers. If you add a tool to your server and restart it, every connected host picks it up with no code change and no redeployment at their end. You’ll watch that happen at the end of this post.
Most MCP servers you’ll come across only implement tools. That’s fine, and it’s where we’ll start in Part 2. We’ll add a resource and a prompt in Part 3, because our scenario has a natural fit for both.
How the client and server talk
Messages are JSON-RPC 2.0. That part is boring and you will rarely look at it directly, which is the point.
What you do need to choose is the transport. There are two that matter:
| Transport | How it runs | Best for |
|---|---|---|
| stdio | The host launches your server as a child process and talks over standard input and output | Local development and tools that run on the same machine |
| Streamable HTTP | Your server is a web application the host calls over HTTP | Remote servers, shared deployments, anything in Azure |
We start with stdio in Part 2 because it needs no hosting and no network configuration. You point VS Code at your executable, it launches it, and you’re done. We move to Streamable HTTP in Part 5, once there’s a reason to.
Important note: if you read older MCP posts you’ll see a transport called HTTP+SSE, where the client posts to one endpoint and reads responses from a separate long lived stream. That transport is now legacy. In the C# SDK it’s disabled by default and marked obsolete. Don’t build anything new on it.
There’s a second thing worth knowing before you read other material, because it changed recently and it caught me out. The HTTP transport in the C# SDK now defaults to stateless mode. The server doesn’t hand out a session id and doesn’t hold per client state in memory, which means you can scale it horizontally with no session affinity. A lot of the guidance you’ll find still assumes stateful sessions. We’ll cover the difference properly in Part 5.
Why this is worth your time as a .NET developer
Two reasons.
- The C# SDK is official. It’s published by the Model Context Protocol organisation and maintained together with Microsoft, and it’s currently at version 2.2.0. This isn’t a community port that might be abandoned.
- Almost nothing here is new to you. An MCP server is a .NET host with dependency injection. The HTTP version is an ASP.NET Core app. Securing it is a standard OAuth resource server. Talking to the database is EF Core. The MCP specific part is a thin layer on top of skills you already have.
What we’re building across the series
The sample application is called BitOfTech Expenses. It’s an expense report system, which I picked because it’s something every reader recognises without explanation, and because approving an expense on behalf of a real person forces us to take identity seriously rather than hand waving it.
The finished server exposes seven tools:
search_expense_reportsandget_expense_reportfor reading, backed by Azure SQL through EF Core.create_expense_report,submit_expense_reportandapprove_expense_reportfor writing, which is where authorization starts to matter.convert_currency, which calls a public REST API so we can show how a tool talks to an external service.notify_employee, which sends an email through Azure Communication Services. This one has a side effect you can’t undo, so it’s the tool we use to demonstrate human approval.
The company expense policy is exposed as a resource, and reviewing a report against that policy is exposed as a prompt.
Four clients call that same server. VS Code Copilot, Claude, an agent we write ourselves, and Microsoft Foundry Agent Service. The server is written once, and none of them needs anything special from it.
How we get there
It starts as a console application with two tools and a hard coded list of expense reports, talking to Copilot over standard input and output. That part takes an evening, and it already works.
Then the hard coded list becomes a real database, and the tools start calling an outside API. The console application turns into a web service, so other machines can reach it. We write our own agent, which is how you find out what a model really does with the tools you gave it. Mine called one of them wrongly, and the fix was a sentence of English rather than code.
Then identity, which takes the longest. A tool that approves an expense for someone has to know who is asking, and that answer cannot come from the model.
After that it goes into a container, into Azure, and behind a real sign in. The last post looks at what it costs to run, how to see what it is doing, and what happens when someone hides instructions in your data.
What we’re leaving out, and why
A series that tries to cover everything ends up covering nothing well. These are deliberately out of scope:
- MCP Apps and MCP Tasks. Both are interesting extensions, one for interactive UI inside a host and one for long running tool calls. Both are young. I’d rather get the fundamentals solid first.
- Sampling and roots. These let a server ask the host to run a model completion, or to tell it which directories it can see. We won’t be using either.
- Writing our own MCP host. We build a client and an agent in Part 6. Building a full host is a different project.
What you need to follow along
- The .NET 10 SDK.
- VS Code with the GitHub Copilot extension. The free Copilot tier is enough for everything we do.
- An Azure subscription, from Part 4 onwards.
- The Azure CLI, also from Part 4 onwards. Every resource we create is created with it, and there are no portal steps to follow instead.
- Working knowledge of C# and ASP.NET Core.
Note: Parts 1 to 3 run entirely on your machine and need no Azure account at all. From Part 4 the data lives in Azure SQL, and that runs on the free offer, which gives you 100,000 vCore seconds and 32 GB per month at no charge. When you reach the limit the database pauses rather than billing you. If you’d rather stay offline, the repository includes a docker-compose.yml for SQL Server, and the only thing that changes is the connection string.
Using an MCP server before we write one
Reading about a protocol only gets you so far. Before we build anything, I want you to see one working, because once you’ve watched a model call a tool you didn’t write, the rest of the series makes a lot more sense.
We’ll connect VS Code to the Microsoft Learn MCP Server. It’s a remote server run by Microsoft that gives a model search access to official Microsoft documentation. I picked it because it needs no API key, no sign up and no cost, and because it’s genuinely useful to keep switched on afterwards.
Step 1: Create the MCP configuration file
Create a folder for this series, open it in VS Code, and add a file at .vscode/mcp.json with the following content.
|
1 2 3 4 5 6 7 8 |
{ "servers": { "microsoft-learn": { "type": "http", "url": "https://learn.microsoft.com/api/mcp" } } } |
What we have done here is the following:
serversis a dictionary. Each key is a name you choose, and it’s what VS Code displays in the interface. You can list as many servers as you like here.typeset tohttptells VS Code to use the Streamable HTTP transport. In Part 2 we’ll usestdioinstead, which launches a local process.urlis the server endpoint. There’s no authentication section, because this particular server doesn’t require one.
Note: putting this file in .vscode/mcp.json scopes it to this workspace, which is what we want for the series. If you’d rather have a server available in every project, run the MCP: Open User Configuration command from the Command Palette and add it there instead.
Step 2: Start the server
Save the file and VS Code shows a small Start action above the server entry. Click it. If you miss it, open the Command Palette and run MCP: List Servers, then pick microsoft-learn and start it from there.
The same list lets you see the server’s status and open its output log, which is the first place to look when a server won’t connect. We’ll be using that log a lot in Part 2.
Step 3: Confirm the tools are available
Open the Copilot Chat panel and switch the mode selector to Agent. This matters. Ask mode doesn’t call tools, so if you leave it on the default you’ll wonder why nothing happens.
Click the tools icon in the chat input box. You should see microsoft-learn in the list with three tools under it:
microsoft_docs_searchruns a semantic search across Microsoft’s official documentation.microsoft_docs_fetchretrieves a full documentation page and converts it to markdown.microsoft_code_sample_searchsearches official Microsoft and Azure code samples, with an optional language filter.
Those three names and descriptions weren’t configured by you. VS Code asked the server what it could do, and the server answered. That exchange is the tools/list call, and it’s the reason a host can work with a server it has never seen before.
Step 4: Ask something the model can’t answer on its own
The trick to seeing a tool call is to ask a question where the model’s training data is likely to be stale or thin. Try this one:
|
1 |
What are the current monthly limits of the Azure SQL Database free offer, and what happens when I hit them? Use the Microsoft Learn tools. |
I’ve told it explicitly to use the tools. You often don’t need to, but models vary in how eagerly they reach for a tool, and being direct removes one variable while you’re checking that your setup works.
Step 5: Look at what actually happened
In the chat response you’ll see a collapsed entry showing that a tool ran. Expand it. You get the tool name, the arguments the model chose, and the raw result the server returned.
Spend a minute on this, because it’s the whole protocol in one screen:
- The model read the tool descriptions and decided
microsoft_docs_searchwas the right one. - It wrote the arguments itself. Nobody mapped your question onto a query string.
- VS Code sent the call over HTTP to Microsoft’s server and got JSON back.
- That JSON went into the model’s context, and the answer you’re reading was written from it.
Everything we build from Part 2 onwards is us writing the server side of that exchange. The model behaviour, the argument generation and the transport are all handled for us.
Summary
MCP gives you one way to expose a capability to any AI application that speaks the protocol, instead of writing that integration once per host. A server exposes tools, resources and prompts, and it talks to hosts over stdio locally or Streamable HTTP remotely.
You’ve now got a working MCP server connected to VS Code, even though you haven’t written any code yet. In Part 2 we write our own. We’ll create the BitOfTech Expenses server as a .NET console application using the MCP C# SDK, expose the first two tools over stdio, and wire it into Copilot exactly the way we wired in the Microsoft Learn server here.
The source code for this tutorial is available on GitHub.
Follow me on X @TJoudeh and on LinkedIn
References:
- Model Context Protocol official documentation
- Model Context Protocol specification
- MCP C# SDK on GitHub
- MCP C# SDK documentation
- Use MCP servers in VS Code
- Microsoft Learn MCP Server overview
- Azure SQL Database free offer
- Install the Azure CLI





Leave a Reply