Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,23 @@ 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);
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(
selectorData.getId(),
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);
Expand All @@ -108,31 +107,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);
Comment on lines 127 to 133
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handlerRule now guards against ruleData == null, but it can still call addTool(server.getPath(), ...) later with a null/blank server.getPath() (e.g., selector had no URI condition or cached server path is empty). That can throw inside ShenyuMcpServerManager (ConcurrentHashMap null key). Consider also gating tool registration on server != null && StringUtils.isNotBlank(server.getPath()) (consistent with the removeRule check).

Copilot uses AI. Check for mistakes.
Expand All @@ -145,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(),
Expand All @@ -158,10 +153,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();
}
Expand All @@ -171,4 +171,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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.net.URI;
import java.util.concurrent.ConcurrentHashMap;

/**
Expand Down Expand Up @@ -172,7 +173,7 @@ private <T> T getOrCreateTransport(final String normalizedPath, final String pro
* @return normalized path
*/
private String processPath(final String uri) {
return normalizeServerPath(extractBasePath(uri));
return normalizeServerPath(uri);
}

/**
Expand Down Expand Up @@ -244,7 +245,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)
Expand Down Expand Up @@ -285,39 +286,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.
*
Expand Down Expand Up @@ -390,7 +369,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 {
Expand Down Expand Up @@ -442,7 +421,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);
Expand Down Expand Up @@ -485,7 +464,7 @@ private boolean isToolNotFoundError(final Throwable error) {
* @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<>();
}
Expand All @@ -501,17 +480,119 @@ private String normalizeServerPath(final String path) {
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.
Expand Down
Loading
Loading