---
title: "Use server tools with IChatClient in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/use-server-tools-with-ichatclient/
description: "Turn Model Context Protocol® server tools into Microsoft.Extensions.AI functions and call them through IChatClient. Includes an offline runnable example."
version: "1.0.1"
---

# Use server tools with IChatClient in C#

**OneModelContext** is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModelContext.Extensions.AI) · [GitHub](https://github.com/recursive-ai-oss/onemodelcontext-csharp) · [Docs](https://recursiveai.org/onemodelcontext/docs/) · [Download example (.zip)](https://recursiveai.org/onemodelcontext/cookbook/use-server-tools-with-ichatclient/use-server-tools-with-ichatclient.zip)

Give an `IChatClient` Model Context Protocol® (MCP) server tools; this example runs offline without an API key.

```shell
dotnet add package RecursiveAI.OneModelContext
dotnet add package RecursiveAI.OneModelContext.Extensions.AI
dotnet add package Microsoft.Extensions.AI
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using Microsoft.Extensions.AI;
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Extensions.AI;

async Task<string> AskAsync(ContextClient client,
    IChatClient model)
{
    using var chat =
        new FunctionInvokingChatClient(model);
    var tools = await client.ToAIFunctionsAsync();
    return (await chat.GetResponseAsync(
        "Weather tomorrow?",
        new ChatOptions { Tools = [.. tools] })).Text;
}
```

Unzip [the example](https://recursiveai.org/onemodelcontext/cookbook/use-server-tools-with-ichatclient/use-server-tools-with-ichatclient.zip), then `dotnet run`.

Create the connected `ContextClient` with `ModelContext.ConnectAsync`, using a server command, URL or transport, as the full program does. Add this function to your program and call it with that client and your model’s `IChatClient`. It returns the final response text. The full program below supplies both objects and performs the same function-calling loop without a live service.

A source generator turns `[Tool]` methods into tools, which is why the server tool class is `partial`. The adapter package turns discovered tools into `AIFunction` instances; `Microsoft.Extensions.AI` supplies `FunctionInvokingChatClient`. The download pins Microsoft.Extensions.AI to 10.11.0 alongside the adapter’s 1.0.1 release.

The offline client asks for Lisbon’s forecast once, then returns the tool result. It is scripted behavior, not a model deciding which tool to call. A tool failure becomes text starting with `Error: ` so the chat client can receive the failure.

## The full program

```csharp
using Microsoft.Extensions.AI;
using RecursiveAI.OneModelContext.Extensions.AI;
using System.IO.Pipelines;
using RecursiveAI.OneModelContext;

var toServer = new Pipe();
var toClient = new Pipe();
var server = new ContextServer("example", "1.0.0");
new ExampleTools().AddTo(server);
using var stop = new CancellationTokenSource();
var serving = server.RunAsync(ContextTransport.Stream(
    toServer.Reader.AsStream(), toClient.Writer.AsStream()), stop.Token);
await using var client = await ModelContext.ConnectAsync(
    ContextTransport.Stream(toClient.Reader.AsStream(), toServer.Writer.AsStream()));
var functions = await client.ToAIFunctionsAsync();
using var chat = new FunctionInvokingChatClient(new ScriptedChatClient());
var response = await chat.GetResponseAsync("Weather tomorrow?",
    new ChatOptions { Tools = [.. functions] });
Console.WriteLine(response.Text);
await stop.CancelAsync();
try { await serving; }
catch (OperationCanceledException) { }

public partial class ExampleTools
{
    [Tool(ReadOnly = true)]
    public string Forecast(string city) => $"Tomorrow in {city}: sunny, 21 degrees.";
}

public sealed class ScriptedChatClient : IChatClient
{
  public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
  {
    var results = messages.SelectMany(m => m.Contents).OfType<FunctionResultContent>().ToList();
    if (results.Count > 0)
    {
      return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, $"The tool said: {results[^1].Result}")));
    }

    var tool = options?.Tools?.OfType<AIFunctionDeclaration>().First() ?? throw new InvalidOperationException("No tools were offered.");
    var call = new FunctionCallContent("call-1", tool.Name, new Dictionary<string, object?> { ["city"] = "Lisbon" });
    return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, [call])));
  }

  public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
  {
    var response = await GetResponseAsync(messages, options, cancellationToken);
    foreach (var update in response.ToChatResponseUpdates())
    {
      yield return update;
    }
  }

  public object? GetService(Type serviceType, object? serviceKey = null) => null;

  public void Dispose()
  {
  }
}
```

`dotnet run` prints:

```text
The tool said: Tomorrow in Lisbon: sunny, 21 degrees.
```

## Variations

Invoke the adapted function directly, without a chat loop. Replace the code from `using var chat` through `Console.WriteLine(response.Text)` in the full program with:

```csharp
var result = await functions[0].InvokeAsync(
    new AIFunctionArguments { ["city"] = "Lisbon" });
Console.WriteLine(result);
```

```text
Tomorrow in Lisbon: sunny, 21 degrees.
```

## Related recipes

- [Build a server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Connect to a server](https://recursiveai.org/onemodelcontext/cookbook/connect-to-a-server/)
- [Require OAuth sign-in on a server](https://recursiveai.org/onemodelcontext/cookbook/require-sign-in-on-a-server/)

---

## The short answer

To use Microsoft.Extensions.AI with MCP tools (Model Context Protocol®), connect with OneModelContext, call `ToAIFunctionsAsync`, and pass the resulting functions as `ChatOptions.Tools`. Wrap the model client in `FunctionInvokingChatClient` when you want its tool calls executed locally. Discovery and execution are separate steps; handing a model the tool descriptions alone does not implement the invocation loop.

## How do I use server tools with Microsoft.Extensions.AI?

The function at the top takes two explicit inputs: a connected protocol client and a model client. The adapter lists the server’s tools and creates `AIFunction` objects with their names, descriptions and input schemas. The function-invoking wrapper sends these definitions to the underlying chat client, invokes requested functions, then supplies the results back to the conversation.

The download includes everything needed to see that exchange: a generated forecast tool, two in-memory pipes, a protocol client and a scripted `IChatClient`. The forecast returns fixed sample text for the supplied city. There is no HTTP weather lookup and no live model behind the printed response.

This separation lets you keep transport setup out of the chat code. A connected client can use stdio or HTTP; the adapter consumes its tool listing either way. Keep the protocol client alive until the chat call finishes because the adapted functions use that connection when invoked.

## How do I call a tool through IChatClient?

An `IChatClient` can request a function call by returning function-call content. Something must execute it and send the result back. In this example that something is `FunctionInvokingChatClient`, rather than a hand-written loop.

The scripted implementation makes the sequence visible in the full program. On its first call it chooses the offered function and requests `city = "Lisbon"`. On the next call it finds the function result in the conversation and returns the sentence printed above. This is useful for exercising the plumbing without credentials or model variability. It does not predict what a particular model will do with the same prompt.

When substituting a real provider, supply its configured `IChatClient` in place of `ScriptedChatClient`. Its service configuration and credentials belong to that provider’s setup. The OneModelContext adapter is not itself a model or an account. The download deliberately exercises only the offline path.

## Can I invoke the adapted functions without a model?

Yes. The variation invokes an `AIFunction` directly with an `AIFunctionArguments` dictionary. This is a useful distinction when debugging: if direct invocation succeeds but a model never selects the function, the issue is not necessarily protocol transport or tool execution.

The [adapter documentation](https://recursiveai.org/onemodelcontext/docs/adapters/) defines how results cross the boundary. Structured content is returned when available. If every block is text, the adapter returns text. Other results become a list of `AIContent`, including the documented data and URI content representations. Tool failures become text beginning with `Error: `, rather than disappearing as an empty answer.

For arguments supplied by your own code, the adapter serializes using the function’s `JsonSerializerOptions`. Types outside the Microsoft.Extensions.AI defaults need a source-generated context on that function for AOT. The direct-invocation variation uses a string argument, keeping the example within the ordinary scalar case. A model-generated argument and an arbitrary application object are not identical serialization problems.
