拆解Java MCP Server SSE代码

前言

为了能够更加熟悉MCP Server SSE模式,我们需要深入代码内部看看具体的实现,,以便于我们遇到问题时能够更快更好地处理。废话不多说,接下来我们就开始一步一步拆解SSE的代码逻辑。

代码拆解

MCP官方给出的java SSE demo有好几种,我们直接拿出最经典Webmvc实现方式来进行拆解。 MCP Server大概实现我们只需要了解三个类:McpAsyncServer,McpServerTransportProvider、McpServerSession。

  • McpAsyncServer

    McpAsyncServer属于资源管理类,专门管理Tools、Resources、Prompts,负责Tools、Resources、Prompts的新增、删除以及变更通知,我们可以大概看一下相关的方法:

    这样看起来就一目了然了,McpAsyncServer就是用来管理Tools、Resources、Prompts这三大资源的,根据相关方法,我们可以实现动态地新增和删除Tools、Resources、Prompts,并且能够把这种变化告知Mcp Client;

    我们来看一下代码相关代码,以addTool为例:

java 复制代码
@Override  
public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {  
	if (toolSpecification == null) {  
		return Mono.error(new McpError("Tool specification must not be null"));  
	}  
	if (toolSpecification.tool() == null) {  
		return Mono.error(new McpError("Tool must not be null"));  
	}  
	if (toolSpecification.call() == null) {  
		return Mono.error(new McpError("Tool call handler must not be null"));  
	}  
	if (this.serverCapabilities.tools() == null) {  
		return Mono.error(new McpError("Server must be configured with tool capabilities"));  
	}  
	  
	return Mono.defer(() -> {  
		// Check for duplicate tool names  
		if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) {  
			return Mono  
	.error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists"));  
		}  

		// 核心代码
		this.tools.add(toolSpecification);  
		logger.debug("Added tool handler: {}", toolSpecification.tool().name());  
	  
		if (this.serverCapabilities.tools().listChanged()) {  
			return notifyToolsListChanged();  
		}  
		return Mono.empty();  
	});  
}

咱们把里面最核心的两段代码找出来:

java 复制代码
    // 新增Tool
    this.tools.add(toolSpecification);
    // 如果需要通知MCP Client
    if (this.serverCapabilities.tools().listChanged()) { 
	    // 通知MCP Client,Tools有变更 
		return notifyToolsListChanged();  
	}

除了addTool,removeTool的实现也是类似:

java 复制代码
	// 从列表中删除指定的Tool
	boolean removed = this.tools  
.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName)); 
	// 如果删除成功,并且需要通知MCP Client,那么执行通知逻辑
	if (removed) {
		if (this.serverCapabilities.tools().listChanged()) {  
			return notifyToolsListChanged();  
		}
	}

接下来我们看一下通知的实现:

java 复制代码
  @Override  
public Mono<Void> notifyToolsListChanged() {  
	return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);  
}

具体的通知操作交给了TransportProvider来处理; 看了上面的代码拆解,我们对McpAsyncServer已经很了解了,它就是负责管理资源的类。在项目中的应用也非常直接,如果需要对Tools、Resources、Prompts进行任何的变更,都可以使用McpAsyncServer。 可能会有小伙伴疑惑,咱们在demo或者项目使用的是McpSyncServer怎么办?不着急,咱们看一下McpSyncServerd实现就知道了:

java 复制代码
    public McpSyncServer(McpAsyncServer asyncServer) {  
		this.asyncServer = asyncServer;  
	}

	public void addTool(McpServerFeatures.SyncToolSpecification toolHandler) {  
		this.asyncServer.addTool(McpServerFeatures.AsyncToolSpecification.fromSync(toolHandler)).block();  
	}

也就是说,McpSyncServer的实现是McpAsyncServer代理的。所以我们也可以通过McpSyncServer操作Tools、Resources、Prompts。

  • McpServerTransportProvider

    我们先来看一下McpServerTransportProvider的相关实现:

    根据相关实现类的名称,我们就能知道,TransportProvider就是用来实现MCP Client与MCP Server之间的通信方式。 针对不同的技术栈,我们会选择相应的通信方式: 1、HttpServletSseServerTransportProvider:基于Servlet实现的SSE通信方式,不依赖任何web框架。 2、StdioServerTransportProvider:基于操作系统标准输入输出进行数据通信,要求MCP Server与MCP Client运行在同一台计算机上。 3、WebFluxSseServerTransportProvider:基于Spring WebFlux实现的SSE通信方式,适用于使用Spring WebFlux的项目; 4、WebMvcSseServerTransportProvider:基于Spring WebMvc实现的SSE通信方式,适用于使用Spring WebMvc的项目;

    咱们接下来使用WebMvcSseServerTransportProvider作为本次代码解析的对象。 首先咱们回顾一下前面的demo中,创建出TransportProvider用来处理/sse,/mcp/message请求,一般会搭配一个RouterFunction作为请求路由,看一下源码:

    无论是HttpServletSseServerTransportProvider、WebMvcSseServerTransportProvider还是WebFluxSseServerTransportProvider,主要职责就是处理这两个请求: 1、大概来看一下处理/sse请求的相关的代码:

java 复制代码
private ServerResponse handleSseConnection(ServerRequest request) {  
    // 如果链接已关闭,那么直接返回服务不可用
	if (this.isClosing) {  
		return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");  
	}  

	// 生成sessionId
	String sessionId = UUID.randomUUID().toString();  
	logger.debug("Creating new SSE connection for session: {}", sessionId);  
	  
	// Send initial endpoint event  
	try {  
		return ServerResponse.sse(sseBuilder -> {
		    // 设置链接完成后需要做的操作
			sseBuilder.onComplete(() -> {  
				logger.debug("SSE connection completed for session: {}", sessionId);  
				// 移除sessionId
				sessions.remove(sessionId);  
			});  
			// 设置链接超时处理逻辑
			sseBuilder.onTimeout(() -> {  
				logger.debug("SSE connection timed out for session: {}", sessionId);  
				// 移除sessionId
				sessions.remove(sessionId);  
			});  

			// 创建WebMvcMcpSessionTransport,用来构建McpServerSession
			WebMvcMcpSessionTransport sessionTransport = new WebMvcMcpSessionTransport(sessionId, sseBuilder); 
			// 构建 McpServerSession
			McpServerSession session = sessionFactory.create(sessionTransport); 
			// 保存当前链接状态 
			this.sessions.put(sessionId, session);  
	  
			try {  
				// 返回链接sessionId
				sseBuilder.id(sessionId)  
					.event(ENDPOINT_EVENT_TYPE)  
					.data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);  
			} catch (Exception e) {  
				logger.error("Failed to send initial endpoint event: {}", e.getMessage());  
				sseBuilder.error(e);  
			}  
		}, Duration.ZERO);  
	}   catch (Exception e) {  
		logger.error("Failed to send initial endpoint event to session {}: {}", sessionId, e.getMessage());
		// 出现异常移除sessionId  
		sessions.remove(sessionId);
		// 返回500  
		return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 
	}  
}

其实核心代码就只有最关键的几段,主要目标就是创建Session,大概分为以下几步:

  • 生成sessionId;
  • 通过sessionId构建WebMvcMcpSessionTransport(目的是构建McpServerSession);
  • 构建McpServerSession(关键对象);
  • 将session状态(McpServerSession)保存到map中(目标是给/mcp/message请求使用);
  • 将sessionId返回给MCP Client;

2、接下来看一下如何处理/mcp/message请求的

java 复制代码
private ServerResponse handleMessage(ServerRequest request) {
	// 如果链接关闭,返回服务不可用
	if (this.isClosing) {  
		return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");  
	}  
	// 如果请求参数没有sessionId,直接返回,非法请求
	if (!request.param("sessionId").isPresent()) {  
		return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));  
	}  
	// 从请求参数中获取sessionId  
	String sessionId = request.param("sessionId").get(); 
	// 根据sessionId取出对应的McpServerSession
	McpServerSession session = sessions.get(sessionId);  
	// 如果没取到session,直接返回  
	if (session == null) {  
		return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));  
	}  
	  
	try {  
		// 解析请求体
		String body = request.body(String.class);
		// 把请求体反序列化为JSONRPCMessage对象  
		McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);  
	  
		// 调用McpServerSession来处理请求数据
		session.handle(message).block();
		// 返回处理成功
		return ServerResponse.ok().build();  
	} catch (IllegalArgumentException | IOException e) {  
		logger.error("Failed to deserialize message: {}", e.getMessage());  
		// 出现异常直接返回
		return ServerResponse.badRequest().body(new McpError("Invalid message format"));  
	} catch (Exception e) {  
		logger.error("Error handling message: {}", e.getMessage());  
		return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));  
	}  
}

总结一下上面的源码:

  • 获取请求中的sessionId;
  • 根据sessionId取出McpServerSession;
  • 反序列化请求体,转成JSONRPCMessage对象;
  • 把JSONRPCMessage对象交给McpServerSession处理;

所以真正地处理细节是在McpServerSession,TransportProvider只做了两件事情:

  1. 处理/sse请求,与MCP Client建立链接,生成sessionId;
  2. 处理/mcp/message请求,解析请求数据,交给McpServerSession处理;
  • McpServerSession

    McpServerSession主要处理三类数据,请求数据、响应数据和通知数据:

    关于McpServerSession如何处理这些数据,我们可以看AsyncServerImpl类,AsyncServerImpl对象在初始化的时候已经设定好了如何创建McpServerSession:

java 复制代码
AsyncServerImpl(McpServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,  
McpServerFeatures.Async features) {  
	this.mcpTransportProvider = mcpTransportProvider;  
	this.objectMapper = objectMapper;  
	this.serverInfo = features.serverInfo();  
	this.serverCapabilities = features.serverCapabilities();  
	this.instructions = features.instructions();  
	this.tools.addAll(features.tools());  
	this.resources.putAll(features.resources());  
	this.resourceTemplates.addAll(features.resourceTemplates());  
	this.prompts.putAll(features.prompts());  
	  
	Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();  
	  
	// 新增对应的请求处理器
	requestHandlers.put(McpSchema.METHOD_PING, (exchange, params) -> Mono.just(Map.of()));  
	  
	// 新增tools接口处理器  
	if (this.serverCapabilities.tools() != null) {  
		requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());  
		requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());  
	}  
	  
	// 新增resources接口处理器  
	if (this.serverCapabilities.resources() != null) {  
		requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());  
		requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());  
		requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());  
	}  
	  
	// 新增prompts接口处理器  
	if (this.serverCapabilities.prompts() != null) {  
		requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());  
		requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());  
	}  
	  
	// 新增日志接口处理器
	if (this.serverCapabilities.logging() != null) {  
		requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());  
	}  
	  
	Map<String, McpServerSession.NotificationHandler> notificationHandlers = new HashMap<>();  
	// 新增通知处理器  
	notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());  
	  
	List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = features  
	.rootsChangeConsumers();  
	  
	if (Utils.isEmpty(rootsChangeConsumers)) {  
		rootsChangeConsumers = List.of((exchange,  
	roots) -> Mono.fromRunnable(() -> logger.warn(  
	"Roots list changed notification, but no consumers provided. Roots list changed: {}",  
	roots)));  
	}  
	// 新增通知处理器
	notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,  
	asyncRootsListChangedNotificationHandler(rootsChangeConsumers));  
	// 设置SessionFactory,决定了如何创建McpServerSession对象  
	mcpTransportProvider  
	.setSessionFactory(transport -> new McpServerSession(UUID.randomUUID().toString(), transport,  
	this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));  
}

在McpServerSession中,处理请求数据和通知数据,是根据JSONRPCMessage中的method去匹配上面的handler,通过对应的handler来处理目标数据,示例代码如下:

java 复制代码
// 根据method获取对应的handler
var handler = this.requestHandlers.get(request.method());
// 执行对应的处理逻辑
handler.handle(request.params()));

到这一步,我们大概知道McpServerSession是用来处理各类请求、响应和通知数据的,具体的细节将另起一小节来分析,这里就不细讲。

小结

大概来做一个总结,从功能上来分,McpAsyncServer、McpSyncServer是开放给开发人员开使用的,用来管理Tools、Resources、Prompts等资源的;TransportProvider是用来实现通信协议的,这个是可插拔的,想要使用对应的通信协议,创建对应的TransportProvider对象就行;McpServerSession是真正干活的,和通信协议不相干,它根据针对不同的JSONRPCMessage对象调用对应的handler来处理数据,真正地核心业务处理逻辑就在这个对象中。

相关推荐
考虑考虑几秒前
点阵图更改背景文字
java·后端·java ee
ZHE|张恒8 分钟前
Spring Boot 3 + Flyway 全流程教程
java·spring boot·后端
TDengine (老段)33 分钟前
TDengine 数学函数 CRC32 用户手册
java·大数据·数据库·sql·时序数据库·tdengine·1024程序员节
心随雨下1 小时前
Tomcat日志配置与优化指南
java·服务器·tomcat
Kapaseker1 小时前
Java 25 中值得关注的新特性
java
wljt1 小时前
Linux 常用命令速查手册(Java开发版)
java·linux·python
撩得Android一次心动1 小时前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件
canonical_entropy1 小时前
Nop平台到底有什么独特之处,它能用在什么场景?
java·后端·领域驱动设计
chilavert3181 小时前
技术演进中的开发沉思-174 java-EJB:分布式通信
java·分布式
不是株2 小时前
JavaWeb(后端进阶)
java·开发语言·后端