Expose C# methods as Model Context Protocol® tools over stdio.
dotnet new console dotnet add package RecursiveAI.OneModelContext
Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using RecursiveAI.OneModelContext; await ModelContext.ServeAsync(new Calculator()); public partial class Calculator { [Tool] public int Add(int a,int b)=>a+b; }
Download this example (.zip): unzip, then dotnet run.
The snippet at the top waits for a client and prints no application text: standard output carries the protocol.
The full program
The download starts a second copy of itself with --stdio, lists the add tool, calls it and exits. Run dotnet run -- --stdio to serve a real client instead.
using RecursiveAI.OneModelContext; using System.Reflection; if (args.Contains("--stdio")) { await ModelContext.ServeAsync(new Calculator()); return; } // Start this same assembly in server mode, then call it over stdio. var assembly = Assembly.GetExecutingAssembly().Location; await using var client = await ModelContext.ConnectAsync( $"dotnet \"{assembly}\" --stdio"); Console.WriteLine($"Server: {client.ServerInfo.Name}"); foreach (var tool in await client.ListToolsAsync()) Console.WriteLine($"Tool: {tool.Name}"); Console.WriteLine((await client.CallToolAsync("add", new { a = 2, b = 40 })).Text); public partial class Calculator { /// <summary>Add two integers.</summary> [Tool] public int Add(int a, int b) => a + b; }
dotnet run prints:
Server: Example Tool: add 42
Server: Example comes from the download’s project file, Example.csproj: its default assembly name is Example, and ServeAsync uses that name unless you supply a server name.
Keep diagnostic output on standard error in server mode; ordinary text on standard output corrupts the stdio protocol.
The core package includes the source generator for [Tool] methods. Keep the class partial; the generator supplies its registration code. The download pins the package version in Example.csproj.
Variations
Change the arguments. Replace the tool call inside the full program with:
Console.WriteLine((await client.CallToolAsync( "add", new { a = 20, b = 22 })).Text);
42Related recipes
- Connect to a Model Context Protocol® server in C#
- Host a Model Context Protocol® server on ASP.NET Core
- Serve the Model Context Protocol® over HTTP without ASP.NET
- Require OAuth sign-in on a Model Context Protocol® server
- Sign in to a protected Model Context Protocol® server in .NET
The short answer
To build a C# MCP server (Model Context Protocol®), mark methods with [Tool], put them in a partial class and pass an instance to ModelContext.ServeAsync. OneModelContext’s source generator writes the tool registration and argument handling at build time. The core package contains both the server and the client, so the downloadable example can exercise its own server without a model, hosted service or account.
The important distinction is between serving and demonstrating. The snippet at the top serves over stdio. The full program demonstrates a connection by launching a child process in server mode, then prints the server name, tool name and result. Both use the same calculator implementation.
How do I make a Model Context Protocol® server in C#?
Start with a console project targeting .NET 10 and install RecursiveAI.OneModelContext. Keep your tool class partial and give each callable method a [Tool] attribute. The generator supplies registration code; you don’t need to scan assemblies or write JSON dispatch code for this example.
The calculator’s Add method accepts two integers. Its generated tool is named add, and the call supplies properties named a and b. The tool-schema documentation explains supported parameter types, required arguments and descriptions. XML summary and parameter comments supply descriptions that clients can display. Give those descriptions the same care as a public API: a caller should know what the operation does before invoking it.
The tool result is not the transport. Returning an integer makes the result available to the client; it does not mean writing a human-readable line to the server’s console. The client in the full program chooses to print the result’s Text.
How do I run my own server?
For the downloaded example, run dotnet run to see the complete exchange. To leave it waiting for a client, run dotnet run -- --stdio. Configure the consuming application to launch that command and keep its standard input and output connected. There is no HTTP port in this mode.
Open the unzipped example folder as your VS Code workspace. To launch the downloaded server, put this in .vscode/mcp.json, using the editor’s documented local-server format:
{ "servers": { "calculator": { "type": "stdio", "command": "dotnet", "args": ["run", "--", "--stdio"] } } }
The working directory defaults to the workspace folder. The --stdio argument selects the downloaded program’s server branch; without it, that program runs its own demonstration client instead. The equivalent launch command, from the unzipped folder, is:
dotnet run -- --stdioStandard output belongs to the wire protocol. Write diagnostics to standard error, not Console.WriteLine in the server branch. The stdio guide describes the framing, negotiation and shutdown behavior. Closing the client closes the connection; the serving task then finishes under the documented shutdown bounds.
Use stdio when the consuming application should own the child process. Use HTTP when a separately running process should accept connections at a URL. That choice changes the host, not the calculator method.
Can I run a C# server over HTTP?
Yes. The same generated class can be passed to ModelContext.ServeHttpAsync for a local listener, or registered with a ContextServer mapped into ASP.NET Core. The former is a small console application; the latter fits an existing web application and its middleware.
Choose the host before adding deployment configuration. The built-in convenience listener is loopback-only. It is not a remote public web host. For a web application, map a protocol endpoint on ASP.NET Core and configure the host as you would for your other endpoints. Authentication is a separate decision from choosing HTTP: neither changing the transport nor discovering a tool grants permission to call it.
See local HTTP hosting and ASP.NET Core hosting for complete runnable versions, not just different calculator methods.