Spring AI Alibaba实战:三分钟构建股票查询MCP Server

发布时间:2026/10/5 3:54:40
Spring AI Alibaba实战:三分钟构建股票查询MCP Server 这期实战训练营我拿一个特别适合落地的场景开刀用Spring AI Alibaba构建一个股票查询MCP Server。说白了就是把股票行情查询能力通过MCP协议暴露出去让Claude、Cline、通义千问这类AI客户端都能直接调用。不需要申请付费API key不需要复杂的算法一个Spring Boot工程加一个Tool注解本地起服务三分钟就能在AI对话里问出实时股价。适合已经熟悉Spring Boot、想搞懂MCP服务端到底怎么写的同学也适合正在琢磨Spring AI Alibaba工具链但苦于没有完整Demo的人。最近社区里总有人问Spring AI Alibaba是不是停更了我先说结论项目一直在维护只是发版节奏跟着上游Spring AI走不如前端框架那么高频。判断标准很简单去Maven仓库看一眼release时间就知道了。这篇文章里我用的是当前可用的稳定组合你照着搭不会踩版本坑。1. 理解MCP与Spring AI Alibaba的整合思路1.1 MCP协议到底解决了什么问题MCPModel Context Protocol在我眼里是个非常朴素但好用的东西。以前要让AI模型调用外部数据每个模型厂商都要单独开发一套插件规范工具方也得针对不同模型做集成两头折腾。MCP把模型和工具之间的交互协议标准化了相当于给AI的工具调用接口定了一个通用插座标准。你插上这个插座AI就知道你的工具长什么样、需要什么参数、返回什么格式。MCP Server不需要关心调用方是Claude还是通义千问它只负责把工具能力注册进去然后等待客户端发起请求。反过来模型侧也不必知道工具的具体实现只要客户端帮它找到了MCP Server列表它就能按协议去调用。举个生活化的例子MCP协议就像USB接口工具方只要做成USB接口任何支持USB的电脑、手机都能插上用。没有这个标准之前每家都造自己的接口鼠标线、键盘线、充电线互不兼容桌面上一堆乱七八糟的线。1.2 为什么选择Spring AI Alibaba来做这件事构建MCP Server有两条路一条是用MCP官方SDK从头写JSON-RPC协议交互、工具注册、生命周期管理代码量大而且容易出错另一条是用Spring AI这类框架把协议层封装掉开发者只需要写普通的Java方法加个注解就能变成MCP工具。Spring AI Alibaba是阿里在Spring AI基础上的增强和适配补齐了国内开发者的几个痛点DashScope通义千问开箱即用、中文场景支持好、面向国内云环境做了适配。你可以在一个项目里同时跑着模型对话能力和MCP Server能力后面想接Agent或搭一个本地知识库助手都能顺理成章地扩展。很多教程只讲Spring AI官方怎么跑MCP demo到了Spring AI Alibaba这里因为版本和依赖组合的细微差别新手容易绕晕。我这篇把它们组合在一起给你一个可以直接落地跑通的方案。1.3 纯工具Server与Agent应用的区别这里要澄清一个概念。很多人一开始会把构建MCP Server和构建AI应用混在一起以为必须引入一个大模型才能跑。我这次的项目就是一个纯工具型Server不需要加载模型也不需要写Agent逻辑。打个比方你开了一家只提供电力插座的工厂插座上可以插任何电器。MCP Server就是这个插座工厂AI模型是各种电器。你不需要在插座工厂里内置电器只需要保证插座符合标准任何电器插上来都能用。所以这篇文章的项目结构里主线是一个Spring Boot应用核心代码只有几个类一个工具类、一个解析类、一个启动类。没有对话逻辑、没有提示词工程纯粹的工具提供方。这也是我希望你理解的重点MCP Server本质上是服务端它服务的对象是各种支持MCP的AI客户端。2. 项目设计免费行情数据源与代码规则2.1 行情数据源选型对比股票行情的实时数据第一反应是去接券商或者专业数据商的API但那些服务要么收费昂贵要么申请流程繁琐对个人开发者非常不友好。我实测下来免费且稳定的方案里最推荐腾讯的行情接口。新浪接口其实也可以用但强制要求带Referer头否则返回403在Java HttpClient里每次请求都要额外设置请求头稍微麻烦一点。而且新浪接口一次只能查询一个代码批量查询要发多个请求效率不高。腾讯接口把这些都省了不需要认证、不需要额外请求头、支持一次请求多只股票、返回字段里包含买卖五档、涨跌幅、成交量等完整行情数据。我做过一个小对比整理成表格方便你参考对比项腾讯行情接口新浪行情接口是否需要认证不需要不需要是否强制请求头不强制强制Referer头批量查询支持逗号分隔不支持逐个查询返回字段丰富含买卖五档基础行情字段编码GBKGBK稳定性较好偶尔限流生产环境如果需要更严谨的数据建议找正规数据服务商签约这个后面细说。但做技术演示、个人小工具腾讯免费接口完全够用。2.2 股票代码的市场前缀规则A股、港股、美股、北交所不同市场的代码规则不一样做股票查询工具第一个要处理的就是代码归一化问题。A股上海交易所的代码以6开头比如600000是浦发银行用sh做前缀。深圳交易所的代码分别以0、3开头比如000001是平安银行300750是宁德时代用sz做前缀。北交所的代码大多以4、8、9开头用bj做前缀。港股直接用hk加五位代码比如hk00700是腾讯控股。美股用us加代码比如usAAPL是苹果。这里有个非常影响体验的设计点用户在对话里输入股票代码时大概率不会主动带上市场前缀。用户说帮我查一下600000如果工具要求他必须输入sh600000体验就很差。所以我在工具方法里做了自动识别逻辑代码以6开头自动归为上海以0、3开头自动归为深圳以4、8、9开头自动归为北交所。用户不写前缀也能查写了前缀就以用户输入的为准。实测下来这个设计让对话式调用的成功率提升不少。2.3 返回数据结构和编码处理腾讯行情接口的URL长这样https://qt.gtimg.cn/qsh600000,sz000001返回的数据是一段JS变量赋值语句格式如下v_sh6000001~浦发银行~600000~7.31~7.32~7.33~...~;每只股票对应一个v_开头的变量变量名是v_代码值是一串用波浪号分隔的字段。这里有个大坑接口返回的编码是GBK不是UTF-8。如果你的代码里用UTF-8去解码解析出来的中文股票名称会全是乱码。我建议用HttpClient的byte[]响应体方式去接收数据然后手工用GBK编码转成字符串。先用UTF-8解一次看看你一定会看到麻花一样的中文改成GBK之后一切恢复正常。后面排坑部分我还会重点提一次。字段的索引位置我踩过几次坑通过实测梳理出一个相对稳定的关键字段位置表字段索引含义1市场类型1代表上海51代表深圳等2股票名称3股票代码4当前价5昨收6今开7成交量手30时间戳或时间字符串31涨跌额32涨跌幅百分比等一下先别急着把索引写死。腾讯接口历史上调整过字段位置不同时间段的接口返回可能略有差异。我的建议是在开发时先打印一份完整的分割后数组对照着确认你当前环境下的索引位置再做正式解析。这是我在多次接口调试里学到的教训免费接口没有固定文档最可靠的方式是看实际数据。3. 依赖引入与基础配置3.1 版本组合怎么选避免依赖冲突本地运行这个项目的版本组合我直接给你参考JDK 17Spring Boot 3.3.5Spring AI Alibaba 1.0.0.2Spring AI MCP Server 1.0.0用这个组合有一个很关键的好处Spring AI Alibaba 1.0.0.2内部对齐了Spring AI 1.0.0可以保证MCP Server starter和Alibaba starter中的自动配置类不打架。有些同学喜欢追新版本直接上Spring Boot 3.4或者3.5这本身没问题但建议先把项目跑通了再升级。一来Spring AI对Spring Boot版本的兼容性有比较严格的校验二来版本跨越太大会出现一些莫名其妙的类加载错误排查起来费时间。Maven中央仓库里有些Spring AI的starter需要维护BOM版本官方推荐通过spring-ai-bom管理版本号。为了防止版本漂移我在pom里显式声明了spring-ai-bom这样做最稳。3.2 pom.xml里真正需要的几个依赖不要一上来就堆一大堆starter我经历过好几次依赖间互相干扰的情况。这个项目只需要三个核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent dependencies !-- Spring AI Alibaba核心 -- dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0.2/version /dependency !-- MCP Server支持 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId version1.0.0/version /dependency !-- Web支持HTTP传输模式或扩展调试用 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies注意如果你完全走stdio模式spring-boot-starter-web其实不是必需的。我默认带上是因为实际调试时HTTP模式更直观而且后续如果要加SSE端点Web依赖是前提。说到MCP传输模式这里有一个非常重要的决策点MCP Server的stdio模式利用标准输入输出和客户端通信应用本身不能依赖控制台交互HTTP模式则暴露一个服务端地址让客户端通过HTTP轮询或SSE连接。两种模式各有使用场景具体怎么选下一节配合配置讲。3.3 application.yml配置与传输模式选择配置非常简单核心是spring.ai.mcp.server这一块。stdio模式就是默认值不需要特别写transport但如果项目同时引入了Web依赖我建议显式声明transport避免Spring自动切换造成困惑。spring: application: name: stock-mcp-server ai: mcp: server: enabled: true name: stock-mcp-server version: 1.0.0 transport: STDIO logging: file: name: logs/stock-mcp.log为什么我要把日志独立输出到文件因为stdio模式下应用的标准输出通道就是MCP协议的数据通道。如果Spring Boot的日志默认输出到控制台你会把协议报文和日志混在一起直接导致客户端解析失败。配置了logging.file.name之后日志走文件stdout干干净净协议通信就顺畅了。如果你更想要HTTP模式配置改成spring: ai: mcp: server: enabled: true name: stock-mcp-server version: 1.0.0 transport: HTTP base-path: /mcp然后通过指定端口启动时客户端访问http://localhost:8080/mcp即可。HTTP模式的好处是调试时可以直接用浏览器打开地址看到服务状态缺点是需要维护一个HTTP服务常驻运行。我的建议开发阶段用HTTP模式排查问题方便正式给AI客户端用stdio模式资源占用小、启动即用。4. 核心实现Tool工具方法开发4.1 工具类整体结构设计Spring AI Alibaba和Spring AI对工具方法的处理非常简单粗暴你在一个Spring托管Bean里写一个方法加上Tool注解框架启动时自动扫描并注册成MCP工具。整个过程不需要手写任何协议层代码。我建议单独建一个service包把行情逻辑放进去。工具类不直接写HTTP请求逻辑会更清晰所以拆成两层外层StockQueryService是工具方法入口内层用一个独立的行情请求Handler负责发请求和解析。这里有一个设计取舍工具方法参数的类型和描述会直接映射成MCP工具的输入schema所以参数名和描述一定要写得尽量清晰让AI模型能够理解用户说什么话时应该调用这个工具。我在描述里写了支持A股、港股、美股支持批量查询支持省略市场前缀自动识别这些提示词对于一个模型能否正确传参非常关键。4.2 行情请求与解析核心代码先看请求层的代码我用的是JDK原生的HttpClient不需要引入额外的HTTP库。这种轻量请求的场景原生HttpClient够用且无依赖包袱。package com.example.stockmcp.service; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.Charset; import java.time.Duration; public class StockMarketDataHandler { private static final String QT_URL https://qt.gtimg.cn/q; private static final HttpClient HTTP_CLIENT HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); public String fetchQuotes(String codeParams) throws IOException, InterruptedException { String url QT_URL codeParams; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(5)) .GET() .build(); HttpResponsebyte[] response HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray()); // 关键点腾讯接口返回GBK编码必须显式指定 return new String(response.body(), Charset.forName(GBK)); } }再写工具类时要处理代码归一化和返回格式化。工具方法用List接收代码参数不太合适因为MCP schema对List类型支持不差但对话式传参时模型更倾向于传单个String再用逗号拼。我最终选择了String类型多个代码用逗号分隔。package com.example.stockmcp.service; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; Component public class StockQueryService { private final StockMarketDataHandler marketDataHandler new StockMarketDataHandler(); Tool(description 查询股票实时行情。支持A股(如sh600000、sz000001、600000)、港股(如hk00700)、美股(如usAAPL)。多个代码用英文逗号分隔最多查询10只。返回名称、现价、涨跌、涨跌幅、今开、昨收、成交量等信息。) public String queryStock(String stockCodes) { if (stockCodes null || stockCodes.isBlank()) { return 股票代码不能为空例如600000 或 sh600000,sz000001; } ListString normalizedCodes normalizeCodes(stockCodes); if (normalizedCodes.isEmpty()) { return 没有识别到有效的股票代码; } if (normalizedCodes.size() 10) { return 一次最多查询10只股票你当前输入了 normalizedCodes.size() 只; } try { String rawData marketDataHandler.fetchQuotes(String.join(,, normalizedCodes)); return parseQuoteData(rawData, normalizedCodes); } catch (Exception e) { return 行情查询失败 e.getMessage() 请稍后重试; } } private ListString normalizeCodes(String input) { String[] parts input.split([,\\s]); ListString result new ArrayList(); for (String part : parts) { if (part.isBlank()) continue; String code part.trim().toLowerCase(); if (code.startsWith(sh) || code.startsWith(sz) || code.startsWith(bj) || code.startsWith(hk) || code.startsWith(us)) { result.add(code); } else if (code.startsWith(6)) { result.add(sh code); } else if (code.startsWith(4) || code.startsWith(8) || code.startsWith(9)) { result.add(bj code); } else { result.add(sz code); } } return result; } private String parseQuoteData(String rawData, ListString codes) { StringBuilder result new StringBuilder(); for (String code : codes) { String varName v_ code.toLowerCase(); int startIndex rawData.indexOf(varName); if (startIndex 0) { result.append(code).append(未查到数据可能代码无效\n); continue; } int bodyStart rawData.indexOf(, startIndex) 1; int bodyEnd rawData.indexOf(, bodyStart); if (bodyEnd bodyStart) { result.append(code).append(数据格式异常\n); continue; } String[] fields rawData.substring(bodyStart, bodyEnd).split(~); if (fields.length 33) { result.append(code).append(字段不足接口可能已调整请检查解析逻辑\n); continue; } String name fields[1]; String currentPrice fields[3]; String prevClose fields[4]; String openPrice fields[5]; String volume fields[6]; String change fields[31]; String changePercent fields[32]; result.append(String.format( %s(%s) 现价:%s 涨跌:%s 涨跌幅:%s%% 今开:%s 昨收:%s 成交量:%s手, name, code, currentPrice, change, changePercent, openPrice, prevClose, volume)); result.append(\n); } return result.toString().trim(); } }这段代码就是完整可用的工具实现。你可能会注意到返回值我用了自然语言格式的中文描述而不是JSON。这是有意的设计工具返回尽量给模型结构化但人类可读的信息模型能更准确地理解并复述给用户。太复杂的JSON会让模型犯错纯文本又丢失了结构化信息折中做法是把关键字段用标点分隔并拼接成语义清晰的一句话。4.3 参数设计与防御性处理写工具方法时有个容易被忽略的点MCP工具方法面向的是AI模型不是人类用户。人类用户输错代码会自己发现AI模型输错代码则可能一路错到底所以参数校验和异常兜底必须做足。我做了三层防御输入为空时返回提示告诉模型应该怎么传参代码数量上限设为10防止模型一次传几十个代码把HTTP请求拖死请求异常时捕获并返回请稍后重试避免异常堆栈暴露给调用方这里有个细节值得注意我特意在空输入情况下返回了例如600000 或 sh600000,sz000001这样的示例。模型拿到这个返回值后大概率会在下一轮对话里提醒用户补充代码或者自己修正参数重新调用。实测下来这个提示词式的返回对模型纠错非常有帮助比返回一个冷冰冰的参数错误好用得多。5. 本地启动MCP Server与联调验证5.1 打包与启动的两种方式MCP Server的本地启动我推荐两种方式分别对应不同场景。第一种方式是Maven打包成Fat Jar再启动。这种方式适合已经写好代码、准备接入AI客户端的阶段。操作命令很简单mvn clean package -DskipTests java -jar target/stock-mcp-server-0.0.1-SNAPSHOT.jar注意用了spring-boot-maven-plugin打出来的jar才是可执行的胖jar如果你换了打包方式要确认插件配置正确。第二种方式是直接在IDE里启动Spring Boot应用。这种方式适合开发调试阶段方便打断点、观察日志。不过有一个坑必须提前说stdio模式下从IDE启动时占用的stdout通道可能和IDE的控制台日志输出镜像产生冲突最容易出现工具能加载但调用无响应的问题。如果遇到先把日志输出改成文件模式或者临时切到HTTP传输模式调试。我个人的经验是先HTTP模式跑通链路确认工具能正常调用再切回stdio模式接客户端这个顺序能省大量排错时间。5.2 使用MCP Inspector调试工具调试MCP Server最顺手的工具是官方的MCP Inspector。它本质上是一个网页端调试器可以连接你的MCP Server查看工具列表、调用工具、检查返回结果。安装和启动只需要一条命令npx modelcontextprotocol/inspectorlatest启动后终端会打印一个本地地址一般是http://localhost:6274打开它就能看到调试界面。在Transport Type里选STDIOCommand填Java的路径Args填-jar和目标jar的完整路径。如果用的是IDE启动还可以选HTTP模式填上服务地址http://localhost:8080/mcp连通后页面右侧会显示工具列表点你定义好的queryStock输入股票代码就能直接看到工具返回的行情数据。用Inspector的好处是它把MCP协议层的请求响应报文都展示出来了可以看到模型视角下的工具描述和参数schema长什么样。我之前有个工具描述写得含糊模型一直传错参数检查schema才发现问题。这一步特别适合验证工具暴露的信息是否足够清晰。5.3 配置到Cline等AI客户端本地启动验证通过后真正让工具发光的是把它接进实际的AI客户端。以Cline为例它会读取MCP服务器配置文件按里面的command和args把Server作为子进程拉起然后通过stdio通信。配置大致如下{ mcpServers: { stock: { command: java, args: [-jar, /绝对路径/stock-mcp-server-0.0.1-SNAPSHOT.jar] } } }配置文件的具体位置根据客户端版本有差异Cline通常是.cline/mcp_settings.jsonClaude Desktop是claude_desktop_config.json原理都一样。核心点只有一个里面填的路径必须是绝对路径不能用相对路径。配置好之后重启客户端在对话时问一句帮我查一下浦发银行的股价或者说查询600000和000001的实时行情模型会自动判断需要调用股票工具把参数传过去再把返回结果转成自然语言回答你。到这一步整个MCP Server的价值就体现出来了。还有一个细节AI客户端可能在拉起java进程时遇到command not found的问题尤其MacOS上通过GUI方式启动客户端时PATH环境变量可能不含Java路径。解决方式是配置里直接填java的绝对路径。macOS上可以先用/usr/libexec/java_home查一下JDK安装位置再把$JAVA_HOME/bin/java写进去。6. 常见问题与排坑实录6.1 日志污染了stdout导致客户端连不上这是stdio模式最经典的坑。Spring Boot启动时会打印大量banner和日志默认全部输出到标准输出。而stdio模式下标准输出就是MCP协议通道日志混在其中客户端解析协议直接失败症状是配置文件没问题但连接后立刻断开。排查方法在终端直接跑jar包看看终端里除了Spring Boot的日志外有没有打印出类似{jsonrpc:2.0,...}的协议数据。如果两种数据混在一起说明stdout被污染了。解决办法就是前面提到的配置logging.file.name让应用日志写到文件stdout只留给协议。这是MCP Server开发中非常容易踩但文档里很少强调的坑。6.2 中文乱码问题股票名称解析出来全是一堆乱码十有八九是编码处理错了。腾讯接口返回GBK编码你用UTF-8解码就会乱。我提供的代码里显式用了Charset.forName(GBK)这个细节不能省。还有一种情况是返回的字符串中间夹杂着\n被当成换行其实那是行情数据的特殊字符不是编码问题。如果看到一堆\n检查一下split正则是不是漏了。6.3 Tool方法没有被扫描工具加载不出来先检查三件事类上有没有Component注解、方法上有没有Tool注解、启动类是否在工具类的上层包路径。Spring Boot默认扫描启动类所在包及其子包如果工具类放在启动类同级的另一个包树下是扫描不到的。另外确认一下依赖里确实有spring-ai-starter-mcp-server。有些同学只引了spring-ai-alibaba-starter以为MCP能力默认包含实际上Alibaba starter并不强制依赖MCP server starter。6.4 HTTP模式下端口被占用HTTP模式调试时Spring Boot默认端口8080如果本机已有服务占用启动会报端口冲突。改成自定义端口很简单直接在配置里加server.port18080。然后客户端的地址也要同步改成http://localhost:18080/mcp。这不算大坑但确实很容易在明明上一步还能跑重启就不行了的场景里出现。6.5 批量查询的响应超时一次性查10只股票HTTP请求本身没问题但网络链路偶发慢速5秒超时设置可能兜不住。我实测大多数时候1秒内返回但个别情况下会拖到3秒以上。如果你的网络环境不太稳定把请求超时时间放宽到8秒。超时时间设置成多少没有标准答案核心原则是不能让AI模型等太久也不能因为网络抖动频繁失败。6.6 接口字段位置变化的应对前面提到的字段索引问题再提醒一次。免费接口没有固定文档字段位置偶尔会调整。如果某天发现返回值里涨跌幅不对、成交量对不上先别改解析逻辑而是去浏览器打开接口地址把原始返回的字段数组打出来数一遍再修改索引。别问我是怎么知道的我在这个坑上浪费过一整个下午。我个人在实际操作中的体会是做MCP工具型Server难点从来不在写代码而在于三个地方第一把工具的描述写得让模型一眼就懂第二处理好协议层面的细节比如stdout通道和编码第三做好防御性参数校验让模型乱传参数时也能优雅地返回提示。这个股票查询项目跑通之后你可以很轻松地扩展方向接入K线历史数据、做一个自选股列表工具、把行情快照存进本地数据库做统计分析甚至再写一个MCP Client让通义千问直接调用你这个Server整套链路就完美闭环了。先把这29期的项目吃透后面的路就好走多了。