当前位置: 首页 > article >正文

spring-ai 集成 mcp 之投机取巧

主旨

        这篇文章主旨就一点,罗列spring-ai对mcp集成导致出现的一系列问题

分析

        由于mcp未问世之前,就早就已经有了工具调用,源码如下:

public interface ToolCallback {/*** Definition used by the AI model to determine when and how to call the tool.*/ToolDefinition getToolDefinition();/*** Metadata providing additional information on how to handle the tool.*/default ToolMetadata getToolMetadata() {return ToolMetadata.builder().build();}/*** Execute tool with the given input and return the result to send back to the AI* model.*/String call(String toolInput);/*** Execute tool with the given input and context, and return the result to send back* to the AI model.*/default String call(String toolInput, @Nullable ToolContext tooContext) {if (tooContext != null && !tooContext.getContext().isEmpty()) {throw new UnsupportedOperationException("Tool context is not supported!");}return call(toolInput);}}

        该实体接口就是在spring-ai环境下,工具操作就是靠他实现,ToolDefinition是最主要的工具定义,ToolCallback进行操作封装等扩展,这显得很完美,但这只是存在于spring-ai环境下很完美。  

        随着MCP爆火,spring作为一个啥都要集成进去来巩固地位的集大成框架,MCP的集成也自然落到了spirng-ai头上,先不说问题,先讲一下它的实现思路吧,以及对mcp官网提供包的集成。

        先讲一下mcp原生sdk,它提供了和spring-ai 相似的体系,也是有一个和ToolDefinition相似的类叫做McpSchema,这是一个规范是基于json schema的POJO化,也拥有除了tool规范定义的,其他东西的定义(相当于协议层

        mcp的sdk中也有一个McpServerFeatures类,提供了SyncToolSpecification(同步版),AsyncResourceSpecification(异步版)等等这种具体实现,类似于spring-ai下的ToolCallback。(相当于实现层
 

列一个式子

ToolDefinition ≈ McpSchema.Tool

ToolCallback ≈ McpServerFeatures.SyncToolSpecification(这个有同步和异步两个版本)

public record SyncToolSpecification(McpSchema.Tool tool,BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> call) {
}

        上面就是SyncToolSpecification(下文都以同步版为例,不再提及,逻辑相同),由于mcp官方提供的SDK是raw版本,不具备任何开箱即用功能,基本就是仅仅规范了协议。所以这个call函数(工具调用函数)都需要应用框架来自行实现。下面来看看spring-ai是如何实现的:

public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider,McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,ObjectProvider<List<SyncToolSpecification>> tools,ObjectProvider<List<SyncResourceSpecification>> resources,ObjectProvider<List<SyncPromptSpecification>> prompts,ObjectProvider<List<SyncCompletionSpecification>> completions,ObjectProvider<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumers,List<ToolCallbackProvider> toolCallbackProvider) {McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),serverProperties.getVersion());// Create the server with both tool and resource capabilitiesSyncSpecification serverBuilder = McpServer.sync(transportProvider).serverInfo(serverInfo);List<SyncToolSpecification> toolSpecifications = new ArrayList<>(tools.stream().flatMap(List::stream).toList());List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream().map(pr -> List.of(pr.getToolCallbacks())).flatMap(List::stream).filter(fc -> fc instanceof ToolCallback).map(fc -> (ToolCallback) fc).toList();toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties));if (!CollectionUtils.isEmpty(toolSpecifications)) {serverBuilder.tools(toolSpecifications);capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());logger.info("Registered tools: " + toolSpecifications.size() + ", notification: "+ serverProperties.isToolChangeNotification());}List<SyncResourceSpecification> resourceSpecifications = resources.stream().flatMap(List::stream).toList();if (!CollectionUtils.isEmpty(resourceSpecifications)) {serverBuilder.resources(resourceSpecifications);capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification());logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: "+ serverProperties.isResourceChangeNotification());}List<SyncPromptSpecification> promptSpecifications = prompts.stream().flatMap(List::stream).toList();if (!CollectionUtils.isEmpty(promptSpecifications)) {serverBuilder.prompts(promptSpecifications);capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification());logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: "+ serverProperties.isPromptChangeNotification());}List<SyncCompletionSpecification> completionSpecifications = completions.stream().flatMap(List::stream).toList();if (!CollectionUtils.isEmpty(completionSpecifications)) {serverBuilder.completions(completionSpecifications);capabilitiesBuilder.completions();logger.info("Registered completions: " + completionSpecifications.size());}rootsChangeConsumers.ifAvailable(consumer -> {serverBuilder.rootsChangeHandler((exchange, roots) -> consumer.accept(exchange, roots));logger.info("Registered roots change consumer");});serverBuilder.capabilities(capabilitiesBuilder.build());serverBuilder.instructions(serverProperties.getInstructions());return serverBuilder.build();}

        简述一下把,只考虑Tool这一最核心部分,SyncToolSpecification是MCP的SDK里定义的,这一API过于低级,因为不可能让Call函数都让用户去写,所以ToolCallbackProvider这一在spring-ai原始环境就存在的定义 便可以很轻易地完成转换(convert),ToolCallbackProvider作为一个接口(作用是提供ToolCallback,举个具体的例子),比如其实现类 MethodToolCallbackProvider 可以根据传入的 类 来解析其方法 加上一些条件(是否有@Tool注解,有的话就根据注解里的name,description注解来构造ToolDefinition,从而构造ToolCallback)。

        这里看起来好像就只需要在ToolCallback 和 SyncToolSpecification 之间 做一个转换函数,好像就可以了,你我这样想,spring-ai也这样想,于是有了下面函数(核心实现):

public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(ToolCallback toolCallback,MimeType mimeType) {var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());return new McpServerFeatures.SyncToolSpecification(tool, (exchange, request) -> {try {String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request),new ToolContext(Map.of(TOOL_CONTEXT_MCP_EXCHANGE_KEY, exchange)));if (mimeType != null && mimeType.toString().startsWith("image")) {return new McpSchema.CallToolResult(List.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, callResult, mimeType.toString())),false);}return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);}catch (Exception e) {return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(e.getMessage())), true);}});}

        这个代码先不去管,到现在万事大吉,咋们来走一下MCP的tool/call流程,如下图所示:

         如果只看这张图好像还算清晰明了,好像也能顺利执行,但spring-ai对MCP的支持也就仅限于我讲的这些了,下面我会标出他的问题。

问题

兼容性

        这是一个非常非常严重的问题,在ToolCallback进行Call(工具调用)的时候,会将方法的返回值经过一个 默认转化器(可以在@Tool注解里指定)进行格式转换,代码如下:

@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tool {/*** The name of the tool. If not provided, the method name will be used.*/String name() default "";/*** The description of the tool. If not provided, the method name will be used.*/String description() default "";/*** Whether the tool result should be returned directly or passed back to the model.*/boolean returnDirect() default false;/*** The class to use to convert the tool call result to a String.*/Class<? extends ToolCallResultConverter> resultConverter() default DefaultToolCallResultConverter.class;}public final class DefaultToolCallResultConverter implements ToolCallResultConverter {private static final Logger logger = LoggerFactory.getLogger(DefaultToolCallResultConverter.class);@Overridepublic String convert(@Nullable Object result, @Nullable Type returnType) {if (returnType == Void.TYPE) {logger.debug("The tool has no return type. Converting to conventional response.");return JsonParser.toJson("Done");}if (result instanceof RenderedImage) {final var buf = new ByteArrayOutputStream(1024 * 4);try {ImageIO.write((RenderedImage) result, "PNG", buf);}catch (IOException e) {return "Failed to convert tool result to a base64 image: " + e.getMessage();}final var imgB64 = Base64.getEncoder().encodeToString(buf.toByteArray());return JsonParser.toJson(Map.of("mimeType", "image/png", "data", imgB64));}else {logger.debug("Converting tool result to JSON.");return JsonParser.toJson(result);}}}

        明显,toSyncToolSpecification 函数 和 covert函数起了冲突,进行了一个重复结果封装效果,逻辑冲突,mcp的返回协议规范和spring-ai返回应用规范嵌套冲突,从mcp inspector 工具里可以查看到,如下图所示:

        协议规范重复封装,导致可读性极差,极低扩展性,还不如设一个接口,可以共用户自行选择实现协议。

        实际上导致这一切根本上讲 就是spring-ai的投机取巧,把以前Tool的所有东西都直接转到Mcp的Tool里,甚至连一个MCPTool注解都没写, 只有一个转换函数,连适配器都没有,这相当大弊端了,代码生硬不优雅。

扩展性

        相比各位读者,如果仔细读了上述代码,也就能看出来上述代码其实相当普通,扩展性极为有限,这一切根源肯定是因为MCP极为宽泛,和HTTP这种极度落地的协议还是有着相当差距的,但MCP官网又推出了对应SDK,由官方推出的SDK自然而然就成为了钦点唯一 MCP SDK,但遇到更加具体,协议中未曾定义的问题就显得非常麻烦,就比如多模态数据返回。MCP SDK,定义了三种返回类型,text,image,以及一个resource(Mcp里的资源模块)

        最佳实践,重写spiring-ai的 工具结果转化器,设立规范,按照mcpSdk定义的规范,text,Image和Resource来进行结果转化。
        其实本可以很便捷,MCP工具可以很容易的实现新版本协议的annotation扩展以及返回mineType的扩展,只是因为spring-ai固执的使用原有Tool注解,使得功能难以更新,架构烂尾。

总结

        尽管如此spring-ai框架是一款出色的应用性框架,其架构理念颠覆以往所有类型框架,ai技术日新月异,扩展性基于前瞻性,实际上我理解扩展性极为把 Dog,抽取出一个接口Animals,然后提取公共属性,好的扩展性,需要架构者,能够在有功能新加进来时,仍不改变原始结构。

        关于mcp扩展点实在蛮多,我会继续基于我现在的项目进行功能升级,后面也要进行架构重组
项目地址:
gitee: https://gitee.com/facedog/server2mcp-spring-boot-starter.git

github:https://github.com/TheEterna/server2mcp-spring-boot-starter.git

希望大家可以多多star,感谢

http://www.lryc.cn/news/2386268.html

相关文章:

  • 大语言模型的完整训练周期从0到1的体系化拆解
  • 历年北京邮电大学保研上机真题
  • elementUI 中el-date-picker和el-select的样式调整
  • 《仿盒马》app开发技术分享-- 定位获取(端云一体)
  • 黑马点评--基于Redis实现共享session登录
  • Mujoco 学习系列(二)基础功能与xml使用
  • 比特授权云外壳加密支持Android 15!
  • uniapp使用sse连接后端,接收后端推过来的消息(app不支持!!)
  • 历年复旦大学保研上机真题
  • 黑马点评-实现安全秒杀优惠券(使并发一人一单,防止并发超卖)
  • 解决论文中字体未嵌入的问题
  • leetcode 131. Palindrome Partitioning
  • Android本地语音识别引擎深度对比与集成指南:Vosk vs SherpaOnnx
  • 审计报告附注救星!实现Word表格纵向求和+横向计算及其对应的智能校验
  • 人工智能数学基础实验(四):最大似然估计的-AI 模型训练与参数优化
  • 告别延迟!Ethernetip转modbustcp网关在熔炼车间监控的极速时代
  • Kotlin协程优化Android ANR问题
  • Visual Studio Code插件离线安装指南:从市场获取并手动部署
  • 构建安全AI风险识别大模型:CoT、训练集与Agent vs. Fine-Tuning对比
  • 计算机视觉---YOLOv1
  • 无法同步书签,火狐浏览器修改使用国内的账号服务器
  • 动态防御体系实战:AI如何重构DDoS攻防逻辑
  • Kotlin Native与C/C++高效互操作:技术原理与性能优化指南
  • 爬虫核心概念与工作原理详解
  • Flink架构概览,Flink DataStream API 的使用,FlinkCDC的使用
  • vue3前端后端地址可配置方案
  • Es6中怎么使用class实现面向对象编程
  • digitalworld.local: FALL靶场
  • MySQL---库操作
  • 动态规划算法:字符串类问题(2)公共串