This is an automated email from the ASF dual-hosted git repository.
sihengyu 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 c2efae14e7 fix: improve MCP server plugin path handling and backward
compatibility for argsPosition (#6293)
c2efae14e7 is described below
commit c2efae14e795655f3ec37a9e9e78d33bfeedf5dc
Author: aias00 <[email protected]>
AuthorDate: Wed Feb 25 19:37:25 2026 +0800
fix: improve MCP server plugin path handling and backward compatibility for
argsPosition (#6293)
* fix: improve MCP server plugin path handling and backward compatibility
for argsPosition
* fix: enhance MCP server plugin path validation and improve handling of
blank paths
* Update
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandler.java
Co-authored-by: Copilot <[email protected]>
* Update
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
Co-authored-by: Copilot <[email protected]>
* fix: streamline MCP server plugin path handling by removing redundant
checks
* fix: remove unused imports in ShenyuMcpServerManager
---------
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Yu Siheng <[email protected]>
---
.../mcp/generator/McpRequestConfigGenerator.java | 8 +-
.../shenyu/client/mcp/McpServiceEventListener.java | 5 +-
.../server/handler/McpServerPluginDataHandler.java | 72 ++++++----
.../mcp/server/manager/ShenyuMcpServerManager.java | 157 ++++++++++++++++-----
.../mcp/server/request/RequestConfigHelper.java | 12 +-
.../ShenyuSseServerTransportProvider.java | 11 +-
...henyuStreamableHttpServerTransportProvider.java | 1 +
.../mcp/server/McpServerPluginIntegrationTest.java | 6 +-
.../handler/McpServerPluginDataHandlerTest.java | 38 ++++-
.../server/manager/ShenyuMcpServerManagerTest.java | 13 +-
.../server/request/RequestConfigHelperTest.java | 11 ++
11 files changed, 257 insertions(+), 77 deletions(-)
diff --git
a/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java
b/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java
index b5c8ddf608..feea24fd41 100644
---
a/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java
+++
b/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-common/src/main/java/org/apache/shenyu/client/mcp/generator/McpRequestConfigGenerator.java
@@ -60,11 +60,10 @@ public class McpRequestConfigGenerator {
requestTemplate.addProperty(RequestTemplateConstants.METHOD_KEY,
methodType);
// argsPosition
+ JsonObject argsPosition = new JsonObject();
JsonObject methodTypeJson = method.getAsJsonObject(methodType);
JsonArray parameters =
methodTypeJson.getAsJsonArray(OpenApiConstants.OPEN_API_PATH_OPERATION_METHOD_PARAMETERS_KEY);
if (Objects.nonNull(parameters)) {
- JsonObject argsPosition = new JsonObject();
-
for (JsonElement parameter : parameters) {
JsonObject paramObj = parameter.getAsJsonObject();
@@ -77,8 +76,11 @@ public class McpRequestConfigGenerator {
argsPosition.addProperty(name, inValue);
}
}
- requestTemplate.add(RequestTemplateConstants.ARGS_POSITION_KEY,
argsPosition);
}
+ // Keep root-level argsPosition as canonical format used by gateway
parser.
+ root.add(RequestTemplateConstants.ARGS_POSITION_KEY,
argsPosition.deepCopy());
+ // Keep requestTemplate-level argsPosition for backward compatibility.
+ requestTemplate.add(RequestTemplateConstants.ARGS_POSITION_KEY,
argsPosition);
// argsToJsonBody
requestTemplate.addProperty(RequestTemplateConstants.BODY_JSON_KEY,
shenyuMcpRequestConfig.getBodyToJson());
diff --git
a/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java
b/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java
index 4b333b3a69..e9a15032ad 100644
---
a/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java
+++
b/shenyu-client/shenyu-client-mcp/shenyu-client-mcp-register/src/main/java/org/apache/shenyu/client/mcp/McpServiceEventListener.java
@@ -304,6 +304,9 @@ public class McpServiceEventListener extends
AbstractContextRefreshedEventListen
@Override
protected String buildApiSuperPath(final Class<?> clazz, final
ShenyuMcpTool beanShenyuClient) {
Server[] servers = beanShenyuClient.definition().servers();
+ if (servers.length == 0) {
+ return "";
+ }
if (servers.length != 1) {
log.warn("The shenyuMcp service supports only a single server
entry. Please ensure that only one server is configured");
}
@@ -363,7 +366,7 @@ public class McpServiceEventListener extends
AbstractContextRefreshedEventListen
validateClientConfig(shenyuMcpTool, url);
JsonObject openApiJson =
McpOpenApiGenerator.generateOpenApiJson(classShenyuClient, shenyuMcpTool, url);
McpToolsRegisterDTO mcpToolsRegisterDTO =
McpToolsRegisterDTOGenerator.generateRegisterDTO(shenyuMcpTool, openApiJson,
url, namespaceId);
- MetaDataRegisterDTO metaDataRegisterDTO = buildMetaDataDTO(bean,
classShenyuClient, superPath, clazz, method, namespaceId);
+ MetaDataRegisterDTO metaDataRegisterDTO = buildMetaDataDTO(bean,
classShenyuClient, url, clazz, method, namespaceId);
metaDataRegisterDTO.setEnabled(shenyuMcpTool.getEnable());
mcpToolsRegisterDTO.setMetaDataRegisterDTO(metaDataRegisterDTO);
return mcpToolsRegisterDTO;
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandler.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandler.java
index b660e9df84..be8a7af7f9 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandler.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandler.java
@@ -78,15 +78,14 @@ public class McpServerPluginDataHandler implements
PluginDataHandler {
return;
}
- // Get the URI from selector data
- String uri = selectorData.getConditionList().stream()
- .filter(condition ->
Constants.URI.equals(condition.getParamType()))
- .map(ConditionData::getParamValue)
- .findFirst()
- .orElse(null);
-
- String path = StringUtils.removeEnd(uri, SLASH);
- path = StringUtils.removeEnd(path, STAR);
+ String uri = extractSelectorUri(selectorData);
+ if (StringUtils.isBlank(uri)) {
+ return;
+ }
+ String path = normalizeSelectorPath(uri);
+ if (StringUtils.isBlank(path)) {
+ return;
+ }
ShenyuMcpServer shenyuMcpServer =
GsonUtils.getInstance().fromJson(StringUtils.isBlank(selectorData.getHandle())
? DEFAULT_MESSAGE_ENDPOINT : selectorData.getHandle(), ShenyuMcpServer.class);
shenyuMcpServer.setPath(path);
CACHED_SERVER.get().cachedHandle(
@@ -94,8 +93,8 @@ public class McpServerPluginDataHandler implements
PluginDataHandler {
shenyuMcpServer);
String messageEndpoint = shenyuMcpServer.getMessageEndpoint();
// Get or create McpServer for this URI
- if (StringUtils.isNotBlank(uri) &&
!shenyuMcpServerManager.hasMcpServer(uri)) {
- shenyuMcpServerManager.getOrCreateMcpServerTransport(uri,
messageEndpoint);
+ if (StringUtils.isNotBlank(path) &&
!shenyuMcpServerManager.hasMcpServer(path)) {
+ shenyuMcpServerManager.getOrCreateMcpServerTransport(path,
messageEndpoint);
}
if (StringUtils.isNotBlank(path)) {
shenyuMcpServerManager.getOrCreateStreamableHttpTransport(path +
STREAMABLE_HTTP_PATH);
@@ -108,31 +107,27 @@ public class McpServerPluginDataHandler implements
PluginDataHandler {
@Override
public void removeSelector(final SelectorData selectorData) {
+ if (Objects.isNull(selectorData) ||
Objects.isNull(selectorData.getId())) {
+ return;
+ }
UpstreamCacheManager.getInstance().removeByKey(selectorData.getId());
MetaDataCache.getInstance().clean();
CACHED_TOOL.get().removeHandle(CacheKeyUtils.INST.getKey(selectorData.getId(),
Constants.DEFAULT_RULE));
- // Remove the McpServer for this URI
- // First try to get URI from handle, then from condition list
- String uri = selectorData.getHandle();
- if (StringUtils.isBlank(uri)) {
- // Try to get URI from condition list
- uri = selectorData.getConditionList().stream()
- .filter(condition ->
Constants.URI.equals(condition.getParamType()))
- .map(ConditionData::getParamValue)
- .findFirst()
- .orElse(null);
- }
+ String path = normalizeSelectorPath(extractSelectorUri(selectorData));
CACHED_SERVER.get().removeHandle(selectorData.getId());
- if (StringUtils.isNotBlank(uri) &&
shenyuMcpServerManager.hasMcpServer(uri)) {
- shenyuMcpServerManager.removeMcpServer(uri);
+ if (StringUtils.isNotBlank(path) &&
shenyuMcpServerManager.hasMcpServer(path)) {
+ shenyuMcpServerManager.removeMcpServer(path);
}
}
@Override
public void handlerRule(final RuleData ruleData) {
+ if (Objects.isNull(ruleData)) {
+ return;
+ }
Optional.ofNullable(ruleData.getHandle()).ifPresent(s -> {
ShenyuMcpServerTool mcpServerTool =
GsonUtils.getInstance().fromJson(s, ShenyuMcpServerTool.class);
CACHED_TOOL.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData),
mcpServerTool);
@@ -145,7 +140,7 @@ public class McpServerPluginDataHandler implements
PluginDataHandler {
// Create JSON schema from parameters
String inputSchema =
JsonSchemaUtil.createParameterSchema(parameters);
ShenyuMcpServer server =
CACHED_SERVER.get().obtainHandle(ruleData.getSelectorId());
- if (Objects.nonNull(server)) {
+ if (Objects.nonNull(server) &&
StringUtils.isNotBlank(server.getPath())) {
shenyuMcpServerManager.addTool(server.getPath(),
StringUtils.isBlank(mcpServerTool.getName()) ?
ruleData.getName()
: mcpServerTool.getName(),
@@ -158,10 +153,15 @@ public class McpServerPluginDataHandler implements
PluginDataHandler {
@Override
public void removeRule(final RuleData ruleData) {
+ if (Objects.isNull(ruleData)) {
+ return;
+ }
Optional.ofNullable(ruleData.getHandle()).ifPresent(s -> {
CACHED_TOOL.get().removeHandle(CacheKeyUtils.INST.getKey(ruleData));
ShenyuMcpServer server =
CACHED_SERVER.get().obtainHandle(ruleData.getSelectorId());
- shenyuMcpServerManager.removeTool(server.getPath(),
ruleData.getName());
+ if (Objects.nonNull(server) &&
StringUtils.isNotBlank(server.getPath())) {
+ shenyuMcpServerManager.removeTool(server.getPath(),
ruleData.getName());
+ }
});
MetaDataCache.getInstance().clean();
}
@@ -171,4 +171,24 @@ public class McpServerPluginDataHandler implements
PluginDataHandler {
return PluginEnum.MCP_SERVER.getName();
}
+ private String extractSelectorUri(final SelectorData selectorData) {
+ if (Objects.isNull(selectorData) ||
CollectionUtils.isEmpty(selectorData.getConditionList())) {
+ return null;
+ }
+ return selectorData.getConditionList().stream()
+ .filter(condition ->
Constants.URI.equals(condition.getParamType()))
+ .map(ConditionData::getParamValue)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private String normalizeSelectorPath(final String selectorUri) {
+ if (StringUtils.isBlank(selectorUri)) {
+ return selectorUri;
+ }
+ String path = StringUtils.removeEnd(selectorUri, STAR);
+ path = StringUtils.removeEnd(path, SLASH);
+ return StringUtils.defaultIfBlank(path, SLASH);
+ }
+
}
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
index f60589a91a..432fe295a7 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java
@@ -42,6 +42,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.net.URI;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -172,7 +173,7 @@ public class ShenyuMcpServerManager {
* @return normalized path
*/
private String processPath(final String uri) {
- return normalizeServerPath(extractBasePath(uri));
+ return normalizeServerPath(uri);
}
/**
@@ -244,7 +245,7 @@ public class ShenyuMcpServerManager {
* Creates SSE transport provider.
*/
private ShenyuSseServerTransportProvider createSseTransport(final String
normalizedPath, final String messageEndPoint) {
- String messageEndpoint = normalizedPath + messageEndPoint;
+ String messageEndpoint = joinPath(normalizedPath, messageEndPoint);
ShenyuSseServerTransportProvider transportProvider =
ShenyuSseServerTransportProvider.builder()
.objectMapper(objectMapper)
.sseEndpoint(normalizedPath)
@@ -285,39 +286,17 @@ public class ShenyuMcpServerManager {
*/
private void registerRoutes(final String primaryPath, final String
secondaryPath,
final HandlerFunction<?> primaryHandler, final
HandlerFunction<?> secondaryHandler) {
- routeMap.put(primaryPath, primaryHandler);
- routeMap.put(primaryPath + "/**", primaryHandler);
+ String normalizedPrimaryPath = normalizeRoutePath(primaryPath);
+ routeMap.put(normalizedPrimaryPath, primaryHandler);
+ routeMap.put(normalizedPrimaryPath + "/**", primaryHandler);
if (Objects.nonNull(secondaryPath) &&
Objects.nonNull(secondaryHandler)) {
- routeMap.put(secondaryPath, secondaryHandler);
- routeMap.put(secondaryPath + "/**", secondaryHandler);
+ String normalizedSecondaryPath = normalizeRoutePath(secondaryPath);
+ routeMap.put(normalizedSecondaryPath, secondaryHandler);
+ routeMap.put(normalizedSecondaryPath + "/**", secondaryHandler);
}
}
- /**
- * Extract the base path from a URI by removing the /message suffix and
any sub-paths.
- *
- * @param uri The URI to extract base path from
- * @return The base path
- */
- private String extractBasePath(final String uri) {
- String basePath = uri;
-
- // Remove /message suffix if present
- if (basePath.endsWith("/message")) {
- basePath = basePath.substring(0, basePath.length() -
"/message".length());
- }
-
- // For sub-paths, extract the main MCP server path
- String[] pathSegments = basePath.split("/");
- if (pathSegments.length >= 2) {
- // Keep only the first two segments (empty + server-name)
- basePath = "/" + pathSegments[1];
- }
-
- return basePath;
- }
-
/**
* Check if a McpServer exists for the given URI.
*
@@ -390,7 +369,7 @@ public class ShenyuMcpServerManager {
*/
public synchronized void addTool(final String serverPath, final String
name, final String description,
final String requestTemplate, final String
inputSchema) {
- String normalizedPath =
normalizeServerPath(extractBasePath(serverPath));
+ String normalizedPath = processPath(serverPath);
// Remove existing tool first
try {
@@ -442,7 +421,7 @@ public class ShenyuMcpServerManager {
* @param name the tool name
*/
public void removeTool(final String serverPath, final String name) {
- String normalizedPath = normalizeServerPath(serverPath);
+ String normalizedPath = processPath(serverPath);
LOG.debug("Removing tool from shared server - name: {}, path: {}",
name, normalizedPath);
McpAsyncServer sharedServer = sharedServerMap.get(normalizedPath);
@@ -485,7 +464,7 @@ public class ShenyuMcpServerManager {
* @return Set of supported protocols
*/
public Set<String> getSupportedProtocols(final String serverPath) {
- String normalizedPath = normalizeServerPath(serverPath);
+ String normalizedPath = processPath(serverPath);
CompositeTransportProvider compositeTransport =
compositeTransportMap.get(normalizedPath);
return Objects.nonNull(compositeTransport) ?
compositeTransport.getSupportedProtocols() : new HashSet<>();
}
@@ -501,17 +480,119 @@ public class ShenyuMcpServerManager {
return null;
}
- String normalizedPath = path;
+ String normalizedPath = path.trim();
+ if (normalizedPath.isEmpty()) {
+ return "/";
+ }
- // Remove /streamablehttp suffix
- if (normalizedPath.endsWith("/streamablehttp")) {
- normalizedPath = normalizedPath.substring(0,
normalizedPath.length() - "/streamablehttp".length());
- LOG.debug("Normalized Streamable HTTP path from '{}' to '{}' for
shared server", path, normalizedPath);
+ try {
+ URI uri = URI.create(normalizedPath);
+ if (Objects.nonNull(uri.getScheme())) {
+ normalizedPath = uri.getRawPath();
+ }
+ } catch (IllegalArgumentException ignored) {
+ // Keep original input when it's not a full URI.
+ }
+
+ if (Objects.isNull(normalizedPath) || normalizedPath.isEmpty()) {
+ normalizedPath = "/";
+ }
+ if (!normalizedPath.startsWith("/")) {
+ normalizedPath = "/" + normalizedPath;
+ }
+ int queryStart = normalizedPath.indexOf('?');
+ if (queryStart >= 0) {
+ normalizedPath = normalizedPath.substring(0, queryStart);
+ }
+ int fragmentStart = normalizedPath.indexOf('#');
+ if (fragmentStart >= 0) {
+ normalizedPath = normalizedPath.substring(0, fragmentStart);
}
+ normalizedPath = normalizedPath.replaceAll("/{2,}", "/");
+ if (normalizedPath.endsWith("/**")) {
+ normalizedPath = normalizedPath.substring(0,
normalizedPath.length() - "/**".length());
+ }
+ normalizedPath = removeSuffix(normalizedPath, "/message");
+ normalizedPath = removeSuffix(normalizedPath, "/sse");
+ normalizedPath = removeSuffix(normalizedPath, "/streamablehttp");
+
+ if (normalizedPath.length() > 1 && normalizedPath.endsWith("/")) {
+ normalizedPath = normalizedPath.substring(0,
normalizedPath.length() - 1);
+ }
+ if (normalizedPath.isEmpty()) {
+ return "/";
+ }
return normalizedPath;
}
+ private String normalizeRoutePath(final String path) {
+ String routePath = Objects.isNull(path) ? "/" : path;
+ routePath = routePath.trim();
+ if (routePath.isEmpty()) {
+ return "/";
+ }
+
+ try {
+ URI uri = URI.create(routePath);
+ if (Objects.nonNull(uri.getScheme())) {
+ routePath = uri.getRawPath();
+ }
+ } catch (IllegalArgumentException ignored) {
+ // Keep original input when it's not a full URI.
+ }
+
+ if (Objects.isNull(routePath) || routePath.isEmpty()) {
+ routePath = "/";
+ }
+ if (!routePath.startsWith("/")) {
+ routePath = "/" + routePath;
+ }
+
+ int queryStart = routePath.indexOf('?');
+ if (queryStart >= 0) {
+ routePath = routePath.substring(0, queryStart);
+ }
+ int fragmentStart = routePath.indexOf('#');
+ if (fragmentStart >= 0) {
+ routePath = routePath.substring(0, fragmentStart);
+ }
+ routePath = routePath.replaceAll("/{2,}", "/");
+ if (routePath.length() > 1 && routePath.endsWith("/")) {
+ routePath = routePath.substring(0, routePath.length() - 1);
+ }
+ if (routePath.isEmpty()) {
+ return "/";
+ }
+ return routePath;
+ }
+
+ private String joinPath(final String basePath, final String subPath) {
+ String safeBase = normalizeRoutePath(basePath);
+ if (Objects.isNull(subPath) || subPath.trim().isEmpty()) {
+ return safeBase;
+ }
+ String safeSub = subPath.trim();
+ if (safeBase.endsWith("/") && safeSub.startsWith("/")) {
+ return safeBase + safeSub.substring(1);
+ }
+ if (!safeBase.endsWith("/") && !safeSub.startsWith("/")) {
+ return safeBase + "/" + safeSub;
+ }
+ return safeBase + safeSub;
+ }
+
+ private String removeSuffix(final String value, final String suffix) {
+ if (Objects.isNull(value) || Objects.isNull(suffix) ||
suffix.isEmpty()) {
+ return value;
+ }
+ if (value.endsWith(suffix)) {
+ String result = value.substring(0, value.length() -
suffix.length());
+ return result.isEmpty() ? "/" : result;
+ }
+ return value;
+ }
+
/**
* Composite transport provider that delegates to multiple transport
implementations.
* Enhanced with protocol-aware session management and improved error
handling.
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelper.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelper.java
index 556b0cc490..6f60d661c8 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelper.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelper.java
@@ -20,6 +20,8 @@ package org.apache.shenyu.plugin.mcp.server.request;
import com.google.gson.JsonObject;
import org.apache.shenyu.common.utils.GsonUtils;
+import java.util.Objects;
+
/**
* Helper class for parsing and handling requestConfig.
*/
@@ -51,7 +53,15 @@ public class RequestConfigHelper {
* @return the argument position json object
*/
public JsonObject getArgsPosition() {
- return configJson.has("argsPosition") ?
configJson.getAsJsonObject("argsPosition") : new JsonObject();
+ if (configJson.has("argsPosition")) {
+ return configJson.getAsJsonObject("argsPosition");
+ }
+ // Backward compatibility for configs generated with nested
argsPosition.
+ JsonObject requestTemplate = getRequestTemplate();
+ if (Objects.nonNull(requestTemplate) &&
requestTemplate.has("argsPosition")) {
+ return requestTemplate.getAsJsonObject("argsPosition");
+ }
+ return new JsonObject();
}
/**
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuSseServerTransportProvider.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuSseServerTransportProvider.java
index b0bccdf0ef..d1e608d14c 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuSseServerTransportProvider.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuSseServerTransportProvider.java
@@ -27,6 +27,7 @@ import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
+import org.apache.shenyu.plugin.mcp.server.holder.ShenyuMcpExchangeHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@@ -210,7 +211,12 @@ public class ShenyuSseServerTransportProvider implements
McpServerTransportProvi
return Flux.fromIterable(sessions
.values())
.doFirst(() -> LOGGER.debug("Initiating graceful shutdown with
{} active sessions", sessions.size()))
- .flatMap(McpServerSession::closeGracefully).then();
+ .flatMap(McpServerSession::closeGracefully)
+ .then()
+ .doFinally(signalType -> {
+ sessions.keySet().forEach(ShenyuMcpExchangeHolder::remove);
+ sessions.clear();
+ });
}
/**
@@ -259,6 +265,7 @@ public class ShenyuSseServerTransportProvider implements
McpServerTransportProvi
sink.onCancel(() -> {
LOGGER.debug("Session {} cancelled", sessionId);
sessions.remove(sessionId);
+ ShenyuMcpExchangeHolder.remove(sessionId);
});
} catch (Exception e) {
LOGGER.error("Error creating SSE session", e);
@@ -312,11 +319,13 @@ public class ShenyuSseServerTransportProvider implements
McpServerTransportProvi
sink.onCancel(() -> {
LOGGER.info("Session {} cancelled by client", sessionId);
sessions.remove(sessionId);
+ ShenyuMcpExchangeHolder.remove(sessionId);
});
sink.onDispose(() -> {
LOGGER.info("Session {} disposed", sessionId);
sessions.remove(sessionId);
+ ShenyuMcpExchangeHolder.remove(sessionId);
});
} catch (Exception e) {
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
index f67c9c8d51..540d834853 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
@@ -763,6 +763,7 @@ public class ShenyuStreamableHttpServerTransportProvider
implements McpServerTra
public void removeSession(final String sessionId) {
final McpServerSession removedSession = sessions.remove(sessionId);
final StreamableHttpSessionTransport removedTransport =
sessionTransports.remove(sessionId);
+ ShenyuMcpExchangeHolder.remove(sessionId);
if (Objects.nonNull(removedSession) ||
Objects.nonNull(removedTransport)) {
LOGGER.debug("Removed session and transport: {}", sessionId);
}
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginIntegrationTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginIntegrationTest.java
index 1839f88d3d..11a157c2ae 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginIntegrationTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/McpServerPluginIntegrationTest.java
@@ -93,7 +93,7 @@ class McpServerPluginIntegrationTest {
dataHandler.handlerSelector(selectorData);
// Verify that the server can now route to this path
- assertTrue(mcpServerManager.hasMcpServer("/mcp"));
+ assertTrue(mcpServerManager.hasMcpServer("/mcp/test"));
assertTrue(mcpServerManager.canRoute("/mcp/test/sse"));
assertTrue(mcpServerManager.canRoute("/mcp/test/message"));
assertTrue(mcpServerManager.canRoute("/mcp/test/anything"));
@@ -160,7 +160,7 @@ class McpServerPluginIntegrationTest {
// Verify all tools are handled (this tests the fix for the multiple
tools issue)
assertTrue(mcpServerManager.canRoute("/mcp/api/sse"));
- assertTrue(mcpServerManager.hasMcpServer("/mcp"));
+ assertTrue(mcpServerManager.hasMcpServer("/mcp/api"));
// Test that the plugin can handle requests (setup verification only)
// Mock setup removed since we're not executing the plugin
@@ -188,7 +188,7 @@ class McpServerPluginIntegrationTest {
mcpServerManager.getOrCreateStreamableHttpTransport("/mcp/stream/streamablehttp");
assertTrue(mcpServerManager.canRoute("/mcp/stream/streamablehttp"));
- Set<String> protocols = mcpServerManager.getSupportedProtocols("/mcp");
+ Set<String> protocols =
mcpServerManager.getSupportedProtocols("/mcp/stream");
assertTrue(protocols.contains("Streamable HTTP"));
}
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandlerTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandlerTest.java
index 569a96e41a..ad7f35e4ea 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandlerTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandlerTest.java
@@ -86,6 +86,22 @@ class McpServerPluginDataHandlerTest {
verify(shenyuMcpServerManager,
never()).getOrCreateMcpServerTransport(anyString(), anyString());
}
+ @Test
+ void testHandlerSelectorWithoutUriCondition() {
+ ConditionData condition = new ConditionData();
+ condition.setParamType(ParamTypeEnum.HEADER.getName());
+ condition.setParamValue("x-session-id");
+
+ SelectorData selectorData = new SelectorData();
+ selectorData.setId("selector-no-uri");
+ selectorData.setConditionList(Arrays.asList(condition));
+
+ dataHandler.handlerSelector(selectorData);
+
+ verify(shenyuMcpServerManager,
never()).getOrCreateMcpServerTransport(anyString(), anyString());
+ verify(shenyuMcpServerManager,
never()).getOrCreateStreamableHttpTransport(anyString());
+ }
+
@Test
void testHandlerSelectorWithValidData() {
ConditionData condition = new ConditionData();
@@ -102,7 +118,7 @@ class McpServerPluginDataHandlerTest {
dataHandler.handlerSelector(selectorData);
-
verify(shenyuMcpServerManager).getOrCreateMcpServerTransport(eq("/mcp/test/**"),
eq("/message"));
+
verify(shenyuMcpServerManager).getOrCreateMcpServerTransport(eq("/mcp/test"),
eq("/message"));
}
@Test
@@ -138,7 +154,7 @@ class McpServerPluginDataHandlerTest {
dataHandler.removeSelector(selectorData);
- verify(shenyuMcpServerManager).removeMcpServer(eq("/mcp/test/**"));
+ verify(shenyuMcpServerManager).removeMcpServer(eq("/mcp/test"));
}
@Test
@@ -170,6 +186,24 @@ class McpServerPluginDataHandlerTest {
verify(shenyuMcpServerManager, never()).addTool(anyString(),
anyString(), anyString(), anyString(), anyString());
}
+ @Test
+ void testHandlerRuleWithBlankServerPath() {
+ RuleData ruleData = new RuleData();
+ ruleData.setId("rule-blank-path");
+ ruleData.setSelectorId("selector-blank-path");
+ ruleData.setName("testTool");
+ ruleData.setHandle("{\"name\":\"testTool\",\"description\":\"A test
tool\",\"requestConfig\":\"{\\\"url\\\":\\\"/test\\\",\\\"method\\\":\\\"GET\\\"}\",\"parameters\":[]}");
+
+ org.apache.shenyu.plugin.mcp.server.model.ShenyuMcpServer server =
+ new
org.apache.shenyu.plugin.mcp.server.model.ShenyuMcpServer();
+ server.setPath("");
+
McpServerPluginDataHandler.CACHED_SERVER.get().cachedHandle("selector-blank-path",
server);
+
+ dataHandler.handlerRule(ruleData);
+
+ verify(shenyuMcpServerManager, never()).addTool(anyString(),
anyString(), anyString(), anyString(), anyString());
+ }
+
@Test
void testRemoveRule() {
RuleData ruleData = new RuleData();
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManagerTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManagerTest.java
index 7668501355..62554bc18a 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManagerTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManagerTest.java
@@ -154,8 +154,7 @@ class ShenyuMcpServerManagerTest {
shenyuMcpServerManager.getOrCreateStreamableHttpTransport(uri);
- // Use base path since that's what the manager uses internally
- Set<String> protocols =
shenyuMcpServerManager.getSupportedProtocols("/mcp");
+ Set<String> protocols =
shenyuMcpServerManager.getSupportedProtocols("/mcp/test");
assertNotNull(protocols);
assertTrue(protocols.contains("Streamable HTTP"));
}
@@ -170,4 +169,14 @@ class ShenyuMcpServerManagerTest {
assertTrue(shenyuMcpServerManager.hasMcpServer(uri));
assertTrue(shenyuMcpServerManager.hasMcpServer("/mcp/test"));
}
+
+ @Test
+ void testRegisterRouteWithFullUriAndQuery() {
+ String uri =
"http://localhost:9195/mcp/test/streamablehttp?debug=true#anchor";
+
+ shenyuMcpServerManager.getOrCreateStreamableHttpTransport(uri);
+
+
assertTrue(shenyuMcpServerManager.canRoute("/mcp/test/streamablehttp"));
+ assertTrue(shenyuMcpServerManager.hasMcpServer("/mcp/test"));
+ }
}
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelperTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelperTest.java
index 520efbdbb2..aa12ad11f3 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelperTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelperTest.java
@@ -62,6 +62,17 @@ class RequestConfigHelperTest {
assertEquals("body", argsPosition.get("email").getAsString());
}
+ @Test
+ void testArgsPositionCompatibilityWithNestedFormat() {
+ String configStr = "{\"requestTemplate\":{\"url\":\"/api/users\","
+ +
"\"method\":\"POST\",\"argsPosition\":{\"name\":\"body\",\"email\":\"body\"}}}";
+ RequestConfigHelper helper = new RequestConfigHelper(configStr);
+
+ JsonObject argsPosition = helper.getArgsPosition();
+ assertEquals("body", argsPosition.get("name").getAsString());
+ assertEquals("body", argsPosition.get("email").getAsString());
+ }
+
@Test
void testPathParameterBuilding() {
JsonObject argsPosition = new JsonObject();