系列:Spring AI 入门实战——从第一次对话到数仓查询助手
本篇目标:独立启动 MCP 服务,并通过 HTTP 列出、调用一个找表工具。
技术基线:Java 17、Spring Boot 4.1.0、Spring AI 2.0.1。
新工程:warehouse-mcp-server,端口8081。本篇不需要模型 API Key。
1. 把后厨搬出去,需要一张统一的点菜单
上一回,小林让模型成功调用了 Java 方法。小周看到了模拟订单金额,也看到了工具执行日志。
接着,小周带来了第二个需求:“我们另一个助手,也想用这套查数能力。”
如果每个助手各复制一份 Java 代码,后面改一次金额口径,就得提醒好几个项目一起改。
小林决定把工具独立成服务。客户端通过统一的方式发现工具、了解参数、发起调用;服务端专心维护数据能力。
这就是本篇引入 MCP 的原因:让工具能够通过标准协议被发现和调用。
MCP 的全称是 Model Context Protocol。它也支持资源、提示模板等能力,但我们这次只学习工具,先把一条路走通。
2. 三个角色,一张图就够了
| 角色 | 在本系列中是谁 | 负责什么 |
|---|---|---|
| Host | 第 6 篇的 Spring Boot 查数助手 | 接收用户问题、调用模型、组织整体流程 |
| Client | 助手进程里的 MCP 客户端组件 | 与某个 MCP 服务端建立协议连接,发现和调用工具 |
| Server | 本篇新建的工具服务 | 暴露工具,并执行找表、看字段和查数逻辑 |
用户 → 查数助手 Host → 模型 │ └─ MCP Client ──HTTP──> MCP Server ──> 数据源Host 和 Client 不一定是两个独立应用。本系列里,Client 就住在 Host 的 Spring Boot 进程中。Server 才是另一个独立启动的应用。
Tool Calling 与 MCP 也不是二选一:模型仍然通过工具调用表达“我要使用某项能力”;MCP 则承接应用与远程工具服务之间的交互。官方 MCP 概览
今天先不接模型。就像餐厅刚开张,先人工检查点菜单和出菜流程,再让智能点餐系统接进来。
3. 新建一个不带模型依赖的服务
创建第二个 Maven 工程,目录如下:
warehouse-mcp-server/ ├── pom.xml └── src/main/ ├── java/com/example/warehouse/ │ ├── WarehouseApplication.java │ └── WarehouseTools.java └── resources/application.yml完整pom.xml:
<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.0</version><relativePath/></parent><groupId>com.example</groupId><artifactId>warehouse-mcp-server</artifactId><version>1.0.0</version><properties><java.version>17</java.version><spring-ai.version>2.0.1</spring-ai.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webmvc</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>这里没有模型 Starter,也没有 DeepSeek Key。服务端的工作是提供工具;至于谁来决定调用哪个工具,那是客户端与模型协作的事情。
配置application.yml:
server:address:127.0.0.1port:8081spring:application:name:warehouse-mcp-serverai:mcp:server:name:warehouse-demoversion:1.0.0type:SYNCprotocol:STATELESSstateless:mcp-endpoint:/mcp本系列统一使用无状态 Streamable HTTP。你只需先记住:工具服务通过/mcp接收协议请求,不在请求之间保存 MCP 会话状态。业务数据是否持久化是另一回事,下一篇的数据库配置会单独说明。
type: SYNC表示使用同步工具处理方式,protocol: STATELESS表示服务端协议模式,两者不是同一个开关。无状态服务配置
示例只监听本机地址,供本地学习使用。这不是一份可直接对公网开放的生产配置。
启动类WarehouseApplication.java:
packagecom.example.warehouse;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** 独立的 MCP 工具服务,不持有模型 API Key。 */@SpringBootApplicationpublicclassWarehouseApplication{publicstaticvoidmain(String[]args){SpringApplication.run(WarehouseApplication.class,args);}}4. 第一件工具:帮我找找订单表
新增WarehouseTools.java:
packagecom.example.warehouse;importjava.util.List;importorg.springframework.ai.mcp.annotation.McpTool;importorg.springframework.ai.mcp.annotation.McpToolParam;importorg.springframework.stereotype.Component;/** 用一张表演示工具发现,暂不连接数据库。 */@ComponentpublicclassWarehouseTools{publicrecordTableBrief(Stringtable,Stringdescription){}@McpTool(name="table_search",description="按业务关键词寻找订单示例表,返回候选表名和说明")publicList<TableBrief>search(@McpToolParam(description="业务关键词,例如订单或支付",required=true)Stringkeyword){if(keyword==null||keyword.isBlank()||keyword.length()>50){thrownewIllegalArgumentException("关键词长度应为 1 到 50 个字符");}if(keyword.contains("订单")||keyword.contains("支付")||keyword.contains("金额")){returnList.of(newTableBrief("demo_orders","订单支付明细;支持按支付日期统计支付金额"));}returnList.of();}}我们仍然没有连接数据库,只是维护了一张表的目录信息。搜“订单”“支付”“金额”能找到demo_orders;搜“天气”返回空列表。
这足够解释找表工具的职责:返回候选数据资源,而不是立刻返回营业额。
这里使用官方@McpTool、@McpToolParam注解,Starter 会扫描 Spring Bean 并生成工具参数 Schema。官方注解说明
原业务项目使用table.search → table.describe → table.query这样的工具名。教学版保留同样的职责划分,实际名字使用table_search、table_describe、table_query,便于避免不同模型接口对工具名称字符范围的差异。后面所有代码和调用都使用下划线版本。
5. 手动探店:列出工具,再点一道菜
在服务端工程根目录启动:
mvn spring-boot:run现在发送一次初始化请求:
curl-sS-XPOST http://127.0.0.1:8081/mcp\-H'Content-Type: application/json'\-H'Accept: application/json, text/event-stream'\-d'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"manual-check","version":"1.0.0"}}}'响应中的result.protocolVersion是服务端协商后的协议版本。下面命令用环境变量保存它:请把响应里的值填进去;若返回的正是2025-03-26,可以直接照用。
exportMCP_PROTOCOL_VERSION=2025-03-26再发初始化完成通知。通知没有id,不应期待它返回工具数据:
curl-sS-XPOST http://127.0.0.1:8081/mcp\-H'Content-Type: application/json'\-H'Accept: application/json, text/event-stream'\-H"MCP-Protocol-Version:$MCP_PROTOCOL_VERSION"\-d'{"jsonrpc":"2.0","method":"notifications/initialized"}'接着列出工具:
curl-sS-XPOST http://127.0.0.1:8081/mcp\-H'Content-Type: application/json'\-H'Accept: application/json, text/event-stream'\-H"MCP-Protocol-Version:$MCP_PROTOCOL_VERSION"\-d'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'在结果里检查三个东西:工具名table_search、工具描述,以及inputSchema中的keyword参数。响应内容比较长是正常的,菜单上不仅有菜名,还有点菜规则。
最后调用工具:
curl-sS-XPOST http://127.0.0.1:8081/mcp\-H'Content-Type: application/json'\-H'Accept: application/json, text/event-stream'\-H"MCP-Protocol-Version:$MCP_PROTOCOL_VERSION"\-d'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"table_search","arguments":{"keyword":"订单"}}}'工具的业务内容应包含:
[{"table":"demo_orders","description":"订单支付明细;支持按支付日期统计支付金额"}]注意,上面展示的是业务内容,不是完整 MCP 响应。协议外层还有jsonrpc、id、result;工具数据可能包装在content的文本块中。若响应采用事件流形式,还会看到data:前缀。别因为多了一层包装,就以为 Java 方法返回错了。
我们没有创建/table_search这样的 REST 路由。MCP 请求统一发到/mcp,再通过 JSON-RPC 的method和工具name表达操作。
两种“发现”,别看串了
小周看到这里,提出一个很自然的问题:“tools/list和table_search,不都是在找东西吗?”
它们找的对象不同。tools/list找的是能力:这家服务提供搜索、描述还是查询?table_search找的是业务资源:哪张订单表可能包含支付金额?前者像看餐厅菜单,后者像问今天有哪些食材。
下一篇会增加另外两件工具。那时tools/list返回三个工具,table_search仍然只返回一张示例表;工具数量与表数量没有一一对应关系。
再看inputSchema。它描述参数的结构,例如参数是一个对象、包含字符串keyword、这个参数是否必填。客户端由此知道怎样构造调用。但“关键词不能超过 50 个字符”这类业务限制,仍应由工具实现检查,不能因为菜单列出了参数,就假设所有点单都是合法的。
理解这一区别之后,你调试时就知道该检查哪一层:没有发现工具,看注册与连接;发现了工具却找不到表,看搜索规则;参数被拒绝,看工具契约与 Java 校验。
6. 三个常见问题
“为什么浏览器打开/mcp看不到一个漂亮页面?”
它是协议入口,不是网页。请先用上面的 POST 请求测试工具列表和工具调用,不要拿浏览器页面是否好看来判断服务是否正常。
“为什么tools/list是空的?”
检查工具类是否标记@Component、是否位于启动类的扫描包内、是否使用了org.springframework.ai.mcp.annotation.McpTool,以及端口是否连到了当前应用。
“为什么返回 400 或 406?”
先检查请求 JSON、Content-Type、Accept和协议版本头。STATELESS、有状态 Streamable HTTP 与旧 SSE 的示例不能随意混搭;有状态模式还可能需要保存会话标识。本系列后续统一沿用当前配置。
7. 店开起来了,但货架上还没有真数据
小林给小周演示:不需要调用模型,也能列出工具、找到订单示例表。
小周问:“表找到了,里面有哪些字段?”
这个问题问到了下一篇的入口。我们将给服务接上一张本地订单表,补齐查看字段和查询数据两个工具,手动完成一次“找表—看字段—查金额”。
上一篇:《Spring AI 入门(三):Tool Calling,让大模型调用你的 Java 方法》
下一篇:《Spring AI 入门(五):从找表到查数,实现三个实用 MCP 工具》