Building an MCP Client
Key Points
- Connect your .NET app/agent to existing MCP servers (filesystem, GitHub, etc.) or your own.
- Use
McpClient.ConnectAsyncwith appropriate transport (stdio, HTTP). - Discover capabilities (tools, resources, prompts).
- Wrap MCP tools as
AIFunctionfor use inMicrosoft.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)
Read resource
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.