:认识 MCP,编写第一个工具服务)
系列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.xmlprojectxmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion4.1.0/versionrelativePath//parentgroupIdcom.example/groupIdartifactIdwarehouse-mcp-server/artifactIdversion1.0.0/versionpropertiesjava.version17/java.versionspring-ai.version2.0.1/spring-ai.version/propertiesdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-mcp-server-webmvc/artifactId/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project这里没有模型 Starter也没有 DeepSeek Key。服务端的工作是提供工具至于谁来决定调用哪个工具那是客户端与模型协作的事情。配置application.ymlserver: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.javapackagecom.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.javapackagecom.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(nametable_search,description按业务关键词寻找订单示例表返回候选表名和说明)publicListTableBriefsearch(McpToolParam(description业务关键词例如订单或支付,requiredtrue)Stringkeyword){if(keywordnull||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\-HContent-Type: application/json\-HAccept: 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_VERSION2025-03-26再发初始化完成通知。通知没有id不应期待它返回工具数据curl-sS-XPOST http://127.0.0.1:8081/mcp\-HContent-Type: application/json\-HAccept: application/json, text/event-stream\-HMCP-Protocol-Version:$MCP_PROTOCOL_VERSION\-d{jsonrpc:2.0,method:notifications/initialized}接着列出工具curl-sS-XPOST http://127.0.0.1:8081/mcp\-HContent-Type: application/json\-HAccept: application/json, text/event-stream\-HMCP-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\-HContent-Type: application/json\-HAccept: application/json, text/event-stream\-HMCP-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 工具》