首页 / 资讯中心 / 文章详情

手写MCP Server,让大模型调用Java工具

手写MCP Server,让大模型调用Java工具 ★ FEATURED ARTICLE
你的 Java 服务里已经有一堆写好的方法查订单、查库存、发通知。现在要接大模型让 AI 助手能动手调这些方法而不是只会聊天。问题来了——OpenAI 的 Function Calling、Spring AI 的 Tool、LangChain4j 的工具定义格式各不相同你不想为每家客户端都重写一遍适配。MCPModel Context Protocol模型上下文协议就是来解决这件事的一套统一标准写一次Claude、Cursor、各种 Agent 都能直接调用。这篇文章用 Spring AI 2.0 手写一个 MCP Server把 Java 方法暴露成大模型能用的工具代码完整可跑。一、这个问题到底是什么大模型只会说不会做。你跟它聊得再欢它也没法真的去查你的数据库、调你的下单接口。要让它真正办事你得给它工具——也就是把你代码里的方法包一层告诉大模型有这么一个能力参数长这样你可以调用它。麻烦出在包一层这一步。不同框架对工具的定义方式完全不一样OpenAI 用的是 Function Calling要求你用 JSON Schema 描述每个函数的参数Spring AI 用Tool注解LangChain4j 用Tool注解但参数写法和 Spring AI 又不同各家 Agent 平台、各家 IDE 的 AI 插件各自又是一套。结果就是同一个查订单方法你想接 Claude Desktop 得写一遍适配接 Cursor 再写一遍接自家 Agent 平台还得再来一遍。三份几乎一样的胶水代码维护起来是噩梦。MCP 就是来解决这个碎片化的。它是 Anthropic 在 2024 年底提出的一套开放协议定义的是AI 应用客户端和提供能力的程序服务端“之间怎么通信。你可以把它理解成 USB-C 接口以前每个设备要配一种专用线现在一根 Type-C 线全通用。MCP 之于 AI 工具就是那个统一接口”。这篇文章要交付三样东西第一用 Spring AI 2.0 的McpTool注解把一个 Java 方法变成 MCP 工具第二解释背后 JSON Schema 自动生成的原理第三跑起来并用 MCP Inspector 验证。学完你就能把团队现有的 Java 服务低成本地升级成任何大模型都能调用的工具仓库。二、底层原理到底怎么回事先看 MCP 的整体架构一共三个角色Host宿主应用最终用户使用的那个软件比如 Claude Desktop、Cursor 编辑器、你自己的 App。它负责发起对话。Client客户端跑在 Host 内部的一层负责和 Server 建立连接、收发消息。普通开发者通常不用管它。Server服务端你写的、真正提供能力的那部分里面装着一堆工具、资源和提示词。一次工具调用的完整流程是用户在 Claude Desktop 里问查一下 ORD-001 这个订单Host 里的 Client 连上你的 ServerServer 告诉 Client我有query_order这个工具Client 再把工具清单和用户的问题一起交给大模型大模型决定调用query_order并给出参数最后这个调用又通过 Client 传回你的 Server 真正执行。MCP 有三种传输协议决定了 Server 以什么方式被连接协议连接方式典型场景STDIO标准输入输出进程内本机工具比如 Claude Desktop 本地插件Streamable HTTPHTTP独立进程远程部署微服务多客户端共享StatelessHTTP无会话状态云原生、无状态部署SSE 协议在 Spring AI 2.0 里已经废弃官方建议改用 Streamable HTTP。一个 Server 能暴露三种能力**Tools工具**是让大模型执行动作的比如查订单、取消订单**Resources资源**是给大模型提供上下文数据的比如一段配置、一份文档**Prompts提示词**是预置好的提示词模板供客户端复用。本文重点讲 Tools顺带演示 Resources。Spring AI 2.0 的关键设计是注解驱动。你给方法标上McpToolSpring 启动时会扫描所有ComponentBean 里的这些方法然后做两件事用反射读方法签名自动生成 JSON Schema——也就是这份工具的说明书里面写着工具叫啥、每个参数叫什么类型、哪些是必填。把方法包成一个回调注册进 MCP Server。为什么 JSON Schema 这么重要因为大模型调用工具靠的不是直接执行你的代码而是读说明书。它先看 Schema 知道query_order要一个orderId参数、参数是字符串、必填然后才吐出我要调 query_orderorderId 传 ORD-001。所以 Schema 写得好不好直接决定大模型能不能正确调用。Spring AI 帮你自动生成省掉了手写 JSON 这一步也避免了手写和代码不一致的坑。一个容易忽略的点McpTool方法的返回类型也会影响大模型。如果返回的是 String 或 int 这类简单类型直接转成文本回给模型就行如果返回的是自定义对象比如订单信息你得把generateOutputSchema设成true让 Spring 额外生成一份输出结构说明模型才知道返回值里有哪些字段。三、实战手把手写代码我们做一个订单服务 MCP Server暴露两个工具查订单、取消订单和一个资源查配置。环境要求 JDK 21用 Maven 构建。先看 POM。这里只显式声明一个 MCP Server starter它会把 Spring MVC、MCP SDK 这些依赖一起带进来?xml version1.0 encodingUTF-8?projectxmlnshttp://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.1/versionrelativePath//parentgroupIdcom.example/groupIdartifactIdmcp-order-server/artifactIdversion0.0.1-SNAPSHOT/versionnamemcp-order-server/namepropertiesjava.version21/java.versionspring-ai.version2.0.1/spring-ai.version/propertiesdependencies!-- 这个 starter 会传递引入 spring-boot-starter-web 和 MCP SDK --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-mcp-server-webmvc/artifactId/dependency/dependenciesdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion${spring-ai.version}/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project这段配置在干嘛spring-boot-starter-parent固定到 4.1.1这是当前最新的 GA 版本spring-ai-bom用 2.0.1 统一管理 Spring AI 各模块版本避免版本对不齐spring-ai-starter-mcp-server-webmvc是 WebMVC 传输的 MCP Server starter。版本号都查过 Maven Central是最新 GA。配置文件src/main/resources/application.ymlspring:application:name:mcp-order-serverai:mcp:server:name:order-serverversion:1.0.0protocol:STREAMABLEprotocol: STREAMABLE指定用 Streamable HTTP 传输这是官方推荐的远程方式SSE 已废弃。不写的话默认也是 STREAMABLE但显式写出来更清楚。HTTP 端点的默认路径是/mcp。主启动类packagecom.example.mcporderserver;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;SpringBootApplicationpublicclassMcpOrderServerApplication{publicstaticvoidmain(String[]args){SpringApplication.run(McpOrderServerApplication.class,args);}}这是标准的 Spring Boot 启动入口没有特殊配置。注解扫描默认开启会自动发现下面带McpTool的方法。订单信息的数据类用一个 Java recordpackagecom.example.mcporderserver;publicrecordOrderInfo(StringorderId,StringcustomerName,Stringstatus,doubleamount){}这个 record 就是工具的返回值类型。因为它是自定义对象后面在McpTool上要开generateOutputSchema让大模型知道返回结构里有哪些字段。核心的工具类packagecom.example.mcporderserver;importorg.springframework.ai.mcp.annotation.McpTool;importorg.springframework.ai.mcp.annotation.McpToolParam;importorg.springframework.stereotype.Component;ComponentpublicclassOrderTools{McpTool(namequery_order,description根据订单号查询订单信息返回订单状态和金额,generateOutputSchematrue)publicOrderInfoqueryOrder(McpToolParam(description订单号例如 ORD-20240904-001,requiredtrue)StringorderId){// 模拟查询数据库。实际项目里把这里换成 service 或 repository 的调用。returnnewOrderInfo(orderId,张三,已发货,299.00);}McpTool(namecancel_order,description取消一个未发货的订单返回操作结果)publicStringcancelOrder(McpToolParam(description要取消的订单号,requiredtrue)StringorderId){if(orderIdnull||orderId.isBlank()){thrownewIllegalArgumentException(订单号不能为空);}return订单 orderId 已成功取消;}}逐行讲一下McpTool标记这是个工具方法name是工具的唯一标识大模型调用时用这个名字description是给大模型看的说明——描述写得越清楚大模型越知道什么时候该调它。McpToolParam描述参数required true表示这个参数必填。queryOrder返回OrderInfo自定义对象所以generateOutputSchema true。cancelOrder里抛的IllegalArgumentException是 RuntimeExceptionSpring AI 会把它转成错误结果回传给大模型大模型看到后可以自行修正重试。再演示一个资源类把配置暴露给大模型当上下文packagecom.example.mcporderserver;importjava.util.Map;importorg.springframework.ai.mcp.annotation.McpResource;importorg.springframework.stereotype.Component;ComponentpublicclassConfigResource{privatefinalMapString,StringconfigMap.of(support_phone,400-888-1234,return_policy,7天无理由退货运费险由平台承担);McpResource(uriconfig://{key},nameAppConfig,title应用配置,description读取应用级配置项例如客服电话、退换货政策,mimeTypetext/plain)publicStringgetConfig(Stringkey){returnconfig.getOrDefault(key,未找到配置项key);}}McpResource的uri里用了{key}这个模板变量方法参数String key会和它自动对应。大模型可以按需读取这些配置而不必把它们塞进每次的系统提示词里。跑起来mvn spring-boot:run启动成功后服务监听 8080 端口MCP 端点在http://localhost:8080/mcp。验证有两种方式最直观的是用官方的 MCP Inspector一个可视化调试面板命令行执行npx modelcontextprotocol/inspector选择 Streamable HTTP 传输填入http://localhost:8080/mcp就能看到query_order、cancel_order两个工具和AppConfig资源还能直接在面板里手动传参调用如果你想用程序连把之前学过的 Spring AI MCP Client 指向这个地址即可。四、踩坑经验和最佳实践第一SSE 别再用统一上 STREAMABLE。Spring AI 2.0 里 SSE 协议已经标记废弃。如果你还在配置里写spring.ai.mcp.server.protocolSSE虽然能跑但会看到废弃警告。新项目直接用STREAMABLE远程部署用STATELESS。第二HTTP 传输默认没有鉴权别裸奔到公网。官方文档明确警告HTTP 型 MCP Server 默认暴露一个无认证的 JSON-RPC 端点任何能访问该端点的客户端都能列出并调用你的全部工具。对内网工具没问题一旦要暴露到公网必须前置一层安全边界比如网关鉴权或者干脆用 STDIO 走本机进程。第三返回自定义对象一定要开generateOutputSchema。如果你返回的是 record 或 POJO 却没开这个开关模型只能拿到一个模糊的字符串不知道返回值里有哪些字段后续处理容易出错。反过来返回 String、int 这类简单类型时不用开。第四描述写得好工具才被用得对。description和McpToolParam的description是给大模型看的使用说明不是给人看的注释。写清楚什么时候用这个工具、参数是什么格式、参数从哪来比写一堆 Java 注释有用得多。很多工具调用失败的根因不是代码而是描述太含糊。第五区分 RuntimeException 和受检异常。工具方法里抛 RuntimeException比如参数校验失败Spring AI 会把它转成错误结果回传给大模型让模型能自我纠正而抛受检异常比如 IOException会直接让这次工具调用失败、不让模型看到错误。想要模型能兜底重试就用 RuntimeException想要失败就硬停就声明受检异常。第六工具方法的参数类型别乱用。JSON Schema 是从 Java 类型自动推断的所以参数尽量用 String、int、boolean、record 这些能干净映射到 JSON 的类型。用太复杂的泛型或 Optional 可能导致 Schema 生成不理想必要时可以退回到手动接收CallToolRequest的方式自己解析。五、性能对比和技术选型先看三种传输协议怎么选这是决定部署形态的第一件事协议部署复杂度延迟适用场景STDIO低进程内最低本机工具、桌面 AI 插件Streamable HTTP中独立进程低远程服务、多客户端共享Stateless中无状态低云原生、弹性伸缩、无会话需求再看实现方式怎么选注解式McpTool适合绝大多数场景。参数类型固定、能自动生成 Schema、代码最简洁。本文就是这条路。手动CallToolRequest适合参数结构动态变化、需要运行时自定义 Schema 的场景灵活但要多写解析代码。直接手写 MCP SDK适合需要精细控制协议细节的极端场景学习成本和代码量都最高一般没必要。结论日常团队里把现有 Java 服务工具化无脑上 Spring AI 的McpTool注解式方案配 STREAMABLE 或 STDIO 传输性价比最高。别一上来就自己撸 SDK。六、总结MCP 解决的是一件事让写一次工具处处可调用成为现实而不是为每个 AI 客户端重写一遍胶水代码。Spring AI 2.0 把这件事的复杂度压到了最低——一个McpTool注解方法签名自动变 JSON SchemaComponent自动扫描注册几乎零配置。回顾本文我们用 Spring Boot 4.1.1 Spring AI 2.0.1 写了一个订单服务 MCP Server暴露了查订单、取消订单两个工具和一个配置资源代码完整可运行用 MCP Inspector 就能验证。记住三个关键点用 STREAMABLE 协议返回对象要开generateOutputSchemaHTTP 传输上线前必须加鉴权。下一步建议你把团队现有的一个真实 Service 方法包成 MCP 工具接进你常用的 AI 客户端里体验一下感受工具写一次、处处复用的爽感。
阅读完成 · 觉得有帮助?
咨询建站