行情这条赛道,这几年最不缺的就是各种 SDK:有券商交易类的,有免费行情数据类的,也有商业数据商专门给的量化接口。但真正落到“AI 帮我查票、帮我复盘”这种场景时,SDK 再多也不够用——大模型不做函数调用培训,你得把 SDK 能力翻译成它能理解、能调用的工具。最近我在本地折腾了一个叫 stock-sdk-mcp 的项目,核心就是把一套行情 SDK 通过 MCP 协议暴露给 AI 客户端。今天这篇不写虚的,把我实践过程中踩过的坑、验证过的链路、值得直接照抄的代码和配置全部整理出来。准备上车的朋友,读完这篇至少能少走三天的弯路。
这个项目适合谁?一句话总结:你手头已经有能用的行情 SDK,现在想让 Claude Desktop、自研 Agent 或其他支持 MCP 的客户端直接说出“帮我查一下某只票的实时价和最近 5 日走势”,然后真的跑出数据。如果你是纯零基础,没碰过 Python、没配过 MCP,我也会在关键步骤里把原理讲透,跟着抄作业也能跑通。但我的建议是,先别急着复制粘贴,把前两节读完,明白那层壳到底替我们干了什么,后面操作起来会顺手很多。
1. 先搞清楚 stock-sdk-mcp 是什么:行情能力接入 AI 的最小路径
1.1 为什么手里有 SDK,还要多此一举包一层 MCP
很多写脚本的老手会觉得,直接用 SDK 查个行情多简单?几行代码拉个接口不就行了,搞什么 MCP?这种质疑我一开始也有。但你换个角度想:你想让 AI 助手也能做这件事,问题就来了。原来我们写一个爬虫或脚本,是人来控制流程,而现在是大模型来控制流程。大模型压根不认识你 SDK 里那个 get_quote(symbol) 函数,除非你提前告诉它“系统里有个叫 get_quote 的工具,参数是什么,什么时候用”。
我们自己搭这一套工具调用的框架,最朴素的办法是给大模型写一大段函数描述,然后把用户问题、聊天历史、函数定义全部拼进 prompt 里,再自己去解析模型输出里到底是调哪个函数、传什么参数。在小 demo 里还能忍,真到做多个工具的时候,prompt 会越来越长,参数漏传、格式不规范、报错没地方兜底,维护成本直线上升。MCP 解决的就是这部分问题:它把工具注册、参数声明、调用请求、返回结果、错误处理全部标准化了。
把 stock-sdk-mcp 拆开看,它本质不是在创造新的行情获取能力,而是做了一层“适配”——底层还是你熟悉的行情 SDK,负责真正去交易所、数据商那边拉数据;MCP 那侧负责把实时报价、K 线、股票列表这些能力,变成一台台 AI 能看得懂的“插头”。协议层标准化了,客户端就不用天天改。
1.2 MCP 的底层机制,用大白话讲清楚
我习惯把 MCP 理解成 AI 世界的 USB-C 接口。过去你要让 AI 接一个数据库,数据库厂商要专门做一个对接方案;接一个行情软件,又得重新做一套。今天大家统一用 MCP 协议:客户端只要实现了这个协议,就能像电脑识别 U 盘一样,自动识别插上来的行情服务,看到这个服务提供了哪些工具、可以处理哪些指令、参数长什么样。
在技术细节上,MCP 底层走的是 JSON-RPC 2.0 消息格式。不过在实际开发时,我们基本不会手动拼这些 JSON 包,而是直接用官方 SDK。以 Python 为例,FastMCP 这个类帮我们完成了绝大多数脏活:你用装饰器定义一个函数,它自动帮你注册成一个可被 AI 调用的工具;你在 docstring 里写清楚这个工具是干什么的、参数是什么格式,客户端在初始化时就能通过 tools/list 接口拿到这份声明。
1.3 模块划分:这个项目到底是哪几个部分拼起来的
我建议把 stock-sdk-mcp 的整体链路拆成四层,排查问题、设计扩展时思路会清爽很多:
- 数据源接入层:你原本在用的行情 SDK,负责鉴权、选网络节点、维护连接。
- MCP 服务适配层:把 SDK 方法转换成 MCP 的 tool,标明工具名、入参、返回值。
- 传输层:本地调试用 stdio,远程部署用 SSE 或 HTTP,解决 AI 客户端与服务进程之间的消息通道问题。
- 客户端配置层:在 MCP Host(比如一个支持 MCP 的 AI 桌面端)里,用 JSON 配置文件声明“我要启动哪个服务、服务路径是什么”。
这四层里最容易写崩的是第二层和第三层,后面我会专门用章节讲工具函数的边界怎么划、返回字段怎么裁剪,以及 stdio 模式下最常见的进程通信坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 本地跑通:从一行命令到 AI 能查到行情的完整链路
2.1 最小环境准备,代码依赖尽量少
先说清楚,我这里讲的版本是我个人实践下来比较稳的组合:Python 3.10 及以上,安装 MCP 官方 Python SDK,底层那套行情 SDK 按你自己原先的习惯安装在当前环境里即可。MCP Python SDK 的安装命令我写在下面,实际安装时可能会多拉几个依赖包,比如 pydantic、httpx,这些都正常。
bash复制python -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"
这里有个细节值得提醒:如果之前折腾过其他 AI 项目,最好在虚拟环境里装,不要直接往全局环境怼。MCP SDK 的版本迭代比较快,某些旧版本和最新版之间在工具描述的结构上有调整,虚拟环境方便你随时重建。
你手上的行情 SDK 有可能是仅供本机调用的 Python 库,也有可能是附带独立行情终端的客户端。无论如何,我建议先在普通 Python 脚本里做一次冒烟测试,保证不经过 MCP 的情况下,SDK 本身能调通、能拉到数据、能正常打印,再进入下一步。这一步能帮你把“SDK 坏了”和“MCP 适配层坏了”两件事分离开,排查起来会省很多心力。
2.2 一个可以直接修改运行的 MCP Server 骨架
下面这段代码是我实践时搭的最小骨架,具体导入方式和 SDK 方法名,你替换成自己实际用的那套就行。
python复制import json
import os
from datetime import datetime, timedelta
from mcp.server.fastmcp import FastMCP
# 这里按实际行情 SDK 的导入方式来,下面三行是占位示意
# from stock_sdk import StockClient
# client = StockClient(host="...", token="...")
mcp = FastMCP("stock-sdk-mcp")
def _fmt_time(dt: datetime) -> str:
return dt.strftime("%Y-%m-%d %H:%M:%S")
@mcp.tool()
def stock_realtime(symbol: str) -> str:
"""获取某只股票的实时行情快照。适用于用户询问“现在价格是多少”“今天涨了还是跌了”。
参数 symbol 为 6 位数字股票代码,例如 '600519'。
"""
# real = client.get_realtime_quote(symbol)
real = {
"symbol": symbol,
"name": "示例股票",
"price": 48.25,
"prev_close": 47.10,
"change_pct": 2.44,
"high": 48.50,
"low": 46.80,
"time": _fmt_time(datetime.now()),
}
return json.dumps(real, ensure_ascii=False)
@mcp.tool()
def stock_kline(symbol: str, days: int = 5) -> str:
"""获取指定股票最近 N 个交易日的日 K 线数据,days 默认 5,最大不超过 10。
当用户想了解一段时间走势时使用。
"""
# klines = client.get_daily_kline(symbol, count=days)
klines = []
today = datetime.now().date()
for i in range(days):
day = today - timedelta(days=days - i)
klines.append({
"date": day.strftime("%Y-%m-%d"),
"open": 47.00,
"close": 48.25,
"high": 48.50,
"low": 46.80,
"volume": 125000,
})
return json.dumps(klines, ensure_ascii=False)
@mcp.tool()
def stock_search(keyword: str) -> str:
"""按股票名称或代码片段模糊搜索匹配的股票列表。
当用户只给了“茅台”“平安”这类名称,没有给完整代码时,先调用本工具拿到代码。
"""
# items = client.search_stocks(keyword)
items = [{"symbol": "600519", "name": "贵州茅台"}, {"symbol": "601318", "name": "中国平安"}]
return json.dumps(items[:10], ensure_ascii=False)
if __name__ == "__main__":
mcp.run(transport="stdio")
很多第一次上手的朋友会以为这里必须写一堆异步回调、协议处理,看到上面几行代码反而愣住:“这就完了?”对,MCP 协议层已经把复杂度装进 FastMCP 里了。你需要做的,就是定义清楚每个函数的 docstring 和入参类型,因为这串文本最终会被模型读取,作为它判断何时调用工具的依据。
docstring 的质量直接决定 AI 调得准不准。我看到不少人把函数名写得很专业,但 docstring 写得含糊,比如“获取股票数据”——模型根本不知道这个工具适合在哪个意图下触发。正确的姿势是写清楚“用户想了解当前价格/涨跌幅时用这个”、“用户问历史走势时用这个”,必要时把参数示例也写进去。
2.3 让 AI 客户端识别并加载这个服务
服务端代码写好后,不能让 AI 客户端凭空发现它,需要在一份配置文件里登记。以常见的 MCP 客户端为例,配置结构通常长这样:
json复制{
"mcpServers": {
"stock": {
"command": "/绝对路径/.venv/bin/python",
"args": ["/绝对路径/stock_server.py"],
"env": {}
}
}
}
配置里那个 command 路径很容易踩坑。本地如果直接写 python,而当前环境又不是启动客户端时用的那个环境,客户端会找不到你的虚拟环境,直接报进程启动失败。最稳的写法是给虚拟环境里的解释器写绝对路径,例如 /home/me/project/.venv/bin/python。在 Windows 上路径可能带 .exe 后缀,同样建议写绝对路径。
保存配置后,重启 MCP 客户端,让它重新加载服务。正常情况下,客户端会在启动阶段请求一次 tools/list,把 stock-sdk-mcp 暴露的三个工具全部登记到模型可选的工具列表里。
2.4 stdio 和 SSE 怎么选,别等部署时才拍脑袋
默认的 transport="stdio" 模式下,MCP 客户端会以子进程方式拉起你的 Python 服务,两者通过标准输入输出通信。这个方案的好处是零网络配置、延迟低、足够安全,本地个人使用完全够了。但如果你的服务要跑在服务器上,给多个客户端或远程的 Agent 调用,stdio 就不好用了,它要求客户端能直接管理服务进程,远程场景下得改用基于 HTTP 的 SSE 传输方案。
FastMCP 里切换传输模式就一行代码的事:改成 mcp.run(transport="sse"),然后客户端配置里不再写本机 command,而是填服务地址,形如 http://127.0.0.1:8000/sse。这段我先点到为止,个人跑通阶段优先学 stdio,理解链路后再上网络模式会容易很多。
3. 工具封装才是灵魂:从实时报价到 K 线,字段设计决定体验
3.1 工具粒度怎么设计,才能既灵活又不臃肿
把 SDK 里的所有函数一股脑全部映射成 MCP 工具,是我见过最普遍的操作,也是最不可取的。工具太多了,模型每次都要在几十个工具里做选择,既增加误选概率,又浪费 token。我最终采用的方法是“按用户意图聚类”:搜股票代码算一个工具,实时报价算一个工具,K 线历史算一个工具。用户需求无外乎从“这是什么票”到“现在多少钱”,再到“最近走势如何”,聚成几类后模型调用清晰很多。
如果业务中需要批量查询多只股票的报价,建议单独建一个 stock_realtime_batch(symbols: list[str]) 工具,不要指望模型循环调用单只报价工具——实测下来它可能真会乖乖 for 循环,但耗时和 token 开销都比一次性批量查询大得多,而且频次一高很容易触发数据源限流。
3.2 返回字段怎么裁剪,MCP 不是你的数据库转储工具
最开始我封装报价工具时,习惯性把 SDK 返回的完整数据结构整个丢回去,包括几十个不常用的字段。结果 AI 回复的质量没有变高,反而是每次调用返回的数据占据了大量上下文窗口,对话没几轮就开始变慢、费用变高。后来我做了一层精简,只保留模型做分析真正需要的核心字段:
- 实时报价:最新价、涨跌幅、涨跌额、今开、昨收、最高、最低、成交量、时间。
- 日 K 线:日期、开、高、低、收、成交量,最多返回 30 条,超过就按最近 30 条取。
- 搜索工具:代码、名称,最多 10 条。
这样从源头上限制返回体量,比在 prompt 里对模型说“你只关注这几个字段”要可靠得多。模型只会看到你给它的数据,裁剪是过滤噪声最省事的方式。
另外,如果你想给模型提供股票的所属行业、主营业务、近期公告等长期静态资料,不要和实时行情塞在同一个工具里。把静态资料做成另一个工具或 MCP Resource,在需要时按需调用,而不是在一次行情快照里全部倾倒出来。
3.3 时间格式、交易日历与缓存策略
行情数据最怕时间概念混乱。不同数据源返回的时间字段五花八门,有的是 Unix 时间戳,有的是字符串,有的还带时区后缀。我在服务适配层里做了一件事:所有对外返回的时间统一成 %Y-%m-%d %H:%M:%S 字符串,历史 K 线日期统一成 %Y-%m-%d。这样模型在解读数据时不需要再做额外的推断,尤其是“今天是否交易日”“现在盘中有没有数据”这类问题,输出会稳定很多。
缓存这块是后期体验提升最快的一环。实时报价虽然叫实时,但在盘中 3 秒以内的间隔里,价格对绝大多数分析场景没有质的区别。我在服务层加了一个 3 到 5 秒的内存缓存:如果同一个代码在缓存有效期内被重复请求,直接返回缓存,而不是再打一次底层 SDK。别小看这个策略,AI 在分析多只股票时经常会对同一只票连续提问,如果没有缓存,底层接口的压力和限流风险都会明显上升。历史 K 线这种日频数据,缓存时间可以拉到十几分钟甚至更长。
3.4 参数校验:把“输入什么”写死在工具入口
MCP 工具的参数虽然由模型生成,但模型远不可信,尤其是涉及证券代码这类强格式信息时,我见过它一本正经地把“贵州茅台”填到 symbol 参数里。所以我在每个工具的入口都加了一层防御式校验:如果入参不是 6 位数字,先尝试转成字符串后补零,再不行就调用搜索工具转成代码。这一层保护大大减少了因为参数格式错误导致的调用失败。
这里有个很微妙的细节:如果校验失败直接抛异常,有些客户端会把错误信息原样返回给用户,用户看到一堆 Python traceback 体验极差。最好的做法是让校验失败的返回内容带上口语化的解释,比如“未找到代码 60051,请确认股票代码是否正确”,这比一个存储过程式的报错更适合被 AI 理解。毕竟很多 MCP 客户端会把工具返回的文本当作上下文,你不希望这些上下文里飘着一整屏无关的报错堆栈。
4. 实践里最容易翻车的几个问题,我一个个踩过来
4.1 服务起不来,AI 客户端一直说找不到工具怎么办
这类问题占到实践初期故障的 60% 以上。第一个排查点是配置里的路径:确认 args 指向的 Python 文件真实存在,确认 command 指向的解释器路径正确,最好手动在终端执行一遍同样的命令,看有没有 ModuleNotFoundError。
第二个坑跟 stdio 传输相关:在 MCP 服务进程里,所有输出流都会被客户端解析为协议数据,如果你在代码里写了 print() 做调试,这些打印信息会污染通信通道,导致客户端握手异常或工具列表加载失败。日志请使用 loguru 或者标准 logging 模块输出到文件,不要用 print 调式。
第三个坑是行情 SDK 初始化太慢。某些行情 SDK 启动时要建立长连接、拉取基础信息,耗时可能超过 MCP 客户端默认握手超时时间,客户端直接判定服务启动失败。我的解决思路是懒加载:把耗时初始化放到首次调用工具时才执行,而不是在 import 模块或启动 FastMCP 时执行。
4.2 数据更新不及时,或和行情软件里看到的对不上
这个问题偶尔出现一次,就要重点检查是不是缓存策略设置太长了。我在第三节提到的 5 秒实时缓存只适合“稳定”场景,如果服务在晚间或周末运行,行情根本没有更新,数据源接口通常会返回上一交易日的收盘数据。AI 不知道这个问题,它拿到的快照里的时间字段如果是今天,但内容其实是昨天的快照,它可能会生成一个“昨夜收盘价”这种奇怪说法。
一个比较可靠的优化:行情收盘后暂停实时报价工具,或者至少返回数据时带一个明确的 market_status 字段,值是 open、closed、pre_market、post_market。AI 看到 closed 就知道不能把当前数据当实时价格来解读,回答里会自然地带上一句“已收盘,显示的是最近交易日的收盘数据”。
4.3 多数据源与限流问题
我最初只接了一家免费行情源,测试时挺正常,但一旦让 AI 一次性分析几十只票,就频繁触发限流。后来我设计了“主源 + 备用源”的配置:主源被限流或返回异常时,服务自动切换备用源,同时在返回数据里加一个 data_source 字段。这个字段不仅方便排障,也让 AI 在给用户做结论时能说明数据来源,遇到不同源之间数据不一致的情况,它可以自己判断要引用哪一份。
说到数据源不一致,这里也提醒一句:不同数据商对同一只股票的复权处理、成交量单位、涨跌停判断都可能不同。如果前面刚问了实时价,后面又拉了一段历史 K 线,两处数据来自不同源,AI 拼在一起分析时可能会得出互相矛盾的结论。尽量让同一只股票在同一会话内的所有数据来自同一个数据源,做不到就在工具输出字段里写清楚口径。
4.4 被忽略的并发场景:AI 真的会“同时”调用多个工具
很多人的服务只是一台脚本,没有考虑到并发。但 MCP 客户端在一次多轮对话中,可能因为模型判断而同时触发多个工具调用,比如同时查实时报价、K 线和股票搜索。如果后端 SDK 不支持并发或不是线程安全的,就会出现数据串了、连接断开、甚至进程崩溃。给服务加上一个简单的异步锁或使用队列来限制并发深度,能极大地提升稳定性。
比较省事的做法是把服务跑在 mcp.run(transport="sse") 加异步框架下,让 FastMCP 处理并发。如果仍使用 stdio 本地模式,则建议在底层 SDK 调用外层包一个 threading.Lock,确保同一时刻只有一个请求在真正访问 SDK,避免客户端疯狂工具调用把进程打崩。
5. 真实链路演示:AI 怎样一步步完成一次行情分析
看完前面的搭建和避坑,我拿一个生活中的自然语言请求来串起整条链路。假设用户问的是:“帮我查一下中国平安现在多少钱,顺便看看这只票最近 5 天的走势,用简单的话总结一下趋势。”
第一步,AI 收到这句话后会先看当前有哪些可用工具。stock-sdk-mcp 响应 tools/list,列出 stock_search、stock_realtime、stock_kline。模型的推理让它意识到:用户提到了“中国平安”,但它不知道代码是多少,于是先调用 stock_search,传参 keyword="中国平安",拿到代码 601318。
第二步,模型分别调用 stock_realtime(symbol="601318") 和 stock_kline(symbol="601318", days=5)。两个调用几乎同时到达服务端。如果服务端没做并发保护,这里就可能挂掉;做了锁或异步处理之后,两个工具分别按序返回 JSON。实时报价告诉模型当前价格和涨跌幅,K 线数组展示最近 5 个交易日的收盘价脉络。
第三步,模型拿到这些结构化数据后,结合用户的问题组织成自然语言回复,大致是说“中国平安(601318) 现价 XX.XX,比上一个交易日上涨/下跌 X.XX%;最近 5 个交易日分别是……,整体呈……趋势”。整个过程里,我们没有提前编写任何业务逻辑判断,只是给了模型足够干净的数据和正确的工具声明。
一次完整的 MCP 工具调用背后,真正有价值的工作不是那几行启动代码,而是服务端对数据裁剪、缓存、异常提示和并发控制的设计。很多刚接触的人觉得 MCP 是一个框架的神奇魔力,其实它就是个高效搬运工,搬的东西好不好,取决于你在服务端放进去的内容质量。
6. 个人实践后的一点体会,以及这个项目还能往哪走
跑了几天 stock-sdk-mcp,我最直观的感受是,MCP 的价值并不在于省掉那几行调用链路的代码,而在于让自然语言和结构化金融数据之间那层“翻译”变得标准化了。以前每次写一个内部行情小工具,都要单独给前端写一套接口、给模型写一份 function description;现在一套服务能同时服务聊天机器人、桌面助手、自动化工作流,确实省心。
如果要在这个骨架基础上继续扩展,有几个方向特别值得做:把 K 线输出改成更精简的多维摘要格式,等模型真正需要时再拉完整数组;给 stock_realtime 增加“自选股列表”的批量拉取能力;还可以增加一套基于内置规则的涨跌停预警工具,当 AI 问“现在市场有没有异动”时,能主动推送给它,而不是等它一只一只查。
最后再分享一个建议:本地测试时,多准备几个长得像人类口语的问题来压测你的工具声明,比如“帮我看看 601318 怎么样”跟“我想知道今天保险板块表现如何”这两类问法,前者考验参数解析,后者考验工具边界是否足够灵活。把这些问题喂给 AI 跑一遍,你会比看十篇文档更能理解服务端该怎么优化。行情数据这条路,入口是行情 SDK,出口是 AI 的理解力,而能否让两边顺畅对接,就看你的适配层做得够不够干净。
