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:
- The client sends your request to Model (Claude, GPT, Gemini)
- The model checks what tools are available and picks the right one(s)
- The client calls those tools via the MCP server
- The results are returned to Model
- Model turns the results into a clear response
- You see the final answer
Official docs:
- MCP specification and docs: https://modelcontextprotocol.io/
- MCP GitHub org: https://github.com/modelcontextprotocol
- .NET Generic Host docs: https://learn.microsoft.com/dotnet/core/extensions/generic-host
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
- Program.cs: host setup + MCP server registration + DI wiring
- WeatherTools.cs: MCP tool methods (the core contract surface)
- HttpClientExt.cs: reusable HTTP/JSON helper
- appsettings.json: runtime settings like API base URL and user agent
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
- ModelContextProtocol: MCP server framework APIs and attributes.
- Microsoft.Extensions.Hosting: Generic Host lifecycle + dependency injection container.
- Microsoft.Extensions.Configuration.Json: load settings from appsettings.json.
- Microsoft.Extensions.Options.ConfigurationExtensions: strongly typed configuration binding.
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.
- WithStdioServerTransport(): Tells the MCP server to communicate over standard input/output (stdio). This is how AI clients (like Claude Desktop or VS Code Copilot) talk to a locally running MCP server, they launch the process and send JSON messages over stdin/stdout.
- WithToolsFromAssembly(): Scans the current assembly for classes/methods decorated with MCP attributes and registers them as callable tools automatically.
- API:https://api.weather.gov is the U.S. National Weather Service’s public API for accessing real-time weather forecasts, alerts.
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:
- GetAlerts - returns active weather alerts for a US state
- 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.
- alerts tool: for state alerts
- forecast tool: for location forecast
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)
- 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.
- Inspector repo: https://github.com/modelcontextprotocol/inspector
Typical flow:
- Launch inspector.
- Configure transport as stdio.
- Set command to run your server, for example: dotnet run --project /absolute/path/to/weather-mcp.csproj
- Click connect.
- 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:
- Which server name to show in the chat section (weather-dotnet for example)
- Which executable to run (dotnet)
- Which arguments to pass (run --project ... or a .dll path)
- Which environment variables to inject
Conceptually:
{
"mcpServers": {
"weather-dotnet": {
"type": "stdio",
"command": "dotnet",
"args": ["..."],
"env": {}
}
}
}- mcpServers: dictionary of named servers.
- weather-dotnet: your server alias.
- type: "stdio": client communicates through stdin/stdout.
- command: executable to spawn.
- args: command arguments.
- env: environment variables available to your server process.
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:
- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
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
- Throw meaningful exceptions for invalid upstream responses.
- Avoid logging secrets/tokens.
- Keep each tool narrowly scoped and composable.
- Use clear names and rich Description attributes.
- Reuse HttpClient via DI singleton or factory.
- Add timeouts/retries for external APIs where appropriate.
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.