
近几个月我一直在折腾一个挺有意思的方向把行情数据接口封装成标准化的MCP服务。这不是什么新概念说白了就是做一个中间层让AI助手能通过自然语言直接查询和解析股票数据。今天就把我实践stock-sdk-mcp的全过程、踩过的坑和最终沉淀下来的方案整理出来希望能给正在做类似事情的朋友一个参考。先说结论把股票SDK封装成MCP服务这件事核心价值并不在于“调用接口”本身而在于把获取数据—处理参数—组织返回结果这一整条链路标准化让任何支持MCP协议的AI客户端比如Claude Desktop、Cursor、自研Agent框架都能以统一的方式消费这些数据。这篇文章会从设计思路、核心实现、实战部署到问题排查完整走一遍我的实践路径。1. 为什么要把股票SDK包成MCP服务1.1 直接调SDK和走MCP的本质区别以前咱们写行情分析工具基本都是这个套路写一段Python脚本import对应的SDK然后写一堆函数去拿行情、算指标最后print或者存文件。这种方式本身没问题但它有一个天然缺陷——数据和逻辑耦合在同一个程序里其他模块想复用要么import这个脚本要么走HTTP接口再包一层。而AI助手接入就更麻烦了模型没法主动发现你的数据能力你得把函数一个个硬编码进prompt里每加一个新功能prompt就得改一次维护成本极高。MCPModel Context Protocol解决的正是这个问题。它把数据能力抽象成工具toolsAI客户端通过MCP协议自动发现这些工具及其参数定义。大模型看到的是一个结构化描述这里有获取股票行情的工具参数有code股票代码、period周期、adjust复权类型模型自己就知道该传什么参数不需要你在prompt里写死调用方式。这就是协议层接入和代码层调用的本质区别。1.2 stock-sdk-mcp解决的三类实际问题我在实践中梳理了一下这个项目主要解决三个层面的问题。第一是统一数据入口。不同的数据源行情接口、财务数据接口、公告接口协议各不相同有的走HTTP有的是WebSocket有的返回JSON有的返回protobuf。封装进MCP服务后对外暴露的就是统一的工具接口使用方根本不需要关心底层数据源是什么。第二是降低AI客户端的接入成本。AI模型天生擅长自然语言理解和工具调用但需要一个稳定的工具发现机制。MCP服务把SDK能力包装成带描述的tools后模型通过一次initialize握手就能获得全部能力清单接着就能自主决定何时调用、传入什么参数。我实际测试下来GPT和Claude系列的模型对这种工具调用的理解准确率非常高。第三个是工程化治理。SDK里往往有大量不规范的函数——命名随意、参数风格不统一、返回字段有冗余。通过MCP这层薄封装你可以做统一的参数校验、数据裁剪、错误码转换相当于在原有SDK之上建立了一套标准化治理层。实测下来下游消费方的代码量能减少约60%因为很多格式化逻辑都被收敛到了MCP服务内部。2. 整体架构与核心设计思路2.1 MCP服务端的三层结构一个完整的stock-sdk-mcp服务我划分成了三个层次各司其职第一层是传输层负责和AI客户端建立MCP通信。目前Python生态里FastMCP这个库用起来最顺手底层支持stdio和SSE两种传输模式。stdio模式适合本地工具类场景SSE模式适合部署在远程服务器上供多个客户端使用。第二层是工具注册层负责把股票数据能力暴露成MCP tools。每个工具包含名称、描述、参数schemaJSON Schema格式和具体的执行函数。这层是MCP协议的落地核心参数设计好不好直接决定了大模型调用的准确度。第三层是SDK适配层负责对接真实的股票数据SDK。这一层要做参数映射、数据解析、异常捕获和超时控制。设计上要尽量保持和SDK的解耦——换数据源的时候只改这一层工具注册层和传输层完全不用动。2.2 为什么选择FastMCP而不是从零实现我最早也纠结过要不要自己手写MCP协议实现。翻了一遍MCP官方文档之后果断放弃了——协议细节太多光初始化握手、能力协商、消息帧格式这些基础能力写起来就是不小的工程量得不偿失。后来选了FastMCP这个Python库它把MCP Server侧的样板代码收敛到很少的程度你只需要关注工具函数本身的业务逻辑。选FastMCP的另一个考量是生态。目前社区里基于FastMCP的MCP项目最多、文档最全遇到问题基本搜得到答案。版本迭代也比较快我记得从0.x版本开始用到现在主版本已经比较稳定了断崖式变更出现过一次但整体向后兼容性还是可以的。这里补充一个选型经验如果你的AI客户端跑在纯Node.js环境也可以考虑TypeScript版本的MCP SDK但如果是Python生态为主FastMCP几乎是唯一推荐。传输层选stdio还是SSE取决于使用场景——本地个人使用选stdio延迟低多人共用或远程部署SSE才有意义。2.3 工具粒度划分粗了不行细了更不行工具粒度是这类项目里最重要、但做起来最考验经验的设计决策。太粗的话比如一个get_all_stock_data工具一次性返回所有股票全量数据不仅token消耗巨大模型的注意力也会被无关信息稀释反而影响回答质量。太细也不行比如把单只股票的open、high、low、close各拆成一个工具大模型要回答XX股票今天涨了没得连续调用四五个工具才能拿全数据既慢又容易出错。我实践下来比较舒适的粒度是围绕一次完整查询需求设计一个工具。例如获取历史K线数据一个get_kline工具就行参数为股票代码周期复权类型查询实时行情一个get_quote工具参数为股票代码列表获取财务指标一个get_financial_indicator工具参数为代码指标类型。这样模型一次工具调用就能拿到完整数据你说的让模型自己拆解复杂需求为多个工具调用序列才是合理路径。2.4 上下文内数据量的控制策略模型上下文窗口有限股票数据又是典型的高密度信息数据裁剪和摘要策略必须提前设计好。我实测发现一次性向模型塞超过几百行的行情数据生成质量会明显下降因为模型处理长表格类数据的能力还是有限。策略有两层第一层是结果集裁剪从SDK拿到数据后在适配层就做截断默认只返回最近10条或30条通过limit参数控制第二层是字段裁剪很多SDK接口返回几十个字段但实际分析场景经常只用其中六七个在适配层可以做一个字段白名单映射大幅减少传输量。两个策略配合起来一个工具调用的响应通常能控制在2KB以内AI处理起来非常轻松。3. 核心细节解析与实操要点3.1 工具参数Schema让大模型一眼看懂MCP的工具调用依赖参数Schema也就是JSON Schema格式的参数定义。这个Schema写得好不好直接决定大模型调用工具时的意图理解准确率。我的经验是参数名要见名知义description要用完整的自然语句写清楚取值范围和默认值类型要严格定义枚举值要全部列出。拿股票代码这个参数举例如果description写股票代码模型可能会传600519或贵州茅台或600519.SH参差不齐。如果写成股票代码例如600519贵州茅台、000001平安银行、AAPL美股Apple使用不带市场后缀的纯代码模型的准确率会几十个百分点地提升。实践中最体现水平的是枚举值和格式约束的写法。周期参数直接列枚举1d表示日K1w表示周K1M表示月K并且明确说明如果不确定周期默认使用1d。这样模型在用户没有明确指定周期的时候会倾向使用低风险默认值而不是自行猜一个可能对不上的值。3.2 一个标准的工具函数内部要怎么设计工具函数看起来只是普通Python函数但内部结构有一些坑要留意。拿我的get_kline实现举例from fastmcp import FastMCP mcp FastMCP(stock-sdk-mcp) mcp.tool() def get_kline( code: str 600519, period: str 1d, adjust: str qfq, limit: int 30, ) - str: 获取股票历史K线数据 Args: code: 股票代码如600519贵州茅台、000001平安银行 period: K线周期1d日K1w周K1M月K adjust: 复权类型qfq前复权hfq后复权None不复权 limit: 返回最近多少条数据默认30 Returns: 格式化之后的K线数据文本 raw_data stock_sdk.get_kline(code, period, adjust) # 字段裁剪 slim_data [ {date: item[date], close: item[close], high: item[high], low: item[low], volume: item[volume]} for item in raw_data[-limit:] ] # 格式化输出 lines [日期 | 收盘价 | 最高价 | 最低价 | 成交量] for item in slim_data: lines.append(f{item[date]} | {item[close]} | {item[high]} | {item[low]} | {item[volume]}) return \n.join(lines)这里有几个细节值得说明。返回值尽量用格式化文本而不是原始JSON表格形式对模型阅读理解更友好默认值一定要设大模型在信息不足时会倾向用默认值而不是报错好的默认值能避免很多往返调用字段裁剪放在SDK适配层而不是工具函数外是为了保证后续换成其他SDK时工具函数的返回值格式保持一致。3.3 参数校验与错误处理的闭环实践中还有一个容易翻车的地方——参数校验。你永远想象不到大模型会把什么值传给工具。我当时跑测试的时候模型传过贵州茅台这种股票名称而不是代码传过2024-01-01这种日期做period参数甚至传过空字符串。SDK本身对这些脏数据的容忍度极低不是抛异常就是返回None然后程序崩溃。所以参数校验必须在工具函数入口做完整闭环。第一步是类型转换把字符串转成SDK需要的类型第二步是取值范围校验不在枚举范围直接返回友好错误信息比如period参数仅支持1d、1w、1M你传入的是1h请改正后重试第三步是接入SDK时包裹try-except捕获SDK抛出的各种网络异常、权限异常和限流异常统一转换成MCP层面的可读错误返回给模型。这里有个关键心得因为模型的工具调用循环通常会依赖错误信息进行自我修正所以错误信息本身要写成可执行的修正指令。别返回参数错误这种空洞描述要返回参数XXX不支持仅支持A、B、C请改用其中之一调整后重新调用这种带纠正方向的信息。实测下来模型根据这种错误信息自行修正的成功率相当高。3.4 同步接口限流与重试机制行情数据服务普遍有访问频率限制而模型调工具经常是短时间内高频发起不做限流处理很快就会被数据源封掉。我的方案是做一个简单令牌桶每秒最多10次请求超出排队等待连续失败时指数退避重试从1秒起步最大退避到30秒。重试逻辑不是无脑重试。网络超时和链接中断这种瞬时的错误确实可以重试但权限不足、参数非法这种确定性错误重试一万次也没用应当直接返回错误信息。做法是在异常捕获时区分一下错误类型只有可重试的错误才进入重试队列这个细节能省非常多无效请求。4. 实操过程与核心环节实现4.1 环境准备与依赖安装开始之前先把环境搭好。我的推荐方案是用Python 3.10版本以上可以用venv或者conda管理依赖。python -m venv .venv source .venv/bin/activate pip install fastmcp stock-sdk这里有个小提示市面上stock-sdk名字的包不少有的商业SDK包名并不叫这个你需要根据实际使用的数据源SDK来安装。我这里其实用的是抽象后的说法真实项目里可能是akshare、tushare或者某券商的官方SDK按需替换即可。FastMCP是核心依赖必须安装。4.2 服务端完整实现从单工具到多工具我们实现一个包含实时行情和K线查询的MCP服务作为整个项目的骨架。from contextlib import asynccontextmanager from datetime import datetime from typing import Optional from fastmcp import FastMCP # 这里假设已经有一个封装好的数据适配层 stock_adapter from . import stock_adapter mcp FastMCP( stock-sdk-mcp, instructions股票数据查询服务支持实时行情和历史K线。使用时请注意A股代码为6位数字港股代码为5位数字美股代码为字母代码。 ) mcp.tool() def get_realtime_quote(code: str) - str: 查询股票实时行情 Args: code: 股票代码如600519贵州茅台、00700腾讯控股、AAPLApple Inc. Returns: 实时行情文本包含价格、涨跌幅、成交额等关键信息 if not code: return 错误股票代码不能为空。请提供有效的股票代码例如600519或AAPL。 try: quote stock_adapter.get_realtime_quote(code) except stock_adapter.InvalidCodeError: return f错误股票代码{code}无效。请检查代码并重试。A股使用6位数字港股使用5位数字美股使用字母代码。 except stock_adapter.RateLimitError: return 错误请求过于频繁已被限流。请稍等几秒后重试。 except Exception as e: return f错误查询行情失败原因:{str(e)}。请稍后重试。 # 格式化输出便于模型理解 change_pct quote.get(change_pct, 0) trend 上涨 if change_pct 0 else 下跌 if change_pct 0 else 平盘 return ( f{quote[name]}{code}当前行情\n f最新价{quote[price]} 元\n f涨跌幅{change_pct}%{trend}\n f今开{quote[open]}最高{quote[high]}最低{quote[low]}\n f昨收{quote[prev_close]}\n f成交量{quote[volume]}手成交额{quote[amount]} 万元\n f更新时间{quote[time]} ) mcp.tool() def get_kline( code: str, period: str 1d, limit: int 30, adjust: str qfq, ) - str: 获取股票历史K线数据用于趋势分析和技术指标计算 Args: code: 股票代码如600519贵州茅台 period: K线周期可选值为 1d(日K)、1w(周K)、1M(月K)默认1d limit: 返回最近N条数据默认30最大120 adjust: 复权类型qfq前复权hfq后复权none不复权默认qfq # 参数校验 if period not in (1d, 1w, 1M): return f错误period参数{period}仅支持1d、1w、1M三种取值请调整后重试。 try: klines stock_adapter.get_kline(code, periodperiod, adjustadjust) except Exception as e: return f错误获取K线失败原因:{str(e)} if not klines: return f未查询到股票{code}的K线数据请检查代码是否正确。 # 裁剪数据量防止上下文爆炸 klines klines[-min(limit, 120):] lines [日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量] for k in klines: lines.append( f{k[date]} | {k[open]} | {k[close]} | {k[high]} | {k[low]} | {k[volume]} ) lines.append(f\n共返回 {len(klines)} 条记录。) return \n.join(lines) if __name__ __main__: mcp.run(transportstdio)核心点在于让工具函数的description和参数足够规范以及错误信息设计成可自我修正的类型。我在开发过程中实际跑了非常多的模型调用测试这两个细节对最终效果的影响比我预想得还大。4.3 多工具时的组织策略拆分还是合并当工具数量超过20个大模型在单次对话中正确选择工具的概率会下降。我的做法是把同类型的数据查询合并成一个工具用子参数区分把不同类型的功能拆成独立工具。举个例子财务数据就不要再拆成查询市盈率查询市净率查询ROE三个工具了合为一个get_financial_indicator工具用indicator_type参数区分即可。反之获取实时行情和获取K线这类返回结构和用途都完全不同的能力拆开更合适。这样既保证工具的语义清晰又控制总量不上涨。4.4 transport选择stdio vs SSE的取舍FastMCP支持两种运行模式。locals模式stdio适合嵌入式场景比如Claude Desktop调用本地python脚本SSE模式适合部署成独立HTTP服务多个客户端通过网络访问。# stdio模式适合本地、单用户 if __name__ __main__: mcp.run(transportstdio) # SSE模式适合远程服务和多用户 # 如果部署在服务器上对外暴露HTTP端点 if __name__ __main__: mcp.run(transportsse, host0.0.0.0, port8000)我的实践建议是初期阶段先用stdio省事也能快速验证工具逻辑正确性需要多人共用或远端部署的时候再切SSE。切换的时候注意SSE模式下跨域配置、鉴权、HTTPS这些都需要额外处理不是简单地改一行代码就能跑通的。4.5 配置客户端并验证效果把MCP服务接入Claude Desktop需要在配置文件里加上MCP Server的注册信息{ mcpServers: { stock-sdk-mcp: { command: python, args: [/path/to/your/server.py], cwd: /path/to/your/project } } }配置完成后重启客户端在对话中发一个自然语言请求测试效果比如帮我查一下贵州茅台今天的行情或者看一下过去一个月的日K走势。正常情况下AI会识别意图、调用对应工具、解析返回数据然后生成结构化的回答文本。如果工具没有被自动调用优先检查两个地方一是tools名称和description是否有语义歧义二是参数schema是否合理如果某个必填参数模型很难推断就尽量设默认值或改成可选参数。5. 常见问题与排查技巧实录5.1 大模型不调用工具或调用失败的排查方向遇到模型不按预期调用工具的情况别急着改代码先按照下面这个顺序排查。第一层是MCP服务本身有没有正常注册。可以在服务端加日志启动时打印注册的所有工具名称和参数信息。确认工具确实暴露给客户端了。很多情况下工具没注册成功往往是因为Python代码里的语法错误或者依赖缺失导致服务启动异常。第二层是工具的description是否足够明确。模型是基于描述理解工具用途的如果获取股票历史K线数据比get_kline_data这种模糊描述更具体。说得越清楚模型才能做出正确选择。第三层是参数schema是否给到位。前文强调过参数名和描述里必须说清楚格式和取值范围模型才会准确填充参数。提供一个可能参数示例也很有帮助。第四层是返回数据的格式和复杂度。如果返回内容超过模型上下文限制或者格式混乱无法解析模型可能自动放弃依赖工具结果。尽量保持输出短小精悍且用简洁表格文本描述。5.2 数据源权限与限流问题股票数据源基本都有严格的访问控制尤其是实时行情权益校验、频率限制、域名白名单都绕不开。实践中有几个有效策略。多级缓存必做。把高频查询的同一只股票数据在内存中缓存30秒K线数据可以缓存得更久比如5分钟。这样大量重复请求不会真实打到数据源限流压力大幅缓解。接口级兜底。有些数据源对日K、分钟K这种低优先级接口的限流更宽松和实时行情接口不是同一个配额池。对于实时性要求不高的场景比如做技术指标分析可以优先走日K接口而不是实时行情接口。错误识别上数据源返回的限流错误和网络抖动错误必须区分开。限流错误说明触发频率控制需要降速等待网络抖动则说明是瞬时故障可以立即重试。我吃过亏把限流错误也当成网络故障去疯狂重试直接触发数据源封禁数小时的黑名单机制。血的教训。5.3 时区与交易时段问题股票数据天然跟交易时段强相关而这恰恰是新手最容易忽略又最需要注意的地方。数据源返回的时间字段可能是UTC时刻直接用于展示和分析结果可能差8小时。必须在适配层统一做时区转换。第二个问题是交易时段与数据形态。A股盘中实时数据是在变动的收盘后则是固定的。如果你在盘中查询今天涨跌幅数据是动态的如果模型把这个数据当最终结果去推理回答就会有偏差。解决办法是在返回文本里带上数据时间戳明确标示该数据的快照时刻和所属交易时段模型就能自行判断数据是否还有时效性。第三个问题是非交易日的处理。周六日、节假日股票无交易数据。模型如果没有相应的知识可能会把一个周一的今日行情问题误以为是在交易日发出的。比较有效的做法是在服务端做节假日判断如果当前时间是周末或法定假日在返回错误信息时明确告知今天不是交易日最新行情数据为最近交易日X月X日模型就能给出准确的回答。5.4 多股票对比和群组查询的优化用户常问帮我对比一下茅台、五粮液、泸州老窖今天的涨跌幅。如果模型串行调用三次get_realtime_quote效率不高。比较好的做法是设计批量查询工具get_realtime_quotes(codes: list[str])让模型一次传入多个代码SDK层一次请求或并行请求再合并返回整个查询从多次往返变成一次调用。需要注意的是批量工具返回的数据要多只股票并列展示表格里股票代码/名称必须带全否则模型可能混淆数据归属。我实际处理时还会顺手做一次按涨跌幅排序的预处理把排序结果给到模型它可以直接拿来做排名分析省去二次处理。6. 经验总结与优化建议做完这个项目我最深的感受是MCP服务端写起来确实不算复杂真正决定效果的在于细节——工具的参数Schema设计、返回文本的格式化逻辑、错误信息中是否带着纠正方向、数据量控制是否得当。这些细节每一条都直接影响大模型工具调用的准确率和生成质量。优化建议方面我感觉有几个点值得你再投入时间。日志和可观测性建设优先做。给每个工具调用加上日志记录参数、返回数据量和耗时。这套日志对排查问题和持续迭代都很关键可以说这是整个项目里能让你少熬夜的功能。对模型可能产生的幻觉日期等数据要通过返回值里的定时信息主动纠正让模型知道自己拿到的是什么时刻的数据。6.1 可复用的工具描述模板最后我把自己沉淀下来的工具描述模板放出来供直接参考工具名称动词开头的短语如 get_realtime_quote 描述说什么功能 适用场景如获取股票实时行情适用于查询当前价格、当日涨跌 参数设计参考 - 必选参数尽量少能默认就设默认值 - description 里给出代码格式示例和常见取值范围 - 枚举值若存在必须列出全部选项 - 返回格式尽量统一为 Markdown 表格或带标题的文本 - 错误信息应给出修正指引而不是单纯报错模板化之后再增加新工具的效率明显提升每个新工具的调试时间从以小时计降到以分钟计。这个模板基本沉淀了我踩过的大部分有关参数设计的坑。说个额外的体会MCP这层架构虽然是给AI服务设计的但做完之后我发现哪怕是传统程序要接入新的行情数据源走这层标准化的中间层也比直接改业务代码要舒服得多。协议的价值就在于此——一次适配,处处复用,数据源头再怎么换,下游消费方感知不到。从这个角度说,这个方向不单是给AI用的玩具,是有实打实的工程价值的。