1. 为什么我要手写 MCP 服务端和客户端MCP 全称 Model Context Protocol是一套让大模型和智能体用统一方式调用外部工具、读取外部资源的通信规范。你可以把它理解成 AI 世界里的 USB-C 接口以前每接一个工具就要写一套私有 API现在只要双方都遵守 MCP工具就能被任何支持该协议的客户端发现和调用。它适合谁适合正在做智能体落地、想让大模型真正操作本地文件或业务系统的开发者也适合想搞懂 MCP 通信机制而不是只会调包的人。我这次的目标很明确不依赖任何黑盒平台从零写一个最小可运行的 MCP 服务端和一个 MCP 客户端让客户端通过标准输入输出把服务端拉起来完成握手、列出工具、调用工具这一整条链路。同时把模型调用通道统一到 TaoToken 的 Key 上这样服务端里需要访问大模型的能力时不用在多个厂商之间来回切换配置。整篇会给出 config.toml 和 settings.json 的骨架、可复制的服务端与客户端代码以及一次完整的本地联调验证动作。跟着做你能亲眼看到 MCP 的请求和响应长什么样而不是停留在概念层面。2. TaoToken 前置准备统一 Key 与 API 通道在写代码之前先把模型通道准备好。TaoToken 的作用是提供一个统一的 Key 和 API 入口让服务端在需要调用大模型时只认一个地址、一个密钥省掉多平台配置的麻烦。你需要做两件事拿到 Key确认 API 地址。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。第二步进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完成后把 Key 复制出来形如sk-xxxx后面配置里会用到。API 的基础地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 base_url 使用。如果你后面要跑长期编码任务或者 Agent 循环可以了解下 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。只是想先验证模型通不通用模型对话页面就够了https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。Key 的管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这几个地址先记着排障和接入时按需跳转。注意Key 只放在本地配置文件或环境变量里不要硬编码进要提交到仓库的代码。下面示例里我用占位符你替换成自己的。3. 可复制配置config.toml 与 settings.json 骨架MCP 服务端和客户端之间靠配置描述怎么启动、用什么通道。这里给两份骨架一份给服务端读模型参数一份给客户端描述如何拉起服务端。先看服务端的config.toml放在项目根目录# config.toml [model] # TaoToken 统一 API 地址不带查询参数 base_url https://taotoken.net/api # 替换成你在控制台创建的 Key api_key sk-你的Key # 具体模型名按文档填写 model 你的模型名 timeout 60 [mcp] server_name DemoServer transport stdio再看客户端的settings.json它描述客户端如何启动服务端进程{ mcpServers: { demo: { command: python, args: [server.py], env: { TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }这两份配置的分工要分清config.toml是服务端自己读的决定它调用模型时走哪个通道settings.json是客户端读的决定它用什么命令把服务端拉起来、传什么环境变量。把 Key 通过环境变量注入比写死在代码里更安全也方便你在不同机器上切换。4. 服务端实现用 FastMCP 暴露工具与资源先装依赖Python 建议 3.10 以上pip install mcp[cli]然后写server.py。核心是用FastMCP注册工具、资源和提示模板最后mcp.run()以 stdio 方式启动# server.py import os from mcp.server.fastmcp import FastMCP mcp FastMCP(DemoServer) mcp.tool() def add(a: int, b: int) - int: 将两个整数相加 return a b mcp.tool() def get_model_config() - dict: 返回当前模型通道配置不包含密钥明文 return { base_url: os.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api), has_key: bool(os.environ.get(TAOTOKEN_API_KEY)), } mcp.resource(greeting://{name}) def get_greeting(name: str) - str: 根据名字返回问候语 return f你好{name} mcp.prompt() def greet_user(name: str) - str: 生成问候提示模板 return f你好{name}今天我能如何帮助你 if __name__ __main__: mcp.run()这里mcp.tool()注册的是可被客户端调用的函数mcp.resource()注册的是可通过 URI 读取的资源mcp.prompt()注册的是提示模板。get_model_config这个工具故意不返回密钥明文只返回是否已配置避免联调时把敏感信息打印到日志里。服务端本身不主动连模型它只是把能力暴露出去真正调用模型的动作可以放在工具内部用config.toml里的 base_url 和 Key 去请求 TaoToken 的 API。5. 客户端实现与本地联调验证客户端client.py负责启动服务端进程、建立会话、依次调用各类能力# client.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client server_params StdioServerParameters( commandpython, args[server.py], env{ TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api, }, ) async def run_client(): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() print(连接服务器成功) prompts await session.list_prompts() print(可用提示:, prompts) prompt_resp await session.get_prompt( greet_user, arguments{name: Alice} ) print(提示响应:, prompt_resp) resources await session.list_resources() print(可用资源:, resources) content, mime await session.read_resource(greeting://Bob) print(资源内容:, content, mime) tools await session.list_tools() print(可用工具:, tools) result await session.call_tool(add, arguments{a: 3, b: 5}) print(add 结果:, result) cfg await session.call_tool(get_model_config, arguments{}) print(模型配置:, cfg) if __name__ __main__: asyncio.run(run_client())保存两个文件到同一目录运行python client.py预期输出会依次打印连接成功、提示列表、提示响应、资源列表、资源内容、工具列表、add返回 8以及模型配置里has_key为True。看到这些就说明整条 stdio 链路通了客户端拉起服务端、完成 initialize 握手、发现能力、调用工具并拿到结果。这一步跑通你就真正理解了 MCP 的通信机制——它不是魔法就是一套结构化的请求响应。6. 本篇常见错排查报错ModuleNotFoundError: No module named mcp说明依赖没装到当前 Python 环境。确认你用的python和pip是同一个解释器必要时用python -m pip install mcp[cli]。客户端卡住不输出多半是服务端启动失败但错误被吞了。把server.py单独跑一次python server.py看有没有语法错误或导入错误。stdio 模式下服务端不应该往 stdout 打印调试信息否则会污染协议数据调试信息请走 stderr。has_key为False检查settings.json或StdioServerParameters里的env是否真的传进去了Key 名要和代码里os.environ.get的键一致。注意环境变量只在子进程生效父进程的 shell 变量不会自动继承。调用模型时报 401 或地址错误核对 base_url 是否为https://taotoken.net/api不要多加路径或查询参数。Key 是否过期可以在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 复查。接入细节以 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 为准。资源 URI 读不到greeting://{name}里的{name}是动态段读取时要传完整 URI比如greeting://Bob不能只传Bob。7. 下一步把通道接到真实模型与 Agent服务端和客户端跑通后下一步就是把工具内部真正接到模型上。如果你只是想让模型对话验证通道用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 就够。如果你要做长期编码或 Agent 循环建议看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Key 管理和接入文档分别在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Claude Code 相关接入参考 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite 。我自己的做法是先把add这种纯函数工具跑稳再把数据库查询、文件读写这类真实能力逐个挂上去每加一个就用客户端调一次确认返回结构符合预期。这样出问题时能快速定位是工具逻辑还是协议层的问题。