1. 为什么我要自己写一个 MCP 服务器MCP 服务器Model Context Protocol Server说白了就是给大模型外挂的一双手模型本身只会生成文字但通过 MCP 协议它可以调用你注册好的工具函数去查数据库、读文件、发请求、算数据。适合谁适合手里有一堆内部接口、想让 AI 直接调用的后端和桌面开发者。我这次用 C# 从零搓一个最小可用的 MCP 服务器把协议握手、工具注册、调用链路全部跑通再通过 TaoToken 的统一 Key 把 AI 工具接进来让整条链路真正能对话、能执行。很多人卡在第一步以为 MCP 是个很玄的东西。其实它就是一个约定好的 JSON-RPC 通信规范客户端和服务端按格式交换消息服务端告诉客户端我有哪些工具、参数是什么客户端把模型的调用意图翻译成请求发过来服务端执行完把结果塞回去。你只要把这三件事做对MCP 服务器就活了。这篇会交付一个可复制的项目骨架、一份 config.toml 配置示例以及一次完整的本地验证动作。全程不需要你懂什么高深协议跟着敲就行。2. TaoToken 前置准备统一 Key 与通道在写代码之前先把 AI 侧的通道准备好。MCP 服务器本身只负责执行工具真正发起对话、决定调用哪个工具的是模型客户端。我用 TaoToken 来做统一入口好处是一个 Key 走通模型对话和工具调用不用在多个平台之间来回切。你需要先拿到 API Key。打开控制台页面登录后进入 API Keys 管理新建一个 Key 并复制保存。这个 Key 就是后面配置里要填的凭证。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_csharpAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_csharp接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_csharpAPI 的基础地址是https://taotoken.net/api注意这个地址不带任何查询参数配置时直接填这个即可。模型对话、工具调用都走同一个通道省去了维护多套凭证的麻烦。提示Key 只显示一次复制后立刻存到本地配置文件或环境变量里别直接硬编码进提交到仓库的源码。如果你后面要做长期编码或 Agent 类任务可以了解下 Coding Plan它更适合高频调用场景只是验证模型和工具链路的话用模型对话页面配合本地服务就够了。3. 可复制配置项目骨架与 config.toml先建项目。用 .NET CLI 起一个 Web 项目MCP 的 C# SDK 目前以预览包形式提供安装时带上--prerelease。dotnet new web -n McpDemo cd McpDemo dotnet add package ModelContextProtocol --prerelease项目结构保持简单McpDemo/ ├── Program.cs ├── Tools/ │ └── MyTools.cs ├── config.toml └── McpDemo.csprojconfig.toml用来放模型通道和服务器参数避免散落在代码里[server] name mcp-demo transport sse port 5180 [ai] base_url https://taotoken.net/api api_key sk-你的Key model claude-3-5-sonnet [tools] enabled [GeneratePraise, Add, GetServerTime]然后在Program.cs里读取配置、注册 MCP 服务并映射路由using ModelContextProtocol.Server; using Tomlyn; var builder WebApplication.CreateBuilder(args); // 读取 config.toml var configText File.ReadAllText(config.toml); var config Toml.ToModel(configText); builder.Services.AddMcpServer() .WithToolsFromAssembly(); var app builder.Build(); app.MapMcpSse(); app.Run();这里WithToolsFromAssembly()会自动扫描当前程序集里所有带[McpServerToolType]的类把[McpServerTool]标注的方法注册成可调用工具。你不需要手写路由映射SDK 帮你做了。工具类长这样using System.ComponentModel; using ModelContextProtocol.Server; [McpServerToolType] public class MyTools { [McpServerTool] [Description(根据名字生成一句彩虹屁)] public string GeneratePraise(string name) { return ${name} 老师真是玉树临风代码一写一个准。; } [McpServerTool] [Description(计算两个整数之和)] public int Add(int a, int b) { return a b; } [McpServerTool] [Description(返回服务器当前时间)] public string GetServerTime() { return DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss); } }Description特性很关键它不是写给人看的注释而是会随工具列表一起发给模型模型靠它判断这个工具是干嘛的、什么时候该调。描述写得越清楚模型选错工具的概率越低。4. 验证请求跑通一次完整调用代码写完先本地启动dotnet run看到监听http://localhost:5180就说明服务起来了。MCP 的 SSE 端点默认挂在/sse路径下客户端连上后会先收到一条 endpoint 事件里面带着后续发消息用的地址。验证分两步。第一步确认工具列表能正确暴露用 curl 模拟客户端握手curl -N http://localhost:5180/sse正常会持续输出事件流包含event: endpoint和data: /message?sessionIdxxx。拿到 sessionId 后发一条初始化请求curl -X POST http://localhost:5180/message?sessionId你的sessionId \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: { name: curl-test, version: 1.0 } } }返回里会带上服务端的能力声明。接着请求工具列表curl -X POST http://localhost:5180/message?sessionId你的sessionId \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 2, method: tools/list, params: {} }你应该能看到GeneratePraise、Add、GetServerTime三个工具每个都带着参数 schema。最后真正调用一次curl -X POST http://localhost:5180/message?sessionId你的sessionId \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 3, method: tools/call, params: { name: Add, arguments: { a: 3, b: 5 } } }返回结果里content字段会包含8。到这一步MCP 服务器的握手、工具注册、调用链路就全部跑通了。整个过程没有任何魔法就是标准的 JSON-RPC 一来一回。5. 本篇常见错排查启动报端口占用config.toml里默认 5180被占了就改port或者用dotnet run --urls http://localhost:5190临时覆盖。tools/list 返回空数组九成是工具类没加[McpServerToolType]或者方法不是public。SDK 只扫描公开方法私有方法会被忽略。另外确认WithToolsFromAssembly()调用的是当前程序集工具类别放到另一个没被引用的项目里。调用工具报 method not found检查tools/call里的name是否和 C# 方法名完全一致大小写敏感。如果你用了[McpServerTool(Name xxx)]重命名就要用重命名后的名字。SSE 连上但收不到 endpoint 事件确认路由映射用的是MapMcpSse()而不是别的并且请求路径是/sse。有些反向代理会缓冲 SSE 流本地直连一般没这问题。模型侧不调用工具先确认工具描述是否清晰模糊的描述会让模型犹豫。再检查客户端是否真的把工具列表传给了模型有些客户端需要显式开启工具调用能力。用 TaoToken 通道时确认base_url填的是https://taotoken.net/apiKey 没有多余空格。中文参数乱码请求头带上Content-Type: application/json; charsetutf-8服务端默认按 UTF-8 解析一般不会出问题但 curl 在某些终端下需要显式声明。6. 把链路接到 AI 工具上本地验证通过后就可以把 MCP 服务器接到真实的 AI 客户端里。在客户端的 MCP 配置中填入你的 SSE 地址http://localhost:5180/sse模型就能看到你注册的工具。对话时你说帮我算一下 3 加 5模型会自己决定调用Add工具拿到结果再组织成自然语言回复你。模型通道这边统一走 TaoToken 的 API 地址一个 Key 同时管对话和工具调用。想先感受下模型对话效果可以直接在模型对话页面里试要做长期编码或 Agent 任务Coding Plan 更合适接入细节和参数说明都在接入文档里。我踩过的一个坑是一开始把工具描述写得太笼统模型经常在该调用工具的时候选择直接编答案。后来把每个Description改成什么场景下用、参数是什么含义命中率明显上来了。MCP 服务器的质量一半在代码一半在描述。到这里一个能跑、能调、能接 AI 的 C# MCP 服务器就完整了。接下来你可以往MyTools里继续加方法每加一个带[McpServerTool]的公开方法模型就多一项能力。