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;
     }
 }

Reply via email to