Start now →

Schrödinger’s AI Part 13: Let’s Build an MCP Server with .NET

By Rikam Palkar - Microsoft MVP · Published May 11, 2026 · 9 min read · Source: Level Up Coding
AI & CryptoMarket Analysis
Schrödinger’s AI Part 13: Let’s Build an MCP Server with .NET
Schrödinger’s AI is your invitation to look inside. Right now, AI feels like a mystery , wired like a brain, yet running on pure math.
Each article is a new layer of the box. We start with the first spark of an idea and move all the way to the models reshaping everything we thought we knew.
Explore the full code and examples on GitHub: Schrodingers-AI

Part 13: Let’s Build an MCP Server with .NET

We’ve discussed MCP in detail here. If you’d like a deeper explanation, visit: Part 8: Inside the model context protocol

Here’s MCP in short: Model Context Protocol (MCP) is an open protocol that lets AI clients (IDEs, assistants, chat apps, agents) call external tools such as DataBase, services or files.

Let me explain it with an example, Let’s take food delivery app like Uber Eats.

When you, search for “pizza near me” or see restaurants or place an order

Behind the scenes, App calls different services (restaurants, payments, tracking)

Same idea with MCP

Here AI jumps in to help you,

You simply prompt: “Order me my usual pizza”

Now with MCP, AI calls a “get nearby restaurants” tool, then a “place order” tool, then a “track order” tool

What happens behind the scenes

When you ask something:

Official docs:

Let’s build our own MCP local server

Prerequisites

Please ensure if you .NET SDK 8.0+ installed if not download it from: https://dotnet.microsoft.com/download

Project Setup

Create a new console app (MCP servers commonly run as long-lived console processes via stdio).

We’re going to work with an weather app mentioned in MCP Docs

Go ahead and execute following commands to create new console app

dotnet new console -n weather-mcp
cd weather-mcp
dotnet new sln -n mcp-server-dotnet
dotnet sln add weather-mcp.csproj

A minimal structure you will end up with:

(Note: To keep things simple, we are not strictly following the highest coding and structural standards, as that is not the goal here.)

weather-mcp/
├── Program.cs
├── WeatherTools.cs
├── HttpClientExt.cs
├── appsettings.json
└── weather-mcp.csproj

Installing Dependencies

dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions

Restore/build:

dotnet restore
dotnet build

MCP Server Implementation

This is the important part. We will map MCP concepts directly to code.

1. Program.cs

This file is the entry point of the MCP (Model Context Protocol) server. It sets up and runs a hosted .NET application that exposes tools over standard input/output (stdio).

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using ModelContextProtocol;
using System.Net.Http.Headers;

var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
builder.Services.Configure<WeatherApiOptions>(
builder.Configuration.GetSection(WeatherApiOptions.SectionName));
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
builder.Services.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<WeatherApiOptions>>().Value;
var client = new HttpClient
{
BaseAddress = new Uri(options.BaseUrl)
};
client.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue(options.UserAgentProduct, options.UserAgentVersion));
return client;
});
var app = builder.Build();
await app.RunAsync();
public sealed class WeatherApiOptions
{
public const string SectionName = "WeatherApi";
public string BaseUrl { get; set; } = "https://api.weather.gov";
public string UserAgentProduct { get; set; } = "weather-mcp";
public string UserAgentVersion { get; set; } = "1.0";
}

Server initialization:

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

AddMcpServer(): Registers the MCP server into the DI container. This sets up all the internal plumbing needed to handle MCP protocol messages.

2. Helper method to HttpClient: HttpClientExt.cs

using System.Text.Json;

namespace weather_mcp;
internal static class HttpClientExt
{
public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
{
using var response = await client.GetAsync(requestUri);
response.EnsureSuccessStatusCode();
return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
}
}

3.WeatherTools.cs

Here we define the MCP tools that an AI client can call. In this file, there are two tools:

  1. GetAlerts - returns active weather alerts for a US state
  2. GetForecast - returns the forecast for a latitude/longitude

So this file is your “business logic” layer: it calls the weather API, reads JSON, and formats human-readable output.

using System.ComponentModel;
using System.Globalization;
using System.Text.Json;
using ModelContextProtocol.Server;

namespace weather_mcp;
[McpServerToolType]
public static class WeatherTools
{
[McpServerTool, Description("Get weather alerts for a US state code.")]
public static async Task<string> GetAlerts(
HttpClient client,
[Description("Two-letter US state code (for example: CA, NY, TX).")]
string state)
{
using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
var alerts = jsonDocument.RootElement
.GetProperty("features")
.EnumerateArray()
.ToList();
if (alerts.Count == 0)
{
return "No active alerts for this state.";
}
return string.Join("\n--\n", alerts.Select(alert =>
{
var properties = alert.GetProperty("properties");
return $"""
Event: {properties.GetProperty("event").GetString()}
Area: {properties.GetProperty("areaDesc").GetString()}
Severity: {properties.GetProperty("severity").GetString()}
Description: {properties.GetProperty("description").GetString()}
Instruction: {properties.GetProperty("instruction").GetString()}
""";
}));
}
[McpServerTool, Description("Get weather forecast for a location.")]
public static async Task<string> GetForecast(
HttpClient client,
[Description("Latitude of the location.")] double latitude,
[Description("Longitude of the location.")] double longitude)
{
var pointUrl = string.Create(
CultureInfo.InvariantCulture,
$"/points/{latitude},{longitude}");
using var pointDoc = await client.ReadJsonDocumentAsync(pointUrl);
var forecastUrl = pointDoc.RootElement
.GetProperty("properties")
.GetProperty("forecast")
.GetString()
?? throw new InvalidOperationException("Forecast URL missing from points response.");
using var forecastDoc = await client.ReadJsonDocumentAsync(forecastUrl);
var periods = forecastDoc.RootElement
.GetProperty("properties")
.GetProperty("periods")
.EnumerateArray();
return string.Join("\n---\n", periods.Select(period => $"""
{period.GetProperty("name").GetString()}
Temperature: {period.GetProperty("temperature").GetInt32()} deg F
Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
Forecast: {period.GetProperty("detailedForecast").GetString()}
"""));
}
}

Let’s understand what is required to make this an MCP server.

using ModelContextProtocol.Server:

Imports MCP server attributes (McpServerToolType, McpServerTool).

[McpServerToolType]
public static class WeatherTools

The attribute [McpServerToolType] marks this class as containing MCP tool methods.

[McpServerTool, Description("Get weather forecast for a location.")]
public static async Task<string> GetForecast(....

The attribute [McpServerTool] defines a callable MCP tool named GetAlerts.

The model “looks at” the description to choose the right tool. Without good descriptions, tool selection is much less reliable.

Example:

1. User prompt: What is the forecast for Sacramento?

Model checks available tool descriptions, it has following 2 tools.

2. Model sees that second tool is about forecast-by-location, then it chooses this tool and fills arguments like latitude and longitude.

public static async Task<string> GetForecast(
HttpClient client,
[Description("Latitude of the location.")] double latitude,
[Description("Longitude of the location.")] double longitude)
  1. It calls it with coordinates, for example:
    name: GetForecast
    arguments: { latitude: 38.5816, longitude: -121.4944 }

Running the Server

Build and run locally:

dotnet build
dotnet run

Testing the MCP Server

1. Test with MCP Inspector (recommended)

Official MCP Inspector lets you connect to a stdio server and call tools.

Typical flow:

  1. Launch inspector.
  2. Configure transport as stdio.
  3. Set command to run your server, for example: dotnet run --project /absolute/path/to/weather-mcp.csproj
  4. Click connect.
  5. List tools and invoke GetAlerts / GetForecast.

2. Example request/response payloads

Example call for alerts:

{
"method": "tools/call",
"params": {
"name": "GetAlerts",
"arguments": {
"state": "CA"
}
}
}

Example response shape (conceptual):

{
"content": [
{
"type": "text",
"text": "Event: ..."
}
]
}

Example call for forecast:

{
"method": "tools/call",
"params": {
"name": "GetForecast",
"arguments": {
"latitude": 38.5816,
"longitude": -121.4944
}
}
}

Testing from AI clients

Map This Server to AI Clients (Cursor and Claude Code)

This is the part many teams miss: MCP is not only server code, it is also client wiring. The client needs JSON configuration that tells it how to start your server process.

The JSON below is a client-side process definition for your MCP server. It is not sent to the weather API. It is read by the AI client app so it knows:

Conceptually:

{
"mcpServers": {
"weather-dotnet": {
"type": "stdio",
"command": "dotnet",
"args": ["..."],
"env": {}
}
}
}

Two launch styles: DLL vs. CSPROJ

You can run the same .NET MCP server in two common ways.

Option A: Run compiled DLL (recommended for stable/dev-team setups)

{
"mcpServers": {
"weather-dotnet": {
"type": "stdio",
"command": "dotnet",
"args": [
"/ABSOLUTE/PATH/weather-mcp/bin/Debug/net8.0/weather-mcp.dll"
],
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

You must build first: dotnet build. If DLL path changes (framework/configuration), update config.

Option B: Run from project file (dotnet run --project ...)

{
"mcpServers": {
"weather-dotnet": {
"type": "stdio",
"command": "dotnet",
"args": [
"run",
"--project",
"/ABSOLUTE/PATH/weather-mcp/weather-mcp.csproj"
],
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Which file stores this JSON in each client

Claude desktop: Go to settings > developer > click on Edit config.

It will open following file:

Cursor: Go to settings > Tools & MCPs

Do you need to run the MCP server app manually first?

Usually no.

With type: "stdio", the AI client launches your server process automatically using command + args from JSON when needed.

Best Practices

Conclusion

The cat is neither alive nor dead and honestly, that’s the most exciting place to be. There are a lot more layers to uncover.
Explore the full code and examples on GitHub: Schrodingers-AI
Previous: Part 12: Build your AI Clone

Schrödinger’s AI Part 13: Let’s Build an MCP Server with .NET was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

This article was originally published on Level Up Coding and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →