From dcbc652d7620d8b64a0236683999df9a0fa1164d Mon Sep 17 00:00:00 2001 From: liuhy Date: Mon, 9 Feb 2026 19:56:57 +0800 Subject: [PATCH 1/6] fix: improve MCP server plugin path handling and backward compatibility for argsPosition --- .../generator/McpRequestConfigGenerator.java | 8 +- .../client/mcp/McpServiceEventListener.java | 5 +- .../handler/McpServerPluginDataHandler.java | 64 +++++---- .../manager/ShenyuMcpServerManager.java | 132 +++++++++++++----- .../server/request/RequestConfigHelper.java | 12 +- .../ShenyuSseServerTransportProvider.java | 11 +- ...StreamableHttpServerTransportProvider.java | 1 + .../McpServerPluginIntegrationTest.java | 6 +- .../McpServerPluginDataHandlerTest.java | 4 +- .../manager/ShenyuMcpServerManagerTest.java | 3 +- .../request/RequestConfigHelperTest.java | 11 ++ 11 files changed, 181 insertions(+), 76 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 b5c8ddf60868..feea24fd4167 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 static JsonObject generateRequestConfig(final JsonObject openApiJson, fin 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 static JsonObject generateRequestConfig(final JsonObject openApiJson, fin 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 4b333b3a69e6..e9a15032ad1b 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 @@ private static String concatPaths(final String path1, final String path2) { @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 @@ private McpToolsRegisterDTO buildMcpToolsRegisterDTO(final Object bean, final Cl 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 b660e9df84bc..d80a989ef816 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,8 @@ public void handlerSelector(final SelectorData selectorData) { 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); + String path = normalizeSelectorPath(uri); 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 +87,8 @@ public void handlerSelector(final SelectorData selectorData) { 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 +101,27 @@ public void handlerSelector(final SelectorData selectorData) { @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); @@ -158,10 +147,15 @@ public void handlerRule(final RuleData ruleData) { @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 +165,24 @@ public String pluginNamed() { 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 e31493d58747..3c2ad54e912a 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 @@ -43,6 +43,7 @@ import java.util.Set; import java.util.HashSet; import java.util.Collections; +import java.net.URI; /** * Enhanced Manager for MCP servers supporting shared server instances across multiple transport protocols. @@ -151,7 +152,7 @@ private T getOrCreateTransport(final String normalizedPath, final String pro * @return normalized path */ private String processPath(final String uri) { - return normalizeServerPath(extractBasePath(uri)); + return normalizeServerPath(uri); } /** @@ -223,7 +224,7 @@ private McpAsyncServer getOrCreateSharedServer(final String normalizedPath) { * 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) @@ -263,39 +264,17 @@ private ShenyuStreamableHttpServerTransportProvider createStreamableHttpTranspor */ 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. * @@ -368,7 +347,7 @@ public void removeMcpServer(final String uri) { */ 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 { @@ -420,7 +399,7 @@ public synchronized void addTool(final String serverPath, final String name, fin * @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); @@ -463,7 +442,7 @@ private boolean isToolNotFoundError(final Throwable error) { * @return Set of supported protocols */ public Set 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<>(); } @@ -479,17 +458,94 @@ private String normalizeServerPath(final String path) { return null; } - String normalizedPath = path; + String normalizedPath = path.trim(); + if (normalizedPath.isEmpty()) { + return "/"; + } + + 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); + } - // 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); + 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 "/"; + } + if (!routePath.startsWith("/")) { + routePath = "/" + routePath; + } + routePath = routePath.replaceAll("/{2,}", "/"); + if (routePath.length() > 1 && routePath.endsWith("/")) { + routePath = routePath.substring(0, routePath.length() - 1); + } + 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 556b0cc490ec..6f60d661c844 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 @@ 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 JsonObject getRequestTemplate() { * @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 b0bccdf0ef47..d1e608d14c66 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.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 Mono closeGracefully() { 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 Mono handleSseConnection(final ServerRequest request) { 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 Flux> createSseFlux(final ServerRequest request) { 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 31fa494bb3cc..879482fd15f9 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 @@ -667,6 +667,7 @@ private String extractSessionId(final ServerRequest request) { 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 1839f88d3ddc..11a157c2ae18 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 @@ void testCompleteWorkflowFromSelectorToExecution() { 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 @@ void testMultipleToolsScenario() { // 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 @@ void testStreamableHttpProtocol() { mcpServerManager.getOrCreateStreamableHttpTransport("/mcp/stream/streamablehttp"); assertTrue(mcpServerManager.canRoute("/mcp/stream/streamablehttp")); - Set protocols = mcpServerManager.getSupportedProtocols("/mcp"); + Set 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 569a96e41abd..ea5fa23e2830 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 @@ -102,7 +102,7 @@ void testHandlerSelectorWithValidData() { dataHandler.handlerSelector(selectorData); - verify(shenyuMcpServerManager).getOrCreateMcpServerTransport(eq("/mcp/test/**"), eq("/message")); + verify(shenyuMcpServerManager).getOrCreateMcpServerTransport(eq("/mcp/test"), eq("/message")); } @Test @@ -138,7 +138,7 @@ void testRemoveSelector() { dataHandler.removeSelector(selectorData); - verify(shenyuMcpServerManager).removeMcpServer(eq("/mcp/test/**")); + verify(shenyuMcpServerManager).removeMcpServer(eq("/mcp/test")); } @Test 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 766850135591..07fd6a9c7dc2 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 @@ void testGetSupportedProtocolsForStreamableHttp() { shenyuMcpServerManager.getOrCreateStreamableHttpTransport(uri); - // Use base path since that's what the manager uses internally - Set protocols = shenyuMcpServerManager.getSupportedProtocols("/mcp"); + Set protocols = shenyuMcpServerManager.getSupportedProtocols("/mcp/test"); assertNotNull(protocols); assertTrue(protocols.contains("Streamable HTTP")); } 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 520efbdbb2bd..aa12ad11f3cc 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 @@ void testPostRequestWithJsonBody() { 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(); From 31c9f538f9fdc88ad24558fca230085980fd57bc Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 10 Feb 2026 10:39:00 +0800 Subject: [PATCH 2/6] fix: enhance MCP server plugin path validation and improve handling of blank paths --- .../handler/McpServerPluginDataHandler.java | 8 ++++- .../manager/ShenyuMcpServerManager.java | 24 +++++++++++++ .../McpServerPluginDataHandlerTest.java | 34 +++++++++++++++++++ .../manager/ShenyuMcpServerManagerTest.java | 10 ++++++ 4 files changed, 75 insertions(+), 1 deletion(-) 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 d80a989ef816..be8a7af7f9eb 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 @@ -79,7 +79,13 @@ public void handlerSelector(final SelectorData selectorData) { } 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( @@ -134,7 +140,7 @@ public void handlerRule(final RuleData ruleData) { // 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(), 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 3c2ad54e912a..aa889100cb92 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 @@ -510,13 +510,37 @@ private String normalizeRoutePath(final String path) { 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; } 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 ea5fa23e2830..ad7f35e4ea7b 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 @@ void testHandlerSelectorWithEmptyConditions() { 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(); @@ -170,6 +186,24 @@ void testHandlerRuleWithNullHandle() { 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 07fd6a9c7dc2..62554bc18a8a 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 @@ -169,4 +169,14 @@ void testNormalizeServerPathWithStreamableHttp() { 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")); + } } From 754437853c4a510731c9a24c2a5be861c74dd841 Mon Sep 17 00:00:00 2001 From: aias00 Date: Tue, 10 Feb 2026 10:56:38 +0800 Subject: [PATCH 3/6] Update shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/handler/McpServerPluginDataHandler.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../mcp/server/handler/McpServerPluginDataHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 be8a7af7f9eb..33059fbd3ec1 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 @@ -81,8 +81,16 @@ public void handlerSelector(final SelectorData selectorData) { String uri = extractSelectorUri(selectorData); if (StringUtils.isBlank(uri)) { return; + String uri = extractSelectorUri(selectorData); + if (StringUtils.isBlank(uri)) { + // No valid URI condition found; do not cache or create transports. + return; } String path = normalizeSelectorPath(uri); + if (StringUtils.isBlank(path)) { + // Normalization did not yield a usable path; abort handling. + return; + } if (StringUtils.isBlank(path)) { return; } From 823830cf43943eacf57365cb96c2f6420a9453b8 Mon Sep 17 00:00:00 2001 From: aias00 Date: Tue, 10 Feb 2026 10:57:17 +0800 Subject: [PATCH 4/6] Update shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../manager/ShenyuMcpServerManager.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 aa889100cb92..777d8b2c1b1b 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 @@ -520,12 +520,35 @@ private String normalizeRoutePath(final String path) { // Keep original input when it's not a full URI. } + if (Objects.isNull(routePath) || routePath.isEmpty()) { + routePath = "/"; + } + + // Handle full URIs similarly to normalizeServerPath. + 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); + } int queryStart = routePath.indexOf('?'); if (queryStart >= 0) { routePath = routePath.substring(0, queryStart); From a92191f3905853dfbefff45025af0771e05aa492 Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 10 Feb 2026 15:48:20 +0800 Subject: [PATCH 5/6] fix: streamline MCP server plugin path handling by removing redundant checks --- .../handler/McpServerPluginDataHandler.java | 8 ------- .../manager/ShenyuMcpServerManager.java | 22 ------------------- 2 files changed, 30 deletions(-) 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 33059fbd3ec1..be8a7af7f9eb 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 @@ -81,16 +81,8 @@ public void handlerSelector(final SelectorData selectorData) { String uri = extractSelectorUri(selectorData); if (StringUtils.isBlank(uri)) { return; - String uri = extractSelectorUri(selectorData); - if (StringUtils.isBlank(uri)) { - // No valid URI condition found; do not cache or create transports. - return; } String path = normalizeSelectorPath(uri); - if (StringUtils.isBlank(path)) { - // Normalization did not yield a usable path; abort handling. - return; - } if (StringUtils.isBlank(path)) { return; } 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 777d8b2c1b1b..527f365daebc 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 @@ -520,20 +520,6 @@ private String normalizeRoutePath(final String path) { // Keep original input when it's not a full URI. } - if (Objects.isNull(routePath) || routePath.isEmpty()) { - routePath = "/"; - } - - // Handle full URIs similarly to normalizeServerPath. - 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 = "/"; } @@ -541,14 +527,6 @@ private String normalizeRoutePath(final String path) { 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); - } int queryStart = routePath.indexOf('?'); if (queryStart >= 0) { routePath = routePath.substring(0, queryStart); From 169ff77d2c36361e5c71c7e8b1e93859f5c7aba0 Mon Sep 17 00:00:00 2001 From: liuhy Date: Wed, 25 Feb 2026 18:35:02 +0800 Subject: [PATCH 6/6] fix: remove unused imports in ShenyuMcpServerManager --- .../plugin/mcp/server/manager/ShenyuMcpServerManager.java | 2 -- 1 file changed, 2 deletions(-) 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 09989ae49561..432fe295a749 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,8 +42,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.HashSet; -import java.util.Collections; import java.net.URI; import java.util.concurrent.ConcurrentHashMap;