Skip to content

Building an MCP Client

Key Points

  • Connect your .NET app/agent to existing MCP servers (filesystem, GitHub, etc.) or your own.
  • Use McpClient.ConnectAsync with appropriate transport (stdio, HTTP).
  • Discover capabilities (tools, resources, prompts).
  • Wrap MCP tools as AIFunction for use in Microsoft.Extensions.AI / Agent Framework.
  • Senior practices: handle disconnects, validate trust, sandbox.

Connect

Stdio (local server as child process)

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Command = "npx",
    Arguments = ["-y", "@modelcontextprotocol/server-filesystem", "/safe/path"]
});

var client = await McpClient.ConnectAsync(transport);

HTTP/SSE

var transport = new SseClientTransport(new SseClientTransportOptions
{
    Endpoint = new Uri("https://mcp.contoso.com"),
    Headers = { ["Authorization"] = $"Bearer {token}" }
});

var client = await McpClient.ConnectAsync(transport);

Discover

var tools = await client.ListToolsAsync();
foreach (var t in tools)
    Console.WriteLine($"{t.Name}: {t.Description}");

var resources = await client.ListResourcesAsync();
var prompts = await client.ListPromptsAsync();

Use tools as AIFunctions

var aiFunctions = (await client.ListToolsAsync()).Select(t => t.AsAIFunction()).ToArray();

var agent = new ChatClientAgent(chat) { Tools = aiFunctions };

var resp = await agent.InvokeAsync("List files in /workspace");
// Agent calls filesystem MCP tool transparently.

Direct tool call (without LLM)

var result = await client.CallToolAsync("read_file", new()
{
    ["path"] = "/data/report.txt"
});

Read resource

var contents = await client.ReadResourceAsync(new Uri("file:///workspace/README.md"));

Use prompt

var rendered = await client.GetPromptAsync("summarize", new() { ["text"] = "..." });
var resp = await chat.GetResponseAsync(rendered.Messages);

Lifecycle

public class McpClientService(...) : IAsyncDisposable
{
    private readonly McpClient _client = ...;

    public async ValueTask DisposeAsync()
    {
        await _client.DisposeAsync();
    }
}

Always dispose on shutdown — closes transport.

Reconnection

For long-running apps:

public class ResilientMcpClient(IMcpTransport transportFactory) : IAsyncDisposable
{
    private McpClient? _client;

    public async Task<McpClient> GetAsync(CancellationToken ct)
    {
        if (_client?.IsConnected == true) return _client;
        _client = await McpClient.ConnectAsync(transportFactory(), ct);
        return _client;
    }
}

Multiple servers

var fsClient = await McpClient.ConnectAsync(fsTransport);
var ghClient = await McpClient.ConnectAsync(ghTransport);

var allTools = (await fsClient.ListToolsAsync()).Select(t => t.AsAIFunction())
    .Concat((await ghClient.ListToolsAsync()).Select(t => t.AsAIFunction()))
    .ToArray();

var agent = new ChatClientAgent(chat) { Tools = allTools };

Beware tool name collisions; namespace if needed.

Trust verification

var serverInfo = client.ServerInfo;
if (!_trustedServers.Contains(serverInfo.Name)) throw new("Untrusted MCP server");

A malicious server's tool descriptions can include prompt injection.

Timeout

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var result = await client.CallToolAsync("slow_tool", args, cts.Token);

Error handling

try { var result = await client.CallToolAsync(...); }
catch (McpException ex) { /* protocol error */ }
catch (TaskCanceledException) { /* timeout */ }
catch (IOException) { /* transport error */ }

DI

builder.Services.AddSingleton(async sp =>
{
    var transport = new StdioClientTransport(new() { Command = "npx", Arguments = [...] });
    return await McpClient.ConnectAsync(transport);
});

(Use IAsyncDisposable patterns; some DI containers handle differently.)

Use cases

  • .NET agent + filesystem MCP: agent reads/writes files.
  • .NET agent + GitHub MCP: PR management.
  • .NET agent + custom MCP: your domain's API.
  • CI/CD workflow + MCP: agents take build actions.

Security

  • Trust the server: malicious tool descriptions can hijack the LLM.
  • Sandbox: filesystem MCP server restricted to specific dir.
  • Audit: log all tool calls.
  • Rate limit: don't let MCP server overwhelm your agent.

Senior considerations

  • Pin server versions — protocol/tool changes break agents.
  • Health-check periodically — long-running connections drift.
  • Trace every tool call (OTel) — see flow across processes.
  • Restrict to needed servers — least-privilege.

Cross-references