Discover tools and call them without a model or API key.
dotnet add package RecursiveAI.OneModelContextRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using RecursiveAI.OneModelContext; // Pass a server command as the first argument. await using var client = await ModelContext.ConnectAsync(args[0]); Console.WriteLine((await client.CallToolAsync( "add", new { a = 2, b = 40 })).Text);
Download this example (.zip): unzip, then dotnet run.
For the snippet at the top, unzip the calculator server example into a sibling folder named Server (containing Example.csproj). From your client project’s folder, run:
dotnet run -- "dotnet run --project ../Server/Example.csproj -- --stdio"The entire quoted server command becomes args[0]. The client snippet prints:
42The full program
The download includes its own calculator server in the same executable. It starts that child process, lists its tools, calls add and closes the connection. Replace the connection command with your own server to use another process.
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
A server that exits or cannot be started prevents connection; tool argument failures are returned through the tool result.
Variations
List the available tool names. Add this after connecting in the full program:
foreach (var tool in await client.ListToolsAsync()) Console.WriteLine(tool.Name);
addRelated recipes
- Build 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
A C# MCP client (Model Context Protocol®) can connect with ModelContext.ConnectAsync, discover tools with ListToolsAsync and invoke one with CallToolAsync. OneModelContext can launch a server command over stdio or connect to an HTTP endpoint. A model is optional: the code here directly asks a calculator to add two integers.
The downloadable program contains the server as well as the client. That makes the result reproducible without a running service. To use your own server, replace the child-process command with its command line and call one of the tools it advertises.
How do I create a Model Context Protocol® client?
Create a .NET 10 console app, install the core package and await ModelContext.ConnectAsync. For stdio, the string argument is the server command, not its display name. The client starts the process and communicates through its standard streams. Use await using so disposal closes the connection when the work is finished.
The top snippet takes that command from the first application argument. For the downloaded executable, the server mode is --stdio. The full program builds the command from its own assembly path, including quotes around the path so a space in a folder name does not split it into multiple arguments.
Inspect the server before assuming a tool exists. ListToolsAsync returns tool definitions, including names and input schemas. The variation prints those names. For this calculator, the correct name is add; the C# method name Add is not the protocol name.
What is a protocol client for?
It is the calling side of a tool connection. It can discover what a server offers and send structured requests. In this example there is no conversation or probabilistic step: the application picks the tool and arguments itself, then prints the returned text.
This is useful when you want to test an integration before connecting a model, or when application logic already knows which operation to run. A successful connection establishes protocol communication; it does not promise that a particular tool exists or that any arguments you invent are accepted.
The anonymous object in the example becomes the argument object. Its property names match the tool schema. Check IsError when application behavior depends on whether a tool succeeded; tool errors are not the same thing as failing to start the server process. The getting-started guide demonstrates an invalid argument and its returned error.
How do I connect to a remote server from C#?
Use ContextTransport.Http with the endpoint URI instead of a child-process command. The HTTP host must already be running, and the URI must include the mapped path. The client negotiates the protocol rather than requiring you to construct initialization JSON by hand. The HTTP client guide covers the transport’s options and limits.
For a protected endpoint, supply an HTTP client created by OAuthClient.CreateHttpClient. That handler can answer a challenge, obtain a token and replay the request. Merely adding an authorization decorator does not perform interactive sign-in.
For NativeAOT, change the argument serialization too. The object-argument overload shown here uses reflection; the pinned README says it is not trim- or NativeAOT-safe. These examples are ordinary .NET console builds, not AOT projects. An AOT application must use CallToolAsync(name, arguments, typeInfo) with its own source-generated JsonTypeInfo<T> instead of assuming an anonymous object will survive trimming.