前言
前段时间,Anthropic发布了新版本MCP协议,在Remote MCP Server场景下实现了显著改进。此次更新的核心亮点是Streamable HTTP传输机制,它不仅继承了HTTP+SSE的实时消息传输能力,还通过一系列优化使得连接更加稳定、数据传输更具弹性。新版协议采用单一MCP端点同时支持HTTP POST和GET请求,通过强制使用Mcp-Session-Id头实现会话管理,并支持批量请求、响应和通知,以及SSE流的可恢复性。(摘自:开源 Remote MCP Server 一站式托管来啦)
作为java语言最流行的框架Spring,Spring AI模块中也接入了MCP,Spring AI MCP通过Spring Boot接入扩展了MCPJavaSDK,提供客户端和服务器启动器。 使用Spring初始化器引导您的AI应用程序支持MCP。(摘自:Spring AI官网)
今天给大家带来一个使用Spring AI MCP开发的开源项目:easy-remote-mcp,通过简单的实现思路,使用Spring AI MCP一个java应用支持多个MCP Server。为开发者提供一种高效且便捷的方式来管理和整合多个MCP服务器。(目前仅支持工具调用)
话不多说,直接上项目演示和体验地址。
项目演示
项目体验地址:http://123.56.187.33:8082/home (本地址仅供学习参考,未做任何安全防护,不要上传个人私密信息哦。)
核心功能:通过页面配置,把HTTP接口转化为标准MCP工具,并且提供MCP Client能直接访问的sse url。
1.首先进入首页,我们能看到已经创建好的两个示例的MCP Server,大家自己创建的MCP Server也会在这里展示。
因为该网址只为体验学习,不涉及用户登录,所以设置查看密钥来保护大家自己创建的MCP Server,可以在保存时自定义设置查看密钥。
2.进入新增/编辑页面 可以看到当前MCP Server的名称描述和下方提供的工具信息。其中请求url、请求方法、请求体就是需要转换为MCP工具的HTTP接口的信息。
目前仅支持GET和POST请求方式,GET真正请求时会将请求参数直接拼接在请求url上;POST请求则会转为json作为请求体,也就是仅支持application/json。
这里提供一个示例接口,内部是调用了高德地图的api。
点击查看sse访问url就可以看到当前MCP Server的访问url了。
3.进入cline使用我们的MCP Server,配置中新增我们的MCP Server配置。
在cline中使用我们的MCP Server。如果更新了我们的MCP Server,在cline中刷新也是能直接看到最新的信息的。
核心实现方式
Spring AI把McpServer作为Spring Bean进行了管理,所以Spring AI提供的MCP支持默认的就是一个java应用一个MCP Server。在这里我们对McpServer进行自主管理,做到一个java应用管理多个McpServer。
java
// 新增McpServer逻辑处理
// 对于每个新创建的MCP Server,生成一个随机字符串作为唯一标识
String randomStr = RandomUtil.randomString(6);
// 为每个MCP Server创建一个单独的访问url,使用 /randomStr/sse 的格式确定访问url。
// 为这个新的MCP Server创建一个的协议,访问url添加刚刚生成的唯一标识字符串,确保每个MCP Server的唯一性
WebFluxSseServerTransportProvider transportProvider = new WebFluxSseServerTransportProvider(new ObjectMapper(), "", "/" + randomStr + "/sse/message", "/" + randomStr + "/sse");
McpSchema.Implementation serverInfo = new McpSchema.Implementation(mcpServer.getName(), "1.0.0");
io.modelcontextprotocol.server.McpServer.AsyncSpecification serverBuilder = io.modelcontextprotocol.server.McpServer.async(transportProvider).serverInfo(serverInfo);
// 工具转换,把参数转化为Spring AI提供的ToolCallback,这里定义自己的RemoteMcpToolCallback
List<RemoteMcpToolCallback> remoteMcpToolCallbacks = RemoteMcpUtil.convertToolCallback(detailVO.getTools());
// 工具转换,转换为符合MCP规范标准的工具类
List<McpServerFeatures.AsyncToolSpecification> asyncToolSpecification = toAsyncToolSpecification(remoteMcpToolCallbacks);
// 为新的MCP Server添加工具
serverBuilder.tools(asyncToolSpecification);
McpSchema.ServerCapabilities build = McpSchema.ServerCapabilities.builder().tools(true).build();
serverBuilder.capabilities(build);
McpAsyncServer asyncServer = serverBuilder.build();
// 添加到map集合中 以便后续对该MCP Server的更新和删除
serverMap.put(randomStr, asyncServer);
toolSpecificationMap.put(randomStr, asyncToolSpecification);
// 添加新的MCP Server路由
remoteMcpHandlerMapping.addMapping(randomStr, transportProvider.getRouterFunction());
每个MCP Server都有单独的路由url,自定义RemoteMcpHandlerMapping来实现各自的请求处理。
java
@Component
public class RemoteMcpHandlerMapping extends AbstractHandlerMapping{
// 每个MCP Server的路由映射
private final Map<String, RouterFunction<?>> mappings = new HashMap<>();
// DispatcherHandler会调用此方法获取处理当前请求的handler
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// 获取路径中的随机字符串 MCP Server的唯一标识
String randomStr = exchange.getRequest().getURI().getPath().split("/")[1];
if (StrUtil.isBlank(randomStr) || !mappings.containsKey(randomStr)) {
return Mono.empty();
}
ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
// 使用特定的MCP Server的请求处理handler
return mappings.get(randomStr).route(request).doOnNext((handler) -> this.setAttributes(exchange.getAttributes(), request, handler));
}
// path:随机字符串,每个MCP Server的唯一标识
// mapping 路由
public void addMapping(String path, RouterFunction<?> mapping) {
mappings.put(path, mapping);
}
public void clear(String path) {
mappings.remove(path);
}
}
自定义ToolCallback,用于处理工具调用的逻辑。
java
public class RemoteMcpToolCallback implements ToolCallback {
private ToolDefinition toolDefinition;
private ToolMetadata toolMetadata;
private ToolVO toolVO;
@Override
public ToolDefinition getToolDefinition() {
return toolDefinition;
}
@Override
public ToolMetadata getToolMetadata() {
return toolMetadata;
}
// 这里是简单的直接使用http工具类进行http请求,直接返回请求结果
@Override
public String call(String toolInput) {
JSONObject entries = JSONUtil.parseObj(toolInput);
// 如果是GET请求,把参数拼在url上
if (StrUtil.equals(HttpMethod.GET.name(), toolVO.getMethod())) {
StringBuilder url = new StringBuilder(toolVO.getUrl());
if (!entries.isEmpty()) {
url.append("?");
for (Map.Entry entry : entries.entrySet()) {
url.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
url = new StringBuilder(url.substring(0, url.length() - 1));
}
return HttpUtil.get(url.toString());
} else {
return HttpUtil.post(toolVO.getUrl(), entries.toString());
}
}
@Override
public String call(String toolInput, ToolContext tooContext) {
return call(toolInput);
}
}
最后
目前该项目仅完成了初版,已支持核心功能,但在业务逻辑处理和代码上还有很多不完善的地方,后续会新增oauth2验证等;如果存在代码逻辑错误等严重问题,还请多多指正,一起学习共同成长。
我相信,AI Agent结合MCP的未来一定是Local MCP Server与Remote MCP Server相辅相成。