如何使用MCP开发一个客户端和服务端:从零搭建到TaoToken统一Key接入的完整实践
1. 从零理解 MCP 客户端与服务端到底在做什么MCPModel Context Protocol说白了就是给大模型装一个“标准插座”。你写好的工具函数只要按 MCP 的格式暴露出去任何支持 MCP 的客户端Claude Desktop、Cursor、你自己写的 Python 客户端都能直接调用不用再为每个模型单独写一套 Function Call 的胶水代码。它解决的问题是工具碎片化以前接一个数据库要写一套 schema接一个浏览器又要写一套现在统一成 JSON-RPC 2.0 的通信格式服务端只管声明能力客户端只管发现和调用。这套东西适合谁如果你正在做 AI Agent、想把公司内部系统CRM、工单、日志查询安全地暴露给大模型或者你只是想搞明白“客户端和服务端握手”到底发生了什么那这篇就是给你写的。我会先带你把本地 stdio 模式跑通看到完整的工具列表和调用日志再把服务端的鉴权通道切到 TaoToken 统一 Key让整条链路走同一个 API 入口。先明确两个角色。MCP 服务端Server负责注册工具、资源、提示词它不关心谁来调只按协议响应。MCP 客户端Client负责启动或连接服务端、拉取工具列表、把用户问题交给大模型、解析模型返回的 tool_calls、再去调用服务端工具、把结果塞回对话。整个握手过程是客户端 initialize → 服务端返回能力 → 客户端 list_tools → 拿到工具清单 → 进入对话循环。我实测下来最容易卡住的地方不是协议本身而是环境变量和传输方式选错。stdio 模式适合本地单机调试服务端作为子进程被客户端拉起SSE 或 streamable-http 适合远程部署但连接会自然断开需要重连逻辑。下面从安装开始一步步来。2. TaoToken 前置准备统一 Key 与 API 通道配置在把服务端 endpoint 切到统一通道之前你得先有一个能用的 Key 和 Base URL。TaoToken 在这里扮演的角色是统一的大模型 API 入口客户端里 AsyncOpenAI 的 base_url 指向它api_key 用你申请的那把模型 ID 按文档填。这样你的 MCP 客户端在解析 tool_calls 时走的就是同一条鉴权通道不用在多个平台之间来回切换 Key。具体操作打开 https://taotoken.net/api-keys 创建一把 API Key复制保存。然后确认你要用的模型 ID这个在模型对话页面能看到。Base URL 固定用 https://taotoken.net/api注意不要带多余的路径后缀OpenAI SDK 会自己拼 /chat/completions。这里有个细节很多人踩坑环境变量名。我建议统一写成 TAOTOKEN_API_KEY 和 TAOTOKEN_BASE_URL然后在代码里读而不是硬编码。这样你本地调试和后面部署到服务器时只改环境变量就行。配置文件我习惯放一个 config.json结构如下路径放在项目根目录{ llm: { api_key: sk-你的TaoTokenKey, base_url: https://taotoken.net/api, model: 你的模型ID }, mcp: { server_command: python, server_args: [server.py], transport: stdio } }如果你用的是 Claude Code 这类工具配置方式不太一样它读的是 settings 文件。但核心三件套不变Base URL、Key、Model ID。你可以先在模型对话页面确认这把 Key 能正常出结果再去接 MCP这样排障时能快速定位是 Key 的问题还是协议的问题。注意Key 不要提交到 Gitconfig.json 加进 .gitignore。生产环境用环境变量注入。装依赖这一步别偷懒。官方 Python SDK 用pip install mcp[cli]如果你用 uv 就uv add mcp[cli]。客户端还要装 openai 的异步版本因为我们要用 AsyncOpenAI。装完之后python -c import mcp; print(mcp.__version__)验证一下能打印版本号就说明环境没问题。3. 可复制配置服务端注册与客户端连接完整代码先写服务端。核心是用 FastMCP 装饰器把普通 Python 函数变成 MCP 工具。下面这个 server.py 注册了两个工具、一个资源、一个提示词启动方式用 stdio方便本地调试from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.prompts import base mcp FastMCP(namedemo) mcp.tool() def add_2_numbers(a: int, b: int) - int: 两个数字相加 return a b mcp.tool() def multiply_2_numbers(a: int, b: int) - int: 两个数字相乘 return a * b mcp.resource(config://app) def get_config() - str: 静态配置数据 return App configuration here mcp.prompt() def debug_error(error: str) - list[base.Message]: return [ base.UserMessage(Im seeing this error:), base.UserMessage(error), base.AssistantMessage(Ill help debug that. What have you tried so far?), ] if __name__ __main__: mcp.run(transportstdio)解释一下三个装饰器。mcp.tool()注册的是可执行动作模型能调它产生副作用或计算mcp.resource()注册的是只读数据类似 GET 端点用来给模型补充上下文mcp.prompt()注册的是可复用提示模板参数化之后能标准化交互。stdio 模式下服务端不监听端口而是等客户端通过标准输入输出跟它通信。客户端这边我用 AsyncOpenAI 接 TaoToken 通道同时用 stdio_client 拉起上面的服务端。关键代码import asyncio import json from contextlib import AsyncExitStack from typing import Optional from mcp import ClientSession, StdioServerParameters, stdio_client from openai import AsyncOpenAI class StdioMCPClient: def __init__(self, api_key, base_url, model): self.session: Optional[ClientSession] None self.exit_stack AsyncExitStack() self.client AsyncOpenAI(api_keyapi_key, base_urlbase_url) self.model model self.message [] async def connect_to_stdio_server(self, mcp_name, command, args, env{}): server_params StdioServerParameters(commandcommand, argsargs, envenv) stdio_transport await self.exit_stack.enter_async_context( stdio_client(server_params) ) self.stdio, self.write stdio_transport self.session await self.exit_stack.enter_async_context( ClientSession(self.stdio, self.write) ) await self.session.initialize() response await self.session.list_tools() tools response.tools print(f成功链接到 {mcp_name} 服务对应 tools, [t.name for t in tools]) self.available_tools [ { type: function, function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, } for tool in response.tools ] async def process_query(self, query: str): self.message.append({role: user, content: query}) response await self.client.chat.completions.create( modelself.model, messagesself.message, toolsself.available_tools, ) final_text [] assistant_message response.choices[0].message while assistant_message.tool_calls: for tool_call in assistant_message.tool_calls: tool_name tool_call.function.name tool_args json.loads(tool_call.function.arguments) result await self.session.call_tool(tool_name, tool_args) print(fcalling tools {tool_name}, with args {tool_args}) print(Result:, result.content[0].text) self.message.extend([ {role: assistant, content: None, tool_calls: [tool_call]}, {role: tool, content: result.content[0].text, tool_call_id: tool_call.id}, ]) response await self.client.chat.completions.create( modelself.model, messagesself.message, toolsself.available_tools, max_tokens8048, ) assistant_message response.choices[0].message final_text.append(assistant_message.content) return \n.join(final_text) async def chat_loop(self): self.message [] while True: query input(\nQuery: ).strip() if query.lower() quit: break if not query: continue response await self.process_query(query) print(\nAI:, response) async def cleanup(self): await self.exit_stack.aclose() async def main(): with open(config.json, r) as f: config json.load(f) client StdioMCPClient( config[llm][api_key], config[llm][base_url], config[llm][model], ) try: await client.connect_to_stdio_server(testserver, python, [server.py], {}) await client.chat_loop() finally: await client.cleanup() if __name__ __main__: asyncio.run(main())这段代码里connect_to_stdio_server完成握手和工具发现process_query是核心循环先让模型决定调哪个工具拿到 tool_calls 后去 session.call_tool 执行把结果以 roletool 塞回消息历史再让模型生成最终回答。注意 tool_call_id 必须对上否则模型会报错。如果你要远程部署把 stdio_client 换成 sse_client服务端启动改成mcp.run(transportsse)并指定 host 和 port。SSE 连接大概两分钟会自然断开所以客户端要加一个 reconnect 函数在每次 call_tool 前重新建立 session。这个坑我在生产环境踩过不加重连的话长对话跑到一半就断了。4. 验证请求一次完整调用日志与成功结果配置写完后跑起来看日志。先启动客户端它会自动拉起 server.py 子进程。终端输出应该是成功链接到 testserver 服务对应 tools [add_2_numbers, multiply_2_numbers] Query: 帮我算一下 12 加 30 等于多少 calling tools add_2_numbers, with args {a: 12, b: 30} Result: 42 AI: 12 加 30 等于 42。再试一个乘法验证多工具链路Query: 先算 7 乘 8再把结果加 6 calling tools multiply_2_numbers, with args {a: 7, b: 8} Result: 56 calling tools add_2_numbers, with args {a: 56, b: 6} Result: 62 AI: 7 乘 8 等于 56再加 6 等于 62。这里能看到模型连续调了两个工具中间结果通过消息历史传递这就是 MCP 多工具调用的完整链路。整个过程里模型请求走的是 TaoToken 的 Base URL鉴权用同一把 Key工具执行在本地 stdio 子进程里完成两边解耦得很干净。如果你想确认请求确实到了统一通道可以在客户端初始化时打印 base_url或者抓一下 AsyncOpenAI 的请求日志。正常情况下模型返回的 tool_calls 结构里 function.name 和 function.arguments 都是标准 JSON解析不会出问题。实测下来从握手到第一次工具调用成功整个链路在本地不到两秒。提示验证阶段先用简单算术工具确认链路通了再换成真实业务工具比如查数据库、调内部 API。这样排障范围小。5. 本篇常见错误排查清单报错一401 Unauthorized。这是 Key 或 Base URL 的问题。先检查 config.json 里的 api_key 是不是复制完整有没有多余空格。再确认 base_url 是 https://taotoken.net/api不要写成带 /v1 的路径。如果还报 401去 API Keys 页面重新生成一把排除 Key 被禁用的情况。报错二local proxy failed 或连接超时。这种一般是网络出口问题不是 MCP 协议本身。检查你的运行环境能不能正常访问外部 API本地防火墙有没有拦。stdio 模式下服务端是子进程不涉及端口所以这个报错基本都出在客户端请求模型那一步。报错三reading choices 相关错误。典型是模型返回结构不符合预期或者 max_tokens 设太小导致返回被截断。检查 model ID 是否填对TaoToken 通道支持的模型名要和文档一致。另外把 max_tokens 调到 8048 以上避免长回答被切。报错四OAuth 或鉴权头缺失。如果你用的是 Claude Code 或 Cline 这类工具它们读的是自己的 settings 文件不是 config.json。这时候要写全三件套Base URL、Key、Model ID缺一个都会握手失败。Claude Code 的配置在 settings.jsonCline 的 MCP 配置在它自己的 mcp 设置里格式不同但字段含义一样。报错五工具列表为空。客户端连上了但 list_tools 返回空说明服务端装饰器没生效。检查mcp.tool()有没有写在函数上面函数有没有被正确导入。还有一种情况是 stdio 子进程启动失败比如 python 路径不对把 command 改成绝对路径试试。报错六SSE 连接两分钟后断开。这是 SSE 传输的已知行为不是 bug。解决办法是在客户端加 reconnect 逻辑每次 call_tool 前重新 enter_async_context 建立 session。或者直接升级到 streamable-http 传输官方新版本已经支持稳定性更好。排障时建议按顺序来先确认 Key 能单独调通模型再确认服务端能单独 list_tools最后合起来跑。这样能把问题定位到具体环节不用瞎猜。6. 把链路固定下来从调试到长期使用的建议跑通之后我建议你把配置和代码分开管理。config.json 只放环境相关的 Key 和 URL代码里读配置这样换环境只改一个文件。工具函数单独放一个 tools 目录每个业务模块一个文件server.py 只负责注册保持可维护性。如果你打算长期跑 Agent 类任务可以考虑用 Coding Plan 这类方案来管理调用配额和模型切换避免每次手动改 Key。接入文档里有完整的参数说明遇到协议层面的问题可以先查那里。模型对话页面适合快速验证某个模型 ID 能不能用省得在代码里反复试。最后说一个实用技巧在 process_query 里加一个最大循环次数限制比如 while 循环最多跑 5 轮工具调用防止模型陷入无限调用。生产环境里这个保护很有必要我见过模型反复调同一个工具出不来的情况。加上计数器和超时链路就稳了。

相关新闻

Kaggle灾难推文分类实战:小样本NLP工程方法论

Kaggle灾难推文分类实战:小样本NLP工程方法论

1. 这不是“写个模型交上去就完事”的比赛:一场真实NLP工程实战的起点你点开Kaggle上那个标着“Natural Language Processing with Disaster Tweets”的入门级竞赛页面,看到简介里写着“Predict whether a given tweet is about a real disaster or not”…

2026/10/17 17:35:27 阅读更多 →
uniapp中scroll-view与swiper实现tab滑动切换的兼容性陷阱

uniapp中scroll-view与swiper实现tab滑动切换的兼容性陷阱

1. 为什么看似简单的tab滑动切换,会在uniapp里反复翻车?在某跨平台系统重构项目中,我接手了一个已上线半年的Tab页签模块——表面看就是顶部几个按钮下方内容区滑动切换,UI设计师给的交互稿连动效都标得清清楚楚:手指拖…

2026/10/17 17:35:27 阅读更多 →
UE5 C++ 战斗流程状态机实战:从伤害结算到胜负判定与收尾

UE5 C++ 战斗流程状态机实战:从伤害结算到胜负判定与收尾

这次我们来看 UE5 C 从零开始做 CRPG 系列中,战斗系统的第 7.16 课:战斗流程(3)。做 CRPG 的同学基本都会遇到同一个问题:角色能走到敌人面前、技能能放出来,但走到一半流程就乱了——技能打出去了伤害没结…

2026/10/17 17:35:27 阅读更多 →

最新新闻

YOLACT++实时实例分割:从核心架构到工程落地全解析

YOLACT++实时实例分割:从核心架构到工程落地全解析

1. 为什么实例分割值得死磕:从项目背景说起实例分割这个方向,在计算机视觉里一直是个"硬骨头"。目标检测给你一个框,语义分割给你一片区域,而实例分割要的是——每个物体不仅要知道它是什么类别,还要精确到像…

2026/10/17 18:42:05 阅读更多 →
Python:数据科学的瑞士军刀——从数据清洗到模型部署全解析

Python:数据科学的瑞士军刀——从数据清洗到模型部署全解析

Python在数据科学领域被叫作“瑞士军刀”,这个比喻我越想越觉得贴切。瑞士军刀的特点不是某一项功能登峰造极,而是小巧、可靠、什么场景都能掏出来用——开瓶器、螺丝刀、剪刀、镊子、牙签,你未必天天用全,但工具箱里放一把&#…

2026/10/17 18:42:05 阅读更多 →
Unity体绘制Shader实战:从光线步进到医学CT可视化

Unity体绘制Shader实战:从光线步进到医学CT可视化

1. 项目概述:这不是“画个3D图”那么简单,而是让体数据自己“发光说话”“Unity体绘制shader初探”——光看标题,很多人第一反应是:“哦,又一个Unity Shader教程”。但如果你真这么想,接下来的实操大概率会…

2026/10/17 18:42:05 阅读更多 →
MySQL技术面复盘:慢查询、索引失效与主从复制延迟解析

MySQL技术面复盘:慢查询、索引失效与主从复制延迟解析

某大厂OD技术面第三轮,MySQL这一块占了将近一半时间。面试官没有让我背八股文,而是直接贴了一条线上真实出现过的慢查询,从执行计划开始问,一路追到优化器为什么放弃索引、MVCC的ReadView到底怎么生成、从库延迟到底卡在哪一环。说…

2026/10/17 18:42:05 阅读更多 →
网站反爬机制与前端安全实践:从JS混淆到合规数据获取

网站反爬机制与前端安全实践:从JS混淆到合规数据获取

抱歉,这个项目我没法按原样写。“破解某天气网站JS反爬加密”这个方向,本质上是教授如何绕过目标网站的安全防护机制,用于未经授权的数据抓取或访问控制规避。无论标题包装成什么形式,都可能被实际用于非法用途,所以我…

2026/10/17 18:42:05 阅读更多 →
Claude Code代码导航仪:用Tree-sitter+MCP构建高密度代码图谱

Claude Code代码导航仪:用Tree-sitter+MCP构建高密度代码图谱

1. 这不是插件,是给 Claude Code 装上的“代码导航仪”你有没有试过在几十个文件、上千行代码里找一个函数的调用链?不是靠 CtrlClick 跳转——那只能跳一层;也不是靠全局搜索——结果堆满无关日志和测试用例。而是真正看清:这个c…

2026/10/17 18:41:04 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →