Copilot commented on code in PR #6312:
URL: https://github.com/apache/shenyu/pull/6312#discussion_r3013631849


##########
shenyu-plugin/shenyu-plugin-mcp-server/pom.xml:
##########
@@ -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

Review Comment:
   The comment says to update 
`ShenyuStreamableHttpServerTransportProvider.SUPPORTED_SDK_VERSION`, but that 
constant doesn’t exist in `ShenyuStreamableHttpServerTransportProvider` (the 
class currently has no `SUPPORTED_SDK_VERSION`). Either add the constant (if 
intended) or update the comment to reference the actual version source to avoid 
misleading future upgrades.
   ```suggestion
                 any related version constants in 
ShenyuStreamableHttpServerTransportProvider accordingly
   ```



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java:
##########
@@ -761,20 +761,24 @@ private McpSyncServerExchange extractMcpExchange(final 
ToolContext toolContext)
      *
      * @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);
         }

Review Comment:
   There are existing unit tests for `ShenyuToolCallback`, but none cover the 
new error-wrapping behavior that adds SDK version context. Consider adding a 
test where `McpSessionHelper.getSessionId()` throws an `IllegalStateException` 
(reflection failure) and asserting the propagated exception message includes 
the supported SDK version, and another where sessionId is empty to ensure it is 
not misclassified as a compatibility issue.



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/session/McpSessionHelper.java:
##########
@@ -28,9 +30,86 @@
 
 /**
  * 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;

Review Comment:
   `resolveReflectionFields()` runs during class initialization and calls 
`Field#setAccessible(true)`. On Java 9+ this can throw unchecked runtime 
exceptions (e.g., `InaccessibleObjectException`) in addition to 
`SecurityException`; currently those will escape the static initializer and can 
fail class loading with `ExceptionInInitializerError`. Consider catching 
`RuntimeException` around the reflective access, logging, and leaving 
`fieldsResolved=false` so the helper fails gracefully via 
`checkReflectionAvailability()` instead of crashing at class-load time.
   ```suggestion
               fieldsResolved = false;
           } catch (RuntimeException e) {
               LOG.error("SDK COMPATIBILITY ERROR: Unexpected runtime exception 
during reflection field resolution. "
                       + "This may indicate module access restrictions or an 
incompatible SDK version (tested: {}). "
                       + "Error: {}", SUPPORTED_SDK_VERSION, e.getMessage(), e);
               fieldsResolved = false;
   ```



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java:
##########
@@ -761,20 +761,24 @@ private McpSyncServerExchange extractMcpExchange(final 
ToolContext toolContext)
      *
      * @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);

Review Comment:
   The `try` block throws an `IllegalStateException` when `sessionId` is empty, 
but the `catch (IllegalStateException e)` immediately wraps it into a new 
exception that suggests an SDK compatibility issue. This makes non-SDK failures 
(like an empty/missing session) look like compatibility problems; consider only 
wrapping exceptions that originate from `McpSessionHelper` reflection failures 
(e.g., messages starting with "SDK COMPATIBILITY ERROR"), and rethrow other 
`IllegalStateException`s unchanged.
   ```suggestion
               // Only wrap SDK compatibility-related errors; rethrow others 
unchanged
               final String originalMessage = e.getMessage();
               if (originalMessage != null && originalMessage.startsWith("SDK 
COMPATIBILITY ERROR")) {
                   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: " + originalMessage, e);
               }
               throw e;
   ```



##########
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

Review Comment:
   SSE is a streaming (server-push) mechanism, not long-polling. The 
description "`/sse` endpoint with long-polling session support" is inaccurate; 
consider changing it to describe a long-lived streaming connection (and, if 
relevant, how session IDs are managed).
   ```suggestion
   - **SSE (Server-Sent Events)**: `/sse` endpoint providing a long-lived, 
one-way streaming (server-push) connection, with optional session ID–based 
correlation
   - **Streamable HTTP**: Unified endpoint where GET establishes an SSE stream 
and POST sends messages correlated to that stream
   ```



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/session/McpSessionHelper.java:
##########
@@ -51,54 +130,98 @@ public static McpSyncServerExchange 
getMcpSyncServerExchange(final ToolContext t
         }
         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");
         }

Review Comment:
   `getSessionId()` checks `Objects.isNull(session)` after calling 
`getSession()`, but `getSession()` never returns null (it either returns a 
non-null session or throws). This branch is effectively dead code and can be 
removed to avoid misleading future readers about possible return values.



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java:
##########
@@ -761,20 +761,24 @@ private McpSyncServerExchange extractMcpExchange(final 
ToolContext toolContext)
      *
      * @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

Review Comment:
   `extractSessionId()` only catches `IllegalStateException`, but 
`McpSessionHelper.getSessionId()` can also throw `IllegalArgumentException` 
(e.g., missing async exchange/session). That will currently propagate without 
the added context and contradict the method’s Javadoc (`@throws 
IllegalStateException`). Consider catching/wrapping the helper’s 
`IllegalArgumentException` as well (or changing the helper to throw 
`IllegalStateException` consistently for extraction failures).
   ```suggestion
           } catch (IllegalStateException | IllegalArgumentException e) {
               // Re-throw errors with additional context (including potential 
SDK compatibility issues)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to