最近折腾了一个叫stock-sdk-mcp的小项目,本质就是把股票数据 SDK 的能力包装成 MCP Server,让 Claude、Cursor 这类 AI 工具可以直接“开口”问行情。这段时间 MCP 生态确实火,从 Figma MCP、Blender MCP 到各种奇奇怪怪的工具,都在往这个协议上靠。我最初也只是好奇,觉得 AI 能直接调工具是件挺酷的事,但真正动手把一个 SDK 封装成 MCP 服务之后才发现,里面有不少设计取舍和坑,值得记录下来。
这套东西说白了解决了一个很实际的问题:以前想让 AI 帮忙查个股票实时价格、拉一段 K 线数据,要么把数据喂进上下文里,要么让 AI 给你写段 Python 代码然后你再去跑。这两种方式都很别扭,前者浪费 token,后者绕了一大圈。stock-sdk-mcp的思路是把数据能力用 MCP 协议暴露出来,AI 需要数据时直接通过标准方式调用,像一个后厨的订单窗口,AI 点单,服务端出菜,清爽直接。
如果你手上有一堆现成的数据接口或 SDK,正想着怎么让 AI Agent 用起来,又或者刚好想了解 MCP 协议怎么落地上线,这篇实践整理应该能帮你少走几条弯路。里面不会有太多空洞的概念灌输,更多是具体设计、代码实现、踩坑记录。
1. 项目背景与核心痛点
先聊聊为什么需要这样一个项目。很多人第一次接触 MCP 会问:SDK 本来就是给开发者调用的,AI 工具只要会写代码不就行了,为什么还要多包一层协议?
这个问题很关键。AI Agent 虽然能写代码,但不是所有场景都适合“写代码—执行—读输出”的模式。尤其涉及行情数据时,你还需要鉴权、频率控制、错误重试、数据清洗这些逻辑,每次都让 AI 现场写一遍既不安全也不稳定。更现实的问题在于,像 Claude Desktop、Cursor 这类客户端默认是拿不到你本地环境里的 SDK 权限的,它们运行在受控的沙箱环境里,不能随便动你的系统和文件。MCP 在这里就扮演了一个桥的角色,把你自己封装好的数据能力安全地暴露给 AI。
1.1 为什么是 MCP,而不是 Agent Skill 或 Function Call
做这个项目之前,我也认真对比过几种方案。现在网上关于 MCP 和 Agent Skill 有什么区别的讨论挺多,我的理解是这样:Agent Skill 更像是给 AI 的“操作手册”,告诉它遇到什么场景该用什么工具、该怎么组合,侧重行为编排;MCP 则更像硬件上的 USB-C 接口,它定义了一套标准协议,把工具能力统一暴露出来,任何支持 MCP 的客户端都可以即插即用。
Function Call 的概念在 OpenAI 生态里很早就有了,但它和具体厂商绑定,换一个模型平台就要重新适配。MCP 的好处是协议标准化程度更高,Claude、Cursor、甚至自己写的 Web 应用都能接。所以我在设计stock-sdk-mcp时选择直接拥抱 MCP 协议,而不是为某一个模型单独做 Function Call 适配。
1.2 stock-sdk-mcp 具体做什么
这个项目从功能上讲,就是把传统的股票数据调用方式转换成了 AI 可以直接理解的结构化服务。举个例子,以前你要搞到一只股票的日 K 线,大概要经历:找到数据源文档、安装 SDK、写请求代码、处理返回的 JSON 字符串、再自己组装成表格。而现在你在 Cursor 里直接问“帮我看看平安银行最近 30 天的日 K 线走势”,MCP Server 会负责把“平安银行”映射成股票代码 000001,调用底层 SDK 拉取数据,格式化好再返回给 AI,AI 再基于这些数据帮你做分析。
整个流程我整理了一张工具能力表:
| 工具名称 | 功能说明 | 核心参数 |
|---|---|---|
get_stock_quote | 获取实时行情快照(现价、涨跌幅、成交量等) | code、market |
get_stock_kline | 获取历史 K 线数据,支持日/周/月级别 | code、period、count |
get_stock_list | 获取指定市场的股票列表 | market、keyword |
search_stock | 根据名称或代码模糊搜索股票 | keyword |
get_market_index | 获取大盘指数行情 | code |
get_stock_finance | 获取基本面财务数据摘要 | code |
工具数量不算多,但覆盖了日常做投研分析最常用到的几个数据场景。而且每个工具都不需要 AI 去理解复杂的 HTTP 请求、签名逻辑、字段映射关系,它只需要把参数填进去,拿到结果后专注于分析本身。
2. 整体设计与技术选型
说实话,把一个 SDK 封装成 MCP Server 并不复杂,真正需要动脑子的是设计决策。比如传输方式选 stdio 还是 SSE,数据源用免费的还是付费的,底层代码用 Python 还是 TypeScript。这些选型直接决定了项目后期的使用体验和维护成本。
2.1 传输方式:stdio 还是 HTTP/SSE
MCP 协议目前支持两种主流传输方式:标准输入输出(stdio)和 HTTP 的 SSE 模式。这两种方式的适用场景差别还是挺大的。
stdio 模式适合本地客户端,比如 Claude Desktop、Cursor 桌面版。客户端启动时会拉起你的 MCP Server 进程,两者通过标准输入输出通信,好处是没有网络监听、零配置、安全性高。我在开发测试阶段百分之九十的时间都用它,非常方便。
HTTP/SSE 模式则适合把服务部署在远端,或者同时给多个客户端提供能力。比如你在内网服务器上部署了一套数据服务,想让办公室里的几个人都能通过 AI 工具访问,这时候基于 HTTP 的 MCP Server 就是更合适的方案。Sse 模式把请求通过 HTTP 发出去,响应通过 EventStream 推回来,整体做下来也不算复杂,但要额外处理跨域、鉴权、并发这些事。
我这个项目的最终实现是同时支持两种模式。本地预览时用 stdio,部署到内网服务器上时切到 SSE。MCP Python SDK 对这两种模式的支持都比较成熟,切换成本不高,关键是在设计阶段把服务逻辑和传输层解耦。
2.2 数据源与 SDK 选型:免费还是付费
股票数据来源是最核心的决策。国内可选择的数据源无非几类:一是第三方聚合平台如 Tushare、AkShare,二是各券商或数据服务商提供的付费 API,三是直接抓取财经网站公开接口。
我在这个项目里采用了“分层数据源”的策略,主数据源用公开的免费行情接口,底层 SDK 根据可用性自动兜底。之所以不直接依赖某一个收费 SDK,一方面是因为个人项目不想承担太高的数据成本,另一方面是免费接口的稳定性虽然一般,但做 AI 分析场景已经完全够用。如果哪一天免费接口挂了,只需要在数据源适配层新增一个方法,不需要改动 MCP 工具暴露给 AI 的部分。
这里有个很重要的工程原则:MCP 工具层对外暴露的返回格式必须是稳定的,底层数据源可以随时切换。这个思路有点像依赖倒置,把变的部分隔离在适配层里。我见过一些项目把某个数据源的字段结构直接透传给 AI,结果数据源一改文档,AI 工具就全瞎了。稳不稳就看这一层设计得好不好。
2.3 Python + FastMCP 的取舍
技术栈我选了 Python + FastMCP。原因很直接:Python 在数据领域生态最好,行情库、分析库都是现成的;FastMCP 框架本身封装得很干净,通过装饰器就能快速注册工具,学习曲线也低。
如果你之前没接触过 FastMCP,可以理解成一个专门针对 MCP 协议做好的快速开发框架,类似 Flask 之于 Web。它内部帮你处理了协议握手、请求分发、响应格式化这些事情,开发者只需要关注自己提供的工具函数本身。
当然,如果你对 TypeScript 更熟,官方也有 TypeScript SDK。选哪个没有标准答案,但团队里如果有 Python 基础,用它来搞数据类 MCP Server 会舒服很多。
3. 核心实现:从 SDK 到 MCP 工具层
这一节是整篇的实操重点,我会把从 SDK 到 MCP 工具层的核心代码结构和设计思路拆开讲。尽量写得详细一点,因为很多坑是跑完一遍代码之后才会发现的。
3.1 工具定义与 JSON Schema 设计
MCP 工具层本质上就是一组带描述的 JSON Schema。AI 客户端通过协议拿到这些 Schema,就会知道有哪些工具可用、每个工具需要哪些参数。所以“写工具名描述”这件事比很多人想象的重要得多。
一个好的工具描述应该同时说清楚三件事:这个工具是干什么的、输入参数是什么含义、输出会在什么范围内波动。不要小看后面的范围说明,AI 模型对浮点数字精度非常敏感,如果你返回的数据格式不稳定,模型在解读时就会出现幻觉。
我举个例子,最初我给get_stock_quote的返回字段直接用price,后来发现不同数据源给出的价格字段精度不一样,有的带两位小数,有的带四位。AI 模型在分析时如果没被告知价格精度,很可能做出“股价在 10 到 10.0000 之间波动”这种离谱判断。所以在设计工具层时,我给每个返回字段都加了描述,比如“price 表示最新成交价,单位为元,保留两位小数”。
下面是 FastMCP 里一个实际工具的简化代码:
from fastmcp import FastMCP mcp = FastMCP("stock-sdk-mcp") @mcp.tool() def get_stock_quote(code: str, market: str = "sh") -> dict: """获取股票实时行情快照。 Args: code: 股票代码,如 000001 market: 股票市场,sh 表示上交所,sz 表示深交所 Returns: 包含最新价、涨跌幅、成交量、成交额等字段的字典 """ from data_source import fetch_quote data = fetch_quote(market, code) return { "code": code, "market": market, "price": float(data.get("price", 0)), "change_pct": float(data.get("change_pct", 0)), "volume": int(data.get("volume", 0)), "amount": float(data.get("amount", 0)), "timestamp": data.get("timestamp", "") }3.2 数据兼容层:统一返回格式
数据源层的设计决定了这个项目的上限。我一开始觉得行情接口返回什么我就透传什么就行,后来发现免费接口的返回结构不统一,有嵌套字典的、有纯字符串拼逗号的、有直接给中文 key 的。如果这些脏数据直接进入 AI 上下文,不仅浪费 token,还会严重影响后续分析的质量。
所以我做了一件事:定义一套标准化的数据模型,所有数据源返回的数据都先映射到这套模型上,再由工具层输出给 AI。这个标准化过程相当于给底层 SDK 加了“翻译官”。举个例子,有的数据源把涨跌幅字段叫change_percent,有的叫涨跌幅,在我的适配层里它们都会被翻译为change_pct。
这种设计带来两个好处:第一,AI 看到的永远是干净、结构一致的数据;第二,换数据源时只需要改适配层,对上层完全没有影响。做 MCP Server 的人很容易忽略这个标准化步骤,但这恰恰是决定项目长期维护顺不顺畅的分水岭。
3.3 异步调度、超时控制与缓存机制
MCP Server 跑起来之后,你会发现它不只是一个接口转发器,更像一个带调度逻辑的中间层。AI 客户端可能会同时发多个请求过来,比如它要对比五只股票,就会并发调用五次get_stock_quote。如果底层 SDK 是同步阻塞式的,整个服务就会卡在慢请求上,AI 那边表现为长时间没响应。
我在实现里做了两层优化。第一层是异步封装:底层 SDK 的同步方法通过线程池转成异步协程,避免一个慢请求阻塞整个事件循环。第二层是超时控制:免费行情接口偶尔会有 5 到 10 秒的延迟,如果超过 3 秒无响应就直接给 AI 返回“数据源超时,请稍后重试”,而不是让 AI 干等。
还有一个容易被忽略的点是缓存。行情数据里的分钟级快照其实没必要每次都去数据源拉一遍,我在服务里加了一个简单的 TTL 缓存,有效期 5 秒。这个时长的选择不是拍脑袋定的,而是权衡了数据时效性和数据源频率限制后得出的结果。太短了缓存没有意义,太长了你报的价格就失真了。
4. 实操全流程:从零搭建并接上 Claude Desktop
前面把设计和原理都讲完了,这部分直接从零开始动手。我假设你已经有一台能跑 Python 的电脑,系统是 Windows 或 macOS 都行,接下来就按步骤搭。
4.1 环境准备与项目初始化
首先创建项目目录并初始化虚拟环境,我习惯用uv管理 Python 项目,比 pip 加 venv 方便不少:
mkdir stock-sdk-mcp cd stock-sdk-mcp uv init source .venv/bin/activate # Windows 下是 .venv\Scripts\activate uv add fastmcp requests pandas这里解释一下依赖选择。fastmcp是 MCP Server 的核心框架,用来快速注册工具和实现协议层。requests是我自己的数据源适配层要用的 HTTP 客户端。pandas一开始其实没打算加,后来在 K 线数据处理时发现做数据对齐确实省事,就留下了。
4.2 服务端核心代码:数据源适配层
我的项目结构里单独拆了一个data_source.py模块,专门负责和底层数据接口沟通。这个模块在整篇文章里是灵魂,MCP 工具层不要直接碰 HTTP 请求,所有网络请求都收敛到这里。
import requests from datetime import datetime # 本地行情接口的基础 URL,正式使用请替换为真实数据源 BAST_URL = "https://your-data-source.example.com" def _request(url: str, params: dict) -> dict: resp = requests.get(url, params=params, timeout=3) resp.raise_for_status() return resp.json() def fetch_quote(market: str, code: str) -> dict: """获取单只股票的实时行情快照。""" url = f"{BAST_URL}/{market}/{code}/quote" raw = _request(url, {}) # 这里做字段标准化,统一输出固定键名 return { "price": raw.get("now", 0), "change_pct": raw.get("change_pct", 0), "volume": raw.get("volume", 0), "amount": raw.get("turnover", 0), "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } def fetch_kline(market: str, code: str, period: str, count: int) -> list[dict]: """获取历史 K 线,返回按时间正序排列的记录列表。""" url = f"{BAST_URL}/{market}/{code}/kline" params = {"period": period, "count": count} raw = _request(url, params) records = [] for item in raw.get("records", []): records.append({ "date": item.get("date", ""), "open": float(item.get("open", 0)), "high": float(item.get("high", 0)), "low": float(item.get("low", 0)), "close": float(item.get("close", 0)), "volume": int(item.get("volume", 0)) }) return records每个方法做且只做一件事:请求上游接口,拿回数据,统一字段命名。这一步千万不要图省事把raw直接返回,否则你在调试 AI 输出时会被各种奇奇怪怪的字段名搞到崩溃。
4.3 服务端核心代码:MCP 工具注册
接下来是 MCP 工具的注册入口。我在server.py里定义了所有对外暴露的工具函数,并加上详细的文档字符串。你没看错,文档字符串在 MCP 里就是“规格说明书”,AI 能不能理解你的工具,全靠它。
from fastmcp import FastMCP from data_source import fetch_quote, fetch_kline mcp = FastMCP("stock-sdk-mcp") @mcp.tool() def get_stock_quote(code: str, market: str = "sh") -> dict: """获取指定股票的实时行情快照,包括最新价格、涨跌幅度、成交量和成交金额。 返回字段说明: - price: 最新成交价,单位元,保留两位小数 - change_pct: 涨跌幅,单位百分比 - volume: 成交量,单位股 - amount: 成交金额,单位元 - timestamp: 行情时间戳 """ return fetch_quote(market, code) @mcp.tool() def get_stock_kline(code: str, market: str = "sh", period: str = "day", count: int = 30) -> list[dict]: """获取指定股票的历史 K 线数据。 Args: code: 股票代码 market: 股票市场,sh 或 sz period: K线周期,day 日线、week 周线、month 月线 count: 需要返回的K线根数,取值范围 1 到 500 Returns: 按时间正序排列的记录列表,每一条包含日期、开高低收和成交量。 """ if count < 1 or count > 500: raise ValueError("count 参数必须在 1 到 500 之间") return fetch_kline(market, code, period, count) if __name__ == "__main__": mcp.run(transport="stdio")可能有人会问,为什么我在参数校验这里卡得这么死。原因很简单:AI 模型对边界条件的把握很不稳定,如果它想请求 10000 条 K 线,而你的数据源只支持最大 1000 条,返回的结果就会让 AI 的后续分析建立在错误前提下。显式校验并报错,反而比悄悄截断更容易让 AI 理解发生了什么。
4.4 对接客户端:Claude Desktop 与 Cursor
写完了 MCP Server,就要把它接到客户端里用了。Claude Desktop 的配置方式是在配置文件里增加一个mcpServers条目。找到配置文件的路径之后(macOS 在~/Library/Application Support/Claude/,Windows 在%APPDATA%\Claude\),增加下面这段:
{ "mcpServers": { "stock-sdk-mcp": { "command": "uv", "args": ["run", "--directory", "/absolute/path/to/stock-sdk-mcp", "server.py"] } } }注意这里我用的是uv run而不是直接指定 Python 路径。这样做的目的是让 Client 每次启动 MCP Server 时都能自动加载虚拟环境里的依赖,避免出现“找不到 fastmcp 模块”之类的环境问题。如果你更习惯手动管理虚拟环境,直接用/path/to/.venv/bin/python server.py也是可以的,但务必把command和args都写绝对路径。
连接好之后,你在 Claude Desktop 里直接提问“帮我查一下贵州茅台的当前股价”,正常情况下它会自动调用get_stock_quote工具,并返回一个结构化的行情信息。如果在某个客户端里没有看到工具被调用,优先检查客户端的日志文件,基本都会有明确的报错提示。
5. 踩坑记录与常见问题排查
这部分说点实在的。项目跑通不难,但想让它在不同电脑、不同客户端环境下稳定复用,好几个坑是躲不掉的。我整理了一个速查表,另外把几条最典型的排查过程单独展开了。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 客户端启动后 MCP Server 没反应 | command 路径配置错误或虚拟环境未激活 | 终端手动执行一遍启动命令,看有没有报错 |
| 调用工具时报超时 | 数据源响应较慢 | 检查免费接口的时效性,适当调大超时参数 |
| 返回中文乱码 | Windows 控制台编码问题 | 在代码里显式设置 UTF-8 输出,或调整终端代码页 |
| 模型不理解工具参数 | 文档字符串描述不清晰 | 重写函数 docstring,附上参数取值范围和返回字段说明 |
| MCP Server 进程频繁退出 | 依赖环境不一致 | 确保客户端启动的是同一个虚拟环境,不要混用系统 Python |
| 报错 SDK not found | 漏装了底层 SDK | 确认你是否真的需要底层 SDK,直接用 HTTP 适配层就够了 |
5.2 客户端连不上服务端的排查思路
这是一个高频问题,值得单独讲一下排查思路。很多人配置完成之后,发现客户端里看不到任何工具,或者工具调用后一直没有输出。我的做法是三步排查法。
第一步,在终端手动启动 MCP Server,确认服务本身能跑。这是最基础的一步,如果直接在终端跑python server.py都报错,那问题在代码而不是客户端配置。第二步,检查客户端的日志文件。Claude Desktop 的日志通常记录得非常详细,如果 MCP Server 连接失败,日志里会有明确的错误码和路径信息。第三步,确认客户端启动 MCP Server 时的命令行工作目录。有些客户端对相对路径支持不好,建议把项目路径和入口脚本全部改成绝对路径。
这套排查思路在绝大多数情况下都能解决问题。如果你不够确定哪一步出了问题,把日志文件里最后 20 行贴给 AI 工具本身,它往往能帮你快速定位到问题。我自己调试时就这么干过,比自己枯想要快得多。
5.3 我个人的几条经验心得
整个项目做到最后,有几个体会很值得分享。
第一,工具的数量不是越多越好。MCP 的工具列表每多一个,模型在选择时的干扰就多一分。我一开始暴露了十来个工具,里面有好多使用频率很低的边缘功能,结果 AI 反而容易选错。后来砍掉一半,只保留高频核心方法,整体效果反而稳定了。这点也和社区的普遍观点一致:MCP Server 设计更像产品设计,克制比堆功能重要。
第二,参数的默认值要给得足够“安全”。如果某个工具拿默认参数时返回的数据明显不靠谱,模型就不会信任这个工具。比如 K 线数据的默认周期是日线,默认数量是 30 根,这两个值在大多数行情分析场景下都是合理的,模型可以直接调用并得到有意义的回答。养成“默认值即可用”的习惯,对 AI 类产品非常重要。
第三,不要忽视服务端的日志输出。MCP 工具层的日志价值巨大,因为你能看到每一次模型发起调用的完整参数和响应状态。这不仅有助于排查问题,还能让你更好地理解模型的行为模式。如果长期开着服务但从不看日志,等于浪费了一个非常有价值的数据源。
如果你也正在折腾自己的 MCP Server,建议从一套简单的工具开始,跑通链路之后再不断迭代。等把这套思维捋顺了,再回头看任何 SDK 都能很快包装成标准化的 AI 能力。