Building a Powerful MCP Server in .NET with Just 20 Lines of Code

I'm a .NET Developer working on MS Stack for more than 12 years already. I worked with most dotnet-based technologies, from WinForms to Azure Functions.
In my time, I won a dozen hackathons, launched a couple of startups, failed them, and am now working as a lead .NET developer in an enterprise company.
The Model Context Protocol (MCP) enables AI assistants to connect with external tools and data sources. While traditional approaches require complex setups, .NET’s 10 (Preview 4) modern features let you create a fully functional MCP server in remarkably few lines of code.
The 20-Line MCP Server
Here's a complete, production-ready MCP server that could provide multiple useful tools:
#:package Microsoft.Extensions.Hosting@9.0.8
#:package ModelContextProtocol@0.3.0-preview.3
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
// Register the MCP server
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
// Build and run the MCP Server Application
await builder.Build().RunAsync();
//====== TOOLS ======
[McpServerToolType]
public static class TextTools
{
[McpServerTool, Description("Generates a password with specified length and complexity")]
public static string GeneratePassword(int length = 12, bool includeSymbols = true, bool includeNumbers = true)
{
const string letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const string numbers = "0123456789";
const string symbols = "!@#$%^&*()_+-=[]{}|;:,.<>?";
var chars = letters;
if (includeNumbers) chars += numbers;
if (includeSymbols) chars += symbols;
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[Random.Shared.Next(s.Length)]).ToArray());
}
}
That's it! Save this as mcp-server.cs and run with dotnet run mcp-server.cs.
dotnet run app.cs feature, that allows to run cs file directly without any projects, so you would need at least .NET 10 Preview 4 installed to make it work.To explore the benefits of single-file MCPs further, I created an open-source solution: https://anymcp.io/.
Why This Approach is Revolutionary
1. Zero Project Setup
Traditional .NET development requires creating projects, managing dependencies, and complex build configurations. The #:package directive eliminates all that overhead.
2. Automatic Tool Discovery
The WithToolsFromAssembly() method automatically discovers any class marked with [McpServerToolType] and exposes its methods as MCP tools. No manual registration required.
3. Type-Safe by Default
Unlike JSON-based tool definitions, your tools are strongly typed. IntelliSense works, refactoring is safe, and runtime errors are minimized.
4. Production Ready
This isn't a toy example. The server includes proper logging, error handling, and follows MCP protocol specifications.
Expanding Your Server
Adding new tools is trivial. Here's how to add mathematical capabilities:
[McpServerToolType]
public static class MathTools
{
[McpServerTool, Description("Evaluates mathematical expressions safely")]
public static double Calculate(string expression)
{
var table = new System.Data.DataTable();
return Convert.ToDouble(table.Compute(expression.Replace("^", "**"), ""));
}
[McpServerTool, Description("Converts temperatures between units (C, F, K)")]
public static double ConvertTemp(double value, string from, string to) =>
(from.ToLower(), to.ToLower()) switch
{
("c", "f") => value * 9/5 + 32,
("f", "c") => (value - 32) * 5/9,
("c", "k") => value + 273.15,
("k", "c") => value - 273.15,
_ => value
};
}
Advanced Features Made Simple
Complex Return Types
Return tuples, objects, or collections naturally:
[McpServerTool, Description("Analyzes text content")]
public static (int words, int chars, int lines, double readability) AnalyzeText(string text)
{
var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
var sentences = text.Split('.', '!', '?').Length - 1;
var readability = Math.Max(0, 206.835 - 1.015 * (words / Math.Max(sentences, 1)));
return (words, text.Length, text.Split('\n').Length, readability);
}
Error Handling
Exceptions are automatically converted to proper MCP error responses:
[McpServerTool, Description("Fetches and parses web content")]
public static async Task<string> FetchUrl(string url)
{
using var client = new HttpClient();
var response = await client.GetStringAsync(url); // Throws on error - automatically handled
return response;
}
Deployment and Integration
Configuration for MCP Clients
Create .mcp.json in your project root:
{
"servers": {
"MyUtilityServer": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "C:\\path\\to\\your\\mcp-server.cs"]
}
}
}
Docker Deployment
For containerized environments:
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview-alpine
WORKDIR /app
COPY mcp-server.cs .
ENTRYPOINT ["dotnet", "run", "mcp-server.cs"]
Environment variables
Sometimes you might need to pass environment variables into your code, such as tokens or authentication keys. This can be done very easily with this approach.
First, add the input and set up the variable in your .mcp.json file.
{
"inputs": [
{
"id": "user_email",
"description": "Your email",
"type": "promptString",
"password": true
}
],
"servers": {
"DotnetMcpFile": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "C:\\path\\to\\your\\mcp-server.cs"]
"env": {
"USER_EMAIL": "${input:user_email}"
}
}
}
}
Visual Studio will display a prompt where you can enter the data.

To use the data, apply C# Environment methods.
[McpServerToolType]
public static class EnvironmentExamples
{
[McpServerTool, Description("Gets environment variable value with optional default")]
public static string GetEnvironmentVariable(string variableName, string? defaultValue = null)
{
var value = Environment.GetEnvironmentVariable(variableName);
if (string.IsNullOrEmpty(value))
{
if (defaultValue != null)
return $"Environment variable '{variableName}' not found, using default: {defaultValue}";
else
return $"Environment variable '{variableName}' not found";
}
return $"{variableName} = {value}";
}
}
And the result is shown below:

Real-World Example: File System Tools
Here's a practical server that provides file system operations:
[McpServerToolType]
public static class FileTools
{
[McpServerTool, Description("Lists files in directory with optional pattern")]
public static string[] ListFiles(string path, string pattern = "*") =>
Directory.GetFiles(path, pattern).Take(100).ToArray();
[McpServerTool, Description("Gets file information")]
public static object GetFileInfo(string path)
{
var info = new FileInfo(path);
return new {
Name = info.Name,
Size = info.Length,
Modified = info.LastWriteTime,
IsReadOnly = info.IsReadOnly
};
}
[McpServerTool, Description("Safely reads text file with size limits")]
public static string ReadTextFile(string path, int maxLines = 1000) =>
string.Join("\n", File.ReadLines(path).Take(maxLines));
}
Best Practices
1. Security First
Always validate inputs and limit resource usage:
[McpServerTool]
public static string ProcessFile(string path)
{
if (!Path.IsPathFullyQualified(path)) throw new ArgumentException("Use absolute paths");
if (new FileInfo(path).Length > 10_000_000) throw new ArgumentException("File too large");
// ... process safely
}
2. Clear Descriptions
Make your tools discoverable with detailed descriptions:
[McpServerTool, Description("Compresses images while maintaining quality. Supports JPEG (quality 85), PNG (lossless), WebP (quality 90). Max input: 50MB")]
public static byte[] CompressImage(byte[] imageData, string format = "jpeg") { /* ... */ }
3. Async When Needed
Use async for I/O operations:
[McpServerTool, Description("Downloads and processes remote content")]
public static async Task<string> ProcessRemoteData(string url)
{
using var client = new HttpClient();
var data = await client.GetStringAsync(url);
return ProcessData(data);
}
Conclusion
This 20-line approach to MCP servers represents a paradigm shift in how we build AI tool integrations. By leveraging .NET's modern features - top-level programs, package directives, and attribute-based configuration - we've eliminated the traditional complexity barrier.
The result is a development experience that's:
Fast: From idea to working server in minutes
Maintainable: Clear, typed code that's easy to modify
Powerful: Full access to .NET ecosystem and NuGet packages
Professional: Production-ready with proper error handling and logging
Whether you're building personal productivity tools or enterprise AI integrations, this pattern provides the perfect balance of simplicity and capability. Start with the 20-line foundation, then grow your server organically as your needs evolve.
The future of AI tool development is here, and it fits in 20 lines of code.





