spring-ai 集成 mcp 之投机取巧

news2025/7/27 14:02:49

主旨

        这篇文章主旨就一点,罗列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 capabilities
		SyncSpecification 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);

	@Override
	public 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.coloradmin.cn/o/2386268.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大语言模型的完整训练周期从0到1的体系化拆解

以下部分内容参考了AI。 要真正理解大语言模型&#xff08;LLM&#xff09;的创生过程&#xff0c;我们需要将其拆解为一个完整的生命周期&#xff0c;每个阶段的关键技术相互关联&#xff0c;共同支撑最终模型的涌现能力。以下是体系化的训练流程框架&#xff1a; 阶段一&am…

历年北京邮电大学保研上机真题

2025北京邮电大学保研上机真题 2024北京邮电大学保研上机真题 2023北京邮电大学保研上机真题 在线测评链接&#xff1a;https://pgcode.cn/problem?classification1 32位二进制串加法 题目描述 输入一个32位的二进制01串&#xff0c;输出这个数1和3后的32位二进制串。 输入…

《仿盒马》app开发技术分享-- 定位获取(端云一体)

开发准备 上一节我们实现了地址管理页面的数据查询和展示&#xff0c;接下来我们要实现的功能是地址添加相关的&#xff0c;我们想实现的功能是地图选点&#xff0c;那么在地图选点之前我们要做的就是先获取用户当前的定位。获取定位后我们拿到经纬度和其他信息&#xff0c;然…

黑马点评--基于Redis实现共享session登录

集群的session共享问题分析 session共享问题&#xff1a;多台Tomcat无法共享session存储空间&#xff0c;当请求切换到不同Tomcat服务时&#xff0c;原来存储在一台Tomcat服务中的数据&#xff0c;在其他Tomcat中是看不到的&#xff0c;这就导致了导致数据丢失的问题。 虽然系…

Mujoco 学习系列(二)基础功能与xml使用

这篇文章是 Mujoco 学习系列第二篇&#xff0c;主要介绍一些基础功能与 xmI 使用&#xff0c;重点在于如何编写与读懂 xml 文件。 运行这篇博客前请先确保正确安装 Mujoco 并通过了基本功能与GUI的验证&#xff0c;即至少完整下面这个博客的 第二章节 内容&#xff1a; Mujoc…

比特授权云外壳加密支持Android 15!

在信息化时代&#xff0c;多数软件供应商需要适配安卓系统&#xff0c;以扩大市场、满足用户需求并提升竞争力。APK作为Android应用的安装包&#xff0c;包含代码、资源、配置文件等运行所需组件&#xff0c;用于在设备端分发和安装应用。企业在分发软件时&#xff0c;需要通过…

uniapp使用sse连接后端,接收后端推过来的消息(app不支持!!)

小白终成大白 文章目录 小白终成大白前言一、什么是SSE呢&#xff1f;和websocket的异同点有什么&#xff1f;相同点不同点 二、直接上实现代码总结 前言 一般的请求就是前端发 后端回复 你一下我一下 如果需要有什么实时性的 后端可以主动告诉前端的技术 我首先会想到 webso…

历年复旦大学保研上机真题

2025复旦大学保研上机真题 2024复旦大学保研上机真题 2023复旦大学保研上机真题 在线测评链接&#xff1a;https://pgcode.cn/problem?classification1 最大公共子串 题目描述 输入 3 个子串&#xff0c;输出这 3 个子串的最大公共子串。 输入格式 输入包含 3 个子串&…

黑马点评-实现安全秒杀优惠券(使并发一人一单,防止并发超卖)

一.实现优惠券秒杀 1.最原始代码&#xff1a; Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {Resourceprivate ISeckillVoucherService seckillVoucherService;Resourcepriv…

解决论文中字体未嵌入的问题

文章总览&#xff1a;YuanDaiMa2048博客文章总览 解决论文中字体未嵌入的问题 问题描述解决方案&#xff1a;使用 Adobe PDF 打印机嵌入字体&#xff08;WPS版&#xff09;步骤一&#xff1a;打开 PDF 文件步骤二&#xff1a;选择打印到 Adobe PDF步骤三&#xff1a;修改 Adobe…

leetcode 131. Palindrome Partitioning

目录 一、题目描述 二、方法1、回溯法每次暴力判断回文子串 三、方法2、动态规划回溯法 一、题目描述 分割回文子串 131. Palindrome Partitioning 二、方法1、回溯法每次暴力判断回文子串 class Solution {vector<vector<string>> res;vector<string>…

审计报告附注救星!实现Word表格纵向求和+横向计算及其对应的智能校验

在审计工作中&#xff0c;Word附注通常包含很多表格。为了确保附注数字的准确性&#xff0c;我们需要对这些表格进行数字逻辑校验&#xff0c;主要包含两个维度&#xff1a;在纵向上验证合计项金额是否正确&#xff1b;在横向上检查“年末金额年初金额本期增加-本期减少”的勾稽…

人工智能数学基础实验(四):最大似然估计的-AI 模型训练与参数优化

一、实验目的 理解最大似然估计&#xff08;MLE&#xff09;原理&#xff1a;掌握通过最大化数据出现概率估计模型参数的核心思想。实现 MLE 与 AI 模型结合&#xff1a;使用 MLE 手动估计朴素贝叶斯模型参数&#xff0c;并与 Scikit-learn 内置模型对比&#xff0c;深入理解参…

告别延迟!Ethernetip转modbustcp网关在熔炼车间监控的极速时代

熔炼车间热火朝天&#xff0c;巨大的热风炉发出隆隆的轰鸣声&#xff0c;我作为一名技术操控工&#xff0c;正密切关注着监控系统上跳动的各项参数。这套基于EtherNET/ip的监控系统&#xff0c;是我们车间数字化改造的核心&#xff0c;它将原本分散的控制单元整合在一起&#x…

Visual Studio Code插件离线安装指南:从市场获取并手动部署

Visual Studio Code插件离线安装指南&#xff1a;从市场获取并手动部署 一、场景背景二、操作步骤详解步骤1&#xff1a;访问官方插件市场步骤2&#xff1a;定位目标版本步骤3&#xff1a;提取关键参数步骤4&#xff1a;构造下载链接步骤5&#xff1a;下载与安装 三、注意事项 …

计算机视觉---YOLOv1

YOLOv1深度解析&#xff1a;单阶段目标检测的开山之作 一、YOLOv1概述 提出背景&#xff1a; 2016年由Joseph Redmon等人提出&#xff0c;全称"You Only Look Once"&#xff0c;首次将目标检测视为回归问题&#xff0c;开创单阶段&#xff08;One-Stage&#xff09…

爬虫核心概念与工作原理详解

爬虫核心概念与工作原理详解 1. 什么是网络爬虫&#xff1f; 网络爬虫&#xff08;Web Crawler&#xff09;是一种按照特定规则自动抓取互联网信息的程序或脚本&#xff0c;本质是模拟人类浏览器行为&#xff0c;通过HTTP请求获取网页数据并解析处理。 形象比喻&#xff1a;如…

vue3前端后端地址可配置方案

在开发vue3项目过程中&#xff0c;需要切换不同的服务器部署&#xff0c;代码中配置的服务需要可灵活配置&#xff0c;不随着run npm build把网址打包到代码资源中&#xff0c;不然每次切换都需要重新run npm build。需要一个配置文件可以修改服务地址&#xff0c;而打包的代码…

digitalworld.local: FALL靶场

digitalworld.local: FALL 来自 <digitalworld.local: FALL ~ VulnHub> 1&#xff0c;将两台虚拟机网络连接都改为NAT模式 2&#xff0c;攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.182&#xff0c;靶场IP192.168.23.4 3&…

MySQL---库操作

mysql> create database if not exists kuku3; 1.库操作的语法 create database [if not exists] db_name [create_specification [, create_specification] ...] create_specification: [default] character set charset_name [default] collate collation_name详细解释…