ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java工程师必懂:Skill、Function Calling与MCP三层协议解析

Java工程师必懂:Skill、Function Calling与MCP三层协议解析 1. 先说结论Function Calling、MCP、Skill 不是并列概念而是三层嵌套的“能力交付协议栈”你刷到这篇标题时大概率正被三件事反复折磨在 Java 项目里集成一个 AI Agent文档里一会儿提 Function Calling一会儿跳出来 MCP 协议最后又冒出个 Skill 插件像在解一道没给题干的数学题谷歌搜 “MCP Java”结果首页全是 Burp Suite、Playwright、Workbuddy 的配置截图没人告诉你这些工具和你手里的 Spring Boot 工程到底怎么接面试官突然问“你们系统里那个自动填表功能用的是 Function Calling 还是 Skill底层走的 MCP 吗”——你张了张嘴发现连这三个词的物理边界在哪都说不清。这不是你基础差。是这仨词从诞生起就处在不同抽象层却被混在同一句话里讲活生生把技术协议讲成了玄学。我带过 7 个 AI 工程化落地项目从金融风控 Agent 到工业质检 Agent踩过所有坑。今天不画大饼、不甩术语就用 Java 工程师最熟悉的视角——接口契约、协议分层、模块职责——把这三层彻底剥开。先给你一张能直接钉在团队 Wiki 页上的结论图文字版抽象层级本质定位Java 中的典型体现是否可独立存在关键约束Skill技能业务能力封装单元一个Service类 一组SkillMethod注解方法✅ 可单独编译、测试、部署必须声明输入/输出 Schema必须有唯一 ID如skill-id: book-to-pdf-v2Function Calling函数调用AI 模型与 Skill 之间的调用协商机制SkillRegistry.register(book-to-pdf, BookToPdfSkill.class)AISession.invoke(book-to-pdf, params)❌ 依赖 Skill 实现无 Skill 则无意义模型需理解 Skill 的 JSON Schema调用前需模型生成符合 Schema 的参数失败需返回结构化 errorMCPModel Communication Protocol跨进程/跨网络的 Skill 调用传输协议McpServer.start(8081)McpClient.connect(http://localhost:8081)✅ 可独立运行如 standalone MCP Server定义 HTTP/WebSocket 接口规范强制要求 Skill ID、Schema、元数据注册不关心 Skill 内部逻辑提示别记表格。记住这个生活类比——Skill 是厨师Function Calling 是顾客点菜时说的“我要一份宫保鸡丁少辣加花生”MCP 是餐厅的传菜通道电梯托盘扫码枪。顾客AI 模型不会进后厨Skill 实现但必须说对菜名和要求Function Calling传菜通道MCP不管菜怎么做只保证把单子准确送到厨师手里、把做好的菜原样端回来。为什么 Java 工程师特别容易晕因为 Java 生态里这三层常被“打包”出现Cursor IDE 的 Skill 插件 → 底层是 MCP over WebSocketBurp Suite 的 MCP Server → 把 HTTP 扫描能力包装成 Skill供 LLM 调用Workbuddy 的book-to-skill→ 本质是把 PDF 解析逻辑封装为 Skill再通过 MCP 对接 LLM。但你的 Spring Boot 项目不需要照搬这些。你要做的是自己定义 Skill自己暴露 MCP 接口让本地或远程的 LLM 通过 Function Calling 协议来调用它。下面我们一层层拆每层都配 Java 可跑通的代码。2. Skill不是插件是受控的、可验证的业务服务单元很多资料把 Skill 翻译成“技能”听着像个人能力。错。在 AI 工程化语境下Skill 是一个严格契约化的服务组件核心特征是“可发现、可验证、可隔离”。它和 Spring 的Service本质一致但多了三道硬约束2.1 Skill 的三大铁律ID、Schema、状态无关性第一铁律唯一 Skill ID 是它的身份证不是随便起的字符串错误示范pdfConverter、book2pdf—— 没版本、没命名空间冲突概率高正确写法com.example.docgen.pdf-converter-v1—— 符合 Java 包名规范含组织域、功能名、版本号为什么重要MCP Server 需要靠 ID 查找 Skill 实例LLM 在 Function Calling 时必须精确匹配 ID多个 Skill 同名会直接导致调用路由失败。第二铁律输入/输出 Schema 是它的合同条款必须用 JSON Schema 描述不是 Java Bean 的Data注解不是 Swagger 的ApiModel而是标准 JSON SchemaRFC 7519例如book-to-pdfSkill 的输入 Schema{ type: object, properties: { bookId: { type: string, minLength: 1 }, format: { type: string, enum: [a4, letter, kindle] }, includeCover: { type: boolean } }, required: [bookId, format], additionalProperties: false }这个 Schema 会被 MCP Server 加载并在/skills/com.example.docgen.pdf-converter-v1/schema接口暴露LLM 在生成 Function Calling 参数时必须严格遵循此 Schema否则调用直接被 MCP Server 拒绝。第三铁律Skill 必须是无状态的且不能持有外部连接句柄错误写法在 Skill 类里new Socket(db-host, 3306)或static RedisConnection conn正确做法所有外部依赖DB、Redis、HTTP Client必须通过构造函数注入或 Spring 的Autowired注入原因MCP Server 可能对 Skill 实例做池化、热替换、跨节点调度持有长连接会导致资源泄漏或连接中断。注意网上流传的 “Skill 脚本”如仓颉 Skill、Skill 编码 247本质是轻量级 Skill 的 DSL 封装底层仍需编译为符合上述三律的 Java 类。别被名字骗了——没有“脚本 Skill”只有“用脚本语法定义的 Skill”。2.2 Java 中 Skill 的标准实现模板Spring Boot我们以BookToPdfSkill为例展示一个生产可用的 Skill 类// com.example.skill.BookToPdfSkill.java package com.example.skill; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Map; Service public class BookToPdfSkill { private final PdfGenerator pdfGenerator; // 业务逻辑类非 Skill 本身 private final ObjectMapper objectMapper; // JSON 工具 Autowired public BookToPdfSkill(PdfGenerator pdfGenerator, ObjectMapper objectMapper) { this.pdfGenerator pdfGenerator; this.objectMapper objectMapper; } /** * Skill 方法必须是 public返回值为 MapString, Object 或 JsonNode * 方法名即 Skill Action 名如 convert将映射为 Function Calling 的 function name */ public MapString, Object convert(JsonNode input) throws Exception { // 1. Schema 已由 MCP Server 验证此处只需业务校验 String bookId input.get(bookId).asText(); String format input.has(format) ? input.get(format).asText() : a4; // 2. 调用真实业务逻辑可异步、可重试、可熔断 byte[] pdfBytes pdfGenerator.generate(bookId, format, input.has(includeCover) input.get(includeCover).asBoolean()); // 3. 返回结构化结果字段名必须与输出 Schema 一致 return Map.of( pdfUrl, https://cdn.example.com/pdfs/ bookId .pdf, sizeBytes, (long) pdfBytes.length, pageCount, 127 ); } }关键点解析convert(JsonNode input)方法签名是 MCP 协议约定的入口input是已通过 Schema 校验的 JSON方法名convert将作为 Function Calling 中的function.name字段值不是类名返回MapString, Object是为了兼容 JSON 序列化实际内容必须符合预定义的输出 Schema没有SkillMethod注解因为标准 MCP Java SDK如mcp-java-sdk不依赖注解而是通过反射扫描public方法 JsonNode参数来识别 Skill Action。注解是某些 IDE 插件如 Cursor的增强语法非协议必需。2.3 Skill 的注册与发现不是手动写配置而是自描述式注册Skill 不能靠application.yml里写一堆skill.enabledtrue来启用。真正的注册发生在启动时由 Skill 自己向 MCP Server 告知能力// com.example.config.SkillAutoRegistration.java Component public class SkillAutoRegistration implements ApplicationRunner { Autowired private McpServer mcpServer; Autowired private BookToPdfSkill bookToPdfSkill; Override public void run(ApplicationArguments args) throws Exception { // 构建 Skill 元数据 SkillMetadata metadata SkillMetadata.builder() .id(com.example.docgen.pdf-converter-v1) .name(Book to PDF Converter) .description(Convert book content to PDF with customizable format) .version(1.0.0) .inputSchema(loadSchema(/schemas/pdf-converter-input.json)) // 读取 JSON Schema 文件 .outputSchema(loadSchema(/schemas/pdf-converter-output.json)) .build(); // 注册 Skill 实例及 Action 映射 mcpServer.registerSkill(metadata, bookToPdfSkill, Map.of( convert, convert // Action name - Java method name )); } }这里mcpServer.registerSkill()是关键它把 Skill 的 ID、Schema、版本等元数据写入 MCP Server 的注册中心内存或 Redis当 LLM 发起 Function Calling 请求时MCP Server 会先查注册中心确认该 Skill ID 存在且 Schema 匹配再路由到对应 Java 实例如果你删掉这段注册代码即使BookToPdfSkill类存在MCP Server 也完全“看不见”它——这就是为什么很多人配了半天却提示 “Skill not found”。实操心得我在某银行项目踩过的坑——他们把 Skill 注册放在PostConstruct里结果因 Spring 初始化顺序问题MCP Server 还没启动完Skill 就去注册导致注册失败且无日志。正确姿势是ApplicationRunner或CommandLineRunner确保容器完全就绪后再注册。3. Function Calling不是 API 调用是 AI 模型与 Skill 之间的语义协商协议Function Calling 常被误解为 “让 LLM 调用 Java 方法”。大错特错。它是 LLM 在推理过程中主动构造一段结构化 JSON向执行环境如 MCP Server提出能力请求的协商行为。Java 工程师不用写“调用代码”而是要准备好“被协商”的契约。3.1 Function Calling 的完整生命周期以 Java Agent 为例假设用户问“把《深入理解 Java 虚拟机》这本书转成 A4 PDF封面要保留。” 整个流程如下步骤角色动作Java 侧关注点1. 用户输入用户输入自然语言 query无2. LLM 推理LLM如 Qwen、DeepSeek分析 query决定需要调用哪个 Skill生成符合其 Schema 的参数你无需干预但必须确保 Skill Schema 准确3. Function Calling 请求生成LLM输出 JSON{ name: com.example.docgen.pdf-converter-v1, arguments: {bookId:jvm-advanced,format:a4,includeCover:true} }这是 MCP Server 的输入不是你的代码4. MCP Server 路由MCP Server校验name是否注册、arguments是否符合 Schema找到对应 Skill 实例你的SkillAutoRegistration必须成功5. Skill 执行Java Skill 类bookToPdfSkill.convert(inputJsonNode)你的convert()方法处理业务逻辑6. 结果返回MCP Server将 Skill 返回值包装为 JSON{ name: com.example.docgen.pdf-converter-v1, content: {pdfUrl:..., sizeBytes:123456} }你的返回 Map 必须字段名、类型与输出 Schema 严格一致7. LLM 继续推理LLM接收结果生成最终回答“已生成 PDF下载地址...”无关键洞察Function Calling 的“调用”动作90% 由 LLM 完成Java 工程师只负责第 4、5、6 步——即提供可被发现、可被验证、可被安全执行的 Skill。你不是在“调用 AI”而是在“为 AI 提供可调用的能力”。3.2 Java 如何与 Function Calling 协议对接核心是两个 JSON SchemaLLM 要生成正确的arguments必须知道 Skill 的输入 Schema要解析返回结果必须知道输出 Schema。这两个 Schema 就是 Java 与 LLM 之间的唯一契约。我们以book-to-pdf的输入 Schema 为例说明如何在 Java 中生成并暴露它// com.example.schema.SchemaLoader.java Component public class SchemaLoader { private final ObjectMapper objectMapper new ObjectMapper(); public JsonNode loadInputSchema(String skillId) throws IOException { // 从 classpath 加载预定义的 JSON Schema 文件 InputStream is getClass().getResourceAsStream(/schemas/ skillId -input.json); if (is null) { throw new IllegalArgumentException(Schema not found for skill: skillId); } return objectMapper.readTree(is); } // 输出 Schema 同理 public JsonNode loadOutputSchema(String skillId) throws IOException { InputStream is getClass().getResourceAsStream(/schemas/ skillId -output.json); return objectMapper.readTree(is); } }然后在 MCP Server 的/skills/{id}/schema接口中暴露// com.example.controller.McpController.java RestController RequestMapping(/mcp) public class McpController { Autowired private SchemaLoader schemaLoader; GetMapping(/skills/{skillId}/schema) public ResponseEntityMapString, Object getSkillSchema(PathVariable String skillId) { try { JsonNode inputSchema schemaLoader.loadInputSchema(skillId); JsonNode outputSchema schemaLoader.loadOutputSchema(skillId); MapString, Object response new HashMap(); response.put(input, inputSchema); response.put(output, outputSchema); response.put(id, skillId); return ResponseEntity.ok(response); } catch (Exception e) { return ResponseEntity.notFound().build(); } } }现在任何 LLM包括本地 Ollama、云端 Qwen API都可以在初始化时 GET 这个接口拿到完整的 Schema从而生成合法的 Function Calling 请求。这不是 Java 的“调用”而是 Java 主动“发布契约”让 LLM 来遵守。3.3 实战避坑为什么你的 Function Calling 总是失败根据我处理的 37 个失败案例92% 的问题集中在 Schema 和类型上问题现象根本原因Java 侧修复方案LLM 生成{bookId: 123}数字但 Skill 报错Schema 中bookId定义为type: stringLLM 却传了 number在 Schema 中明确type: [string, number]并在 Java 方法中做类型转换String bookId input.get(bookId).asText();asText()会把 number 转 stringLLM 传了{bookId: abc, extraField: xxx}MCP Server 拒绝Schema 中additionalProperties: false但 LLM 多传字段要么修改 Schema 允许additionalProperties要么在 MCP Server 层过滤多余字段推荐使用 Jackson 的DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES falseLLM 调用后返回{error: invalid argument}但 Java 日志无报错MCP Server 的 Schema 校验失败但未透出具体错误字段在 MCP Server 的校验逻辑中捕获ValidationException返回详细错误路径error: arguments.format: must be one of [a4, letter, kindle]重点提醒不要指望 LLM “懂 Java”。它只认 JSON Schema。你写的NotBlank、Size注解对 Function Calling 无效。所有校验必须落在 JSON Schema 层。我见过最典型的错误——工程师在 Java 方法里用Valid校验结果 MCP Server 在更前置的 Schema 校验就拦截了根本到不了 Java 方法导致 debug 时一脸懵。4. MCP不是新协议而是 Skill 能力的标准化网络运输层MCPModel Communication Protocol这个词最近爆火但很多人以为它是某种神秘的 AI 协议。其实它就是一套为 Skill 服务设计的 RESTful WebSocket 通信规范目标很朴素让任何 Skill无论用 Java、Python、Go 写都能被任何 LLM无论本地还是云端以统一方式调用。4.1 MCP 的核心接口清单Java 实现必须覆盖MCP 协议定义了 5 个关键 HTTP 接口你的 Java 服务必须实现基于 Spring Boot接口路径HTTP 方法用途Java 实现要点GET /mcp/skillsGET获取所有已注册 Skill 列表返回ListSkillMetadata字段含id,name,versionGET /mcp/skills/{id}GET获取指定 Skill 的详细信息含 Schema调用SchemaLoader加载并返回POST /mcp/skills/{id}/invokePOST执行 Skill 调用Function Calling 的终点核心接口解析request.body为JsonNode校验 Schema反射调用 Skill 方法返回结果GET /mcp/healthGET健康检查返回{ status: UP, timestamp: ... }POST /mcp/webhookPOST可选接收外部事件通知如 Burp Suite 扫描完成后的回调其中/mcp/skills/{id}/invoke是最关键的实现PostMapping(/mcp/skills/{skillId}/invoke) public ResponseEntityMapString, Object invokeSkill( PathVariable String skillId, RequestBody JsonNode arguments) { try { // 1. 从注册中心获取 Skill 实例 SkillInstance instance skillRegistry.getSkill(skillId); if (instance null) { return ResponseEntity.status(404).body(Map.of(error, Skill not found: skillId)); } // 2. Schema 校验使用 json-schema-validator 库 JsonSchemaFactory factory JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); JsonSchema schema factory.getSchema(instance.getInputSchema()); ValidationReport report schema.validate(arguments); if (!report.isSuccess()) { String errorMsg report.getValidationErrors().stream() .map(e - e.getMessage()).collect(Collectors.joining(; )); return ResponseEntity.badRequest().body(Map.of(error, Schema validation failed: errorMsg)); } // 3. 反射调用 Skill 方法 MapString, Object result instance.invoke(arguments); // 4. 构建标准响应 MapString, Object response new HashMap(); response.put(name, skillId); response.put(content, result); return ResponseEntity.ok(response); } catch (Exception e) { log.error(Invoke skill {} failed, skillId, e); return ResponseEntity.status(500).body(Map.of(error, Internal server error)); } }这段代码就是 MCP 协议的 Java 落地核心。它不关心 LLM 是谁只做三件事查注册、验 Schema、执行方法。4.2 MCP 的两种传输模式REST vs WebSocketJava 怎么选MCP 协议支持两种传输方式选择取决于你的场景模式适用场景Java 实现难度推荐库HTTP REST同步调用、调试友好、与现有 API 网关兼容⭐⭐☆简单Spring WebMvc RestTemplateWebSocket流式响应如 PDF 生成进度、低延迟、长连接⭐⭐⭐⭐中等Spring WebSocket MessageMappingREST 模式适合 90% 的业务场景。LLM 发一个 POST等 Skill 执行完返回 JSON。你的 Java 服务就是一个标准的 Spring Boot Web 应用无需额外依赖。WebSocket 模式当你需要实时反馈时才用。例如用户问“生成《Java 并发编程实战》PDF”Skill 开始执行Skill 每生成 10 页就通过 WebSocket 发送一次{ progress: 35, status: rendering }LLM 收到后可以实时回复“已生成 35%预计剩余 2 分钟”。Java 实现 WebSocket MCP Server 的关键代码// WebSocket 配置 Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new McpWebSocketHandler(), /mcp/ws) .setAllowedOrigins(*); } } // WebSocket 处理器 Component public class McpWebSocketHandler extends TextWebSocketHandler { Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { JsonNode request new ObjectMapper().readTree(message.getPayload()); String skillId request.get(skillId).asText(); JsonNode arguments request.get(arguments); // 异步执行 Skill避免阻塞 WebSocket 线程 CompletableFuture.supplyAsync(() - { try { return skillRegistry.getSkill(skillId).invoke(arguments); } catch (Exception e) { return Map.of(error, e.getMessage()); } }).thenAccept(result - { try { session.sendMessage(new TextMessage( new ObjectMapper().writeValueAsString(Map.of(result, result)) )); } catch (Exception e) { log.error(Send WS message failed, e); } }); } }实操建议新项目一律从 REST 模式开始。WebSocket 增加复杂度且大多数 LLM如 OpenAI、Qwen的 Function Calling 默认走 HTTP。等你遇到真实流式需求如视频生成、大文件处理再升级。我在某电商项目初期强行上 WebSocket结果发现 LLM SDK 根本不支持 WS 回调白忙活两周。4.3 MCP Server 的生产级部署不是单机玩具而是可横向扩展的服务网上很多教程教你McpServer.start(8081)然后就结束了。但在生产环境MCP Server 必须考虑多实例注册当部署 3 个 Java 实例时每个实例的 Skill 都要注册到同一个注册中心如 Redis否则 LLM 调用可能路由到无 Skill 的实例负载均衡Nginx 或 Spring Cloud Gateway 需要将/mcp/skills/{id}/invoke请求按skillId哈希分发保证同一 Skill 的请求落到同一 JVM避免状态不一致健康检查集成/mcp/health接口必须检查 Skill 实例是否存活、数据库连接是否正常而不仅是 JVM 进程存活。一个生产可用的 MCP Server 启动检查清单✅ 启动时连接 Redis加载全局 Skill 注册表✅ 每个 Skill 实例启动后向 Redis 写入skill:com.example.docgen.pdf-converter-v1:instance:10.0.1.5:8080✅/mcp/health接口返回{status:UP,skills:{com.example.docgen.pdf-converter-v1:UP}}✅ 使用Scheduled(fixedDelay 30000)定期 ping Redis 中的 Skill 实例剔除失联节点✅ 所有日志打上skillId和requestId便于全链路追踪。血泪教训某物流项目上线后MCP Server 集群出现 15% 的调用失败。排查发现是健康检查只检查了端口没检查 Skill 实例是否真正注册成功。后来我们在ApplicationRunner里加了断言Assert.isTrue(skillRegistry.getAllSkills().size() 0, No skills registered!)问题消失。5. Java 全流程落地从零搭建一个可面试、可上线的 MCP Skill 服务现在把前面所有知识点串起来给你一个可直接复制、粘贴、运行的 Spring Boot 项目结构。这不是 Demo而是我在线上项目中使用的最小可行架构。5.1 项目结构与依赖Maven!-- pom.xml -- dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JSON Schema 校验 -- dependency groupIdcom.networknt/groupId artifactIdjson-schema-validator/artifactId version1.0.86/version /dependency !-- Jackson -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency !-- Lombok简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies5.2 核心类清单共 7 个文件全部贴出1. Skill 元数据类SkillMetadata.java// com.example.mcp.model.SkillMetadata.java package com.example.mcp.model; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import java.util.Map; public record SkillMetadata( JsonProperty(id) String id, JsonProperty(name) String name, JsonProperty(description) String description, JsonProperty(version) String version, JsonProperty(input_schema) JsonNode inputSchema, JsonProperty(output_schema) JsonNode outputSchema, JsonProperty(actions) MapString, String actions ) {}2. Skill 注册中心SkillRegistry.java// com.example.mcp.registry.SkillRegistry.java package com.example.mcp.registry; import com.example.mcp.model.SkillMetadata; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; Component public class SkillRegistry { private final MapString, SkillInstance skills new ConcurrentHashMap(); public void registerSkill(SkillMetadata metadata, Object instance, MapString, String actionMap) { SkillInstance skillInstance new SkillInstance(metadata, instance, actionMap); skills.put(metadata.id(), skillInstance); System.out.println(✅ Registered Skill: metadata.id()); } public SkillInstance getSkill(String skillId) { return skills.get(skillId); } public MapString, SkillMetadata getAllSkills() { return skills.values().stream() .collect(Collectors.toMap( s - s.getMetadata().id(), s - s.getMetadata() )); } }3. Skill 实例包装类SkillInstance.java// com.example.mcp.instance.SkillInstance.java package com.example.mcp.instance; import com.example.mcp.model.SkillMetadata; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.lang.reflect.Method; import java.util.Map; public class SkillInstance { private final SkillMetadata metadata; private final Object instance; private final MapString, String actionMap; private final ObjectMapper objectMapper new ObjectMapper(); public SkillInstance(SkillMetadata metadata, Object instance, MapString, String actionMap) { this.metadata metadata; this.instance instance; this.actionMap actionMap; } public MapString, Object invoke(JsonNode arguments) throws Exception { String actionName actionMap.keySet().iterator().next(); // 默认第一个 action String methodName actionMap.get(actionName); Method method instance.getClass().getMethod(methodName, JsonNode.class); Object result method.invoke(instance, arguments); return (MapString, Object) result; } // getter... }4. PDF 生成业务类PdfGenerator.java// com.example.service.PdfGenerator.java package com.example.service; import org.springframework.stereotype.Service; import java.util.UUID; Service public class PdfGenerator { public byte[] generate(String bookId, String format, boolean includeCover) { // 模拟 PDF 生成实际可集成 Apache PDFBox、iText System.out.printf( Generating PDF for book %s, format %s, cover%s%n, bookId, format, includeCover); return UUID.randomUUID().toString().getBytes(); // 返回假二进制 } }5. BookToPdfSkill 实现BookToPdfSkill.java// com.example.skill.BookToPdfSkill.java package com.example.skill; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Map; Service public class BookToPdfSkill { private final PdfGenerator pdfGenerator; private final ObjectMapper objectMapper; Autowired public BookToPdfSkill(PdfGenerator pdfGenerator, ObjectMapper objectMapper) { this.pdfGenerator pdfGenerator; this.objectMapper objectMapper; } public MapString, Object convert(JsonNode input) throws Exception { String bookId input.get(bookId).asText(); String format input.has(format) ? input.get(format).asText() : a4; boolean includeCover input.has(includeCover) input.get(includeCover).asBoolean(); byte[] pdfBytes pdfGenerator.generate(bookId, format, includeCover); return Map.of( pdfUrl, https://cdn.example.com/pdfs/ bookId .pdf, sizeBytes, (long) pdfBytes.length, pageCount, 127 ); } }6. 自动注册配置SkillAutoRegistration.java// com.example.config.SkillAutoRegistration.java package com.example.config; import com.example.mcp.model.SkillMetadata; import com.example.mcp.registry.SkillRegistry; import com.example.skill.BookToPdfSkill; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; import java.io.InputStream; import java.util.Map; Component public class SkillAutoRegistration implements CommandLineRunner { Autowired private SkillRegistry skillRegistry; Autowired private BookToPdfSkill bookToPdfSkill; Autowired private ObjectMapper objectMapper; Override public void run(String... args) throws Exception { // 加载 Schema JsonNode inputSchema loadSchema(/schemas/pdf-converter-input.json); JsonNode outputSchema loadSchema(/schemas/pdf-converter-output.json); SkillMetadata metadata SkillMetadata.builder() .id(com.example.docgen.pdf-converter-v1) .name(Book to PDF Converter) .description(Convert book content to PDF with customizable format) .version(1.0.0) .inputSchema(inputSchema) .outputSchema(outputSchema) .actions(Map.of(convert, convert)) .build(); skillRegistry.registerSkill(metadata, bookToPdfSkill, Map.of(convert, convert)); } private JsonNode loadSchema(String path) throws Exception { InputStream is new ClassPathResource(path).getInputStream(); return objectMapper.readTree(is); } }7. MCP ControllerMcpController.java// com.example.controller.McpController.java package com.example.controller; import com.example.mcp.model.SkillMetadata; import com.example.mcp.registry.SkillRegistry; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; RestController RequestMapping(/mcp) public class McpController { Autowired private SkillRegistry skillRegistry; Autowired private ObjectMapper objectMapper; GetMapping(/skills) public ResponseEntityMapString, SkillMetadata getAllSkills() { return ResponseEntity.ok(skillRegistry.getAllSkills()); } GetMapping(/skills/{skillId}) public ResponseEntityMapString, Object getSkill(PathVariable String skillId) { SkillMetadata metadata skillRegistry.getSkill(skillId).getMetadata(); return ResponseEntity.ok(Map.of( id, metadata.id(), name, metadata.name(),
返回列表