This is an automated email from the ASF dual-hosted git repository.
hefengen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 2c44e7ef7e feat(mcp-server): enhance SDK compatibility with reflection
caching and error handling (#6312)
2c44e7ef7e is described below
commit 2c44e7ef7e7bf1b5cb0a3adee68106547c810ee2
Author: aias00 <[email protected]>
AuthorDate: Thu Apr 2 10:03:33 2026 +0800
feat(mcp-server): enhance SDK compatibility with reflection caching and
error handling (#6312)
* 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]>
* fix mcp sdk compatibility error handling
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../shenyu-plugin-mcp-server/MCP_TOOL_EXAMPLES.md | 21 +++
.../MCP_TOOL_EXAMPLES_EN.md | 22 ++-
shenyu-plugin/shenyu-plugin-mcp-server/pom.xml | 42 ++---
.../mcp/server/callback/ShenyuToolCallback.java | 40 +++-
.../mcp/server/session/McpSessionHelper.java | 202 +++++++++++++++++----
.../server/callback/ShenyuToolCallbackTest.java | 55 +++++-
6 files changed, 312 insertions(+), 70 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..933e8c833f 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 a long-lived streaming
connection and session ID correlation
+- **Streamable HTTP**: Unified endpoint handling POST message requests for MCP
operations
+
+### 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
@@ -496,4 +517,3 @@ Implement retry mechanisms and proper error responses at
the business layer.
- Monitor and log API performance
These examples are based on **real interfaces from the shenyu-examples-http
project** and can be used and tested directly in a Shenyu environment.
-
diff --git a/shenyu-plugin/shenyu-plugin-mcp-server/pom.xml
b/shenyu-plugin/shenyu-plugin-mcp-server/pom.xml
index a29abdc65b..4fbf1d518e 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 any
corresponding
+ SDK compatibility documentation in
ShenyuStreamableHttpServerTransportProvider
+ -->
<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..dad414af28 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
@@ -95,6 +95,11 @@ public class ShenyuToolCallback implements ToolCallback {
*/
private static final String STREAMABLE_HTTP_PATH = "/streamablehttp";
+ /**
+ * Prefix used by McpSessionHelper for SDK compatibility failures.
+ */
+ private static final String SDK_COMPATIBILITY_ERROR_PREFIX = "SDK
COMPATIBILITY ERROR";
+
/**
* Regex pattern for template variable interpolation in tool inputs.
**/
@@ -761,20 +766,35 @@ 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 the session ID is blank or an SDK
compatibility issue blocks extraction
+ * @throws IllegalArgumentException if the exchange is missing required
session state
*/
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 (RuntimeException e) {
+ if (!isSdkCompatibilityError(e)) {
+ throw 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");
+ }
+
+ private boolean isSdkCompatibilityError(final RuntimeException exception) {
+ return exception instanceof IllegalStateException
+ && StringUtils.hasText(exception.getMessage())
+ &&
exception.getMessage().startsWith(SDK_COMPATIBILITY_ERROR_PREFIX);
}
/**
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..fe3c114f4c 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,97 @@ 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) {
+ clearReflectionFieldCache();
+ 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());
+ } catch (SecurityException e) {
+ clearReflectionFieldCache();
+ LOG.error("SDK COMPATIBILITY ERROR: Security manager blocked
reflection access. "
+ + "Field resolution failed: {}", e.getMessage());
+ } catch (RuntimeException e) {
+ clearReflectionFieldCache();
+ 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);
+ }
+ }
+
+ private static void clearReflectionFieldCache() {
+ asyncExchangeFieldCache = null;
+ sessionFieldCache = null;
+ fieldsResolved = false;
+ }
+
/**
* Get McpSyncServerExchange from ToolContext.
*
@@ -51,54 +141,94 @@ 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);
- if (Objects.isNull(session)) {
- throw new IllegalArgumentException("Session is required in
McpAsyncServerExchange");
- }
- McpServerSession mcpServerSession = (McpServerSession) session;
- return mcpServerSession.getId();
+ public static String getSessionId(final McpSyncServerExchange
mcpSyncServerExchange) {
+ return getSession(mcpSyncServerExchange).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;
}
}
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallbackTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallbackTest.java
index 39035071b9..69acb2848f 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallbackTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallbackTest.java
@@ -39,8 +39,11 @@ import org.springframework.web.server.ServerWebExchange;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@@ -148,10 +151,15 @@ class ShenyuToolCallbackTest {
.thenReturn(mcpSyncServerExchange);
mcpSessionHelperMock.when(() -> McpSessionHelper.getSessionId(any()))
.thenReturn("");
-
- assertThrows(RuntimeException.class, () -> {
+
+ final RuntimeException exception =
assertThrows(RuntimeException.class, () -> {
shenyuToolCallback.call("{}", toolContext);
});
+
+ assertEquals("Tool execution failed: Session ID is empty – it should
have been set earlier by handleMessageEndpoint",
+ exception.getMessage());
+ assertInstanceOf(IllegalStateException.class, exception.getCause());
+ assertFalse(exception.getMessage().contains("SDK compatibility"));
}
@Test
@@ -175,6 +183,49 @@ class ShenyuToolCallbackTest {
});
}
+ @Test
+ void testCallWithSdkCompatibilitySessionErrorAddsVersionContext() {
+ when(toolDefinition.name()).thenReturn("testTool");
+ shenyuToolCallback = new ShenyuToolCallback(toolDefinition);
+
+ final ToolContext toolContext = new ToolContext(new HashMap<>());
+ mcpSessionHelperMock.when(() ->
McpSessionHelper.getMcpSyncServerExchange(any()))
+ .thenReturn(mcpSyncServerExchange);
+ mcpSessionHelperMock.when(() -> McpSessionHelper.getSessionId(any()))
+ .thenThrow(new IllegalStateException("SDK COMPATIBILITY ERROR:
reflection access failed"));
+ mcpSessionHelperMock.when(McpSessionHelper::getSupportedSdkVersion)
+ .thenReturn("0.17.0");
+
+ final RuntimeException exception =
assertThrows(RuntimeException.class, () -> {
+ shenyuToolCallback.call("{}", toolContext);
+ });
+
+ assertTrue(exception.getMessage().contains("Tool execution failed:
Failed to extract session ID from MCP exchange."));
+ assertInstanceOf(IllegalStateException.class, exception.getCause());
+ assertTrue(exception.getCause().getMessage().contains("Tested SDK
version: 0.17.0."));
+ assertTrue(exception.getCause().getMessage().contains("Original error:
SDK COMPATIBILITY ERROR: reflection access failed"));
+ }
+
+ @Test
+ void testCallWithMissingSessionStateDoesNotWrapAsCompatibilityError() {
+ when(toolDefinition.name()).thenReturn("testTool");
+ shenyuToolCallback = new ShenyuToolCallback(toolDefinition);
+
+ final ToolContext toolContext = new ToolContext(new HashMap<>());
+ mcpSessionHelperMock.when(() ->
McpSessionHelper.getMcpSyncServerExchange(any()))
+ .thenReturn(mcpSyncServerExchange);
+ mcpSessionHelperMock.when(() -> McpSessionHelper.getSessionId(any()))
+ .thenThrow(new IllegalArgumentException("Session is required
in McpAsyncServerExchange"));
+
+ final RuntimeException exception =
assertThrows(RuntimeException.class, () -> {
+ shenyuToolCallback.call("{}", toolContext);
+ });
+
+ assertEquals("Tool execution failed: Session is required in
McpAsyncServerExchange", exception.getMessage());
+ assertInstanceOf(IllegalArgumentException.class, exception.getCause());
+ assertFalse(exception.getMessage().contains("SDK compatibility"));
+ }
+
@Test
void testCallWithValidSetup() throws Exception {
when(toolDefinition.name()).thenReturn("testTool");