1. 为什么 C# 开发者需要自己写 MCP Server 和 ClientMCP 全称 Model Context Protocol你可以把它理解成 AI 世界里的 USB-C 接口模型本身只会聊天但通过 MCP 这个统一协议它就能插上数据库、文件系统、内部 API、时间服务等外部能力。对 C# 开发者来说这件事的意义在于——你不需要为每个模型单独写一套对接逻辑只要按 MCP 标准封装一次所有支持 MCP 的客户端都能调用你的工具。这篇教程面向的是零基础但会一点 C# 的开发者目标很明确从空目录开始搭出一个能跑的 MCP Server再写一个控制台 Client 去调用它最后用 TaoToken 统一 Key 通道把模型请求接进来形成模型 → Client → Server → 工具的完整链路。全程用 .NET 8.0Windows、macOS、Linux 都能跟。我试过把 Server 和 Client 拆成两个独立项目这样调试时能清楚看到每一步是谁在发请求、谁在返回结果比塞进一个工程里更容易排错。下面按这个思路走。2. 前置准备TaoToken 统一 Key 与 API 通道在写代码之前先把模型侧的通道准备好。MCP Client 最终要调用模型来理解用户意图如果每个模型都单独配一套 Key代码里会到处是硬编码。TaoToken 的作用就是提供一个统一的 API 入口你只需要一个 Key就能在 Client 里通过 OpenAI 兼容格式访问不同模型。你需要做两件事第一注册并拿到 API Key。访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 完成账号注册然后进入控制台创建 Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建后复制那串以sk-开头的字符串后面配置里会用到。第二确认 API 基地址。TaoToken 的 API 端点是https://taotoken.net/api注意这个地址不带任何查询参数直接作为BaseAddress使用。它兼容 OpenAI 的/v1/chat/completions路径所以你在 C# 里用HttpClient或官方 OpenAI SDK 都能直接对接。注意API Key 不要写进代码提交到 Git。本地开发用appsettings.Development.json或环境变量生产环境用密钥管理服务。如果你只是想先验证模型通道是否通可以打开模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 手动发一条消息确认 Key 有效后再写代码能省掉很多到底是 Key 错还是代码错的排查时间。3. 可复制配置config.toml 与 settings.json 骨架MCP 生态里有两类配置文件容易混淆一类是 MCP 客户端比如 Claude Desktop、Cline 等用来描述要连接哪些 Server的配置通常是 JSON另一类是 Server 自身的运行配置可以用 TOML。我们两个都给出来你按需取用。先看 MCP Client 侧的settings.json骨架它描述的是客户端要启动哪个 Server 进程、传什么参数{ mcpServers: { edt-time-server: { command: dotnet, args: [ run, --project, ./EDT.McpServer.WebHost/EDT.McpServer.WebHost.csproj ], env: { TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }这段配置的意思是客户端会执行dotnet run启动我们的 Server 项目并把 TaoToken 的 Key 和 Base URL 通过环境变量注入进去。Server 内部读取这两个变量就能在需要时调用模型。再看 Server 侧的config.toml用来管理工具开关和超时[server] name edt-time-server version 0.1.0 transport sse port 5001 [model] provider taotoken base_url https://taotoken.net/api default_model gpt-4o-mini timeout_seconds 30 [tools] enable_time true enable_weather falsetransport sse表示用 Server-Sent Events 做传输这是 MCP 早期最常用的方式调试直观。base_url指向 TaoToken 的 API 地址default_model你可以按需换成控制台里支持的任意模型。提示TOML 里的port要和后面Program.cs里实际监听的端口一致否则 Client 会连到空端口上报连接被拒绝。4. 搭建 MCP Server从项目创建到工具注册4.1 创建项目与安装 SDK打开终端执行dotnet new webapi -n EDT.McpServer.WebHost -f net8.0 cd EDT.McpServer.WebHost dotnet add package ModelContextProtocol --version 0.1.0-preview.4 dotnet add package ModelContextProtocol.AspNetCore --version 0.1.0-preview.4ModelContextProtocol是核心协议库ModelContextProtocol.AspNetCore提供 Web 宿主集成。版本号用 preview 版即可正式版发布后直接升级。4.2 编写 Program.cs把默认的Program.cs替换成下面这段using ModelContextProtocol.AspNetCore; using EDT.McpServer.WebHost.Tools; var builder WebApplication.CreateBuilder(args); builder.Services .AddMcpServer() .WithToolsFromAssembly(); var app builder.Build(); app.MapMcp(); app.Run();AddMcpServer()注册 MCP 服务WithToolsFromAssembly()会自动扫描当前程序集里所有带[McpServerTool]特性的类并注册为可调用工具。MapMcp()是关键它会挂载/sse端点Client 就是连这个地址。4.3 写一个时间工具在Tools/TimeTool.cs里using ModelContextProtocol.Server; using System.ComponentModel; namespace EDT.McpServer.WebHost.Tools; [McpServerTool] public static class TimeTool { [Description(根据城市名称返回当前时间)] public static string GetTime( [Description(城市名称例如成都)] string city) { return $当前时间{DateTime.Now:yyyy-MM-dd HH:mm:ss} - 城市{city}; } }[Description]特性很重要它会被序列化进工具的元数据模型靠这段描述判断什么时候该调用这个工具。描述写得越清楚模型选错工具的概率越低。4.4 启动并验证 Serverdotnet run控制台会输出监听地址默认类似https://localhost:5001。用浏览器或 curl 访问https://localhost:5001/sse如果看到持续的事件流或握手信息说明 Server 已经起来了。如果报证书错误本地开发可以先用http://localhost:5000测试生产再上 HTTPS。5. 搭建 MCP Client连接 Server 并调用工具5.1 创建控制台项目dotnet new console -n EDT.McpClient.Console -f net8.0 cd EDT.McpClient.Console dotnet add package ModelContextProtocol --version 0.1.0-preview.45.2 编写连接与调用代码using ModelContextProtocol.Client; var client await McpClientFactory.CreateAsync(new() { Id time, Name Time MCP Server, TransportType TransportTypes.Sse, Location https://localhost:5001/sse }); var tools await client.ListToolsAsync(); Console.WriteLine($发现 {tools.Count} 个工具); var result await client.CallToolAsync( GetTime, new Dictionarystring, object? { { city, 成都 } }); foreach (var content in result.Content) { if (content.Type text) Console.WriteLine(content.Text); }McpClientFactory.CreateAsync负责建立连接TransportTypes.Sse对应 Server 的/sse端点。ListToolsAsync先列出所有可用工具确认 Server 注册成功CallToolAsync传入工具名和参数字典返回的内容里取text类型打印。5.3 运行结果dotnet run预期输出发现 1 个工具 当前时间2025-01-15 14:32:07 - 城市成都看到这行字说明 Server 和 Client 已经连通工具调用链路完整。6. 把 TaoToken 接进 Client让模型决定调用哪个工具前面 Client 是手动指定工具名真实场景里应该由模型根据用户输入自动选择。这一步用 TaoToken 的 API 通道完成。在 Client 项目里加一个ModelClient.csusing System.Net.Http.Headers; using System.Text; using System.Text.Json; public class ModelClient { private readonly HttpClient _http; public ModelClient(string apiKey) { _http new HttpClient { BaseAddress new Uri(https://taotoken.net/api) }; _http.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, apiKey); } public async Taskstring ChatAsync(string prompt) { var payload new { model gpt-4o-mini, messages new[] { new { role user, content prompt } } }; var json JsonSerializer.Serialize(payload); var resp await _http.PostAsync( /v1/chat/completions, new StringContent(json, Encoding.UTF8, application/json)); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadAsStringAsync(); } }调用时从环境变量读 Keyvar apiKey Environment.GetEnvironmentVariable(TAOTOKEN_API_KEY) ?? throw new InvalidOperationException(缺少 TAOTOKEN_API_KEY); var model new ModelClient(apiKey); var reply await model.ChatAsync(现在成都几点了); Console.WriteLine(reply);这样 Client 就同时具备两种能力通过 MCP 调用本地工具通过 TaoToken 调用模型。你可以把工具列表序列化后塞进模型的 system prompt让模型输出要调用哪个工具、传什么参数Client 解析后再执行CallToolAsync形成完整的 Agent 循环。如果你打算长期做编码类 Agent建议了解一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它在长上下文和代码场景下的额度策略更适合持续调用。接入细节可以参考文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。7. 本篇常见错误排查报错一Connection refused连不上 /sse。先确认 Server 是否真的在跑dotnet run的窗口有没有异常退出。再确认端口Program.cs里没显式指定端口时ASP.NET Core 默认走launchSettings.json里的配置可能和你以为的不一样。最稳的办法是在app.Run()前加app.Urls.Add(http://localhost:5001);强制固定。报错二401 Unauthorized调模型失败。检查TAOTOKEN_API_KEY环境变量是否真的注入到了 Client 进程。在settings.json里配的env只对通过 MCP 启动的 Server 生效Client 自己跑的时候要单独设。Windows 用setxmacOS/Linux 用export或者直接在 IDE 的启动配置里填。报错三Tool not found: GetTime。工具名大小写敏感CallToolAsync里传的名字必须和 C# 方法名完全一致。另外确认WithToolsFromAssembly()扫描的程序集包含TimeTool类如果工具类在另一个项目里需要显式WithToolsFromAssembly(typeof(TimeTool).Assembly)。报错四SSE 连接建立后立刻断开。常见于 HTTPS 自签证书场景Client 不信任本地证书。开发阶段把Location改成http://地址或者给HttpClient配一个跳过证书校验的HttpClientHandler仅限本地调试别带到生产。报错五模型返回了工具调用意图但 Client 没执行。这是逻辑问题不是配置问题。你需要自己写解析层把模型输出的 JSON 解析成工具名和参数再调CallToolAsync。MCP 协议本身不负责模型自动调工具那是 Client 的编排职责。8. 下一步把链路跑通之后做什么Server 和 Client 连通只是起点。接下来可以做的方向有几个把TimeTool换成真实业务工具比如查订单、读数据库、调内部 API给 Server 加认证用 JWT 或 API Key 限制谁能调把多个 Server 注册到同一个 Client让模型在多个工具集之间选择。验证模型通道是否稳定可以直接用模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 发几条复杂指令观察响应质量。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到参数问题先查文档再改代码。Key 管理统一在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 建议给不同项目建不同的 Key方便按项目排查调用量。最后提醒一句MCP Server 暴露的是真实能力别把生产数据库直连进去。先在测试环境把工具逻辑跑稳再考虑上线。