
1. 本地 MCP Server 与 Client 到底解决什么问题MCP 全称 Model Context Protocol你可以把它理解成一套“模型和外部工具之间的 USB 接口标准”。以前想让大模型读一个本地文件、查一次数据库、调一个内部接口每个模型厂商、每个框架的写法都不一样换一个模型就得重写一遍胶水代码。MCP 把这件事抽象成 Server 和 Client 两端Server 负责暴露能力资源、工具、提示词Client 负责连接模型和 Server双方用统一的 JSON-RPC 消息通信。你只要按协议写一次 Server任何支持 MCP 的 Client 都能直接接上。这篇要做的是从零搭一个本地 MCP Server再写一个本地 MCP Client用 stdio 传输把两端跑通最后通过 TaoToken 的统一 Key 和 API 通道接入模型完成一次端到端的调用测试。适合谁看适合已经会写 Python、想搞清楚 MCP 通信机制而不是只会点按钮的开发者。我试过把 Server 和 Client 拆成两个文件分别调试比一上来就塞进某个 IDE 插件里更容易定位问题。核心检索词先摆出来本地 MCP Server 搭建、MCP Client 联调、MCP stdio 传输、MCP 端到端调用测试。这几个词贯穿全文你照着做就能复现。先说清楚整体链路。MCP 的传输方式常见有两种stdio 和 SSE。stdio 是 Client 把 Server 当子进程启动通过标准输入输出收发消息适合本地开发和单机工具SSE 是 Server 跑成一个 HTTP 服务Client 通过网络连适合远程共享。本文先用 stdio 跑通因为它不涉及端口和网络排障面最小后面再给 SSE 的配置方便你扩展到多 Client 场景。一个容易踩的坑是很多人以为 MCP Server 必须连模型才能跑。其实 Server 本身完全不碰模型它只负责“提供数据”和“执行动作”。模型调用发生在 Client 侧Client 把 Server 暴露的资源或工具描述转成模型能理解的上下文模型决定要不要调用Client 再把调用结果回传。理解这一点你就知道为什么 Server 可以独立测试——本文的 Client 第一步就是先不接模型直接列出资源确认协议通了再上模型。环境准备很简单Python 3.10 以上装官方 SDK。命令如下建议放虚拟环境里避免和系统包冲突。python -m venv mcp-env source mcp-env/bin/activate pip install mcp[cli] anyio click pydantic uvicorn starlette装完可以用pip show mcp确认版本。SDK 迭代比较快如果某个 API 名字对不上优先看mcp包里的types.py和server/lowlevel比翻文档快。下面进入 Server 的编写。2. TaoToken 前置统一 Key 与 API 通道准备在写 Client 接模型之前先把模型通道准备好。MCP Client 本身不绑定任何模型厂商它需要一个能发 chat/completions 请求的入口。TaoToken 在这里的作用是提供统一的 Key 和 API 通道你拿到一个 Key、一个 Base URL就能在 Client 里调用模型不用为每个模型单独配一套鉴权和地址。先注册并登录控制台地址是 https://taotoken.net/console 。进去之后在 API Keys 页面创建一个 Key复制出来保存好这个 Key 只在创建时完整显示一次。创建入口https://taotoken.net/api-keys 。拿到 Key 之后你需要记住两个东西Base URL 是https://taotoken.net/apiKey 形如sk-开头的一串字符。这两个值后面会写进 Client 的配置里。注意 Base URL 不要加 UTM 参数直接用它作为请求前缀即可。模型 ID 怎么选在模型对话页面可以先试跑一下确认某个模型 ID 可用再写进代码。模型对话入口https://taotoken.net/model-chat 。如果你打算长期跑编码类 Agent 任务可以看 Coding Planhttps://taotoken.net/coding-plan 它更适合高频调用场景。这里给一个最小验证确认你的 Key 和通道是通的。用 curl 发一条最简单的请求curl https://taotoken.net/api/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: 只回复两个字通了}] }如果返回里有choices字段和内容说明通道没问题。如果返回 401先检查 Key 有没有复制完整、有没有多余空格如果返回模型不存在换一个模型 ID 再试。这一步过了再往下写 Client 接模型才有意义否则你会分不清是 MCP 的问题还是模型通道的问题。把 Key 存到环境变量里别硬编码进代码方便后面切换export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api到这里前置就绪。接下来是本文最核心的部分可复制的 Server 和 Client 配置。3. 可复制配置Server 与 Client 完整代码这一节给出两个文件server.py和client.py以及一份把模型接进来的settings片段。路径按你实际存放位置改我这里假设都放在/root/domcp/下。先写 Server。它暴露三个文本资源支持 stdio 和 SSE 两种传输。代码里用Server(mcp-simple-resource)建实例注册list_resources和read_resource两个处理器。# /root/domcp/server.py import anyio import click import mcp.types as types from mcp.server.lowlevel import Server from pydantic import FileUrl SAMPLE_RESOURCES { greeting: Hello! This is a sample text resource., help: This server provides a few sample text resources for testing., about: This is the simple-resource MCP server implementation., } click.command() click.option(--port, default10005, helpPort to listen on for SSE) click.option( --transport, typeclick.Choice([stdio, sse]), defaultstdio, helpTransport type, ) def main(port: int, transport: str) - int: app Server(mcp-simple-resource) app.list_resources() async def list_resources() - list[types.Resource]: return [ types.Resource( uriFileUrl(ffile:///root/domcp/{name}.txt), namename, descriptionfA sample text resource named {name}, mimeTypetext/plain, ) for name in SAMPLE_RESOURCES.keys() ] app.read_resource() async def read_resource(uri: FileUrl) - str | bytes: name uri.path.replace(.txt, ).lstrip(/) if name not in SAMPLE_RESOURCES: raise ValueError(fUnknown resource: {uri}) return SAMPLE_RESOURCES[name] if transport sse: from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Mount, Route sse SseServerTransport(/messages/) async def handle_sse(request): async with sse.connect_sse( request.scope, request.receive, request._send ) as streams: await app.run( streams[0], streams[1], app.create_initialization_options() ) starlette_app Starlette( debugTrue, routes[ Route(/sse, endpointhandle_sse), Mount(/messages/, appsse.handle_post_message), ], ) import uvicorn uvicorn.run(starlette_app, host0.0.0.0, portport) else: from mcp.server.stdio import stdio_server async def arun(): async with stdio_server() as streams: await app.run( streams[0], streams[1], app.create_initialization_options() ) anyio.run(arun) return 0 if __name__ __main__: main()再写 Client。第一版先不接模型只做协议联调启动 Server 子进程初始化会话列出资源读一个资源。# /root/domcp/client.py import asyncio from mcp.types import AnyUrl from mcp.client.session import ClientSession from mcp.client.stdio import StdioServerParameters, stdio_client async def main(): async with stdio_client( StdioServerParameters(commandpython, args[/root/domcp/server.py]) ) as (read, write): async with ClientSession(read, write) as session: await session.initialize() resources await session.list_resources() print(resources) print(----------------------) resource await session.read_resource(AnyUrl(file:///greeting.txt)) print(resource) asyncio.run(main())然后是接模型的配置片段。MCP Client 调模型时把 Base URL、Key、Model ID 三件套写进一个 settings 结构。下面这份 JSON 可以直接作为你项目里的配置模板路径和字段名按你工程改{ mcpServers: { simple-resource: { command: python, args: [/root/domcp/server.py], transport: stdio } }, model: { base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: gpt-4o-mini } }如果你用的是支持 MCP 的编辑器或 Agent 工具配置通常长这样注意 Base URL、Key、Model ID 三个字段一个都不能少# 示例MCP 客户端配置片段 [mcp_servers.simple-resource] command python args [/root/domcp/server.py] [model] base_url https://taotoken.net/api api_key sk-你的Key model_id gpt-4o-mini配置写好后先别急着接模型按下一节的步骤验证协议层。4. 验证请求与成功结果从 stdio 联调到端到端先跑协议层。在/root/domcp/下执行python client.py正常输出应该能看到三个资源以及 greeting 的内容。类似这样metaNone nextCursorNone resources[Resource(uriUrl(file:///root/domcp/greeting.txt), namegreeting, ...), Resource(uriUrl(file:///root/domcp/help.txt), namehelp, ...), Resource(uriUrl(file:///root/domcp/about.txt), nameabout, ...)] ---------------------- metaNone contents[TextResourceContents(uriUrl(file:///greeting.txt), mimeTypetext/plain, textHello! This is a sample text resource.)]看到resources[...]和contents[...]就说明 Server 和 Client 的 stdio 通道通了。这一步不涉及模型纯粹验证 MCP 协议。接着做端到端。在 Client 里加一段把列出的资源内容拼成上下文调用模型让模型基于资源回答。核心是构造请求体Base URL 用 TaoToken 的通道。import os, httpx async def ask_model(question: str, context: str) - str: base_url os.environ[TAOTOKEN_BASE_URL] api_key os.environ[TAOTOKEN_API_KEY] payload { model: gpt-4o-mini, messages: [ {role: system, content: f你可以参考以下资料回答\n{context}}, {role: user, content: question}, ], } async with httpx.AsyncClient(timeout60) as client: resp await client.post( f{base_url}/chat/completions, headers{Authorization: fBearer {api_key}}, jsonpayload, ) resp.raise_for_status() return resp.json()[choices][0][message][content]把它接进main()在read_resource之后调用text resource.contents[0].text answer await ask_model(greeting 资源里说了什么, text) print(模型回答, answer)再跑一次python client.py。成功的话你会先看到资源列表再看到模型基于 greeting 内容给出的回答。到这里本地 MCP Server Client TaoToken 模型通道的端到端链路就跑通了。如果你想验证 SSE 传输启动 Serverpython server.py --transport sse --port 10005然后用支持 SSE 的 Client 连http://localhost:10005/sse。SSE 模式下 Server 是常驻进程适合多个 Client 共享同一个 Server。stdio 模式每次 Client 启动都会拉起一个新 Server 进程适合单机调试。验证模型通道是否正常也可以直接在模型对话页面手动发一条消息对比结果https://taotoken.net/model-chat 。如果页面能通、代码不通问题多半在 Key 或请求体格式而不是通道本身。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来。你跑的时候大概率会遇到下面几个逐个对照。401 Unauthorized。最常见。原因通常是 Key 没带、带错、或者环境变量没生效。检查echo $TAOTOKEN_API_KEY有没有值请求头是不是Authorization: Bearer sk-xxxBearer 和 Key 之间有一个空格。如果 Key 是从网页复制的注意别把首尾空格带进去。还有一种情况是用了旧 Key去 https://taotoken.net/api-keys 重新生成一个。local proxy failed / connection refused。这个报错一般出现在 Client 连本地 Server 时。stdio 模式下检查StdioServerParameters里的command和args路径对不对python是不是当前虚拟环境里的解释器。如果你在虚拟环境里装包但 Client 用系统 python 启动就会找不到mcp模块。解决方法是把command写成虚拟环境里的绝对路径比如/root/mcp-env/bin/python。SSE 模式下检查 Server 有没有真的起来、端口有没有被占用lsof -i:10005看一眼。reading choices / KeyError choices。这个报错说明请求发出去了但返回体里没有choices字段。常见原因是模型 ID 写错或者请求体结构不对。先打印resp.status_code和resp.text看服务端到底返回了什么。如果返回的是错误信息按信息改如果返回的是别的结构检查你的 payload 是不是少了messages。另外model字段必须是通道支持的模型 ID写一个不存在的名字就会走到这个分支。OAuth / unauthorized_client。如果你在某个编辑器或 Agent 工具里配置 MCP可能会遇到 OAuth 相关报错。这通常是因为工具默认走了 OAuth 流程而你的场景只需要 API Key。检查配置里是不是同时存在 OAuth 和 API Key 两套鉴权把 OAuth 相关字段去掉只保留 Base URL Key Model ID 三件套。如果工具强制要求 OAuth换用支持 API Key 直连的配置方式。资源读不到 / Unknown resource。Server 里read_resource用uri.path解析文件名如果你传的 URI 和注册时的不一致就会抛ValueError。注册时是file:///root/domcp/greeting.txt读取时也要用同样的路径或者像示例里那样用file:///greeting.txt并在解析时lstrip(/)。路径大小写、斜杠数量都要对。Client 卡住不返回。stdio 模式下如果 Server 里有print输出到 stdout会污染 JSON-RPC 消息流导致 Client 解析失败或卡住。调试信息一律用sys.stderr或日志文件别用print。这个坑很隐蔽因为 Server 单独跑看起来正常一接 Client 就出问题。排障时建议按层隔离先单独跑 Server 确认能启动再跑不接模型的 Client 确认协议通最后接模型确认通道通。哪一层报错就查哪一层别混在一起调。接入文档在 https://taotoken.net/doc 配置细节可以对照看。6. 把 MCP 接进长期编码与 Agent 工作流协议跑通只是起点。真正有价值的是把 MCP Server 变成你日常编码和 Agent 工作流的一部分。比如你可以写一个 Server 暴露项目里的配置文件、接口文档、数据库 schemaClient 侧让模型按需读取而不是每次手动粘贴上下文。这样模型拿到的信息更准你也不用反复复制。扩展方向有几个。第一把资源换成动态读取比如read_resource里实时读文件而不是返回常量这样文件改了模型就能看到最新内容。第二加工具tools而不只是资源让模型能执行动作比如跑测试、查日志。第三把 stdio 换成 SSE让多个 Client 共享一个 Server适合团队场景。如果你要长期跑编码类 Agent 任务调用频率会比较高可以看 Coding Planhttps://taotoken.net/coding-plan 它在高频场景下更合适。配置方式还是那三件套Base URL 用https://taotoken.net/apiKey 用你创建的Model ID 按任务选。需要新建或轮换 Key 就去 https://taotoken.net/api-keys 。最后给一个实用建议把 Server 的资源和工具设计得“小而专”一个 Server 只干一件事。比如文件读取一个 Server、数据库查询一个 Server、内部接口调用一个 Server。这样调试简单复用性也高。Client 侧按需组合多个 Server模型看到的能力边界清晰出错也容易定位。MCP 的价值不在于单个 Server 多强大而在于这套标准让你能把各种能力像积木一样拼起来。