1. 为什么我要自己写一个 C# 版 MCP Client
市面上现成的 MCP Client 不少,Claude Desktop、Cursor、各种开源 GUI 都能连 MCP Server,日常查资料、跑工具确实够用。但真到了企业项目里,问题就来了:客户端要嵌进自己的 .NET 服务里,要统一走公司内部的 API 通道,要能读 appsettings.json 动态切换模型和端点,还要把工具调用结果写进自己的日志和审计链路。这些需求,免费客户端一个都满足不了。
所以这篇不聊怎么装现成软件,而是用 C# 从零搭一个可配置的 MCP Client 骨架,把模型调用统一收敛到 TaoToken 的 API 通道上。你跟着做完,会得到一个能跑通 MCP 握手、能列出工具、能发一次真实模型请求的最小可用客户端。适合有 .NET 基础、想把自研客户端接进统一 API 通道的开发者。全程 .NET 8 + 官方 ModelContextProtocol SDK,配置全部外置,不写死任何 Key。
先说清楚 MCP 是什么:Model Context Protocol,一套让客户端和工具服务端对话的协议。Client 负责发起连接、列工具、调工具;Server 负责暴露工具。传输层常见两种,SSE 走 HTTP 长连接,Stdio 走本地进程管道。这篇先用 SSE 把链路跑通,Stdio 留到自定义 Server 那篇再展开。
2. 前置准备:TaoToken 统一 Key 与项目初始化
2.1 为什么模型通道要单独抽出来
MCP Client 本身只管协议握手和工具调度,它不负责“用哪个模型”。真正干活的是背后的 LLM。如果每个项目各自填一家厂商的 Key、各自处理不同的请求格式,维护成本会爆炸。TaoToken 在这里的角色就是统一入口:一个 Key、一个 BaseUrl,兼容主流模型调用格式,客户端侧只认这一套配置。这样你换模型、加模型,改的是配置文件,不是代码。
注册和拿 Key 的入口在这里,先拿到再往下走:
控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
API 根地址统一用https://taotoken.net/api,注意这个不带任何跟踪参数,代码里就写这个。
2.2 新建控制台项目并装 SDK
打开终端,建一个 .NET 8 控制台项目:
dotnet new console -n McpClientDemo cd McpClientDemo dotnet add package ModelContextProtocol --prerelease--prerelease必须加,因为官方 C# SDK 目前还是预览版(0.1.0-preview.x 系列)。装完可以在 csproj 里确认一下版本号。这个包提供了McpClientFactory、McpClient以及传输层相关类型,是后面所有代码的基础。
顺手把配置读取需要的包也加上,.NET 8 自带Microsoft.Extensions.Configuration系列,但 JSON 提供程序要显式引:
dotnet add package Microsoft.Extensions.Configuration.Json dotnet add package Microsoft.Extensions.Configuration.Binder3. 可复制的 appsettings.json 配置骨架
3.1 配置文件长什么样
在项目根目录建appsettings.json,内容如下。这份骨架把 MCP Server 端点、TaoToken 通道、模型名全部外置,代码里不出现任何硬编码:
{ "Mcp": { "Transport": "Sse", "Endpoint": "https://your-mcp-server.example.com/sse", "ClientName": "McpClientDemo", "ClientVersion": "1.0.0" }, "TaoToken": { "BaseUrl": "https://taotoken.net/api", "ApiKey": "sk-替换成你自己的Key", "Model": "claude-3-5-sonnet", "TimeoutSeconds": 60 } }几个字段说明一下。Mcp.Endpoint换成你实际要连的 MCP Server 地址,SSE 协议一般以/sse结尾。TaoToken.ApiKey从上面 API Keys 页面拿。Model填你要用的模型标识,具体支持哪些可以在模型对话页面试:
模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite
3.2 让配置文件参与编译输出
控制台项目默认不会把 appsettings.json 复制到输出目录,得在 csproj 里加一段:
<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>不加这段,运行时ConfigurationBuilder找不到文件,会直接抛异常。这是新手最容易踩的第一个坑。
3.3 定义强类型配置类
建一个AppConfig.cs,把配置映射成对象,避免到处GetSection字符串:
public class McpOptions { public string Transport { get; set; } = "Sse"; public string Endpoint { get; set; } = string.Empty; public string ClientName { get; set; } = "McpClientDemo"; public string ClientVersion { get; set; } = "1.0.0"; } public class TaoTokenOptions { public string BaseUrl { get; set; } = "https://taotoken.net/api"; public string ApiKey { get; set; } = string.Empty; public string Model { get; set; } = string.Empty; public int TimeoutSeconds { get; set; } = 60; }这样后面注入的时候类型清晰,改配置也不会漏字段。
4. 从零实现 MCP Client 核心代码
4.1 加载配置并创建传输层
在Program.cs里先把配置读进来:
using Microsoft.Extensions.Configuration; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; var config = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); var mcpOptions = config.GetSection("Mcp").Get<McpOptions>()!; var taoOptions = config.GetSection("TaoToken").Get<TaoTokenOptions>()!; Console.WriteLine($"准备连接 MCP Server: {mcpOptions.Endpoint}");注意SetBasePath(AppContext.BaseDirectory),指向输出目录,和前面 csproj 的复制设置对应。用reloadOnChange: true是为了后面改配置不用重启。
接着创建 SSE 传输实例:
var transport = new SseClientTransport(new SseClientTransportOptions { Endpoint = new Uri(mcpOptions.Endpoint), Name = mcpOptions.ClientName });SseClientTransportOptions里Endpoint是必填的,Name会作为客户端标识发给 Server,方便服务端做区分。
4.2 建立连接并列出工具
用工厂方法创建客户端,这一步就是 MCP 握手:
var client = await McpClientFactory.CreateAsync(transport); Console.WriteLine("MCP 握手完成,连接已建立"); var tools = await client.ListToolsAsync(); Console.WriteLine($"发现 {tools.Count} 个工具:"); foreach (var tool in tools) { Console.WriteLine($" - {tool.Name}: {tool.Description}"); }McpClientFactory.CreateAsync内部会完成协议版本协商、能力交换。如果这一步卡住或抛异常,八成是 Endpoint 不通或协议不匹配,排查方法见第 6 节。
4.3 把工具调用接到 TaoToken 通道
MCP Client 拿到工具列表后,真正的推理请求要发给模型。这里用 HttpClient 走 TaoToken 的统一通道,把工具描述作为上下文传进去:
using System.Net.Http.Headers; using System.Text; using System.Text.Json; var http = new HttpClient { BaseAddress = new Uri(taoOptions.BaseUrl), Timeout = TimeSpan.FromSeconds(taoOptions.TimeoutSeconds) }; http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", taoOptions.ApiKey); var toolSummary = string.Join("\n", tools.Select(t => $"- {t.Name}: {t.Description}")); var payload = new { model = taoOptions.Model, messages = new[] { new { role = "user", content = $"当前可用工具如下:\n{toolSummary}\n请用一句话说明你能帮我做什么。" } } }; var json = JsonSerializer.Serialize(payload); var response = await http.PostAsync( "/v1/chat/completions", new StringContent(json, Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); Console.WriteLine("模型返回:"); Console.WriteLine(body);这段代码把 MCP 的工具发现结果和模型调用串起来了。BaseUrl用https://taotoken.net/api,路径拼/v1/chat/completions,认证走 Bearer。Key 从配置读,不写死在代码里。
5. 验证请求:一次完整的握手与调用
5.1 运行前检查清单
跑之前确认三件事:appsettings.json 里的Mcp.Endpoint是真实可达的 MCP Server;TaoToken.ApiKey已替换;csproj 里配置复制那段加上了。然后:
dotnet run5.2 预期输出
正常的话,控制台会依次打印:
准备连接 MCP Server: https://your-mcp-server.example.com/sse MCP 握手完成,连接已建立 发现 3 个工具: - fetch: 抓取指定网页内容 - search: 执行关键词搜索 - calc: 执行数学计算 模型返回: {"id":"...","choices":[{"message":{"role":"assistant","content":"我可以帮你抓取网页、搜索信息并做计算。"}}]}看到“MCP 握手完成”这行,说明协议层通了;看到模型返回内容,说明 TaoToken 通道也通了。这两步都过,最小可用客户端就算跑通。
5.3 用模型对话页面交叉验证
如果模型返回异常,先去模型对话页面用同样的模型名发一条消息,确认 Key 和模型本身没问题:
模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite
页面能正常回,说明问题在客户端代码;页面也报错,说明是 Key 或模型配置的问题。这个二分法能省很多排查时间。
6. 本篇常见错误排查
6.1 握手阶段超时或连接被拒
最常见的是Mcp.Endpoint写错。SSE 端点必须以/sse结尾,写成根路径会 404。另外确认网络能直连该地址,企业内网可能需要走内部网关。如果 Server 用的是 Stdio 协议,用 SSE 传输去连必然失败,得换StdioClientTransport。
6.2 配置文件读不到
报FileNotFoundException或配置全为默认值,检查两点:csproj 里有没有CopyToOutputDirectory;SetBasePath是不是指向了AppContext.BaseDirectory。用相对路径Directory.GetCurrentDirectory()在dotnet run和直接跑 exe 时行为不一致,容易出问题。
6.3 模型请求返回 401 或 403
Key 没替换、Key 前后有空格、或者Authorization头拼错都会导致。确认格式是Bearer sk-xxx,中间一个空格。另外 BaseUrl 别写成带路径的形式,https://taotoken.net/api后面代码里再拼/v1/chat/completions,两处别重复。
6.4 工具列表为空
握手成功但ListToolsAsync返回 0 个工具,说明 Server 端没注册工具,或者当前客户端权限不够。换一个已知有工具的 Server 端点验证,排除是客户端问题还是服务端问题。
6.5 长期编码场景的配置建议
如果你打算把这个骨架用在日常编码或 Agent 长任务里,频繁手动填 Key 不现实。可以了解下 Coding Plan,它面向的就是长期编码和 Agent 场景的稳定通道:
Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
接入细节和参数说明都在文档里,遇到协议层问题先翻文档:
接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
6.6 一个容易忽略的坑:HttpClient 复用
上面示例里每次运行 new 一个 HttpClient,在控制台 demo 里没问题,但搬到常驻服务里必须用IHttpClientFactory或单例复用。频繁创建 HttpClient 会导致端口耗尽,这个坑在压测时才会暴露,提前注意。
7. 把骨架接进你的项目
到这里,一个可配置的 C# MCP Client 骨架就成型了:配置外置在 appsettings.json,传输层用 SSE,工具发现走官方 SDK,模型调用统一收敛到 TaoToken 通道。你可以在此基础上加工具调用循环、加日志、加重试,把它变成真正能用的客户端。
下一步建议先把 Stdio 传输也补上,这样本地工具和远程工具都能覆盖。自定义 MCP Server 的部分,等传输层两种都跑通后再动手会顺很多。代码里所有 Key 都从配置读,别图省事写死,这是能长期维护的前提。