moremind commented on code in PR #5999:
URL: https://github.com/apache/shenyu/pull/5999#discussion_r2192731416


##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.mcp.server.manager;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.plugin.mcp.server.callback.ShenyuToolCallback;
+import org.apache.shenyu.plugin.mcp.server.definition.ShenyuToolDefinition;
+import 
org.apache.shenyu.plugin.mcp.server.transport.ShenyuSseServerTransportProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.mcp.McpToolUtils;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.reactive.function.server.HandlerFunction;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manager for MCP servers. Maps URIs to McpServer instances.
+ */
+@Component
+public class ShenyuMcpServerManager {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ShenyuMcpServerManager.class);
+
+    private static final String SLASH = "/";
+
+    /**
+     * AntPathMatcher for pattern matching.
+     */
+    private final AntPathMatcher pathMatcher = new AntPathMatcher();
+
+    /**
+     * Map to store URI to McpServer mapping.
+     */
+    private final Map<String, McpAsyncServer> mcpServerMap = new 
ConcurrentHashMap<>();
+
+    /**
+     * Map to store URI to McpServer mapping.
+     */
+    private final Map<String, ShenyuSseServerTransportProvider> 
mcpServerTransportMap = new ConcurrentHashMap<>();
+
+    private final Map<String, HandlerFunction<?>> routeMap = new 
ConcurrentHashMap<>();
+
+    /**
+     * Get or create a McpServer instance for the given URI.
+     * Uses pattern matching to find the best matching server.
+     *
+     * @param uri The URI to create or get a McpServer for
+     * @return The McpServer for the URI
+     */
+    public ShenyuSseServerTransportProvider 
getOrCreateMcpServerTransport(final String uri) {
+        // First try exact match
+        ShenyuSseServerTransportProvider transport = 
mcpServerTransportMap.get(uri);
+        if (Objects.nonNull(transport)) {
+            return transport;
+        }
+
+        // Then try to find existing transport that matches the pattern
+        String basePath = extractBasePath(uri);
+        transport = mcpServerTransportMap.get(basePath);
+        if (Objects.nonNull(transport)) {
+            LOG.debug("Found existing transport for base path '{}' for URI 
'{}'", basePath, uri);
+            return transport;
+        }
+
+        // Create new transport for the base path
+        return mcpServerTransportMap.computeIfAbsent(basePath, 
this::createMcpServerTransport);
+    }
+
+    /**
+     * 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
+        // This assumes MCP server paths don't contain multiple levels
+        // You may need to adjust this logic based on your specific URL 
structure
+        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.
+     *
+     * @param uri The URI to check
+     * @return true if a McpServer exists, false otherwise
+     */
+    public boolean hasMcpServer(final String uri) {
+        return mcpServerMap.containsKey(uri);
+    }
+
+    /**
+     * Check if a McpServer can route requests for the given URI.
+     * Uses AntPathMatcher to support pattern matching.
+     *
+     * @param uri The URI to check
+     * @return true if the URI is routable, false otherwise
+     */
+    public boolean canRoute(final String uri) {
+        // First try exact match for performance
+        if (routeMap.containsKey(uri)) {
+            return true;
+        }
+
+        // Then try pattern matching for each registered pattern
+        for (String pattern : routeMap.keySet()) {
+            if (pathMatcher.match(pattern, uri)) {
+                LOG.debug("URI '{}' matches pattern '{}'", uri, pattern);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Remove a McpServer for the given URI.
+     *
+     * @param uri The URI to remove the McpServer for
+     */
+    public void removeMcpServer(final String uri) {
+        LOG.info("Removed McpServer for URI: {}", uri);
+        ShenyuSseServerTransportProvider transportProvider = 
mcpServerTransportMap.remove(uri);
+        if (Objects.nonNull(transportProvider)) {
+            transportProvider
+                    .closeGracefully()
+                    .doOnSuccess(aVoid -> LOG.info("Successfully closed 
McpServer for URI: {}", uri))
+                    .doOnError(e -> LOG.error("Error closing McpServer for 
URI: {}", uri, e))
+                    .subscribe();
+        }
+        mcpServerMap.remove(uri);
+    }
+
+    /**
+     * Create a new McpServer for the given URI.
+     *
+     * @param uri The URI to create a McpServer for
+     * @return The McpAsyncServer for the new McpServer
+     */
+    private ShenyuSseServerTransportProvider createMcpServerTransport(final 
String uri) {
+        String path = StringUtils.removeEnd(uri, SLASH);
+        LOG.info("Creating new McpServer for URI: {}", path);
+        String messageEndpoint = path + "/message";

Review Comment:
   same question



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/callback/ShenyuToolCallback.java:
##########
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.mcp.server.callback;
+
+import com.google.common.collect.Maps;
+import com.google.gson.JsonObject;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.dto.MetaData;
+import org.apache.shenyu.common.enums.RpcTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.api.context.ShenyuContext;
+import org.apache.shenyu.plugin.base.cache.MetaDataCache;
+import org.apache.shenyu.plugin.mcp.server.definition.ShenyuToolDefinition;
+import org.apache.shenyu.plugin.mcp.server.holder.ShenyuMcpExchangeHolder;
+import org.apache.shenyu.plugin.mcp.server.request.BodyWriterExchange;
+import org.apache.shenyu.plugin.mcp.server.request.RequestConfig;
+import org.apache.shenyu.plugin.mcp.server.request.RequestConfigHelper;
+import org.apache.shenyu.plugin.mcp.server.response.ShenyuMcpResponseDecorator;
+import org.apache.shenyu.plugin.mcp.server.session.McpSessionHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+public class ShenyuToolCallback implements ToolCallback {
+    
+    private static final Logger LOG = 
LoggerFactory.getLogger(ShenyuToolCallback.class);
+    
+    private static final int DEFAULT_TIMEOUT_SECONDS = 30;
+    
+    private final ToolDefinition toolDefinition;
+    
+    public ShenyuToolCallback(final ToolDefinition toolDefinition) {
+        this.toolDefinition = Objects.requireNonNull(toolDefinition, 
"ToolDefinition cannot be null");
+    }
+    
+    @NonNull
+    @Override
+    public ToolDefinition getToolDefinition() {
+        return this.toolDefinition;
+    }
+    
+    @NonNull
+    @Override
+    public String call(@NonNull final String input) {
+        return call(input, new ToolContext(Maps.newHashMap()));
+    }
+    
+    @NonNull
+    @Override
+    public String call(@NonNull final String input, final ToolContext 
toolContext) {
+        Objects.requireNonNull(input, "Input cannot be null");
+        Objects.requireNonNull(toolContext, "ToolContext cannot be null");
+        
+        try {
+            // Get MCP session information
+            final McpSyncServerExchange mcpSyncServerExchange = 
getMcpSyncServerExchange(toolContext);
+            final String sessionId = getSessionId(mcpSyncServerExchange);
+            
+            // Get tool definition and request configuration
+            final ShenyuToolDefinition shenyuToolDefinition = 
validateAndGetToolDefinition();
+            final String configStr = 
validateAndGetRequestConfig(shenyuToolDefinition);
+            
+            // Get original exchange object
+            final ServerWebExchange originExchange = 
ShenyuMcpExchangeHolder.get(sessionId);
+            if (Objects.isNull(originExchange)) {
+                throw new IllegalStateException("No exchange found for 
sessionId: " + sessionId);
+            }
+            ShenyuPluginChain chain = 
originExchange.getAttribute(Constants.CHAIN);
+            Assert.notNull(chain, "ShenyuPluginChain cannot be null");
+            
+            final CompletableFuture<String> future = new CompletableFuture<>();
+            
+            // Build new exchange object
+            final ServerWebExchange decoratedExchange = 
buildExchange(originExchange, future, sessionId, configStr, input);
+            
+            LOG.info("[MCP] Executing plugin chain for session: {}", 
sessionId);
+            chain.execute(decoratedExchange).doOnSubscribe(s -> 
LOG.info("[MCP] Subscribed to plugin chain for session: {}", 
sessionId)).doOnError(e -> {
+                LOG.error("[MCP] Error processing request for session: {}", 
sessionId, e);
+                future.completeExceptionally(e);
+            }).doOnSuccess(v -> LOG.info("[MCP] Plugin chain completed 
successfully for session: {}", sessionId)).doOnCancel(() -> {
+                LOG.warn("[MCP] Plugin chain was cancelled for session: {}", 
sessionId);
+                future.completeExceptionally(new RuntimeException("Request was 
cancelled"));
+            }).subscribe();
+            
+            try {
+                return future.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            } catch (Exception e) {
+                LOG.error("[MCP] Error waiting for response for session: {}", 
sessionId, e);
+                throw new RuntimeException("Error waiting for response: " + 
e.getMessage(), e);
+            }
+        } catch (Exception e) {
+            LOG.error("Failed to process request: {}", e.getMessage(), e);
+            throw new RuntimeException("Failed to process request: " + 
e.getMessage(), e);
+        }
+    }
+    
+    private McpSyncServerExchange getMcpSyncServerExchange(final ToolContext 
toolContext) {
+        final McpSyncServerExchange exchange = 
McpSessionHelper.getMcpSyncServerExchange(toolContext);
+        if (Objects.isNull(exchange)) {
+            throw new IllegalStateException("Failed to get MCP sync server 
exchange");
+        }
+        return exchange;
+    }
+    
+    private String getSessionId(final McpSyncServerExchange exchange) throws 
NoSuchFieldException, IllegalAccessException {
+        final String sessionId = McpSessionHelper.getSessionId(exchange);
+        if (!StringUtils.hasText(sessionId)) {
+            throw new IllegalStateException("Session ID cannot be empty");
+        }
+        return sessionId;
+    }
+    
+    private ShenyuToolDefinition validateAndGetToolDefinition() {
+        if (!(this.toolDefinition instanceof ShenyuToolDefinition)) {
+            throw new IllegalStateException("Invalid tool definition type");
+        }
+        return (ShenyuToolDefinition) this.toolDefinition;
+    }
+    
+    private String validateAndGetRequestConfig(final ShenyuToolDefinition 
definition) {
+        final String config = definition.requestConfig();
+        if (!StringUtils.hasText(config)) {
+            throw new IllegalStateException("Request configuration cannot be 
empty");
+        }
+        LOG.debug("Request configuration: {}", config);
+        return config;
+    }
+    
+    private ServerWebExchange buildExchange(final ServerWebExchange 
originExchange, final CompletableFuture<String> future, final String sessionId, 
final String configStr, final String input) {
+        // Parse input parameters
+        final JsonObject inputJson = GsonUtils.getInstance().fromJson(input, 
JsonObject.class);
+        
+        if (Objects.isNull(inputJson)) {
+            throw new IllegalArgumentException("Invalid input JSON format");
+        }
+        // Parse request configuration
+        final RequestConfigHelper configHelper = new 
RequestConfigHelper(configStr);
+        final JsonObject requestTemplate = configHelper.getRequestTemplate();
+        final JsonObject argsPosition = configHelper.getArgsPosition();
+        final String urlTemplate = configHelper.getUrlTemplate();
+        final String method = configHelper.getMethod();
+        final boolean argsToJsonBody = configHelper.isArgsToJsonBody();
+        
+        final String path = RequestConfigHelper.buildPath(urlTemplate, 
argsPosition, inputJson);
+        final JsonObject bodyJson = 
RequestConfigHelper.buildBodyJson(argsToJsonBody, argsPosition, inputJson);
+        
+        final RequestConfig requestConfig = new RequestConfig(method, path, 
bodyJson, requestTemplate, argsToJsonBody);
+        
+        final ServerHttpRequest.Builder requestBuilder = originExchange
+                .getRequest()
+                .mutate()
+                .method(HttpMethod.valueOf(requestConfig.getMethod()))
+                .header("sessionId", sessionId)
+                .header("Accept", "application/json");
+        
+        // Add custom headers
+        if (requestConfig.getRequestTemplate().has("headers")) {
+            for (final var headerElem : 
requestConfig.getRequestTemplate().getAsJsonArray("headers")) {
+                final JsonObject headerObj = headerElem.getAsJsonObject();
+                requestBuilder.header(headerObj.get("key").getAsString(), 
headerObj.get("value").getAsString());
+            }
+        }
+        
+        // Set Content-Type
+        if (isRequestBodyMethod(requestConfig.getMethod())) {
+            requestBuilder.header("Content-Type", "application/json");
+        } else {
+            requestBuilder.headers(httpHeaders -> 
httpHeaders.remove("Content-Type"));
+        }
+        
+        // Set URI
+        try {
+            final URI oldUri = originExchange.getRequest().getURI();
+            final String newUriStr = oldUri.getScheme() + "://" + 
oldUri.getAuthority() + path;
+            requestBuilder.uri(new URI(newUriStr));
+        } catch (URISyntaxException e) {
+            throw new RuntimeException("Invalid URI: " + e.getMessage(), e);
+        }
+        
+        // Create response decorator
+        final ServerHttpResponseDecorator responseDecorator = new 
ShenyuMcpResponseDecorator(originExchange.getResponse(), sessionId, future, 
configHelper.getResponseTemplate());
+        
+        // Build final exchange object
+        final ServerWebExchange decoratedExchange = 
originExchange.mutate().request(requestBuilder.build()).response(responseDecorator).build();
+        
+        // Handle request body
+        if (isRequestBodyMethod(requestConfig.getMethod()) && 
requestConfig.getBodyJson().size() > 0) {
+            return new BodyWriterExchange(decoratedExchange, 
requestConfig.getBodyJson().toString());
+        }
+        
+        // Set context and attributes
+        final ShenyuContext shenyuContext = 
originExchange.getAttribute(Constants.CONTEXT);
+        if (Objects.nonNull(shenyuContext)) {
+            String decoratedPath = 
decoratedExchange.getRequest().getPath().value();
+            MetaData metaData = 
MetaDataCache.getInstance().obtain(decoratedPath);
+            if (Objects.nonNull(metaData) && 
Boolean.TRUE.equals(metaData.getEnabled())) {
+                decoratedExchange.getAttributes().put(Constants.META_DATA, 
metaData);
+                shenyuContext.setRpcType(metaData.getRpcType());
+            } else {
+                shenyuContext.setRpcType(RpcTypeEnum.HTTP.getName());
+            }
+            shenyuContext.setPath(decoratedPath);
+            shenyuContext.setRealUrl(decoratedPath);
+            decoratedExchange.getAttributes().put(Constants.CONTEXT, 
shenyuContext);
+        }
+        
+        return decoratedExchange;
+    }
+    
+    private boolean isRequestBodyMethod(final String method) {
+        return "POST".equalsIgnoreCase(method) || 
"PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method);

Review Comment:
   not use magic value



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/holder/ShenyuMcpExchangeHolder.java:
##########
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.mcp.server.holder;
+
+import org.springframework.web.server.ServerWebExchange;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class ShenyuMcpExchangeHolder {
+
+    private static final Map<String, ServerWebExchange> EXCHANGE_MAP = new 
ConcurrentHashMap<>();

Review Comment:
   may oom? please think a lot



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/manager/ShenyuMcpServerManager.java:
##########
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.mcp.server.manager;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.plugin.mcp.server.callback.ShenyuToolCallback;
+import org.apache.shenyu.plugin.mcp.server.definition.ShenyuToolDefinition;
+import 
org.apache.shenyu.plugin.mcp.server.transport.ShenyuSseServerTransportProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.mcp.McpToolUtils;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.reactive.function.server.HandlerFunction;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manager for MCP servers. Maps URIs to McpServer instances.
+ */
+@Component
+public class ShenyuMcpServerManager {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ShenyuMcpServerManager.class);
+
+    private static final String SLASH = "/";
+
+    /**
+     * AntPathMatcher for pattern matching.
+     */
+    private final AntPathMatcher pathMatcher = new AntPathMatcher();
+
+    /**
+     * Map to store URI to McpServer mapping.
+     */
+    private final Map<String, McpAsyncServer> mcpServerMap = new 
ConcurrentHashMap<>();
+
+    /**
+     * Map to store URI to McpServer mapping.
+     */
+    private final Map<String, ShenyuSseServerTransportProvider> 
mcpServerTransportMap = new ConcurrentHashMap<>();
+
+    private final Map<String, HandlerFunction<?>> routeMap = new 
ConcurrentHashMap<>();
+
+    /**
+     * Get or create a McpServer instance for the given URI.
+     * Uses pattern matching to find the best matching server.
+     *
+     * @param uri The URI to create or get a McpServer for
+     * @return The McpServer for the URI
+     */
+    public ShenyuSseServerTransportProvider 
getOrCreateMcpServerTransport(final String uri) {
+        // First try exact match
+        ShenyuSseServerTransportProvider transport = 
mcpServerTransportMap.get(uri);
+        if (Objects.nonNull(transport)) {
+            return transport;
+        }
+
+        // Then try to find existing transport that matches the pattern
+        String basePath = extractBasePath(uri);
+        transport = mcpServerTransportMap.get(basePath);
+        if (Objects.nonNull(transport)) {
+            LOG.debug("Found existing transport for base path '{}' for URI 
'{}'", basePath, uri);
+            return transport;
+        }
+
+        // Create new transport for the base path
+        return mcpServerTransportMap.computeIfAbsent(basePath, 
this::createMcpServerTransport);
+    }
+
+    /**
+     * 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")) {

Review Comment:
   I think you can define this as a config value



##########
shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/request/RequestConfigHelper.java:
##########
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.mcp.server.request;
+
+import com.google.gson.JsonObject;
+import org.apache.shenyu.common.utils.GsonUtils;
+
+/**
+ * Helper class for parsing and handling requestConfig.
+ */
+public class RequestConfigHelper {
+    
+    private final JsonObject configJson;
+    
+    /**
+     * Constructor.
+     *
+     * @param requestConfig the request config string
+     */
+    public RequestConfigHelper(final String requestConfig) {
+        this.configJson = GsonUtils.getInstance().fromJson(requestConfig, 
JsonObject.class);
+    }
+    
+    /**
+     * Get the config json object.
+     *
+     * @return the config json object
+     */
+    public JsonObject getConfigJson() {
+        return configJson;
+    }
+    
+    /**
+     * Get the request template json object.
+     *
+     * @return the request template json object
+     */
+    public JsonObject getRequestTemplate() {
+        return configJson.getAsJsonObject("requestTemplate");
+    }
+    
+    /**
+     * Get the argument position mapping.
+     *
+     * @return the argument position json object
+     */
+    public JsonObject getArgsPosition() {
+        return configJson.has("argsPosition") ? 
configJson.getAsJsonObject("argsPosition") : new JsonObject();
+    }
+    
+    /**
+     * Get the response template json object.
+     *
+     * @return the response template json object, or null if not present
+     */
+    public JsonObject getResponseTemplate() {
+        return configJson.has("responseTemplate") ? 
configJson.getAsJsonObject("responseTemplate") : null;
+    }
+    
+    /**
+     * Get the url template string.
+     *
+     * @return the url template string
+     */
+    public String getUrlTemplate() {
+        return getRequestTemplate().get("url").getAsString();
+    }
+    
+    /**
+     * Get the HTTP method string.
+     *
+     * @return the HTTP method string
+     */
+    public String getMethod() {
+        JsonObject requestTemplate = getRequestTemplate();
+        return requestTemplate.has("method") ? 
requestTemplate.get("method").getAsString() : "GET";
+    }
+    
+    public boolean isArgsToJsonBody() {
+        JsonObject requestTemplate = getRequestTemplate();
+        return requestTemplate.has("argsToJsonBody") && 
requestTemplate.get("argsToJsonBody").getAsBoolean();
+    }
+    
+    public boolean isArgsToUrlParam() {
+        JsonObject requestTemplate = getRequestTemplate();
+        return requestTemplate.has("argsToUrlParam") && 
requestTemplate.get("argsToUrlParam").getAsBoolean();
+    }
+    
+    /**
+     * Build the request path based on the URL template and argument positions.
+     *
+     * @param urlTemplate the URL template
+     * @param argsPosition the argument position mapping
+     * @param inputJson the input JSON object
+     * @return the constructed request path
+     */
+    public static String buildPath(final String urlTemplate, final JsonObject 
argsPosition,
+                                   final JsonObject inputJson) {
+        // First, check if the input value is already a complete URL
+        for (String key : argsPosition.keySet()) {
+            if (inputJson.has(key)) {
+                String value = inputJson.get(key).getAsString();
+                // If the input value is already a complete URL, return it 
directly
+                if (value.startsWith("http://";) || 
value.startsWith("https://";) || value.contains("?")) {
+                    return value;

Review Comment:
   why contains `?`



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

To unsubscribe, e-mail: [email protected]

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

Reply via email to