做股票开发的朋友应该都有过类似的体验:AI 写代码很强,但当你问它“当前这只股票的最新价是多少”时,它要么答非所问,要么直接告诉你“我没有实时数据”。这不是 AI 本身笨,而是它缺少一根连接行情数据源的管道。我这段时间正好在做 stock-sdk-mcp 这个项目,核心目标就是把这根管道打通:把一套股票数据 SDK 包装成 MCP 服务,让 AI 客户端能够直接调用实时行情、K 线、基本面等接口。这篇文章是我对这次实践的完整整理,覆盖选型思路、架构拆解、实操过程和踩过的坑,适合正在做 AI 工具链集成的开发者参考。
先说结论:把股票数据能力接进 AI,不是难在写接口,而是难在“协议适配”“数据约束”“异常处理”这三件事。stock-sdk-mcp 本质上是一个很薄的适配层,但它牵扯到的细节比想象中多得多。
1. 这个项目解决的真实痛点:AI 看得懂代码,却查不了行情
1.1 从一次尴尬的对话说起
我在测试某个支持 MCP 的 AI 客户端时,问过一个问题:“帮我查一下某股票最近五个交易日的收盘价。”
AI 回答得非常有条理,列出了步骤:先获取股票代码,再调用行情接口,然后解析数据。但最后它补了一句:“我没有实时行情数据源,请手动查一下。”
这就是问题的本质。AI 模型本身就是一个“知识库 + 推理引擎”,它的训练数据里可能有某只股票的历史行情片段,但绝对没有实时价格。你让它写一个爬虫去抓行情,它可能给你一段可以跑的代码,但你不能指望它在对话过程中真的去执行那段代码。
所以,要让 AI 具备查询股票数据的能力,关键不在于模型,而在于给它一个“可调用的外部工具”。这也是 Agent 类应用的标准套路:模型负责理解意图、规划步骤、生成调用参数,工具负责真正执行动作。
1.2 传统方案的三个问题
在想到 MCP 之前,我先试过几条常规路径,每条都有痛点:
| 方案 | 基本思路 | 痛点 |
|---|---|---|
| 让 AI 生成 HTTP 请求代码 | 由 AI 生成 fetch/requests 代码,用户自己复制去执行 | 返回结果是代码而不是数据,对话体验断裂;代码可能还有错 |
| 自己做 API 封装并暴露给客户端 | 把行情接口封装成本地 HTTP 服务,让 AI 通过 Function Calling 调用 | 需要针对不同的 AI 平台单独适配函数协议,换一个客户端就要重新对接 |
| 把数据预埋进提示词 | 提前把一部分行情数据作为上下文发给 AI | 数据是静态的,只能查导出的时刻;token 消耗大;无法交互式查询 |
这三个方案最让我头疼的是“协议碎片化”。Function Call 在不同的 AI 平台上有不同的写法,有的用 JSON Schema,有的用参数列表,有的要求回调函数。如果我为每个平台各维护一套适配层,工作量是指数级上升的。
1.3 MCP 为什么能收拾这个局面
MCP(Model Context Protocol)的核心思路很简单:把“AI 客户端”和“外部工具服务”之间的交互方式统一成一个标准协议。服务端只需要实现一套协议,所有支持 MCP 的客户端都能复用。
打个比方,传统做法的味道有点像每家电器都要用自己的专用插座,而 MCP 相当于把插座接口统一成了国标。你只需要把股票数据能力做成一个“国标插座”,任何支持 MCP 的 AI 客户端都能插上去用。
stock-sdk-mcp 这个项目的定位,就是做一个标准的 MCP Server,把底层的股票数据 SDK 能力封装成 MCP 工具。AI 客户端通过 MCP 协议发现这些工具,按协议约定传入参数,服务端取出行情数据再按协议返回,整个过程对 AI 来说就像调用本地函数。
2. 整体架构与核心选型:我是怎么设计 stock-sdk-mcp 的
2.1 服务端的三层结构
在实际动手前,我把 stock-sdk-mcp 的代码结构设计成三层,避免所有逻辑堆在一个文件里:
stock-sdk-mcp/ ├── server.py # MCP Server 入口:管理生命周期、注册工具 ├── tools/ │ ├── registry.py # 工具注册表:把函数映射成 MCP tools │ ├── quote.py # 实时报价工具 │ ├── kline.py # K 线工具 │ ├── stock_list.py # 股票列表与基础信息工具 │ └── search.py # 股票代码搜索工具 ├── datasource/ │ ├── client.py # 对底层行情 SDK 的封装 │ ├── cache.py # 本地缓存,降低重复请求 │ └── errors.py # 统一异常定义 └── config.yaml # 服务端可配置文件你可能会问:为什么需要 registry?因为 MCP Server 不是“写死路由”,它要对外广播自己有哪些工具、每个工具接收什么参数。registry 充当的是“工具清单”的角色,统一收集函数定义和参数 Schema,再交给 SDK 暴露给客户端。
2.2 MCP 协议里的三个关键动作
MCP 协议的交互流程其实就三个方面,搞懂这三个动作,整个项目的骨架就清晰了:
- 工具发现:客户端向服务端请求“你有哪些工具”。服务端返回工具名、描述、参数 Schema。AI 看到这些信息后,才知道自己能调用什么。
- 工具调用:客户端根据 AI 生成的参数,向服务端发起调用请求。服务端执行函数,拿到数据后返回结构化结果。
- 错误反馈:调用过程中如果出错(比如参数不对、限频),服务端要把错误信息返回给客户端,让 AI 能够据此调整参数重试。
我重点打磨的是工具描述文本。MCP 协议本身不限制描述怎么写,但 AI 非常依赖描述文本去理解工具的用途。比如,同样是“获取报价”这个动作,描述里有没有写清楚“支持 A 股、港股、美股”、“代码格式为 6 位数字”、“返回数据单位为元”,直接影响 AI 会不会传错参数。
2.3 为什么单独封装一个 datasource 层
数据源这一层很容易被忽略,但它决定了整个项目的稳定性。
stock-sdk-mcp 没有直接让 tool 函数去调行情 SDK,而是中间加了一个 datasource/client.py。理由是:底层行情 SDK 的接口形态不是我定义的,它可能有初始化、鉴权、网络重试等一堆逻辑。如果这些逻辑散落在各个工具函数里,后面的维护就是灾难。
datasource 层只暴露给它上层四个方法:
async def get_quote(symbol: str) -> dict async def get_kline(symbol: str, period: str, start: str, end: str) -> list[dict] async def get_stock_list(market: str) -> list[dict] async def search_symbol(keyword: str) -> list[dict]所有工具函数只调用这四个方法,不直接碰 SDK。这样做的直接好处是:如果某一天要换行情数据源,只需要改 datasource 层的实现,上面的工具逻辑一行都不用动。
3. 从零接入的完整实操:把服务跑起来并接进 AI 客户端
3.1 环境准备
我的基础环境是 Python 3.11,操作系统不限,macOS / Linux / Windows 均可。主要依赖是 MCP 官方 Python SDK 和底层行情 SDK。
安装命令很简单:
pip install mcp pip install stock-sdk # 这里用“stock-sdk”作为底层行情 SDK 的代称需要注意的是版本兼容问题。MCP SDK 处于快速迭代阶段,不同的版本在不同协议细节上有些差异。我实测中遇到过一个小问题:某个新版本 SDK 对 tool 参数名做了规范化,要求参数必须是 snake_case,否则注册时报错。这类问题没有捷径,只能在装完依赖后先跑一个最小示例验证 SDK 是否正常工作。
3.2 创建最小 MCP Server
我先把最小的服务端骨架搭出来,只注册一个工具,确认能跑通再扩展:
from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio server = Server("stock-mcp") @server.list_tools() async def handle_list_tools(): return [ { "name": "get_stock_quote", "description": "获取股票最新报价。参数 symbol 为 6 位股票代码。", "inputSchema": { "type": "object", "properties": { "symbol": { "type": "string", "description": "6 位股票代码,例如 600000" } }, "required": ["symbol"] } } ] @server.call_tool() async def handle_call_tool(name: str, arguments: dict): if name == "get_stock_quote": symbol = arguments["symbol"] quote = await get_quote(symbol) return [{"type": "text", "text": json.dumps(quote, ensure_ascii=False)}] raise ValueError(f"未知工具: {name}") async def main(): async with server.run_stdio_server(): pass if __name__ == "__main__": import asyncio asyncio.run(main())这段代码虽然短,但它是整个项目的地基。你注意 chatpy 里handle_list_tools返回的inputSchema,这是 AI 判断“怎么调用”的唯一依据,写得好不好直接影响 AI 的调用成功率。
3.3 用 MCP Inspector 做本地联调
跑起来之后,别急着接客户端,先用 MCP Inspector 这类调试工具测一遍:
mcp-inspector然后把服务端的启动命令填进去,通常是:
python server.pyInspector 会列出服务端暴露的工具列表、参数 Schema,以及调用结果。这一步我会重点验证三件事:
- 工具列表里有没有多余的工具(少而精更好)
- 参数 Schema 是否被正确解析
- 调用真实接口后,返回数据格式是否稳定
实测中发现,Inspector 是排查“客户端看不到工具”这个问题的最快方式。如果 Inspector 里能看到,但某个 AI 客户端看不到,那大概率是客户端配置没刷新,把服务端重启一下就好。
3.4 接入支持 MCP 的客户端
我自己用的是某款支持 MCP 协议的服务端型客户端应用(为了不引入品牌歧义,下文统一叫它“某客户端”)。接入方式一般是在它的配置文件中增加一个条目:
{ "mcpServers": { "stock-data": { "command": "python", "args": ["/path/to/stock-sdk-mcp/server.py"], "env": { "STOCK_API_KEY": "your-api-key" } } } }配置文件里比较折腾的是command字段。如果你的 Python 环境是虚拟环境,建议写绝对路径,比如/path/to/venv/bin/python,而不是裸写python,否则客户端可能在 PATH 里找不到解释器。
接完之后,在客户端对话框里输入“查一下 600000 的最新价格”,AI 应该会先识别出工具调用意图,再填入 symbol 参数,最后展示返回的行情数据。第一次打通的时候,那个体验还是很有成就感的。
4. 行情数据对接中踩过的坑:限频、时区、字段口径与数据源稳定性
4.1 限频问题比想象中严重
股票行情数据源普遍有访问频率限制。有的按每秒请求数限制,有的按每月累计次数限制。
我在第一次接入时,没有做任何限频保护,测试阶段手动调用还好,一旦接入 AI 客户端,情况就变了——AI 在一个对话里可能会连续调用多次工具,比如先搜索股票代码,再查 K 线,再查实时报价,加上自己的调试请求,很快就触发数据源的 403 报错。
处理方案有几层:
- 在 datasource 层统一加一个异步信号量,控制并发数:
import asyncio _semaphore = asyncio.Semaphore(5) async def get_quote(symbol: str) -> dict: async with _semaphore: return await _sdk.get_quote(symbol)- 对高频访问做本地缓存,缓存时间根据数据类型设置。实时报价缓存 3 秒,K 线缓存 60 秒(因为分钟级 K 线更新频率本来就不高)。
- 在工具描述里明确告诉 AI:“本服务受限于数据源频率,请勿连续重复调用相同参数。”
最后这条最有效。AI 看到描述里的约束之后,会主动去合并重复请求,这是我在实测中观察到的明显变化。
4.2 时区与交易日历
股票数据的时间字段最容易阴沟翻船。不同数据源返回的时间格式五花八门:有的返回2025-01-17 15:00:00,有的返回2025-01-17,有的干脆返回时间戳。
更要命的是时区。某些数据源默认返回的是 UTC 时间,而我本地客户端显示的是北京时间(UTC+8),两者相差 8 小时。如果不做处理,AI 在复盘当日行情时,会经常遇到“今天的数据还是空的”或者“把昨天 16 点的数据当成了今天”这种问题。
我的做法是:
- 所有工具在返回数据时,强制统一为 ISO 8601 字符串,且带时区偏移,例如
2025-01-17 15:00:00+08:00。 - 对于直接给时间戳的接口,程序里统一转换后再返回。
- 在工具描述里写清楚:“所有时间为北京时间,已包含时区偏移。”
AI 本身对时间格式很敏感,如果你返回的数据格式不一致,它在做“最近 5 个交易日”这种筛选时就会算错。统一格式是减少这类错误的最廉价手段。
4.3 字段口径不统一
这是个特别隐蔽的坑。某数据源返回的“涨幅”可能已经是百分数(比如 3.5 代表 3.5%),而另一个数据源可能返回的是小数(0.035)。
如果接进来的 SDK 连这个都变,AI 返回给用户的“涨幅 0.035%”就会非常奇怪。
stock-sdk-mcp 的处理方式是:在 datasource 层做一次字段标准化。我定义了一套内部统一的字段名:
| 内部字段 | 含义 | 标准单位/格式 |
|---|---|---|
| symbol | 股票代码 | 6 位数字,如 600000 |
| name | 股票名称 | 字符串 |
| price | 最新价 | 单位:元 |
| change_pct | 涨跌幅 | 单位:% |
| volume | 成交量 | 单位:手 |
| amount | 成交额 | 单位:元 |
| open / high / low / close | 开高低收 | 单位:元 |
| timestamp | 时间 | ISO 8601 带时区偏移 |
所有底层 SDK 数据进入内部数据结构前,都要经过一个 translate 映射函数。这样即使底层数据源调整字段,也只影响 datasource 层,工具层和 AI 看到的接口是稳定的。
4.4 数据源不稳定时的容错策略
即使是大厂的数据源,偶尔也会出现连接超时、返回空数据、返回异常大的数值。我在项目里加了三个保护机制:
- 超时控制:每次请求设定明确的超时时间,避免拖死整个 MCP Server。
- 空数据兜底:如果接口返回空列表,不加包装、直接返回带提示的空结构,例如
{"data": [], "message": "非交易时间或暂无数据"},让 AI 看到之后自行判断。 - 异常信息语义化:把底层 SDK 抛出的异常统一转换为业务错误码,加在返回结构里。
这样做的原因是,AI 客户端在执行过程中如果遇到明显错误,它会尝试修正参数重新调用。但如果错误信息是Connection refused这种底层网络错误,AI 是无法自行处理的,它只能把错误原样扔给用户。语义化错误输出之后,AI 至少能告诉用户“服务端上游连接暂不可用,请稍后重试”,体验完全不一样。
在错误处理上,我想特别提醒一点:不要在错误信息里暴露 API Key、完整 URL、SDK 版本这类敏感信息。一方面是为了安全,另一方面是避免这些噪音污染 AI 的上下文。我一开始直接抛底层异常,AI 的上下文里出现了一整段调用栈,非常影响后续对话质量。
5. 工具设计上的几个细节:让 AI 少说废话、多拿到有用数据
5.1 不要把“搜索”和“报价”合并成一个工具
我最初设计时偷懒,把股票代码模糊搜索和获取报价合并成一个get_stock_info工具,参数叫keyword,AI 传keyword = "平安银行",服务端内部先搜索再返回报价。
这个设计看起来很聪明,实际上很糟。原因有二:
- 搜索可能返回多个候选股票,工具不知道该选哪个。
- 当 AI 只需要做“模糊搜索”时,它也不得不把“查询报价”的动作绑定进来,被逼返回一堆不需要的字段。
后来我把工具拆成两个:
search_stock(keyword):返回候选股票列表,只包含代码、名称、市场。get_stock_quote(symbol):只接受明确的 6 位代码,返回行情。
这样拆开之后,AI 的调用链路变得非常清晰:先搜,拿到代码后,再基于代码查详情。中间如果 AI 拿不定选哪只股票,它还会主动问用户“找到多个匹配项,请问要选择哪一个?”,交互反而更好了。这也给了我一个教训:工具不是越强大越好,而是边界越清晰越好。这和写函数的原则是一样的,一个函数只干一件事。放到 MCP 场景里依然成立。
5.2 K 线工具的时间范围与周期约束
K 线这种工具,参数很容易失控。AI 可能传一个start = "2000-01-01",数据源直接炸掉。设计 K 线工具时,我刻意加上约束:
- 周期可选值限定为
1d、1w、1m。 - 开始时间限制为一年内。
- 每次返回的最大 K 线根数限制为 500,超出则提示 AI 调整时间范围。
这些约束不仅写在参数 Schema 里,我还用文字写在工具描述里。为什么双写?因为有些 AI 客户端只会解析 JSON Schema 的 description 字段,有些则会把整个工具描述都读一遍。双写可以保证不管哪种客户端,AI 都能看到限制。
实测下来的效果:AI 传非法周期的次数明显下降,偶尔传错也能被服务端清晰地拒绝。
5.3 返回数据的精简
AI 的上下文窗口是有限的。如果一次调用返回了一整年的分钟级 K 线,就算 AI 接得住,用来理解用户意图的上下文也被挤占了。
stock-sdk-mcp 在返回数据层做了两次精简:
- 字段级精简:比如 K 线工具默认只返回日期、开盘、收盘、最高、最低、成交量,绝不返回额外的统计字段。
- 记录级精简:条数太多时,服务端主动截断并返回一个
"truncated": true标记,AI 看到标记后会知道数据不完整,必要时自动缩小时间范围。
你可以把这一层理解为“服务端替 AI 做了一次数据处理”。数据进到 AI 上下文之前,已经是最精炼的形态了,这样 AI 就可以把主要算力放在分析和推理上,而不是解析一大堆无关字段。
5.4 缓存命中与上下文一致性
缓存很好用,但也引入了一个新的问题:AI 在不同时间去查同一只股票的实时价格,第二次拿到的是否还是旧数据?
我的处理原则是:标准缓存时长按工具类型区分,实时报价缓存极短,K 线缓存稍长。但更重要的是,在返回时带上数据时间戳。
例如返回结构:
{ "symbol": "600000", "name": "浦发银行", "price": 10.25, "timestamp": "2025-01-17 15:00:00+08:00", "cached": true }cached字段明确告诉 AI 这份数据来自缓存。AI 在转述给用户时,会注意到这个标记,从而避免把缓存数据说成“刚刚实时获取的”。这个细节对体验影响很大,一开始我没加 cached 字段,AI 经常把 5 秒前的缓存报价说成“最新报价”,用户对比行情软件发现对不上,就会觉得这套工具不靠谱。
6. 实盘对话场景下的一些观察:AI 调用行情的真实行为模式
6.1 AI 会先搜索还是直接猜代码
接入完成后,我花了大量时间在真实对话场景中观察 AI 的行为模式。一个有意思的发现是:如果你不给 AI 搜索工具,只给报价工具,它会非常果断地从记忆里猜一个股票代码出来填进去。
这个行为很危险。AI 记住的代码可能过期了,或者代码本身是错的。比如某个股票代码因为某种原因变更过,AI 仍然会基于训练数据给出旧代码。所以我后来把search_stock工具调整成所有其他股票工具的前置推荐。在工具描述里我写了一句:“在查询具体行情前,如果用户未提供 6 位数字代码,请先调用 search_stock 确认代码。”
有了这个提示,AI 会养成“先搜索,再查询”的习惯,出错率大幅下降。
6.2 token 消耗比你想象的多
MCP 工具调用流程中,工具定义本身也会占用上下文 token。如果注册了 20 个工具,每个工具的长描述有 300 字,这 20 个工具定义的 token 开销就是非常可观的。
更麻烦的是,某些客户端会把全部工具定义都放进每次请求的上下文中。所以我控制了一个度:只注册必要的工具,描述尽量简洁,既保证了 AI 能准确理解,又不至于让上下文被无关工具塞满。
我最终保留的工具数量是 4 个:search_stock、get_stock_quote、get_kline、get_stock_list。之前考虑过的“资金流向”工具虽然很炫,但考虑到描述写起来太长、数据源调用成本高,最后砍掉了。砍完之后的体验反而更顺。
6.3 多轮对话中的状态管理
MCP 工具调用是不保存状态的。AI 在上一轮查询了 600000,这一轮如果你直接说“那它的周 K 线呢”,AI 需要自己能记住上一轮的上下文,才能将 600000 作为默认参数传给 K 线工具。
这个状态是靠 AI 的对话上下文自己维护的,服务端不需要做任何事。但服务端可以做得更好:在返回数据里尽量带上 symbol,让 AI 后续引用更方便。比如查询报价返回了 symbol,AI 如果后续要调 K 线,就知道该填哪个代码。
6.4 交易时段和大小盘差异
关于 A 股的交易时段(上午和下午各一段连续竞价时间),AI 经常会理解错:它能说出来“交易时间”的定义,但未必知道“周末不开盘”或“午休时间不算”。给 K 线和报价工具加上服务端校验后,我发现 AI 很少出现把非交易时间拿到数据当成正常行情的问题。
7. 后续可以做的扩展与个人总结
做这个项目之前,我一直觉得“让 AI 查股票数据”是很自然的事,真正做完才发现,中间隔了协议、数据规范、上下文管理、缓存策略,还有一堆细节优化。它到现在还谈不上完美,但已经能稳定地完成几类核心任务:实时报价查询、K 线历史回看、股票模糊搜索。
如果你想在此基础上继续扩展,我觉得有几个方向值得考虑:
- 增加基本面数据工具,把财报、估值指标、行业对比等结构化数据接入。这类数据天然适合 AI 做分析,但是数据结构复杂,对字段标准化的要求更高。
- 支持投资组合的批量查询。一次调用返回多只股票的报价,适合做资产全景分析场景。
- 做一个精简的“AI 行情播报”提示词模板。让 AI 在拿到数据后自动生成一句简洁总结,而不是把 JSON 原样丢给用户。
最后说一点个人体会:这类 MCP Server 项目的核心价值不在于工程师多写好几百行代码,而在于把“AI 能力边界”往外推了一小步。这种“接口标准化 + 数据治理 + 工具提示词设计”的组合思路,不只是股票数据领域能用,任何想把外部数据能力接入 AI 的场景都可以参考。下次你要做类似项目,提前把数据源层的边界划清楚,多替 AI 想一步——它需要什么,你能给它什么,它拿到后能不能直接用。把这几个问题想透,项目就成功了一大半。