.Net MCP Server

dotnet-mcp-server

It seems with rise in popularity of AI LLMs the code we generate is starting to feel language agnostic, now more than ever I can apply what I know if .Net to any other langauge, even Rust with its none garbage collection antics!

Im not ready to loose focus from .Net, I mean getting better right? :D And MCP is all the rage so why not learn to build my own MCP server?

I found this Video from Nick Chapsas | Getting Started with MCP and this blog post from James Montemagno Build a Model Context Protocol (MCP) server in C# super useful, they explain the core concepts and how to implement MCP in .Net. PS: James your monkey examples are cool 🐒

So I decided to make my own notes as I built a .Net MCP Server using STDIO transports, potentially next I will try Streamable HTTP but Im a noob, so STDIO it is!

Core Concepts

MCP is a standard protocol for connecting LLMs (clients) with external servers that expose resources, prompts, and tools in a structured, schema-validated way.

Core Concepts
  1. Servers

    • Tools, APIs, or data sources that implement MCP.
    • Expose resources, prompts, and tools.
  2. Clients

    • Typically an LLM runtime (e.g., ChatGPT, Claude, or a local agent).
    • Connect to servers via MCP without needing implementation details.
  3. Resources

    • Data collections that can be listed, queried, or retrieved.
    • Examples: files, database rows, calendar events.
    • Come with schemas so the LLM understands the structure.
  4. Prompts

    • Server-defined instruction templates.
    • Help the model interact with resources consistently.
    • Example: “Summarize this document in JSON with fields {title, summary}.”
  5. Tools

    • Actions the server can perform, triggered through MCP.
    • Examples: send an email, create a GitHub issue, insert a DB row.
    • Include input/output schemas to define expected arguments/results.
  6. Schemas & Validation

    • Structured JSON schemas for inputs and outputs.
    • Ensure predictable and reliable interactions.
  7. Transport Layer

    • MCP is transport-agnostic.
    • Can run over stdio, WebSockets, or other transports.
    • Flexible for both local and remote integrations.
  8. Security & Permissions

    • Servers declare the resources/tools they expose.
    • Clients can enforce permissions and sandboxing.
    • Provides safety when handling sensitive data.

Create New Project

This is just a standard console application that can run in a Docker container, you dont have to pop it in a container, MCP can attach and run local code, this is pretty cool, expecially for debugging.

1
cd dev && dotnet new console -n Demo.MCP

Install Packages

You can use the dotnet add package commands like below but Im pretty lazy and just update the .csproj and rebuild

1
2
3
4
5
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.4" />
</ItemGroup>

Proper way, dont follow my antics:

1
2
3
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Http

ModelContextProtocol is a pre release package (0.3.0-preview.4) as of 14/09/2025. Its the official C# SDK for the Model Context Protocol, enabling .NET applications, services, and libraries to implement and interact with MCP clients and servers. - nuget.org

Microsoft.Extensions.Hosting contains the .NET Generic Host HostBuilder which layers on the Microsoft.Extensions.Hosting.Abstractions package. - nuget.org

Microsoft.Extensions.Http provides AddHttpClient extension methods for IServiceCollection, IHttpClientFactory interface and its default implementation. - nuget.org

Add Your first Tool

I try group things in my applications by WHAT THEY DO as apposed to WHAT THEY ARE, some devs follow this, some dont. Just do whats right for your future maintainability. Or talk about it like I did and then just add it to the root anyway ( ͡° ͜ʖ ͡°)

1
dotnet new class -n CarlBlogSearchTool

Then update the class with the tool function and annotation. So here SearchCarlPatonBlog is the tool and the annotations McpServerToolType and McpServerTool will be used, probably with reflection to find all the tools in the console app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace Demo.MCP;

[McpServerToolType]
public partial class CarlBlogSearchTool(HttpClient client)
{
[McpServerTool, Description("Search Carl Paton Blog for the given keyword using search.xml")]
public async Task<List<BlogSearchResult>> SearchCarlPatonBlog(string keyword)
{
var url = "https://carlpaton.github.io/search.xml";
Console.WriteLine($"[MCP] Fetching XML from: {url}");
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var xml = await response.Content.ReadAsStringAsync();
Console.WriteLine($"[MCP] Received XML, length: {xml.Length}");

var results = new List<BlogSearchResult>();
try
{
var doc = new System.Xml.XmlDocument();
doc.LoadXml(xml);
var entryNodes = doc.GetElementsByTagName("entry");
foreach (System.Xml.XmlNode entry in entryNodes)
{
var titleNode = entry["title"];
var linkNode = entry["link"];
string? linkUrl = null;
if (linkNode != null)
{
var hrefAttr = linkNode.Attributes?["href"];
linkUrl = hrefAttr != null ? hrefAttr.Value : linkNode.InnerText;
}
if (titleNode != null && linkUrl != null &&
titleNode.InnerText.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
results.Add(new BlogSearchResult
{
Title = titleNode.InnerText,
Url = $"https://carlpaton.github.io{linkUrl}"
});
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[MCP] Error parsing XML: {ex.Message}");
return [];
}

return results;
}
}

… notice how I promoted my own blog but actually just copied James? Its almost like thats what developers do every day 🤫

Scaffold Program.cs

Here I just copied what James published, AddMcpServer, WithStdioServerTransport and WithToolsFromAssembly are fluent api extension methods from IMcpServerBuilder

I then added AddHttpClient to resolve the HttpClient in my tool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});

builder.Services
.AddHttpClient()
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();

await builder.Build().RunAsync();

Add the MCP Server

You can do this in two ways

  • Add the source locally as mentioned earlier
  • Add from a container registry

Add the source locally

From VS Code -> Ctrl+Shift+P -> type MCP -> MCP: List Servers -> Add Server -> Command (STDIO) -> Name it DemoMCP -> Enter, Enter -> Global

The IDE then opens mcp.json which is what configures these servers, you can start the server there. I like to attach to the console.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"servers": {
"DemoMcp": {
"type": "stdio",
"command": "dotnet",
"args": [
"run",
"--project",
"C:\\dev\\carlpaton\\Demo.MCP\\src\\Demo.MCP.csproj"
]
}
},
"inputs": []
}

Add from a container registry

This is useful if you have published the MCP to a container registry

Call the MCP

Now that I have configured the DemoMcp it can be called from Co-Pilot

  1. Make sure the MCP is runnnig From VS Code -> Ctrl+Shift+P -> type MCP -> MCP: List Servers -> DemoMCP -> Start Server

  2. Then execute an example prompt like find blog posts with the keyword "C#"

  3. I dont have auto allow set, so it asks for permission each time

confirm-mcp-execution

  1. This will then list out the responses

mcp-results

  1. Because you are using a LLM, you can prompt something like show me the contents of the post about hosted services

fetch-mcp

To be fair, this shows my MCP is pretty useless as it stands because the LLM has a intrinsic fetch tool that allows it to fetch contents over HTTP.

monkey-hide

However this is just an example tool showing how to build a MCP Server with a tool SearchCarlPatonBlog

Testing

When I was testing I just wrote an integration test CarlBlogSearchToolTests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Demo.MCP.Tests;

public class CarlBlogSearchToolIntegrationTests
{
[Fact]
public async Task SearchCarlPatonBlog_RealRequest_ReturnsResults()
{
// Arrange
var keyword = "C#";
using var httpClient = new HttpClient();
var tool = new CarlBlogSearchTool(httpClient);

// Act
var results = await tool.SearchCarlPatonBlog(keyword);

// Assert
Assert.NotNull(results);
Assert.NotEmpty(results);
Assert.All(results, r => Assert.False(string.IsNullOrWhiteSpace(r.Title)));
Assert.All(results, r => Assert.False(string.IsNullOrWhiteSpace(r.Url)));
Assert.Contains(results, r => r.Url.Contains("carlpaton.github.io", System.StringComparison.OrdinalIgnoreCase));
}
}

Source Code

The full source code for Demo.MCP is at https://github.com/carlpaton/Demo.MCP