周末帮一个做量化的朋友调试 Claude Desktop,他开口就是一句:"我让它查一下平安银行今天的收盘价,它非说自己是 AI 没法连实时数据。"这其实是很多人对 stock-sdk-mcp 这类项目的误解——不是模型变笨了,而是模型默认根本没拿到行情数据的入口。这周我把 stock-sdk-mcp 从环境配置、工具映射、客户端注册到实际调用踩坑完整跑了一遍,顺手把过程中所有值得记的东西整理成这篇实践稿。文章会偏实操,适合第一次接触 MCP、想尽快把 A 股/港美股行情接入到 AI 助手里的人,也适合那些已经跑通但被各种玄学问题折腾过的人对照排查。
1. 先弄清楚一个问题:模型为什么需要"股票 SDK 的 MCP 壳"
1.1 没有统一标准时,大家是怎么让 AI 查股票的
早期接行情数据进 LLM 应用,常见的做法有三种。第一种是"快照拼 prompt",把 CSV 或 Excel 行情快照直接塞到对话上下文里,让模型读表格做分析。这招对付小型数据还行,但数据量大一点就废了,而且每一次都得手工刷新快照,根本谈不上实时。
第二种是自建 Function Calling 网关。你自己定义一批函数,比如 get_realtime_quote(symbol),然后把函数签名和描述传给模型。模型碰到需要实时数据的问题时,会返回一个函数调用请求,你的网关收到后再去请求数据源,把结果回填给模型。这套方案技术上没问题,但问题在于:每换一个模型就要重写一套函数协议,OpenAI 的函数描述格式和 Anthropic 的工具格式不一样,本地模型又是一套;每加一个数据源,又要同步维护一层参数映射。我见过好几个团队把大量精力耗在"适配各家模型"而不是"做好数据服务"上,最终整个链路变得无比脆肉。
第三种是把数据喂给 RAG,让模型基于向量检索回答。这个方法适合处理财报、公告这类静态长文本,但遇到"现在价格是多少""今天涨没涨"这种强时效问题,RAG 的实时性和准确性都不够用,检索出来的可能还是几天前的旧数据。
这三种方案我都试过,最后共同的结论是:问题根本不在于模型能力,而在于缺少一个"把数据工具标准化地暴露给模型"的中间层。每个人都在重复造轮子,但造出来的轮子互不兼容。
1.2 MCP 把数据工具变成了模型的"外接器官"
MCP(Model Context Protocol)是近几年模型工具调用领域最重要的标准化尝试。它定义了客户端(Claude Desktop、Cursor 这类 AI 应用)和服务器(数据服务端)之间的通信协议:基于 JSON-RPC 2.0,客户端通过 tools/list 发现服务器上有哪些工具,通过 tools/call 调用具体工具,核心就是这套"发现—调用"模型。
你可以把 MCP 理解成 USB-C 接口。以前每个设备需要一根专门的线,各个厂商的接口形状和电压还不一样;现在统一成了同一个标准,任何支持 USB-C 的设备插上就能用。MCP 做的事情就是把股票 SDK、数据库、企业内部 API 这些"设备",统一包装成模型可以即插即用的"外设"。
重要的是理解一点:MCP 不是说让模型自己去发 HTTP 请求。实际的行情请求仍然是由运行在本机的 MCP server 发出的,模型只做两件事:根据用户的自然语言问题,从工具列表里挑选合适的工具,生成符合参数要求的调用请求;然后把工具返回的结果,组织成自然语言答案。所以"AI 查不到实时数据"这个问题的本质,不是模型能力不足,而是缺少这条桥梁,而 stock-sdk-mcp 就是这座桥本身。
1.3 stock-sdk-mcp 在整条链路里的位置
从数据流来看,整个链路是这样的:股票数据源 SDK(Tushare Pro、AkShare、富途 OpenAPI 等)负责从数据源拉取行情和财务数据;stock-sdk-mcp 的工具层负责把 SDK 的函数签名翻译成模型能理解的语义化工具,并且做参数合法性校验、返回字段裁剪、错误处理;再通过 MCP 协议与客户端通信;最后客户端把用户问题连同工具能力一起交给大模型。
所以这个项目的核心职责,用大白话说就是三件事:一是"翻译",把 SDK 冷冰冰的参数转成模型能看懂的描述;二是"加工",把 SDK 返回的几 MB 原始 JSON 过滤成模型回答问题真正需要的字段;三是"兜底",处理 token 失效、接口超时、数据权限不足等各种意外情况,并给模型一个合理的错误提示,让它不要瞎编数据。
我在实践里选的组合是 Tushare Pro 作为行情数据源,python 版 MCP SDK 构建服务端,客户端分别接了 Claude Desktop 和 Cursor。下文所有配置和排查思路都基于这套组合,但其中大部分逻辑对 AkShare、Baostock 一样适用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 跑通前需要准备的环境与最小配置
2.1 依赖选型:数据源、运行时和客户端
先解决数据源的问题。市面上的免费/付费行情接口我大致分成三类:
- AkShare:免费,直接爬公开网页聚合数据,接口数量多,但个别接口会因目标网站改版突然失效,稳定性一般,适合个人研究。
- Tushare Pro:积分制,接口规范、返回字段稳定,几乎不存在反爬问题,适合做规范化的工具层。低积分用户的接口权限有限,部分高级接口需要提高积分。
- Baostock:免费,历史数据质量可靠,但没有特别丰富的实时行情接口,适合回测场景。
我这版实践选了 Tushare Pro,理由是它的 token 模式非常简单,工具层只需要在启动时读一次 token,后续所有请求都走同一个鉴权机制,不需要像爬虫类接口那样维护会话和频率控制。另外它的日线、实时行情、财务指标接口字段都比较规整,方便做"模型友好化"处理。
运行时我建议用 Python 3.11 及以上版本。Python 3.10 也能跑,但我测试时发现新版 MCP SDK 对 3.10 的 typing 语法兼容偶尔会出怪问题,没必要在这种地方浪费时间。依赖安装建议用虚拟环境隔离,后面讲客户端配置时会说明原因。
MCP 客户端方面,Claude Desktop 和 Cursor 是两种最常见的接入方式。Claude Desktop 适合日常对话式查询,直接说"帮我看看最近五日平安银行的走势"就行;Cursor 适合在编码场景里内联调用工具,比如写量化策略时顺手取数据。
2.2 初始化项目和标准目录
我习惯把整个项目放在独立的目录里,结构大概是:
text复制stock-sdk-mcp/
├── server.py # MCP server 入口
├── tools/
│ ├── __init__.py
│ ├── quotes.py # 实时行情工具
│ ├── klines.py # K线工具
│ ├── financials.py # 财务指标工具
│ └── calendar.py # 交易日历工具
├── utils/
│ ├── cache.py # TTL 缓存
│ └── data_source.py # 数据源统一封装
├── .env.example
├── requirements.txt
└── README.md
初始化步骤并不复杂,我用 uv 管理 Python 项目,命令如下:
bash复制git clone <你的仓库地址> stock-sdk-mcp
cd stock-sdk-mcp
uv venv .venv --python 3.11
source .venv/bin/activate
uv pip install -e ".[server]"
cp .env.example .env
注意 .env 文件里放的是各种敏感配置,比如 Tushare token。我在 .env.example 里会留下占位符:
dotenv复制TUSHARE_TOKEN=your_token_here
MCP_LOG_LEVEL=WARNING
MCP_CACHE_TTL=10
TUSHARE_TOKEN 就是你在 Tushare 个人主页拿到的 token,MCP_LOG_LEVEL 建议默认 WARNING 级别。很多人在这个阶段就把日志级别设成 DEBUG,后面跑起来会发现日志狂刷,反而影响判断。
2.3 客户端注册命令
Claude Desktop 的配置文件在 macOS 是 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 在 %APPDATA%\Claude\claude_desktop_config.json。配置内容如下:
json复制{
"mcpServers": {
"stock-sdk": {
"command": "/Users/me/stock-sdk-mcp/.venv/bin/python",
"args": ["/Users/me/stock-sdk-mcp/server.py"],
"env": {
"TUSHARE_TOKEN": "填入token",
"MCP_LOG_LEVEL": "WARNING"
}
}
}
}
这里有几个坑必须先说清楚。command 一定要写绝对路径,而且是虚拟环境里的 python 解释器路径,不是系统全局 python。否则客户端会用自己的环境启动 server,到时候 import 不到 mcp 库,全部失败。token 放进 env 字段而不是写死在 server.py 里,这样万一你的配置文件被别人看到,至少还有一点保护。
Cursor 那边更简单,直接在项目的 .cursor/mcp.json 里写:
json复制{
"mcpServers": {
"stock-sdk": {
"command": "/Users/me/stock-sdk-mcp/.venv/bin/python",
"args": ["/Users/me/stock-sdk-mcp/server.py"]
}
}
}
Cursor 会自动读取当前项目的 MCP 配置,在 Settings 界面也能看到连接状态。
2.4 一个最简的冒烟测试
配置好后一定要先做冒烟测试,别直接问专业问题。先问客户端:"列出你现在可以用的股票数据工具。"如果 MCP 挂载成功,它会列出工具清单,比如 get_realtime_quote、get_daily_kline 这些。
再问一个具体但简单的问题:"查询平安银行(000001.SZ)今天的最新价格,只返回收盘价和涨跌幅。" 如果模型说"我无法获取实时数据",说明工具没挂上,回到配置检查;如果模型返回了参数错误,说明工具已经通了,只是 description 写得不够清晰,模型在猜参数,下一步要去优化工具定义。
冒烟测试通过后再接入真实业务问题,这一小节的意义是帮你划定"到底是链路问题还是工具定义问题"的排查边界。
3. 从 SDK 函数到 MCP 工具:核心映射逻辑拆解
3.1 工具模式的标准结构
MCP 协议里每个工具的定义就三项:name、description、inputSchema。name 是模型用来识别的,inputSchema 是模型用来生成参数的,而 description 是模型理解"这个工具什么时候该用、参数怎么填"的主要依据。
我见过很多实现把 description 写得很敷衍,比如"获取股票行情",结果模型根本不知道该传什么参数。好一点的 description 应该包含:这个工具解决什么问题;每个参数的单位、格式、取值范围;一个典型的调用示例;以及什么情况下不要用这个工具。
给个例子,我封装日 K 线工具时,inputSchema 大概是这个样子:
json复制{
"name": "get_daily_kline",
"description": "获取指定股票在指定日期范围内的日K线数据,返回OHLC、成交量、成交额。日期格式务必使用YYYYMMDD,例如20250101。股票代码格式为交易所代码+点+证券代码,例如000001.SZ或600000.SH。不支持美股。当日数据在收盘后才会更新。",
"inputSchema": {
"type": "object",
"properties": {
"ts_code": {
"type": "string",
"description": "股票代码,例如000001.SZ"
},
"start_date": {
"type": "string",
"description": "开始日期,格式YYYYMMDD"
},
"end_date": {
"type": "string",
"description": "结束日期,格式YYYYMMDD"
},
"limit": {
"type": "integer",
"description": "最大返回条数,默认120,最大1000",
"default": 120
}
},
"required": ["ts_code", "start_date", "end_date"]
}
}
你会发现我在 description 里强制声明了日期格式和股票代码格式。这不是废话,是血泪教训。模型默认会臆想各种格式,比如把日期写成 "2025-01-01",把股票代码写成 "000001"(没有交易所后缀),这些都会导致 SDK 调用直接报错。
3.2 我整理的工具清单与调用场景
整个项目里我实际保留的工具并不多,但每个都能覆盖一类典型问题。工具清单如下表:
| 工具名 | 底层 SDK 函数 | 典型使用场景 | 返回要点 |
|---|---|---|---|
| get_realtime_quote | pro.realtime_quote / 简单行情接口 | "现在价格多少""今天涨了还是跌了" | 最新价、涨跌幅、成交量、最高最低 |
| get_daily_kline | pro.daily + pro.adj_factor | "最近一个月走势""均线分析" | 日期、OHLC、成交量、复权状态 |
| get_stock_list | pro.stock_basic | "沪深300成分股有哪些""筛选创业板股票" | 代码、名称、行业、上市日期 |
| get_financial_overview | pro.fina_indicator | "这家公司 ROE 是多少""对比两家公司毛利" | 报告期、ROE、毛利率、负债率 |
| get_trade_calendar | pro.trade_cal | "下一个交易日是哪天""这段时间有几个交易日" | 日期、是否开市 |
工具粒度是个值得斟酌的问题。Tushare 的接口非常多,如果我把每个接口都原样暴露成一个 MCP 工具,模型在工具列表里会迷路,选错工具的概率也会变大。我最终保留的粒度原则是:一个自然语言问题,尽量能由一次工具调用解决。你要是问"分析一下最近一年的走势",模型不应该需要分别调用复权因子接口、日线接口再自己拼数据,而是应该直接调 get_daily_kline 并且返回前复权后的价格。
3.3 返回数据的"模型友好化"处理
SDK 返回的原始数据通常不适合直接丢给模型。Tushare 的日线接口一条记录有几十个字段,其中不少字段连人都不一定看得懂,模型拿到这种噪音数据后,要么理解偏差,要么就干脆胡编。所以我在工具层做了一层强制加工。
我的做法是:裁剪字段、重命名字段、统一单位、丢弃 NaN。日线只保留日期、开高低收、成交量、成交额几个核心字段;成交量的单位统一成手,成交额统一成万元,避免模型把"手"和"股"搞混。日期统一转成 YYYY-MM-DD 字符串,这样模型不用再自己算时区。
一个典型的 K 线返回处理函数看起来是这样:
python复制def build_kline_result(df):
# 强制限制条数,避免返回太大触发超时
df = df.tail(120)
records = []
for row in df.itertuples():
records.append({
"date": row.trade_date,
"open": float(row.open),
"high": float(row.high),
"low": float(row.low),
"close": float(row.close),
"volume_lot": int(row.vol),
"amount_wan": round(row.amount / 10000, 2)
})
return records
经验就是:返回字段宁少勿多。"够模型回答问题"永远比"数据完整"更重要。
4. 使用过程中最值得记录的几类坑
4.1 坑一:日志打进 stdout,工具时好时坏
现象很玄幻:MCP server 显示连接成功,但调用一次成功、一次失败,报错五花八门,有的是 Parse error,有的是超时。我一开始怀疑是 token 问题,直接在 Python 里单独调用 SDK 函数,毫秒级返回超时正常;又怀疑是网络代理问题,换了局域网和手机热点都一样。
后来我把 server 单独用 MCP Inspector 跑起来,一个细节让我瞬间清醒:MCP 的 stdio 模式是通过标准输入输出通信的,JSON-RPC 消息和 server 自身的 log 在同一个流里。我的代码里有几个 print() 调试语句,还有日志库被配置成了输出到 stdout,这些额外字符直接把协议帧切碎了,客户端解析就炸。这就是为什么时好时坏——日志输出少的时候侥幸没碰上,输出一多就完蛋。
修复很简单:日志模块统一走 stderr,或者直接写文件,绝对不允许向 stdout 打印任何非协议内容。框架的 run() 函数只负责协议输出,这一点千万不要覆盖。
排查这个坑可以复现的路径是:先用独立进程调用 mcp-run 或者 inspector 观察协议帧,再接入客户端;而不是在对话里反复试错。
4.2 坑二:复权口径不一致,K 线和实时价对不上
有次调接口,实时行情显示平安银行最新价 10.25,日线数据最后一根却是 9.87,涨幅怎么都对不上。第一反应是数据源坏了,重启 server、重拉数据都没效果,差点把 SDK 换掉。
后来我把同一交易日的 OHLC 从日线接口拉出来和实时接口对比,发现行情字段类型、时间戳都对得上,唯一的差异是收盘价系统性偏低。再一查,日线接口默认开了前复权(qfq),而实时行情用的是未复权的当前价。前复权会调整历史价格,但最新价格理论不应该变——直到我意识到自己拉的数据落在了除权日附近,前复权算法会把除权前的价格全部下修,看起来就成了"历史价格比当前价高"。
问题根因不是数据源错,而是复权口径不一致。解决方式是在工具层强制指定复权参数,并且把复权状态直接写在返回字段里。日线接口默认返回前复权,实时行情与未复权日线对比;而回测场景需要用后复权或前复权,看盘场景用不复权。这些口径如果不说清楚,模型回答用户的问题时必然前后矛盾。
顺带一提,如果你的服务部署在海外服务器上,时区问题也很值得注意。部分数据源的时间字段返回的是 UTC,直接展示会出现"日期比实际少一天"的错觉,我的做法是在工具层强制统一成 Asia/Shanghai 时区,再输出字符串日期。
4.3 坑三:模型贪多,一次要三年日线直接超时
用户问"分析一下平安银行 2021 年以来的走势",本来是个正常需求,但实际跑起来客户端直接超时。看日志发现模型调用了 get_daily_kline,参数里没有传 limit,工具层也没设默认值,结果后端真的把 2021 年到现在的每一个交易日都返回了,数据量大到超过了客户端一次性渲染的承载能力。
这个坑的本质是:模型对"数据量"没有感知,它只会按照 description 里的字段清单生成参数,而不会去估算返回结果的大小。所以工具层必须自己把上限控制住。我在 get_daily_kline 里强制限制了返回条数,默认 120 条,最大 1000 条,超出部分截断或者提示模型缩小日期范围。
更优雅的做法是语义分级:当用户问的是"近一年走势"时,工具层返回日线但只给最近 120 个交易日;当用户说"长期趋势"时,工具层主动降级返回月线,告诉模型"数据已按周线聚合"。我在 description 里写了一句引导:"涉及长时间窗口的趋势分析,优先使用 K 线聚合数据,而不是逐日返回。"模型看到这句话,真的会少调很多次大范围日线请求。
4.4 坑四:token 权限不够,接口时灵时不灵
还有一类问题不属于代码 bug,而是数据源权限造成的。我的 Tushare token 在低积分阶段,财务指标接口偶尔返回"抱歉,您没有访问该接口的权限"。但这个报错不是每次都在,因为有些接口的缓存会命中,有些不会,表现就是"昨天还能用,今天不行"。
排查链路比较简单:先确认 token 是否有效,在 Tushare 控制台手动调一次接口,如果返回权限错误,基本就是积分不够;然后去查具体接口需要的积分值。
但解决方式不能是"提示用户去充积分"就完了。我在工具层做了降级方案:优先用 Tushare,遇到权限错误时,自动切换到一个免费数据源封装,并把返回来源字段标注为 akshare,同时提醒模型"该数据来自免费源,字段精度可能略有差异"。这一步让整个 server 在 token 权限不足时仍然可用,而不是直接挂掉或者让模型胡编。
5. 让接入更稳:缓存、限流与安全配置
5.1 为什么必须做缓存
MCP 工具一旦接入对话客户端,模型在一个会话里就可能对同一只股票发起多次完全相同的调用。比如用户说"分析平安银行",模型会先调实时行情,再调日线,甚至会在分析中途再次调实时行情确认最新价。每次调用都实打实消耗数据源额度,而且 Tushare 这类接口都有频率限制。
所以我给工具层加了 TTL 缓存。实时行情缓存 5 秒,日线数据缓存到当天收盘之后(日期变化前都直接命中),财务指标缓存 1 天。实现不复杂,一个装饰器就能解决:
python复制import time
from functools import wraps
_cache = {}
def ttl_cache(ttl: float):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
key = (func.__name__, args, tuple(sorted(kwargs.items())))
now = time.time()
hit = _cache.get(key)
if hit and now - hit[0] < ttl:
return hit[1]
result = func(*args, **kwargs)
_cache[key] = (now, result)
return result
return wrapper
return decorator
这里注意一点:Tushare 的日线数据盘中也会更新,如果缓存 TTL 设成一天,盘中价格变了但你拿到的是早上的快照。所以我建议实时相关接口的 TTL 都控制在 5-10 秒,日线数据可以放宽到半小时,收盘后用户再查就是当日最终数据了。
5.2 限流和超时保护
数据源接口频率限制是一个容易忽略的问题。Tushare 是按积分和接口维度限制每分钟调用次数的,如果多个用户同时通过远程 MCP 服务访问,很容易瞬间打满限额。解决方式是在 server 里加一个简单的限流器,按接口名做互斥锁或者信号量:
python复制import asyncio
from contextlib import asynccontextmanager
_rate_limiter = asyncio.Semaphore(5)
@asynccontextmanager
async def rate_guard():
async with _rate_limiter:
yield
限流之外,单次工具调用必须加超时。SDK 请求如果卡住不返回,MCP server 会一直占着客户端资源,最终表现就是整个对话卡死。我用 asyncio.timeout 包裹数据源请求,单次调用超过 15 秒直接抛出超时错误,并让模型知道"本次取数超时,请缩小数据范围后重试"。
5.3 安全边界:token 别乱放
MCP 工具的能力边界等于数据源 token 的权限边界。如果你的 server 被配置到共享环境里,任何能连上这个 server 的客户端都可以通过模型把 token 权限范围内的数据全部问出来。所以我说这句话:token 只能放在服务端环境变量,不能提交到 git,更不能写进客户端的共享配置。
我在 .gitignore 里强制忽略了 .env 文件,同时在 README 里写了说明。对远程部署的场景,前端走反向代理加鉴权,token 只出现在 server 进程的环境变量里。
6. 从单机到团队服务:如何把 MCP server 挂到远程
6.1 换传输方式
stdio 模式只适合个人本机使用,因为它是客户端直接拉起本地进程,天然与客户端同生命周期。团队协作时,大家不可能都去克隆你的仓库、配你的 token,更合理的做法是把 server 部署到一台公共机器上,其他成员通过远程地址接入。
MCP 的远程传输方式主要有 Streamable HTTP 和 SSE 两种,FastMCP 这类框架切起来极其简单,把 transport 参数从 stdio 改成 http 就行,但要注意这不再是本地进程了,你的 server 从"只有本机能访问"变成了"网络上的任何人都可能访问"。
远程模式必须做两件事:鉴权和 TLS。MCP 协议本身支持客户端在请求头里带认证信息,常见做法是在网关层校验 Bearer token。如果你直接把服务裸奔在公网,理论上任何人都能把你数据源的额度耗光。
6.2 部署结构建议
我实践中的部署结构比较简单,但每一步都踩过坑,整理如下:
text复制用户 → MCP 客户端 → 反向代理(TLS + 鉴权) → MCP server(uvicorn) → 数据源 SDK
反向代理我用 Caddy 或者 Nginx,负责把外网域名转成内部服务,同时挂一层 Bearer token 校验。MCP server 用 uvicorn 启动,监听内网端口,进程守护交给 systemd 或者 supervisor,崩溃自动拉起。
有一点比较容易被忽略:远程模式下,MCP server 的时区、日志、缓存都是进程级别的,不像本机调试那么直观。我建议至少把访问日志单独写一个文件,方便排查"谁在什么时候调用了哪个工具"。
6.3 后续还能怎么扩
把 MCP server 跑稳之后,这个模式的可扩展性比传统 API 网关要好不少。我在实际使用中有几个方向可以分享:一是给常用自选股做一个专门的 get_watchlist 工具,把模型每次都要拼股票代码的痛点消掉;二是把公告、财务报表摘要做成 Resources 类型,让模型在回答里关联引用;三是加一层"自然语言查询到 SQL"的复合工具,在财务数据场景下特别有用。
这些扩展的核心都围绕着同一句话:工具边界越清晰,模型就越不会乱说话。每次加新工具,花时间最多的不是写实现代码,而是设计 description 和返回字段。
最后分享一个调试技巧:我后来写了一个最小的 MCP 客户端脚本,用官方 mcp 库的 stdio_client 直接在终端里手动调用工具。每次改完字段映射,先跑一遍这个脚本确认工具返回格式正确,再放进对话环境去接模型。这比在 Chat 窗口里反复试错快得多,现在我把它当成回归测试用,也是我这次实践下来收获最大的一个习惯。
