This is an automated email from the ASF dual-hosted git repository. liuhongyu pushed a commit to branch feat/mcp-server-sdk-upgrade in repository https://gitbox.apache.org/repos/asf/shenyu.git
commit 9567a8419889e70dd08b0813e9bf24376e63262d Author: liuhy <[email protected]> AuthorDate: Tue Mar 31 13:29:15 2026 +0800 feat(mcp-server): enhance SDK compatibility with reflection caching and error handling - Add reflection field caching in McpSessionHelper for better performance and reliability - Add SUPPORTED_SDK_VERSION constant for SDK compatibility tracking - Enhance error messages with SDK version information in ShenyuToolCallback - Add SDK compatibility notes in pom.xml documenting reflection usage - Update MCP_TOOL_EXAMPLES.md and MCP_TOOL_EXAMPLES_EN.md with SDK version compatibility table This change improves compatibility with MCP SDK 0.17.0 by: - Caching reflection fields at class load time - Adding graceful degradation when reflection fails - Documenting known limitations and supported SDK versions Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES.md | 21 +++ .../MCP_TOOL_EXAMPLES_EN.md | 21 +++ shenyu-plugin/shenyu-plugin-mcp-server/pom.xml | 42 ++--- .../mcp/server/callback/ShenyuToolCallback.java | 24 +-- .../mcp/server/session/McpSessionHelper.java | 189 +++++++++++++++++---- 5 files changed, 233 insertions(+), 64 deletions(-) diff --git a/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES.md b/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES.md index d1457ffd5a..38d1eadafc 100644 --- a/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES.md +++ b/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES.md @@ -2,6 +2,27 @@ 本文档基于 **shenyu-examples-http** 项目中的真实接口,提供了 Shenyu MCP Server Plugin 的各种工具配置示例,涵盖不同的 HTTP 请求方法、参数类型和配置方式。 +## SDK 版本兼容性 + +本插件基于以下 SDK 版本开发和测试: + +| 依赖 | 版本 | +|------|------| +| MCP SDK (io.modelcontextprotocol.sdk:mcp-bom) | 0.17.0 | +| Spring AI (org.springframework.ai:spring-ai-bom) | 1.1.2 | +| Spring Boot | 3.3.1 | + +### 支持的协议 + +- **SSE (Server-Sent Events)**: `/sse` 端点,支持长连接会话 +- **Streamable HTTP**: 统一端点,支持 GET (SSE 流) 和 POST (消息) 请求 + +### 已知限制 + +1. **Session 管理**: 使用反射机制访问 SDK 内部字段获取 Session ID,SDK 版本升级可能影响兼容性 +2. **工具调用超时**: 默认 60 秒,可通过 `requestTemplate.timeout` 配置 +3. **CORS 支持**: 通过 `shenyu.cross.allowedHeaders` 配置允许的请求头 + ## 1. 简单 GET 请求示例 ### 1.1 无参数 GET 请求 diff --git a/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES_EN.md b/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES_EN.md index 5cbcfc74f4..77fd28337e 100644 --- a/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES_EN.md +++ b/shenyu-plugin/shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES_EN.md @@ -2,6 +2,27 @@ This document provides comprehensive examples of tool configurations for the Shenyu MCP Server Plugin based on **real interfaces from the shenyu-examples-http project**, covering different HTTP request methods, parameter types, and configuration patterns. +## SDK Version Compatibility + +This plugin is developed and tested with the following SDK versions: + +| Dependency | Version | +|------------|---------| +| MCP SDK (io.modelcontextprotocol.sdk:mcp-bom) | 0.17.0 | +| Spring AI (org.springframework.ai:spring-ai-bom) | 1.1.2 | +| Spring Boot | 3.3.1 | + +### Supported Protocols + +- **SSE (Server-Sent Events)**: `/sse` endpoint with long-polling session support +- **Streamable HTTP**: Unified endpoint supporting GET (SSE stream) and POST (message) requests + +### Known Limitations + +1. **Session Management**: Uses reflection to access SDK internal fields for Session ID retrieval; SDK version upgrades may affect compatibility +2. **Tool Call Timeout**: Default 60 seconds, configurable via `requestTemplate.timeout` +3. **CORS Support**: Configure allowed headers via `shenyu.cross.allowedHeaders` + ## 1. Simple GET Request Examples ### 1.1 GET Request with No Parameters diff --git a/shenyu-plugin/shenyu-plugin-mcp-server/pom.xml b/shenyu-plugin/shenyu-plugin-mcp-server/pom.xml index a29abdc65b..c25bf6e26d 100644 --- a/shenyu-plugin/shenyu-plugin-mcp-server/pom.xml +++ b/shenyu-plugin/shenyu-plugin-mcp-server/pom.xml @@ -41,10 +41,6 @@ <artifactId>shenyu-web</artifactId> <version>${project.version}</version> </dependency> -<!-- <dependency>--> -<!-- <groupId>org.springframework.ai</groupId>--> -<!-- <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>--> -<!-- </dependency>--> <dependency> <groupId>org.apache.shenyu</groupId> <artifactId>shenyu-plugin-base</artifactId> @@ -55,43 +51,47 @@ <artifactId>shenyu-loadbalancer</artifactId> <version>${project.version}</version> </dependency> + + <!-- + Spring AI and MCP SDK Dependencies + + SDK Compatibility Notes: + - Current tested version: MCP SDK 0.17.0, Spring AI 1.1.2 + - McpSessionHelper uses reflection to access McpSyncServerExchange.exchange and + McpAsyncServerExchange.session fields for session ID extraction + - ShenyuStreamableHttpServerTransportProvider uses reflection to access + McpServerSession.initialized and McpServerSession.state fields for session verification + - When upgrading SDK versions, verify that these internal fields still exist and + update McpSessionHelper.SUPPORTED_SDK_VERSION and + ShenyuStreamableHttpServerTransportProvider.SUPPORTED_SDK_VERSION accordingly + --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-model</artifactId> </dependency> - + <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp</artifactId> </dependency> - + + <!-- MCP SDK - Explicit version from parent BOM --> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-spring-webflux</artifactId> + <!-- Version managed by mcp-bom in parent pom.xml (${mcp.version}) --> </dependency> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-json-jackson2</artifactId> + <!-- Version managed by mcp-bom in parent pom.xml (${mcp.version}) --> </dependency> -<!-- <dependency>--> -<!-- <groupId>org.springframework.ai</groupId>--> -<!-- <artifactId>spring-ai-starter-mcp-server</artifactId>--> -<!-- </dependency>--> - - + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> - -<!-- <dependency>--> -<!-- <groupId>org.springframework.boot</groupId>--> -<!-- <artifactId>spring-boot-starter-test</artifactId>--> -<!-- </dependency>--> -<!-- <dependency>--> -<!-- <groupId>org.springframework</groupId>--> -<!-- <artifactId>spring-test</artifactId>--> -<!-- </dependency>--> + <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> diff --git a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java index 9cf25383e8..a5e751bd4f 100644 --- a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java +++ b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java @@ -761,20 +761,24 @@ public class ShenyuToolCallback implements ToolCallback { * * @param mcpExchange the MCP sync server exchange * @return the session ID - * @throws IllegalStateException if session ID cannot be extracted + * @throws IllegalStateException if session ID cannot be extracted (SDK compatibility issue) */ private String extractSessionId(final McpSyncServerExchange mcpExchange) { - final String sessionId; try { - sessionId = McpSessionHelper.getSessionId(mcpExchange); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - if (StringUtils.hasText(sessionId)) { - LOG.debug("Extracted session ID: {}", sessionId); - return sessionId; + final String sessionId = McpSessionHelper.getSessionId(mcpExchange); + if (StringUtils.hasText(sessionId)) { + LOG.debug("Extracted session ID: {}", sessionId); + return sessionId; + } + throw new IllegalStateException("Session ID is empty – it should have been set earlier by handleMessageEndpoint"); + } catch (IllegalStateException e) { + // Re-throw SDK compatibility errors with additional context + throw new IllegalStateException( + "Failed to extract session ID from MCP exchange. " + + "This may indicate an SDK compatibility issue. " + + "Tested SDK version: " + McpSessionHelper.getSupportedSdkVersion() + ". " + + "Original error: " + e.getMessage(), e); } - throw new IllegalStateException("Session ID is empty – it should have been set earlier by handleMessageEndpoint"); } /** diff --git a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/session/McpSessionHelper.java b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/session/McpSessionHelper.java index 923cdea479..7a137f08e2 100644 --- a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/session/McpSessionHelper.java +++ b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/session/McpSessionHelper.java @@ -20,6 +20,8 @@ package org.apache.shenyu.plugin.mcp.server.session; import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ToolContext; import java.lang.reflect.Field; @@ -28,9 +30,86 @@ import java.util.Objects; /** * Helper class for handling McpSession related operations. + * + * <p>SDK Compatibility Note: This class uses reflection to access internal SDK fields + * that may change between versions. The following SDK versions are tested and supported: + * <ul> + * <li>MCP SDK 0.17.0 (current)</li> + * <li>Spring AI 1.1.2 (current)</li> + * </ul> + * + * <p>If reflection fails (e.g., due to SDK API changes), this class will throw + * IllegalStateException with clear error messages indicating SDK compatibility issues. + * + * @since 2.7.0.2 */ public class McpSessionHelper { - + + private static final Logger LOG = LoggerFactory.getLogger(McpSessionHelper.class); + + /** + * SDK version for compatibility checking. + * This should be updated when testing with new SDK versions. + */ + private static final String SUPPORTED_SDK_VERSION = "0.17.0"; + + /** + * Cached field reference for McpSyncServerExchange.exchange field. + * Null if not yet resolved or resolution failed. + */ + private static volatile Field asyncExchangeFieldCache; + + /** + * Cached field reference for McpAsyncServerExchange.session field. + * Null if not yet resolved or resolution failed. + */ + private static volatile Field sessionFieldCache; + + /** + * Flag indicating whether reflection fields have been resolved. + */ + private static volatile boolean fieldsResolved; + + /** + * Lock for resolving reflection fields. + */ + private static final Object FIELD_RESOLVE_LOCK = new Object(); + + static { + resolveReflectionFields(); + } + + /** + * Resolves reflection fields for accessing internal SDK state. + * This method is called during class initialization and logs any compatibility issues. + */ + private static void resolveReflectionFields() { + try { + // Resolve asyncExchange field from McpSyncServerExchange + asyncExchangeFieldCache = McpSyncServerExchange.class.getDeclaredField("exchange"); + asyncExchangeFieldCache.setAccessible(true); + LOG.info("Successfully resolved McpSyncServerExchange.exchange field for SDK compatibility"); + + // Resolve session field from McpAsyncServerExchange + sessionFieldCache = McpAsyncServerExchange.class.getDeclaredField("session"); + sessionFieldCache.setAccessible(true); + LOG.info("Successfully resolved McpAsyncServerExchange.session field for SDK compatibility"); + + fieldsResolved = true; + LOG.info("MCP SDK reflection fields resolved successfully. Tested with SDK version: {}", SUPPORTED_SDK_VERSION); + } catch (NoSuchFieldException e) { + LOG.error("SDK COMPATIBILITY ERROR: Failed to resolve reflection fields. " + + "This indicates the MCP SDK API has changed. " + + "Tested version: {}, Current SDK may be incompatible. " + + "Missing field: {}", SUPPORTED_SDK_VERSION, e.getMessage()); + fieldsResolved = false; + } catch (SecurityException e) { + LOG.error("SDK COMPATIBILITY ERROR: Security manager blocked reflection access. " + + "Field resolution failed: {}", e.getMessage()); + fieldsResolved = false; + } + } + /** * Get McpSyncServerExchange from ToolContext. * @@ -51,54 +130,98 @@ public class McpSessionHelper { } return mcpSyncServerExchange; } - + /** * Get sessionId from McpSyncServerExchange. * + * <p>Uses reflection to access internal SDK fields. If reflection fails, + * an IllegalStateException is thrown with SDK compatibility information. + * * @param mcpSyncServerExchange the McpSyncServerExchange instance * @return the session id string - * @throws NoSuchFieldException if field not found - * @throws IllegalAccessException if field not accessible + * @throws IllegalStateException if SDK reflection fails (API incompatibility) */ - public static String getSessionId(final McpSyncServerExchange mcpSyncServerExchange) - throws NoSuchFieldException, IllegalAccessException { - Field asyncExchangeField = mcpSyncServerExchange.getClass().getDeclaredField("exchange"); - asyncExchangeField.setAccessible(true); - Object session = getSession(mcpSyncServerExchange, asyncExchangeField); + public static String getSessionId(final McpSyncServerExchange mcpSyncServerExchange) { + McpServerSession session = getSession(mcpSyncServerExchange); if (Objects.isNull(session)) { throw new IllegalArgumentException("Session is required in McpAsyncServerExchange"); } - McpServerSession mcpServerSession = (McpServerSession) session; - return mcpServerSession.getId(); + return session.getId(); } - + /** - * Get sessionId from McpSyncServerExchange. + * Get McpServerSession from McpSyncServerExchange. + * + * <p>Uses reflection to access internal SDK fields. If reflection fails, + * an IllegalStateException is thrown with SDK compatibility information. * * @param mcpSyncServerExchange the McpSyncServerExchange instance - * @return the session id string - * @throws NoSuchFieldException if field not found - * @throws IllegalAccessException if field not accessible + * @return the McpServerSession instance + * @throws IllegalStateException if SDK reflection fails (API incompatibility) */ - public static McpServerSession getSession(final McpSyncServerExchange mcpSyncServerExchange) - throws NoSuchFieldException, IllegalAccessException { - Field asyncExchangeField = mcpSyncServerExchange.getClass().getDeclaredField("exchange"); - asyncExchangeField.setAccessible(true); - Object session = getSession(mcpSyncServerExchange, asyncExchangeField); - if (Objects.isNull(session)) { - throw new IllegalArgumentException("Session is required in McpAsyncServerExchange"); + public static McpServerSession getSession(final McpSyncServerExchange mcpSyncServerExchange) { + checkReflectionAvailability(); + + try { + Object asyncExchange = asyncExchangeFieldCache.get(mcpSyncServerExchange); + if (Objects.isNull(asyncExchange)) { + throw new IllegalArgumentException("McpAsyncServerExchange is required in McpSyncServerExchange"); + } + McpAsyncServerExchange mcpAsyncServerExchange = (McpAsyncServerExchange) asyncExchange; + Object session = sessionFieldCache.get(mcpAsyncServerExchange); + if (Objects.isNull(session)) { + throw new IllegalArgumentException("Session is required in McpAsyncServerExchange"); + } + return (McpServerSession) session; + } catch (IllegalAccessException e) { + throw new IllegalStateException( + "SDK COMPATIBILITY ERROR: Failed to access SDK internal fields via reflection. " + + "This indicates the MCP SDK API has changed. " + + "Tested SDK version: " + SUPPORTED_SDK_VERSION + ". " + + "Error: " + e.getMessage(), e); } - return (McpServerSession) session; } - - private static Object getSession(final McpSyncServerExchange mcpSyncServerExchange, final Field asyncExchangeField) throws IllegalAccessException, NoSuchFieldException { - Object asyncExchange = asyncExchangeField.get(mcpSyncServerExchange); - if (Objects.isNull(asyncExchange)) { - throw new IllegalArgumentException("McpAsyncServerExchange is required in McpSyncServerExchange"); + + /** + * Checks if reflection fields are available and throws an informative exception if not. + * + * @throws IllegalStateException if reflection fields are not available + */ + private static void checkReflectionAvailability() { + if (!fieldsResolved || Objects.isNull(asyncExchangeFieldCache) || Objects.isNull(sessionFieldCache)) { + // Attempt to re-resolve fields in case of delayed class loading + synchronized (FIELD_RESOLVE_LOCK) { + if (!fieldsResolved) { + resolveReflectionFields(); + } + } + + if (!fieldsResolved || Objects.isNull(asyncExchangeFieldCache) || Objects.isNull(sessionFieldCache)) { + throw new IllegalStateException( + "SDK COMPATIBILITY ERROR: MCP SDK reflection fields are not available. " + + "The MCP SDK version may be incompatible with this implementation. " + + "Tested SDK version: " + SUPPORTED_SDK_VERSION + ". " + + "Please verify SDK version compatibility or check logs for field resolution errors."); + } } - McpAsyncServerExchange mcpAsyncServerExchange = (McpAsyncServerExchange) asyncExchange; - Field sessionField = mcpAsyncServerExchange.getClass().getDeclaredField("session"); - sessionField.setAccessible(true); - return sessionField.get(mcpAsyncServerExchange); + } + + /** + * Checks if the SDK reflection fields are available for use. + * This can be used for proactive compatibility checking. + * + * @return true if reflection fields are resolved and available + */ + public static boolean isReflectionAvailable() { + return fieldsResolved && Objects.nonNull(asyncExchangeFieldCache) && Objects.nonNull(sessionFieldCache); + } + + /** + * Returns the SDK version that this implementation has been tested with. + * + * @return the supported SDK version string + */ + public static String getSupportedSdkVersion() { + return SUPPORTED_SDK_VERSION; } }
