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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_csharp
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_csharp
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_csharp
API 的基础地址是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?sessionId=xxx。拿到 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/api,Key 没有多余空格。
中文参数乱码:请求头带上Content-Type: application/json; charset=utf-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]的公开方法,模型就多一项能力。