wangyf10 3 dagen geleden
commit
d370b46dc0
52 gewijzigde bestanden met toevoegingen van 3473 en 0 verwijderingen
  1. 209 0
      .gitignore
  2. 91 0
      pom.xml
  3. 82 0
      yusys-ai-demo-agent/pom.xml
  4. 11 0
      yusys-ai-demo-agent/src/main/java/com/example/demo/DemoAgentApplication.java
  5. 89 0
      yusys-ai-demo-agent/src/main/java/com/example/demo/config/CommonConfig.java
  6. 151 0
      yusys-ai-demo-agent/src/main/java/com/example/demo/config/DemoAgentConfig.java
  7. 107 0
      yusys-ai-demo-agent/src/main/java/com/example/demo/controller/HelloController.java
  8. 35 0
      yusys-ai-demo-agent/src/main/resources/application.yml
  9. 62 0
      yusys-ai-demo-mcp/pom.xml
  10. 11 0
      yusys-ai-demo-mcp/src/main/java/com/example/demo/DemoMcpApplication.java
  11. 17 0
      yusys-ai-demo-mcp/src/main/java/com/example/demo/config/McpConfig.java
  12. 17 0
      yusys-ai-demo-mcp/src/main/java/com/example/demo/mcp/WeatherService.java
  13. 43 0
      yusys-ai-demo-mcp/src/main/java/com/example/demo/test/McpStateLessClientGatewayTest.java
  14. 35 0
      yusys-ai-demo-mcp/src/main/java/com/example/demo/test/McpStateLessClientTest.java
  15. 44 0
      yusys-ai-demo-mcp/src/main/resources/application.yml
  16. 59 0
      yusys-ai-gateway-agent/pom.xml
  17. 5 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/Cons.java
  18. 19 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/GatewayAgentApplication.java
  19. 18 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/config/GatewayConfig.java
  20. 89 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/config/UserSessionLoadBalancer.java
  21. 24 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/config/UserSessionLoadBalancerConfiguration.java
  22. 140 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/TokenMetricFilter.java
  23. 61 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspAgentBeforeRouteToRequestUrlFilter.java
  24. 28 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspAgentEndRouteToRequestUrlFilter.java
  25. 108 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspAgentReactiveLoadBalancerClientFilter.java
  26. 25 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/util/TokenUtil.java
  27. 84 0
      yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/util/UriTools.java
  28. 51 0
      yusys-ai-gateway-agent/src/main/resources/application.yml
  29. 55 0
      yusys-ai-gateway-mcp/pom.xml
  30. 5 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/Cons.java
  31. 16 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/GatewayMcpApplication.java
  32. 18 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/config/GatewayConfig.java
  33. 48 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspBeforeRouteToRequestUrlFilter.java
  34. 28 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspEndRouteToRequestUrlFilter.java
  35. 108 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspReactiveLoadBalancerClientFilter.java
  36. 82 0
      yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/util/UriTools.java
  37. 48 0
      yusys-ai-gateway-mcp/src/main/resources/application.yml
  38. 56 0
      yusys-ai-http2mcp/mcp/user.txt
  39. 54 0
      yusys-ai-http2mcp/pom.xml
  40. 17 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/Http2McpApplication.java
  41. 50 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/config/McpServerStatelessAutoConfiguration.java
  42. 37 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/config/YuspMcpTransportContext.java
  43. 55 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/mcp/Http2McpDto.java
  44. 170 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/mcp/Http2McpToolCallback.java
  45. 63 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/mcp/Http2McpToolDefinition.java
  46. 155 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/service/Http2McpParseService.java
  47. 82 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/service/McpToolsUpdate2NacosService.java
  48. 75 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/test/HiRest.java
  49. 45 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/test/McpClientGatewayTest.java
  50. 35 0
      yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/test/McpClientTest.java
  51. 410 0
      yusys-ai-http2mcp/src/main/java/com/alibaba/cloud/ai/mcp/register/NacosStatelessMcpRegister.java
  52. 46 0
      yusys-ai-http2mcp/src/main/resources/application.yml

+ 209 - 0
.gitignore

@@ -0,0 +1,209 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py.cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# UV
+#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#uv.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+#poetry.toml
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+uv.lock
+.envrc
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+**/coder_venv/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
+#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+#  and can be added to the global gitignore or merged into this file. However, if you prefer, 
+#  you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Cursor
+#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
+#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
+#  refer to https://docs.cursor.com/context/ignore-files
+.cursorignore
+.cursorindexingignore
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
+
+storage/
+data/
+mysql_data/
+.DS_Store
+.build_version

+ 91 - 0
pom.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>4.0.0</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>cn.com.yusys.ai</groupId>
+    <artifactId>yusys-ai-platform</artifactId>
+    <version>1.0</version>
+    <packaging>pom</packaging>
+    <name>yusys-ai-platform</name>
+    <description>yusys-ai-platform</description>
+
+    <modules>
+        <module>yusys-ai-gateway-agent</module>
+        <module>yusys-ai-gateway-mcp</module>
+        <module>yusys-ai-http2mcp</module>
+        <module>yusys-ai-demo-agent</module>
+        <module>yusys-ai-demo-mcp</module>
+    </modules>
+
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-cloud.version>2025.1.0</spring-cloud.version>
+        <spring-cloud-alibaba.version>2025.1.0.0</spring-cloud-alibaba.version>
+        <spring-ai-alibaba.version>2.0.0-M1.1</spring-ai-alibaba.version>
+        <spring-ai.version>2.0.0-M1</spring-ai.version>
+        <agentscope.version>2.0.0</agentscope.version>
+        <hutool.version>5.8.46</hutool.version>
+        <nacos.version>3.2.2</nacos.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.ai</groupId>
+                <artifactId>spring-ai-bom</artifactId>
+                <version>${spring-ai.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba.cloud.ai</groupId>
+                <artifactId>spring-ai-alibaba-extensions-bom</artifactId>
+                <version>${spring-ai-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>io.agentscope</groupId>
+                <artifactId>agentscope-bom</artifactId>
+                <version>${agentscope.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba.nacos</groupId>
+                <artifactId>nacos-client</artifactId>
+                <version>${nacos.version}</version>
+            </dependency>
+        </dependencies>
+
+    </dependencyManagement>
+</project>

+ 82 - 0
yusys-ai-demo-agent/pom.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>cn.com.yusys.ai</groupId>
+        <artifactId>yusys-ai-platform</artifactId>
+        <version>1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>com.example</groupId>
+    <artifactId>yusys-ai-demo-agent</artifactId>
+    <version>1.0.0</version>
+    <name>yusys-ai-demo-agent</name>
+    <description>yusys-ai-demo-agent</description>
+
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-commons</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.agentscope</groupId>
+            <artifactId>agentscope-extensions-agui</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.agentscope</groupId>
+            <artifactId>agentscope-extensions-a2a-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.agentscope</groupId>
+            <artifactId>agentscope-extensions-nacos-a2a</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.agentscope</groupId>
+            <artifactId>agentscope-extensions-model-dashscope</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.agentscope</groupId>
+            <artifactId>agentscope-harness</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>protobuf-java</artifactId>
+                    <groupId>com.google.protobuf</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-opentelemetry</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 11 - 0
yusys-ai-demo-agent/src/main/java/com/example/demo/DemoAgentApplication.java

@@ -0,0 +1,11 @@
+package com.example.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class DemoAgentApplication {
+    public static void main(String[] args) throws Exception {
+        SpringApplication.run(DemoAgentApplication.class, args);
+    }
+}

+ 89 - 0
yusys-ai-demo-agent/src/main/java/com/example/demo/config/CommonConfig.java

@@ -0,0 +1,89 @@
+package com.example.demo.config;
+
+import com.alibaba.nacos.api.ai.AiFactory;
+import com.alibaba.nacos.api.ai.AiService;
+import com.alibaba.nacos.api.ai.model.a2a.AgentCapabilities;
+import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
+import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint;
+import com.alibaba.nacos.api.ai.model.a2a.AgentInterface;
+import com.alibaba.nacos.api.exception.NacosException;
+import io.agentscope.core.a2a.agent.A2aAgent;
+import io.agentscope.core.agent.Agent;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+@Configuration
+public class CommonConfig {
+    private static final Logger LOG = LoggerFactory.getLogger(CommonConfig.class);
+
+    @Bean
+    public String initOpenTelemetry(OpenTelemetry t) {
+        GlobalOpenTelemetry.set(t);
+        return "initOpenTelemetry";
+    }
+
+    @Bean
+    public AiService aiService(Environment env) throws NacosException {
+        Properties props = new Properties();
+        props.put("serverAddr", env.getProperty("agent.nacos.server-addr"));
+        props.put("username", env.getProperty("agent.nacos.username"));
+        props.put("password", env.getProperty("agent.nacos.password"));
+        AiService aiService = AiFactory.createAiService(props);
+        return aiService;
+    }
+
+    @Bean
+    public String agentRegister(Environment env,
+                                AiService aiService,
+                                @Autowired(required = false) List<Agent> agents) throws Exception{
+        if(null!=agents && !agents.isEmpty()) {
+            for (Agent agent : agents) {
+                if (agent instanceof A2aAgent) {
+                    continue;
+                }
+                String agentName = agent.getName();
+                String version = "1.0";
+                AgentCard card = new AgentCard();
+                card.setName(agentName);
+                card.setVersion(version);
+                card.setDescription(agent.getDescription());
+                AgentCapabilities cap = new AgentCapabilities();
+                cap.setStreaming(false);
+                cap.setPushNotifications(false);
+                cap.setStateTransitionHistory(false);
+                cap.setExtendedAgentCard(false);
+                card.setCapabilities(cap);
+                AgentInterface itf = new AgentInterface();
+                itf.setUrl("xxx");
+                itf.setProtocolBinding("POST");
+                itf.setProtocolVersion("0.0");
+                card.setSupportedInterfaces(Collections.singletonList(itf));
+                try {
+                    aiService.releaseAgentCard(card);
+                } catch (Exception e) {
+                    LOG.warn(e.getMessage());
+                }
+
+                AgentEndpoint agentEndpoint = new AgentEndpoint();
+                agentEndpoint.setVersion(version);
+                agentEndpoint.setAddress(env.getProperty("spring.cloud.client.ip-address"));
+                agentEndpoint.setPort(Integer.parseInt(env.getProperty("server.port")));
+                agentEndpoint.setTransport("AGUI");
+                agentEndpoint.setPath("/" + agentName);
+                agentEndpoint.setTenant(env.getProperty("agent.tenant"));
+                aiService.registerAgentEndpoint(agentName, agentEndpoint);
+            }
+        }
+        return "agentRegister";
+    }
+}

+ 151 - 0
yusys-ai-demo-agent/src/main/java/com/example/demo/config/DemoAgentConfig.java

@@ -0,0 +1,151 @@
+package com.example.demo.config;
+
+import io.agentscope.core.ReActAgent;
+import io.agentscope.core.agent.Agent;
+import io.agentscope.core.permission.PermissionContextState;
+import io.agentscope.core.permission.PermissionMode;
+import io.agentscope.core.tool.Toolkit;
+import io.agentscope.core.tool.mcp.McpSyncClientWrapper;
+import io.agentscope.core.tracing.OtelTracingMiddleware;
+import io.agentscope.extensions.model.dashscope.DashScopeChatModel;
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.opentelemetry.api.trace.Span;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.time.Duration;
+import java.util.List;
+
+@Configuration
+public class DemoAgentConfig {
+
+    @Bean
+    public DashScopeChatModel yuspModel(Environment env) {
+        DashScopeChatModel model = DashScopeChatModel.builder()
+                .baseUrl(env.getProperty("agent.baseUrl"))
+                .apiKey(env.getProperty("agent.api-key"))
+                .modelName(env.getProperty("agent.model"))
+                .build();
+        return model;
+    }
+
+    @Bean
+    public Agent demoAgent(DashScopeChatModel yuspModel) {
+        ReActAgent agent =
+                ReActAgent.builder()
+                        .name("DemoAgent")
+                        .description("问答助手")
+                        .sysPrompt("你是一个有帮助的助手。")
+                        .model(yuspModel)
+                        .toolkit(new Toolkit())
+                        .build();
+        return agent;
+    }
+
+    @Bean
+    public Agent demoAgentAGUI(DashScopeChatModel yuspModel) {
+        ReActAgent agent =
+                ReActAgent.builder()
+                        .name("DemoAgentAGUI")
+                        .description("问答助手")
+                        .sysPrompt("你是一个有帮助的助手。")
+                        .model(yuspModel)
+                        .toolkit(new Toolkit())
+                        .build();
+        return agent;
+    }
+
+    @Bean
+    public ReActAgent demoAgentMCP(DashScopeChatModel yuspModel,Environment env) {
+        HttpClientStreamableHttpTransport transport =
+                HttpClientStreamableHttpTransport.builder(env.getProperty("agent.mcp-gateway"))
+                        .httpRequestCustomizer(new McpSyncHttpClientRequestCustomizer(){
+                            @Override
+                            public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body, McpTransportContext context) {
+                                builder.header("MCPSERVER","yusys-ai-demo-mcp");
+                                // 获取当前 Span
+                                Span currentSpan = Span.current();
+                                if (currentSpan != null && currentSpan.getSpanContext().isValid()) {
+                                    String traceId = currentSpan.getSpanContext().getTraceId();
+                                    String spanId = currentSpan.getSpanContext().getSpanId();
+                                    // 构建 W3C traceparent 请求头
+                                    String traceparent = String.format("00-%s-%s-01", traceId, spanId);
+                                    builder.header("traceparent", traceparent);
+                                }
+                            }
+                        })
+                        .build();
+        McpSyncClient client = McpClient.sync(transport)
+                .requestTimeout(Duration.ofSeconds(30))  // 每个请求的超时时间
+                .build();
+        client.initialize();
+
+        McpSyncClientWrapper wrapper = new McpSyncClientWrapper("mcpClient",client);
+        Toolkit toolkit = new Toolkit();
+        toolkit.registerMcpClient(wrapper).block();
+        ReActAgent agent =
+                ReActAgent.builder()
+                        .name("DemoAgentMCP")
+                        .description("天气助手(MCP)")
+                        .sysPrompt("专业查询天气的智能体")
+                        .model(yuspModel)
+                        .middlewares(List.of(new OtelTracingMiddleware()))
+                        .toolkit(toolkit)
+                        .permissionContext(PermissionContextState.builder()
+                                .mode(PermissionMode.BYPASS)
+                                .build())
+                        .build();
+        return agent;
+    }
+
+    @Bean
+    public ReActAgent userAgent(DashScopeChatModel yuspModel,Environment env) {
+        HttpClientStreamableHttpTransport transport =
+                HttpClientStreamableHttpTransport.builder(env.getProperty("agent.mcp-gateway"))
+                        .httpRequestCustomizer(new McpSyncHttpClientRequestCustomizer(){
+                            @Override
+                            public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body, McpTransportContext context) {
+                                builder.header("MCPSERVER","yusys-ai-http2mcp");
+                                // 获取当前 Span
+                                Span currentSpan = Span.current();
+                                if (currentSpan != null && currentSpan.getSpanContext().isValid()) {
+                                    String traceId = currentSpan.getSpanContext().getTraceId();
+                                    String spanId = currentSpan.getSpanContext().getSpanId();
+                                    // 构建 W3C traceparent 请求头
+                                    String traceparent = String.format("00-%s-%s-01", traceId, spanId);
+                                    builder.header("traceparent", traceparent);
+                                }
+                            }
+                        })
+                        .build();
+        McpSyncClient client = McpClient.sync(transport)
+                .requestTimeout(Duration.ofSeconds(30))  // 每个请求的超时时间
+                .build();
+        client.initialize();
+
+        McpSyncClientWrapper wrapper = new McpSyncClientWrapper("mcpClient",client);
+        Toolkit toolkit = new Toolkit();
+        toolkit.registerMcpClient(wrapper).block();
+        ReActAgent agent =
+                ReActAgent.builder()
+                        .name("UserAgent")
+                        .description("获取用户信息")
+                        .sysPrompt("专业获取用户信息的智能体")
+                        .model(yuspModel)
+                        .middlewares(List.of(new OtelTracingMiddleware()))
+                        .toolkit(toolkit)
+                        .permissionContext(PermissionContextState.builder()
+                                .mode(PermissionMode.BYPASS)
+                                .build())
+                        .build();
+        return agent;
+    }
+
+}

+ 107 - 0
yusys-ai-demo-agent/src/main/java/com/example/demo/controller/HelloController.java

@@ -0,0 +1,107 @@
+package com.example.demo.controller;
+
+import io.a2a.spec.Message;
+import io.agentscope.core.ReActAgent;
+import io.agentscope.core.a2a.agent.utils.MessageConvertUtil;
+import io.agentscope.core.agent.Agent;
+import io.agentscope.core.agent.RuntimeContext;
+import io.agentscope.core.agui.adapter.AguiAdapterConfig;
+import io.agentscope.core.agui.adapter.AguiAgentAdapter;
+import io.agentscope.core.agui.event.AguiEvent;
+import io.agentscope.core.agui.model.RunAgentInput;
+import io.agentscope.core.message.Msg;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+public class HelloController {
+
+    Logger LOG = LoggerFactory.getLogger(HelloController.class);
+
+    @Autowired
+    @Qualifier("demoAgent")
+    private Agent demoAgent;
+    // A2A
+    @PostMapping(value = "/DemoAgent")
+    public Message a2aServer(
+            @RequestBody Message message) {
+
+        LOG.info("DemoAgent 请求开始");
+        Msg input = MessageConvertUtil.convertFromMessage(message, "DemoAgent");
+        Msg output = demoAgent.call(input).block();
+        List<Msg> data = new ArrayList<>();
+        data.add(output);
+        LOG.info("DemoAgent 请求结束");
+        return MessageConvertUtil.convertFromMsg(data);
+    }
+
+
+    @Autowired
+    @Qualifier("demoAgentAGUI")
+    private Agent demoAgentAGUI;
+    AguiAdapterConfig aguiConfig = AguiAdapterConfig.builder()
+            .enableReasoning(true)
+            .runTimeout(Duration.ofMinutes(5))
+            .build();
+
+    // AGUI
+    @PostMapping(value = "/DemoAgentAGUI",
+            produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
+    public Flux<AguiEvent> demoAgentAGUI(
+            @RequestBody RunAgentInput input) {
+        LOG.info("DemoAgentAGUI 请求开始");
+        AguiAgentAdapter adapter = new AguiAgentAdapter(demoAgentAGUI, aguiConfig);
+        Flux<AguiEvent> t = adapter.run(input);
+        LOG.info("DemoAgentAGUI 请求结束");
+        return t;
+    }
+
+
+    @Autowired
+    @Qualifier("demoAgentMCP")
+    private ReActAgent demoAgentMCP;
+    //MCP
+    @PostMapping(value = "/DemoAgentMCP")
+    public Msg getWeather(
+            @RequestBody Message message) {
+        Msg input = MessageConvertUtil.convertFromMessage(message, "DemoAgentMCP");
+        String sessionId = UUID.randomUUID().toString();
+        RuntimeContext context =RuntimeContext.builder().
+                userId(sessionId)
+                .sessionId(sessionId).build();
+        LOG.info("DemoAgentMCP 请求开始");
+        Msg output = demoAgentMCP.call(List.of(input),context).block();
+        LOG.info("DemoAgentMCP 请求结束");
+        return output;
+    }
+
+    @Autowired
+    @Qualifier("userAgent")
+    private ReActAgent userAgent;
+    @PostMapping(value = "/UserAgent")
+    public Msg getUserInfo(
+            @RequestBody Message message) {
+        Msg input = MessageConvertUtil.convertFromMessage(message, "UserAgent");
+        String sessionId = UUID.randomUUID().toString();
+        RuntimeContext context =RuntimeContext.builder().
+                userId(sessionId)
+                .sessionId(sessionId).build();
+        LOG.info("UserAgent 请求开始");
+        Msg output = userAgent.call(List.of(input),context).block();
+        LOG.info("UserAgent 请求结束");
+        return output;
+    }
+
+}

+ 35 - 0
yusys-ai-demo-agent/src/main/resources/application.yml

@@ -0,0 +1,35 @@
+server:
+  port: 8091
+management:
+  defaults:
+    metrics:
+      export:
+        enabled: false
+  tracing:
+    sampling:
+      probability: 1.0
+  opentelemetry:
+    tracing:
+      export:
+        otlp:
+          endpoint: http://127.0.0.1:9200/_otlp/v1/traces  # OTLP 追踪端点
+          transport: http
+spring:
+  application:
+    name: yusys-ai-demo-agent
+  reactor:
+    context-propagation: auto
+agent:
+  nacos:
+    server-addr: 127.0.0.1:8848
+    username: nacos
+    password: nacos
+  tenant: sys # sys、user、all
+  mcp-gateway: http://127.0.0.1:6666
+  baseUrl: https://dashscope.aliyuncs.com
+  api-key: sk-ws-H.EDRHPPL.90jF.MEUCIGmnGBQKWzzBB3T97hEtvyRQfgYPZTrYRyaihbGAoiYbAiEAtLvFVgt_DlFfP0aqLNRu_yWvjeK6LARMbmbV11d-tpU
+  model: qwen-plus
+logging:
+  pattern:
+    correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
+  include-application-name: false

+ 62 - 0
yusys-ai-demo-mcp/pom.xml

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>cn.com.yusys.ai</groupId>
+        <artifactId>yusys-ai-platform</artifactId>
+        <version>1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>com.example</groupId>
+    <artifactId>yusys-ai-demo-mcp</artifactId>
+    <version>1.0.0</version>
+    <name>yusys-ai-demo-mcp</name>
+    <description>yusys-ai-demo-mcp</description>
+
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud.ai</groupId>
+            <artifactId>spring-ai-alibaba-starter-mcp-registry</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.agentscope</groupId>
+            <artifactId>agentscope-harness</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-opentelemetry</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 11 - 0
yusys-ai-demo-mcp/src/main/java/com/example/demo/DemoMcpApplication.java

@@ -0,0 +1,11 @@
+package com.example.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class DemoMcpApplication {
+    public static void main(String[] args) throws Exception {
+        SpringApplication.run(DemoMcpApplication.class, args);
+    }
+}

+ 17 - 0
yusys-ai-demo-mcp/src/main/java/com/example/demo/config/McpConfig.java

@@ -0,0 +1,17 @@
+package com.example.demo.config;
+
+import com.example.demo.mcp.WeatherService;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.ai.tool.method.MethodToolCallbackProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class McpConfig {
+    @Bean
+    public ToolCallbackProvider feedbackTools() {
+        return MethodToolCallbackProvider.builder()
+                .toolObjects(new WeatherService())
+                .build();
+    }
+}

+ 17 - 0
yusys-ai-demo-mcp/src/main/java/com/example/demo/mcp/WeatherService.java

@@ -0,0 +1,17 @@
+package com.example.demo.mcp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+
+public class WeatherService {
+    Logger LOG = LoggerFactory.getLogger(WeatherService.class);
+    @Tool(name = "getWeather", description = "获取指定城市的天气")
+    public String getWeather(
+            @ToolParam(description = "城市名称") String city) {
+        String answer = city +":晴,26.2°C";
+        System.out.println(answer);
+        LOG.info("获取天气:"+answer);
+        return answer;
+    }
+}

+ 43 - 0
yusys-ai-demo-mcp/src/main/java/com/example/demo/test/McpStateLessClientGatewayTest.java

@@ -0,0 +1,43 @@
+package com.example.demo.test;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+public class McpStateLessClientGatewayTest {
+
+    public static void main(String[] args) {
+        HttpClientStreamableHttpTransport transport =
+                HttpClientStreamableHttpTransport.builder("http://127.0.0.1:6666/mcp")
+                        .httpRequestCustomizer(new McpSyncHttpClientRequestCustomizer(){
+                            @Override
+                            public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body, McpTransportContext context) {
+                                builder.header("MCPSERVER","yusys-ai-demo-mcp");
+                            }
+                        })
+                        .build();
+        McpSyncClient client = McpClient.sync(transport)
+                .requestTimeout(Duration.ofSeconds(30))  // 每个请求的超时时间
+                        .build();
+        client.initialize();
+
+        Map<String, Object> p = new HashMap();
+        p.put("city","北京");
+        McpSchema.CallToolResult re = client.callTool(
+                McpSchema.CallToolRequest.builder()
+                        .name("getWeather")
+                        .arguments(p)
+                        .build());
+        System.out.println(re.content());
+        client.close();
+    }
+}

+ 35 - 0
yusys-ai-demo-mcp/src/main/java/com/example/demo/test/McpStateLessClientTest.java

@@ -0,0 +1,35 @@
+package com.example.demo.test;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+public class McpStateLessClientTest {
+
+    public static void main(String[] args) {
+        HttpClientStreamableHttpTransport transport =
+                HttpClientStreamableHttpTransport.builder("http://127.0.0.1:7778/mcp")
+                        .build();
+        McpSyncClient client = McpClient.sync(transport)
+                .requestTimeout(Duration.ofSeconds(30)).build();
+        client.initialize();
+        System.out.println("获取工具描述");
+        McpSchema.ListToolsResult ts = client.listTools();
+        ts.tools().forEach(t->{
+            System.out.println(t.name()+"->"+t.description());
+        });
+        Map<String, Object> p = new HashMap();
+        p.put("city","北京");
+        McpSchema.CallToolResult re = client.callTool(
+                McpSchema.CallToolRequest.builder()
+                        .name("getWeather")
+                        .arguments(p).build());
+        System.out.println("执行工具结果:"+re.content());
+        client.close();
+    }
+}

+ 44 - 0
yusys-ai-demo-mcp/src/main/resources/application.yml

@@ -0,0 +1,44 @@
+server:
+  port: 7778
+management:
+  defaults:
+    metrics:
+      export:
+        enabled: false
+  tracing:
+    sampling:
+      probability: 1.0                    # 采样率
+  opentelemetry:
+    tracing:
+      export:
+        otlp:
+          endpoint: http://127.0.0.1:9200/_otlp/v1/traces  # OTLP 追踪端点
+          transport: http
+spring:
+  reactor:
+    context-propagation: auto
+  application:
+    name: yusys-ai-demo-mcp
+  ai:
+    mcp:
+      server:
+        name: ${spring.application.name}
+        version: 1.0
+        protocol: STATELESS
+        type: ASYNC
+        instructions: "MCP服务"
+        stateless:
+          mcp-endpoint: /mcp
+    alibaba:
+      mcp:
+        nacos:
+          server-addr: 127.0.0.1:8848
+          namespace: public
+          # username: nacos
+          # password: nacos
+          register:
+            enabled: true
+logging:
+  pattern:
+    correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
+  include-application-name: false

+ 59 - 0
yusys-ai-gateway-agent/pom.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>cn.com.yusys.ai</groupId>
+        <artifactId>yusys-ai-platform</artifactId>
+        <version>1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>yusys-ai-gateway-agent</artifactId>
+    <name>yusys-ai-gateway-agent</name>
+    <description>智能体网关</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <!-- Nacos 服务发现 -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+
+        <!-- 负载均衡:网关按 lb:// 路由到下游服务时需要 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-opentelemetry</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 5 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/Cons.java

@@ -0,0 +1,5 @@
+package cn.com.yusys.ai.gateway.agent;
+
+public class Cons {
+    public final static String YUSP_AGNET_ROUTE_NAME = "YUSP_AGNET_ROUTE_NAME";
+}

+ 19 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/GatewayAgentApplication.java

@@ -0,0 +1,19 @@
+package cn.com.yusys.ai.gateway.agent;
+
+import cn.com.yusys.ai.gateway.agent.config.UserSessionLoadBalancerConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
+
+/**
+ * 网关启动类。
+ */
+@EnableDiscoveryClient
+@SpringBootApplication
+@LoadBalancerClients(defaultConfiguration = UserSessionLoadBalancerConfiguration.class)
+public class GatewayAgentApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(GatewayAgentApplication.class, args);
+    }
+}

+ 18 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/config/GatewayConfig.java

@@ -0,0 +1,18 @@
+package cn.com.yusys.ai.gateway.agent.config;
+
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class GatewayConfig {
+    @Bean
+    public RouteLocator catchAllRouteLocator(RouteLocatorBuilder builder) {
+        return builder.routes()
+                .route("catch_all_route",
+                        r -> r.path("/**")
+                                .uri("lb://agentgateway"))
+                .build();
+    }
+}

+ 89 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/config/UserSessionLoadBalancer.java

@@ -0,0 +1,89 @@
+package cn.com.yusys.ai.gateway.agent.config;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.*;
+import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.function.SingletonSupplier;
+import reactor.core.publisher.Mono;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 扩展负载均衡策略
+ * 请求头中有会话id,将转发到与会话id关联的节点
+ * 请求头中没有会话id,将轮询转发
+ */
+public class UserSessionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
+    AtomicInteger position = new AtomicInteger((new Random()).nextInt(1000));
+    Logger LOGGER = LoggerFactory.getLogger(UserSessionLoadBalancer.class);
+    private final String serviceId;
+    private final String sessionKey;
+    private final SingletonSupplier<ServiceInstanceListSupplier> serviceInstanceListSingletonSupplier;
+    public UserSessionLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
+                                   String serviceId,String sessionKey) {
+        this.serviceInstanceListSingletonSupplier = SingletonSupplier.of(() -> (ServiceInstanceListSupplier)serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new));
+        this.serviceId = serviceId;
+        this.sessionKey = sessionKey;
+    }
+    @Override
+    public Mono<Response<ServiceInstance>> choose(Request request) {
+        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSingletonSupplier.obtain();
+
+        RequestDataContext re = (RequestDataContext)request.getContext();
+        HttpHeaders heads = re.getClientRequest().getHeaders();
+        String sessionId = null;
+        if(null!=heads){
+            @Nullable List<String> sessions = heads.get(sessionKey);
+            if(null!=sessions && !sessions.isEmpty()){
+                sessionId = sessions.get(0);
+                LOGGER.info("获取到用户会话ID:{}",sessionId);
+            }
+        }
+        final String sessionIdT = sessionId;
+        return supplier.get(request).next().map((serviceInstances) -> this.processInstanceResponse(sessionIdT,supplier, serviceInstances));
+    }
+
+    private Response<ServiceInstance> processInstanceResponse(String sessionId,ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
+        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(sessionId,serviceInstances);
+        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
+            ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
+        }
+        return serviceInstanceResponse;
+    }
+
+    private Response<ServiceInstance> getInstanceResponse(String sessionId,List<ServiceInstance> instances) {
+        if (instances.isEmpty()) {
+            LOGGER.error("No servers available for service: " + this.serviceId);
+            return new EmptyResponse();
+        } else if (null == sessionId) {//  没有会话ID就使用轮询方式的负载均衡
+            int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
+            ServiceInstance instance = instances.get(pos % instances.size());
+            return new DefaultResponse(instance);
+        } else {//  有会话ID,需要根据会话id找到唯一的实例
+            // 先排序保证数据位置固定
+            instances.sort(
+                    Comparator.nullsLast(
+                            Comparator.comparing(ServiceInstance::getHost)
+                                    .thenComparing(
+                                            ServiceInstance::getPort,
+                                            Comparator.nullsLast(Comparator.naturalOrder())
+                                    )
+                    )
+            );
+            int index = (sessionId.hashCode() & Integer.MAX_VALUE) % instances.size();
+            ServiceInstance instance = instances.get(index);
+            return new DefaultResponse(instance);
+        }
+    }
+}

+ 24 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/config/UserSessionLoadBalancerConfiguration.java

@@ -0,0 +1,24 @@
+package cn.com.yusys.ai.gateway.agent.config;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+
+/**
+ * 自定义负载均衡配置类
+ */
+public class UserSessionLoadBalancerConfiguration {
+    // 2. 定义负载均衡器 Bean
+    @Bean
+    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
+                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
+        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
+        String sessionKey = environment.getProperty("agent.session-key-in-header");
+        return new UserSessionLoadBalancer(
+                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
+                name,sessionKey);
+    }
+}

+ 140 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/TokenMetricFilter.java

@@ -0,0 +1,140 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.util.TokenUtil;
+import org.jspecify.annotations.Nullable;
+import org.reactivestreams.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 对返回报文做收集(旁路复制,不影响正常流式转发),找到token的消耗情况
+ * 基于过滤器实现,必须在NettyWriteResponseFilter之前执行
+ */
+public class TokenMetricFilter implements GlobalFilter, Ordered {
+    private static final Logger LOGGER = LoggerFactory.getLogger(TokenMetricFilter.class);
+    @Value("${harness.head-user-id:userid}")
+    private String headUserId;
+
+    public TokenMetricFilter(){
+
+    }
+    @Override
+    public int getOrder() {
+        // 必须早于 NettyWriteResponseFilter(-1),这样它拿到的是我们装饰过的 response
+        return -2;
+    }
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        ServerHttpResponse original = exchange.getResponse();
+        DataBufferFactory bufferFactory = original.bufferFactory();
+
+        // 每个请求一个累积缓冲,用于最终打印“完整报文”
+        ByteArrayOutputStream bodyStream = new ByteArrayOutputStream();
+
+        ServerHttpResponseDecorator decorated = new ServerHttpResponseDecorator(original) {
+            // 普通 HTTP 走这里
+            @Override
+            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
+                return super.writeWith(Flux.from(body).map(buf -> tapHttp(buf, bodyStream, bufferFactory)));
+            }
+
+            // SSE流式走这里
+            @Override
+            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
+                return super.writeAndFlushWith(
+                        Flux.from(body).map(inner ->
+                                Flux.from(inner).map(buf -> tapSSE(buf, bufferFactory,exchange))));
+            }
+        };
+        ServerWebExchange mutated = exchange.mutate().response(decorated).build();
+        return chain.filter(mutated).doFinally(sig -> {
+            // 流结束(正常/取消/异常)时
+            if (bodyStream.size() > 0) {
+                processBody(mutated, bodyStream.toString(StandardCharsets.UTF_8));
+            }
+        });
+    }
+
+    /**
+     * 旁路复制单块:读出字节 -> 复制一份记日志 -> 重新 wrap 成新 buffer 继续下发。
+     * 不 join、不缓冲整个流,逐块透传,SSE 实时性不受影响。
+     */
+    private DataBuffer tapHttp(DataBuffer buffer, ByteArrayOutputStream bodyStream, DataBufferFactory factory) {
+        int len = buffer.readableByteCount();
+        byte[] bytes = new byte[len];
+        buffer.read(bytes);                 // 读出(readerIndex 移到末尾)
+        DataBufferUtils.release(buffer);    // 释放原 buffer,避免内存泄漏
+        bodyStream.write(bytes, 0, bytes.length);// 收集复制的报文字节码
+        return factory.wrap(bytes);         // 重新包成新 buffer 下发给客户端
+    }
+
+    private DataBuffer tapSSE(DataBuffer buffer, DataBufferFactory factory,ServerWebExchange exchange) {
+        byte[] bytes = new byte[buffer.readableByteCount()];
+        buffer.read(bytes);
+        DataBufferUtils.release(buffer);
+        String chunk = new String(bytes, StandardCharsets.UTF_8);
+        processBody(exchange,chunk);
+        return factory.wrap(bytes);
+    }
+
+    private void processBody(ServerWebExchange exchange, String body) {
+        try {
+            if (null == body || body.equals("")) {
+                return;
+            }
+            ServerHttpRequest request = exchange.getRequest();
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("[请求]{},返回报文:{}", request.getPath(), body);
+            }
+
+            int inputToken = TokenUtil.extractInputTokens(body);
+            int outputTokens = TokenUtil.extractOutputTokens(body);
+            int totalTokens = extractTotalTokens(body);
+            if (totalTokens > 0) {
+                report(exchange, inputToken,outputTokens,totalTokens);
+            }
+        }catch (Exception e){
+            LOGGER.warn("token使用量获取失败",e);
+        }
+    }
+
+    private void report(ServerWebExchange exchange,int inputTokens,int outputTokens,int totalTokens){
+        @Nullable List<String> v = exchange.getRequest().getHeaders().get(headUserId);
+        String userId = "";
+        if(null!=v && !v.isEmpty()){
+            userId = v.get(0);
+        }
+        Object agentName = exchange.getAttributes().get(YuspAgentBeforeRouteToRequestUrlFilter.AGENT_SERVICE_NAME);
+        LOGGER.info("[{}]请求智能体[{}],inputTokens[{}],outputTokens[{}],totalTokens[{}]",userId, agentName,inputTokens,outputTokens, totalTokens);
+    }
+
+    static Pattern pattern = Pattern.compile("\"total_?tokens\"\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
+    private static int extractTotalTokens(String input) {
+        // 不区分大小写,支持 total_tokens 或 totalTokens
+        Matcher matcher = pattern.matcher(input);
+        if (matcher.find()) {
+            return Integer.parseInt(matcher.group(1));
+        }
+        return 0;
+    }
+}

+ 61 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspAgentBeforeRouteToRequestUrlFilter.java

@@ -0,0 +1,61 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.Cons;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+
+@Component
+public class YuspAgentBeforeRouteToRequestUrlFilter implements GlobalFilter, Ordered {
+    Logger LOGGER = LoggerFactory.getLogger(YuspAgentBeforeRouteToRequestUrlFilter.class);
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        String agentServerName = getServerName(exchange);
+        exchange.getAttributes().put(AGENT_SERVICE_NAME, agentServerName);
+        if (null != agentServerName) {
+            if (!agentServerName.contains("::")) {
+                agentServerName = agentServerName + "::1.0";
+            }
+            LOGGER.info("接收到Agent请求:" + agentServerName);
+            URI targetUri = URI.create("lb://" + agentServerName);
+            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, targetUri);
+
+            Route route = (Route) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
+            exchange.getAttributes().put(Cons.YUSP_AGNET_ROUTE_NAME, route);
+            exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
+            return chain.filter(exchange);
+        } else {
+            throw new RuntimeException("非法请求");
+        }
+
+    }
+
+    public static final String AGENT_SERVICE_NAME = "AGENT_SERVICE_NAME";
+
+    private String getServerName(ServerWebExchange exchange) {
+        Object serverName = exchange.getAttribute(AGENT_SERVICE_NAME);
+        if (null != serverName) {
+            return serverName.toString();
+        } else {
+            String pathStr = exchange.getRequest().getURI().getPath();
+            String[] t = pathStr.split("/");
+            return t[1];
+        }
+    }
+
+    @Override
+    public int getOrder() {
+        return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER - 1;
+    }
+}

+ 28 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspAgentEndRouteToRequestUrlFilter.java

@@ -0,0 +1,28 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.Cons;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+@Component
+public class YuspAgentEndRouteToRequestUrlFilter implements GlobalFilter, Ordered {
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        Route route = (Route)exchange.getAttribute(Cons.YUSP_AGNET_ROUTE_NAME);
+        exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR,route);
+        return chain.filter(exchange);
+    }
+
+    @Override
+    public int getOrder() {
+        return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER+1;
+    }
+}

+ 108 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspAgentReactiveLoadBalancerClientFilter.java

@@ -0,0 +1,108 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.util.UriTools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.*;
+import org.springframework.cloud.client.loadbalancer.CompletionContext.Status;
+import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
+import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
+import org.springframework.cloud.gateway.support.NotFoundException;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@Component
+public class YuspAgentReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {
+    private static final Logger log = LoggerFactory.getLogger(ReactiveLoadBalancerClientFilter.class);    public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
+    private final LoadBalancerClientFactory clientFactory;
+    private final GatewayLoadBalancerProperties properties;
+    public YuspAgentReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, GatewayLoadBalancerProperties properties) {
+        super(clientFactory, properties);
+        this.clientFactory = clientFactory;
+        this.properties = properties;
+    }
+
+
+    public int getOrder() {
+        return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER;
+    }
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
+        String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
+        if (url != null && ("lb".equals(url.getScheme()) || "lb".equals(schemePrefix))) {
+            ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
+            String serviceId = getServiceId(url);
+            Set<LoadBalancerLifecycle> supportedLifecycleProcessors =
+                    LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(this.clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), RequestDataContext.class, ResponseData.class, ServiceInstance.class);
+            DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest(new RequestDataContext(new RequestData(exchange.getRequest(), exchange.getAttributes()), this.getHint(serviceId)));
+            return this.choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext((response) -> {
+                if (!response.hasServer()) {
+                    supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, response)));
+                    throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
+                } else {
+                    ServiceInstance retrievedInstance = (ServiceInstance)response.getServer();
+                    URI uri = exchange.getRequest().getURI();
+                    Objects.requireNonNull(retrievedInstance, "retrievedInstance can not be null");
+                    String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
+                    if (schemePrefix != null) {
+                        overrideScheme = url.getScheme();
+                    }
+
+                    DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance, overrideScheme);
+                    URI requestUrl = UriTools.reconstructURI(serviceInstance, uri);
+
+                    log.info("agent {} chosen {}" ,serviceId, requestUrl);
+
+                    exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
+                    exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
+                    supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onStartRequest(lbRequest, response));
+                }
+            }).then(chain.filter(exchange)).doOnError((throwable) -> supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.FAILED, throwable, lbRequest, (Response)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR))))).doOnSuccess((aVoid) -> supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.SUCCESS, lbRequest, (Response)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR), new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest(), exchange.getAttributes()))))));
+        } else {
+            return chain.filter(exchange);
+        }
+    }
+    private String getServiceId(URI url){
+        String[] tt = url.toString().split("//");
+        if(tt.length==2) {
+            return tt[1];
+        }
+        return null;
+    }
+    protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
+        return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
+    }
+
+    private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId, Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
+        ReactorLoadBalancer<ServiceInstance> loadBalancer = (ReactorLoadBalancer)this.clientFactory.getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
+        if (loadBalancer == null) {
+            throw new NotFoundException("No loadbalancer available for " + serviceId);
+        } else {
+            supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onStart(lbRequest));
+            return loadBalancer.choose(lbRequest);
+        }
+    }
+
+    private String getHint(String serviceId) {
+        LoadBalancerProperties loadBalancerProperties = this.clientFactory.getProperties(serviceId);
+        Map<String, String> hints = loadBalancerProperties.getHint();
+        String defaultHint = (String)hints.getOrDefault("default", "default");
+        String hintPropertyValue = (String)hints.get(serviceId);
+        return hintPropertyValue != null ? hintPropertyValue : defaultHint;
+    }
+}

+ 25 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/util/TokenUtil.java

@@ -0,0 +1,25 @@
+package cn.com.yusys.ai.gateway.agent.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TokenUtil {
+    static Pattern inputpattern = Pattern.compile("\"input_?tokens\"\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
+
+    public static int extractInputTokens(String input) {
+        Matcher matcher = inputpattern.matcher(input);
+        if (matcher.find()) {
+            return Integer.parseInt(matcher.group(1));
+        }
+        return 0;
+    }
+
+    static Pattern outputpattern = Pattern.compile("\"output_?tokens\"\\s*:\\s*(\\d+)", Pattern.CASE_INSENSITIVE);
+    public static int extractOutputTokens(String input) {
+        Matcher matcher = outputpattern.matcher(input);
+        if (matcher.find()) {
+            return Integer.parseInt(matcher.group(1));
+        }
+        return 0;
+    }
+}

+ 84 - 0
yusys-ai-gateway-agent/src/main/java/cn/com/yusys/ai/gateway/agent/util/UriTools.java

@@ -0,0 +1,84 @@
+package cn.com.yusys.ai.gateway.agent.util;
+
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.util.*;
+
+/**
+ * URL地址拼接工具类
+ */
+public class UriTools {
+    private static final String DEFAULT_SCHEME = "http";
+    private static final String PERCENTAGE_SIGN = "%";
+    private static final String DEFAULT_SECURE_SCHEME = "https";
+
+    private static final Map<String, String> INSECURE_SCHEME_MAPPINGS;
+
+    static {
+        INSECURE_SCHEME_MAPPINGS = new HashMap<>();
+        INSECURE_SCHEME_MAPPINGS.put(DEFAULT_SCHEME, DEFAULT_SECURE_SCHEME);
+        INSECURE_SCHEME_MAPPINGS.put("ws", "wss");
+    }
+
+    public static URI reconstructURI(ServiceInstance serviceInstance, URI original) {
+        if (serviceInstance == null) {
+            throw new IllegalArgumentException("Service Instance cannot be null.");
+        }
+        return doReconstructURI(serviceInstance, original);
+    }
+
+    private static URI doReconstructURI(ServiceInstance serviceInstance, URI original) {
+        String host = serviceInstance.getHost();
+        String scheme = Optional.ofNullable(serviceInstance.getScheme())
+                .orElse(computeScheme(original, serviceInstance));
+        int port = computePort(serviceInstance.getPort(), scheme);
+
+        if (Objects.equals(host, original.getHost()) && port == original.getPort()
+                && Objects.equals(scheme, original.getScheme())) {
+            return original;
+        }
+
+        boolean encoded = containsEncodedParts(original);
+
+        String path = serviceInstance.getMetadata().get("__nacos.agent.endpoint.path__");
+        return UriComponentsBuilder.fromPath(path).scheme(scheme).host(host).port(port).build(encoded).toUri();
+    }
+
+    private static int computePort(int port, String scheme) {
+        if (port >= 0) {
+            return port;
+        }
+        if (Objects.equals(scheme, DEFAULT_SECURE_SCHEME)) {
+            return 443;
+        }
+        return 80;
+    }
+
+    private static String computeScheme(URI original, ServiceInstance serviceInstance) {
+        String originalOrDefault = Optional.ofNullable(original.getScheme()).orElse(DEFAULT_SCHEME);
+        if (serviceInstance.isSecure() && INSECURE_SCHEME_MAPPINGS.containsKey(originalOrDefault)) {
+            return INSECURE_SCHEME_MAPPINGS.get(originalOrDefault);
+        }
+        return originalOrDefault;
+    }
+
+    private static boolean containsEncodedParts(URI uri) {
+        boolean encoded = (uri.getRawQuery() != null && uri.getRawQuery().contains(PERCENTAGE_SIGN))
+                || (uri.getRawPath() != null && uri.getRawPath().contains(PERCENTAGE_SIGN))
+                || (uri.getRawFragment() != null && uri.getRawFragment().contains(PERCENTAGE_SIGN));
+        // Verify if it is really fully encoded. Treat partial encoded as unencoded.
+        if (encoded) {
+            try {
+                UriComponentsBuilder.fromUri(uri).build(true);
+                return true;
+            }
+            catch (IllegalArgumentException ignore) {
+            }
+            return false;
+        }
+        return false;
+    }
+}

+ 51 - 0
yusys-ai-gateway-agent/src/main/resources/application.yml

@@ -0,0 +1,51 @@
+server:
+  port: 9999
+management:
+  defaults:
+    metrics:
+      export:
+        enabled: false
+  tracing:
+    sampling:
+      probability: 1.0
+  opentelemetry:
+    tracing:
+      export:
+        otlp:
+          endpoint: http://127.0.0.1:9200/_otlp/v1/traces  # OTLP 追踪端点
+          transport: http
+spring:
+  application:
+    name: yusys-ai-gateway-agent
+  cloud:
+    nacos:
+      server-addr: 127.0.0.1:8848
+      username: nacos
+      password: nacos
+      discovery:
+        namespace: public
+        group: agent-endpoints
+    gateway:
+      server:
+        webflux:
+          httpclient:
+            # 响应超时:1小时
+            response-timeout: 3600s
+            # 连接超时:10秒
+            connect-timeout: 10000
+            # 最大连接数(避免连接耗尽)
+            max-connections: 200
+            # 获取连接超时
+            acquire-timeout: 45000
+          discovery:
+            locator:
+              enabled: true
+              lower-case-service-id: false
+  reactor:
+    context-propagation: auto
+agent:
+  session-key-in-header: sessionid
+logging:
+  pattern:
+    correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
+  include-application-name: false

+ 55 - 0
yusys-ai-gateway-mcp/pom.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>cn.com.yusys.ai</groupId>
+        <artifactId>yusys-ai-platform</artifactId>
+        <version>1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>yusys-ai-gateway-mcp</artifactId>
+    <name>yusys-ai-gateway-mcp</name>
+    <description>MCP网关</description>
+
+    <dependencies>
+        <!-- Spring Cloud Gateway(WebFlux 响应式),2025.1.x 起 artifactId 为 ...-server-webflux -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
+        </dependency>
+
+        <!-- Nacos 服务发现 -->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+
+        <!-- 负载均衡:网关按 lb:// 路由到下游服务时需要 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-opentelemetry</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 5 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/Cons.java

@@ -0,0 +1,5 @@
+package cn.com.yusys.ai.gateway.agent;
+
+public class Cons {
+    public final static String YUSP_ROUTE_NAME = "YUSP_ROUTE_NAME";
+}

+ 16 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/GatewayMcpApplication.java

@@ -0,0 +1,16 @@
+package cn.com.yusys.ai.gateway.agent;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+/**
+ * 网关启动类。
+ */
+@EnableDiscoveryClient
+@SpringBootApplication
+public class GatewayMcpApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(GatewayMcpApplication.class, args);
+    }
+}

+ 18 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/config/GatewayConfig.java

@@ -0,0 +1,18 @@
+package cn.com.yusys.ai.gateway.agent.config;
+
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class GatewayConfig {
+    @Bean
+    public RouteLocator catchAllRouteLocator(RouteLocatorBuilder builder) {
+        return builder.routes()
+                .route("catch_all_route",
+                        r -> r.path("/**")
+                                .uri("lb://agentAll"))
+                .build();
+    }
+}

+ 48 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspBeforeRouteToRequestUrlFilter.java

@@ -0,0 +1,48 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.Cons;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.Ordered;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+
+@Component
+public class YuspBeforeRouteToRequestUrlFilter implements GlobalFilter, Ordered {
+    Logger LOGGER = LoggerFactory.getLogger(YuspBeforeRouteToRequestUrlFilter.class);
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        ServerHttpRequest request = exchange.getRequest();
+        String mcpServer = request.getHeaders().getFirst("MCPSERVER");
+        if(null!=mcpServer) {
+            if(!mcpServer.contains("::")){
+                mcpServer = mcpServer + "::1.0";
+            }
+            LOGGER.info("接收到mcp请求:"+mcpServer);
+            URI targetUri = URI.create("lb://" + mcpServer);
+            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, targetUri);
+
+            Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
+            exchange.getAttributes().put(Cons.YUSP_ROUTE_NAME,route);
+            exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
+            return chain.filter(exchange);
+        }else {
+            throw new RuntimeException("非法请求");
+        }
+    }
+
+    @Override
+    public int getOrder() {
+        return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER-1;
+    }
+}

+ 28 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspEndRouteToRequestUrlFilter.java

@@ -0,0 +1,28 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.Cons;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+@Component
+public class YuspEndRouteToRequestUrlFilter implements GlobalFilter, Ordered {
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        Route route = (Route)exchange.getAttribute(Cons.YUSP_ROUTE_NAME);
+        exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR,route);
+        return chain.filter(exchange);
+    }
+
+    @Override
+    public int getOrder() {
+        return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER+1;
+    }
+}

+ 108 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/filter/YuspReactiveLoadBalancerClientFilter.java

@@ -0,0 +1,108 @@
+package cn.com.yusys.ai.gateway.agent.filter;
+
+import cn.com.yusys.ai.gateway.agent.util.UriTools;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.loadbalancer.*;
+import org.springframework.cloud.client.loadbalancer.CompletionContext.Status;
+import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
+import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
+import org.springframework.cloud.gateway.support.NotFoundException;
+import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
+import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
+import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
+import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@Component
+public class YuspReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {
+    private static final Logger log = LoggerFactory.getLogger(YuspReactiveLoadBalancerClientFilter.class);    public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
+    private final LoadBalancerClientFactory clientFactory;
+    private final GatewayLoadBalancerProperties properties;
+
+    public YuspReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, GatewayLoadBalancerProperties properties) {
+        super(clientFactory,properties);
+        this.clientFactory = clientFactory;
+        this.properties = properties;
+    }
+
+    public int getOrder() {
+        return 10150;
+    }
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
+        String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
+        if (url != null && ("lb".equals(url.getScheme()) || "lb".equals(schemePrefix))) {
+            ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
+            String serviceId = getServiceId(url);
+            Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(this.clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), RequestDataContext.class, ResponseData.class, ServiceInstance.class);
+            DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest(new RequestDataContext(new RequestData(exchange.getRequest(), exchange.getAttributes()), this.getHint(serviceId)));
+            return this.choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext((response) -> {
+                if (!response.hasServer()) {
+                    supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, response)));
+                    throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
+                } else {
+                    ServiceInstance retrievedInstance = (ServiceInstance)response.getServer();
+                    URI uri = exchange.getRequest().getURI();
+                    Objects.requireNonNull(retrievedInstance, "retrievedInstance can not be null");
+                    String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
+                    if (schemePrefix != null) {
+                        overrideScheme = url.getScheme();
+                    }
+
+                    DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance, overrideScheme);
+                    URI requestUrl = UriTools.reconstructURI(serviceInstance, uri);
+
+                    log.info("service {} chosen {}" ,serviceId, requestUrl);
+
+                    exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
+                    exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
+                    supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onStartRequest(lbRequest, response));
+                }
+            }).then(chain.filter(exchange)).doOnError((throwable) -> supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.FAILED, throwable, lbRequest, (Response)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR))))).doOnSuccess((aVoid) -> supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.SUCCESS, lbRequest, (Response)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR), new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest(), exchange.getAttributes()))))));
+        } else {
+            return chain.filter(exchange);
+        }
+    }
+
+    private String getServiceId(URI url){
+        String[] tt = url.toString().split("//");
+        if(tt.length==2) {
+            return tt[1];
+        }
+        return null;
+    }
+
+    protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
+        return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
+    }
+
+    private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId, Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
+        ReactorLoadBalancer<ServiceInstance> loadBalancer = (ReactorLoadBalancer)this.clientFactory.getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
+        if (loadBalancer == null) {
+            throw new NotFoundException("No loadbalancer available for " + serviceId);
+        } else {
+            supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onStart(lbRequest));
+            return loadBalancer.choose(lbRequest);
+        }
+    }
+
+    private String getHint(String serviceId) {
+        LoadBalancerProperties loadBalancerProperties = this.clientFactory.getProperties(serviceId);
+        Map<String, String> hints = loadBalancerProperties.getHint();
+        String defaultHint = (String)hints.getOrDefault("default", "default");
+        String hintPropertyValue = (String)hints.get(serviceId);
+        return hintPropertyValue != null ? hintPropertyValue : defaultHint;
+    }
+}

+ 82 - 0
yusys-ai-gateway-mcp/src/main/java/cn/com/yusys/ai/gateway/agent/util/UriTools.java

@@ -0,0 +1,82 @@
+package cn.com.yusys.ai.gateway.agent.util;
+
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+public class UriTools {
+    private static final String DEFAULT_SCHEME = "http";
+    private static final String PERCENTAGE_SIGN = "%";
+    private static final String DEFAULT_SECURE_SCHEME = "https";
+
+    private static final Map<String, String> INSECURE_SCHEME_MAPPINGS;
+
+    static {
+        INSECURE_SCHEME_MAPPINGS = new HashMap<>();
+        INSECURE_SCHEME_MAPPINGS.put(DEFAULT_SCHEME, DEFAULT_SECURE_SCHEME);
+        INSECURE_SCHEME_MAPPINGS.put("ws", "wss");
+    }
+
+    public static URI reconstructURI(ServiceInstance serviceInstance, URI original) {
+        if (serviceInstance == null) {
+            throw new IllegalArgumentException("Service Instance cannot be null.");
+        }
+        return doReconstructURI(serviceInstance, original);
+    }
+
+    private static URI doReconstructURI(ServiceInstance serviceInstance, URI original) {
+        String host = serviceInstance.getHost();
+        String scheme = Optional.ofNullable(serviceInstance.getScheme())
+                .orElse(computeScheme(original, serviceInstance));
+        int port = computePort(serviceInstance.getPort(), scheme);
+
+        if (Objects.equals(host, original.getHost()) && port == original.getPort()
+                && Objects.equals(scheme, original.getScheme())) {
+            return original;
+        }
+
+        boolean encoded = containsEncodedParts(original);
+        return UriComponentsBuilder.fromUri(original).scheme(scheme).host(host).port(port).build(encoded).toUri();
+    }
+
+    private static int computePort(int port, String scheme) {
+        if (port >= 0) {
+            return port;
+        }
+        if (Objects.equals(scheme, DEFAULT_SECURE_SCHEME)) {
+            return 443;
+        }
+        return 80;
+    }
+
+    private static String computeScheme(URI original, ServiceInstance serviceInstance) {
+        String originalOrDefault = Optional.ofNullable(original.getScheme()).orElse(DEFAULT_SCHEME);
+        if (serviceInstance.isSecure() && INSECURE_SCHEME_MAPPINGS.containsKey(originalOrDefault)) {
+            return INSECURE_SCHEME_MAPPINGS.get(originalOrDefault);
+        }
+        return originalOrDefault;
+    }
+
+    private static boolean containsEncodedParts(URI uri) {
+        boolean encoded = (uri.getRawQuery() != null && uri.getRawQuery().contains(PERCENTAGE_SIGN))
+                || (uri.getRawPath() != null && uri.getRawPath().contains(PERCENTAGE_SIGN))
+                || (uri.getRawFragment() != null && uri.getRawFragment().contains(PERCENTAGE_SIGN));
+        // Verify if it is really fully encoded. Treat partial encoded as unencoded.
+        if (encoded) {
+            try {
+                UriComponentsBuilder.fromUri(uri).build(true);
+                return true;
+            }
+            catch (IllegalArgumentException ignore) {
+            }
+            return false;
+        }
+        return false;
+    }
+}

+ 48 - 0
yusys-ai-gateway-mcp/src/main/resources/application.yml

@@ -0,0 +1,48 @@
+server:
+  port: 6666
+management:
+  defaults:
+    metrics:
+      export:
+        enabled: false
+  tracing:
+    sampling:
+      probability: 1.0
+  opentelemetry:
+    tracing:
+      export:
+        otlp:
+          endpoint: http://127.0.0.1:9200/_otlp/v1/traces  # OTLP 追踪端点
+          transport: http
+spring:
+  reactor:
+    context-propagation: auto
+  application:
+    name: yusys-ai-gateway-mcp
+  cloud:
+    nacos:
+      server-addr: 127.0.0.1:8848
+      # username: nacos
+      # password: nacos
+      discovery:
+        namespace: public
+    gateway:
+      server:
+        webflux:
+          httpclient:
+            # 响应超时:1小时
+            response-timeout: 3600s
+            # 连接超时:10秒
+            connect-timeout: 10000
+            # 最大连接数(避免连接耗尽)
+            max-connections: 200
+            # 获取连接超时
+            acquire-timeout: 45000
+          discovery:
+            locator:
+              enabled: true
+              lower-case-service-id: false
+logging:
+  pattern:
+    correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
+  include-application-name: false

+ 56 - 0
yusys-ai-http2mcp/mcp/user.txt

@@ -0,0 +1,56 @@
+{
+    "toolSpecification": {
+        "tools": [
+            {
+                "name": "getUserInfo",
+                "description": "获取用户基本信息",
+                "inputSchema": {
+                    "type": "object",
+                    "properties": {
+                        "userName": {
+                            "type": "string",
+                            "description": "用户姓名"
+                        }
+                    },
+                    "required": [
+                        "userName"
+                    ],
+                    "additionalProperties": false
+                },
+                "outputSchema": null
+            }
+        ],
+        "toolsMeta": {
+            "getUserInfo": {
+                "enabled": true
+            }
+        }
+    },
+    "requestBodyTemplateForHttp": {
+        "body": {
+            "ask": {
+                "name": "{userName}"
+            }
+        }
+    },
+    "responseBodyTemplateForMcp": {
+        "body": "response.ask"
+    },
+    "endpoints": [
+        {
+            "protocol": "http",
+            "address": "127.0.0.1",
+            "port": 7777,
+            "path": "/userinfo",
+            "headers": [
+                {
+                    "variables": {
+                        "token-xxx": {
+                            "value": "yujjkhgtyjjk"
+                        }
+                    }
+                }
+            ]
+        }
+    ]
+}

+ 54 - 0
yusys-ai-http2mcp/pom.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>cn.com.yusys.ai</groupId>
+        <artifactId>yusys-ai-platform</artifactId>
+        <version>1.0</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>yusys-ai-http2mcp</artifactId>
+    <name>yusys-ai-http2mcp</name>
+    <description>HTTP转MCP</description>
+    <properties>
+
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+        <dependency>
+             <groupId>com.alibaba.cloud.ai</groupId>
+             <artifactId>spring-ai-alibaba-starter-mcp-registry</artifactId>
+         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-opentelemetry</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 17 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/Http2McpApplication.java

@@ -0,0 +1,17 @@
+package cn.com.yusys.ai.http2mcp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动类。
+ */
+@SpringBootApplication
+@EnableScheduling
+public class Http2McpApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(Http2McpApplication.class, args);
+    }
+}

+ 50 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/config/McpServerStatelessAutoConfiguration.java

@@ -0,0 +1,50 @@
+package cn.com.yusys.ai.http2mcp.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.server.McpTransportContextExtractor;
+import io.modelcontextprotocol.server.transport.WebFluxStatelessServerTransport;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.server.ServerRequest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Configuration
+public class McpServerStatelessAutoConfiguration {
+    @Value("${http-mcp.removeHeaders}")
+    private List<String> removeHeaders;
+    /**
+     * 扩展支持-头信息传递
+     * @param objectMapper
+     * @param serverProperties
+     * @return
+     */
+    @Bean
+    public WebFluxStatelessServerTransport webFluxStatelessServerTransport(@Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper, McpServerStreamableHttpProperties serverProperties) {
+        return WebFluxStatelessServerTransport.builder().jsonMapper(new JacksonMcpJsonMapper(objectMapper)).messageEndpoint(serverProperties.getMcpEndpoint()).contextExtractor(new McpTransportContextExtractor(){
+            @Override
+            public McpTransportContext extract(Object request) {
+                Map<String, List<String>> metadata = new HashMap();
+                if(request instanceof ServerRequest){
+                    ServerRequest requestT = (ServerRequest) request;
+                    ServerRequest.Headers hs = requestT.headers();
+                    if(null!=hs) {
+                        hs.asHttpHeaders().forEach((k, v) -> {
+                            if(!removeHeaders.contains(k)){
+                                metadata.put(k, v);
+                            }
+                        });
+                    }
+                }
+                return new YuspMcpTransportContext(metadata);
+            }
+        }).build();
+    }
+}

+ 37 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/config/YuspMcpTransportContext.java

@@ -0,0 +1,37 @@
+package cn.com.yusys.ai.http2mcp.config;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.util.Assert;
+
+import java.util.List;
+import java.util.Map;
+
+public class YuspMcpTransportContext implements McpTransportContext {
+    private final Map<String, List<String>> metadata;
+
+    public YuspMcpTransportContext(Map<String, List<String>> metadata) {
+        Assert.notNull(metadata, "The metadata cannot be null");
+        this.metadata = metadata;
+    }
+
+    public Object get(String key) {
+        return this.metadata.get(key);
+    }
+
+    public boolean equals(Object o) {
+        if (o != null && this.getClass() == o.getClass()) {
+            YuspMcpTransportContext that = (YuspMcpTransportContext)o;
+            return this.metadata.equals(that.metadata);
+        } else {
+            return false;
+        }
+    }
+
+    public Map<String, List<String>> getMetadata() {
+        return metadata;
+    }
+
+    public int hashCode() {
+        return this.metadata.hashCode();
+    }
+}

+ 55 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/mcp/Http2McpDto.java

@@ -0,0 +1,55 @@
+package cn.com.yusys.ai.http2mcp.mcp;
+
+import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo;
+import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
+
+import java.util.List;
+import java.util.Map;
+
+public class Http2McpDto {
+    private String fileName;
+    private McpToolSpecification toolSpecification;
+    private List<McpEndpointInfo> endpoints;
+    private Map<String,Object> requestBodyTemplateForHttp;
+    private Map<String,Object> responseBodyTemplateForMcp;
+
+    public McpToolSpecification getToolSpecification() {
+        return toolSpecification;
+    }
+
+    public void setToolSpecification(McpToolSpecification toolSpecification) {
+        this.toolSpecification = toolSpecification;
+    }
+
+    public List<McpEndpointInfo> getEndpoints() {
+        return endpoints;
+    }
+
+    public void setEndpoints(List<McpEndpointInfo> endpoints) {
+        this.endpoints = endpoints;
+    }
+
+    public Map<String, Object> getRequestBodyTemplateForHttp() {
+        return requestBodyTemplateForHttp;
+    }
+
+    public void setRequestBodyTemplateForHttp(Map<String, Object> requestBodyTemplateForHttp) {
+        this.requestBodyTemplateForHttp = requestBodyTemplateForHttp;
+    }
+
+    public Map<String, Object> getResponseBodyTemplateForMcp() {
+        return responseBodyTemplateForMcp;
+    }
+
+    public void setResponseBodyTemplateForMcp(Map<String, Object> responseBodyTemplateForMcp) {
+        this.responseBodyTemplateForMcp = responseBodyTemplateForMcp;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+}

+ 170 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/mcp/Http2McpToolCallback.java

@@ -0,0 +1,170 @@
+package cn.com.yusys.ai.http2mcp.mcp;
+
+import cn.com.yusys.ai.http2mcp.config.YuspMcpTransportContext;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo;
+import com.alibaba.nacos.api.ai.model.mcp.registry.Input;
+import com.alibaba.nacos.api.ai.model.mcp.registry.KeyValueInput;
+import com.alibaba.nacos.shaded.com.google.common.collect.Maps;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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 java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class Http2McpToolCallback implements ToolCallback {
+    static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    static {
+        OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+    }
+    private static final Logger LOGGER = LoggerFactory.getLogger(Http2McpToolCallback.class);
+
+    private Http2McpToolDefinition toolDefinition;
+    public Http2McpToolCallback(Http2McpToolDefinition toolDefinition){
+        this.toolDefinition = toolDefinition;
+
+    }
+    @Override
+    public ToolDefinition getToolDefinition() {
+        return toolDefinition;
+    }
+
+    @Override
+    public String call(String input) {
+        return  call(input, new ToolContext(Maps.newHashMap()));
+    }
+
+    @Override
+    public String call(String input, ToolContext toolContext) {
+        List<McpEndpointInfo> endpoints = toolDefinition.getEndpoints();
+        if(null==endpoints || endpoints.isEmpty()){
+            throw new RuntimeException("无可以访问的地址,工具调用异常");
+        }
+        String toolName = toolDefinition.name();
+        String desc = toolDefinition.description();
+        LOGGER.info("开始执行:"+toolName+"("+desc+"), 输入参数:"+input);
+
+        // 随机获取节点
+        McpEndpointInfo endpoint = getRandomElement(endpoints);
+        String protocol = endpoint.getProtocol();
+        if(null==protocol){
+            protocol = "http";
+        }
+        String path = endpoint.getPath();
+        if(!path.startsWith("/")){
+            path = "/" + path;
+        }
+        Map<String, List<String>> heads = getHeader(endpoint);
+        Map<String, List<String>> headsAll = getHeaderFromContext(toolContext);
+        headsAll.putAll(heads);
+        String url = protocol + "://" + endpoint.getAddress() + ":" + endpoint.getPort()+path;
+        try(HttpResponse response = HttpRequest.post(url)
+                .body(getRequestBody(input))
+                .header(headsAll).header("Content-Type", "application/json")
+                .timeout(60000)
+                .execute();) {
+            if (200 == response.getStatus()) {
+                return getResponseBody(response.body());
+            } else {
+                throw new RuntimeException("服务端500");
+            }
+        }catch (Exception e){
+            LOGGER.error("执行http请求异常",e);
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    private String getRequestBody(String input) throws JsonProcessingException {
+        Map<String,Object> paramMap = OBJECT_MAPPER.readValue(input,Map.class);
+        Map<String, Object> request = toolDefinition.getRequestBodyTemplateForHttp();
+        Object template = request.get("body");
+        String templateStr = OBJECT_MAPPER.writeValueAsString(template);
+        String resultJson = StrUtil.format(templateStr, paramMap);
+        return resultJson;
+    }
+
+    private String getResponseBody(String responseStr) throws JsonProcessingException {
+        if(null == responseStr){
+            return null;
+        }
+        try {
+            JSONObject response = new JSONObject();
+            JSONObject jsonObject = JSONUtil.parseObj(responseStr);
+            response.put("response", jsonObject);
+            String path = toolDefinition.getResponseBodyTemplateForMcp().get("body").toString();
+            Object weather = JSONUtil.getByPath(response, path);
+            return weather.toString();
+        }catch (Exception e){
+            return responseStr;
+        }
+    }
+
+    public static <T> T getRandomElement(List<T> list) {
+        if (list == null || list.isEmpty()) return null;
+        int index = ThreadLocalRandom.current().nextInt(list.size());
+        return list.get(index);
+    }
+
+    /**
+     * 获取txt文件中定义的头信息
+     * @param endpoint
+     * @return
+     */
+    private Map<String,List<String>> getHeader(McpEndpointInfo endpoint){
+        Map<String,List<String>> heads = new HashMap<>();
+        List<KeyValueInput> hs = endpoint.getHeaders();
+        if(null!=hs && !hs.isEmpty()){
+            for(KeyValueInput h:hs){
+                Map<String, Input> vs = h.getVariables();
+                if(null!=vs){
+                    Set<String> keys = vs.keySet();
+                    for(String key:keys){
+                        Input p = vs.get(key);
+                        if(null!=p){
+                            String value = p.getValue();
+                            if(null!=value){
+                                List<String> vT = new ArrayList<>();
+                                vT.add(value);
+                                heads.put(key,vT);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return heads;
+    }
+
+    /**
+     * 获取前端透传的头信息
+     * @param toolContext
+     * @return
+     */
+    private Map<String, List<String>> getHeaderFromContext(ToolContext toolContext){
+        Map<String, Object> context = toolContext.getContext();
+        if(null != context && context.containsKey("exchange")){
+            Object contextRelT = context.get("exchange");
+            if(null!=contextRelT){
+                YuspMcpTransportContext contextRel = (YuspMcpTransportContext) contextRelT;
+                Map<String, List<String>> header = contextRel.getMetadata();
+                if(null!=header){
+                    return header;
+                }
+            }
+        }
+        return new HashMap<>();
+    }
+}

+ 63 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/mcp/Http2McpToolDefinition.java

@@ -0,0 +1,63 @@
+package cn.com.yusys.ai.http2mcp.mcp;
+
+import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo;
+import com.alibaba.nacos.common.utils.JacksonUtils;
+import org.jspecify.annotations.Nullable;
+import org.springframework.ai.tool.definition.ToolDefinition;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class Http2McpToolDefinition implements ToolDefinition {
+    protected @Nullable String name;
+
+    protected @Nullable String description;
+
+    protected @Nullable Object inputSchema;
+
+    List<McpEndpointInfo> endpoints;
+
+    private Map<String,Object> requestBodyTemplateForHttp;
+    private Map<String,Object> responseBodyTemplateForMcp;
+
+    public Http2McpToolDefinition(String name,
+                                  String description,
+                                  Object inputSchema,
+                                  List<McpEndpointInfo> endpoints,
+                                  Map<String,Object> requestBodyTemplateForHttp,
+                                  Map<String,Object> responseBodyTemplateForMcp) {
+        this.name = name;
+        this.description = description;
+        this.inputSchema = inputSchema;
+        this.endpoints = endpoints;
+        this.requestBodyTemplateForHttp = requestBodyTemplateForHttp;
+        this.responseBodyTemplateForMcp = responseBodyTemplateForMcp;
+    }
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public String description() {
+        return description;
+    }
+
+    @Override
+    public String inputSchema() {
+        return JacksonUtils.toJson(Objects.requireNonNull(this.inputSchema, "Tool inputSchema must not be null"));
+    }
+
+    public List<McpEndpointInfo> getEndpoints() {
+        return endpoints;
+    }
+
+    public Map<String, Object> getRequestBodyTemplateForHttp() {
+        return requestBodyTemplateForHttp;
+    }
+
+    public Map<String, Object> getResponseBodyTemplateForMcp() {
+        return responseBodyTemplateForMcp;
+    }
+}

+ 155 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/service/Http2McpParseService.java

@@ -0,0 +1,155 @@
+package cn.com.yusys.ai.http2mcp.service;
+
+import cn.com.yusys.ai.http2mcp.mcp.Http2McpDto;
+import cn.com.yusys.ai.http2mcp.mcp.Http2McpToolCallback;
+import cn.com.yusys.ai.http2mcp.mcp.Http2McpToolDefinition;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import com.alibaba.nacos.api.ai.model.mcp.McpEndpointInfo;
+import com.alibaba.nacos.api.ai.model.mcp.McpTool;
+import com.alibaba.nacos.api.ai.model.mcp.McpToolMeta;
+import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.server.McpStatelessAsyncServer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.mcp.McpToolUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.util.*;
+
+@Service
+public class Http2McpParseService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(Http2McpParseService.class);
+
+    @Autowired
+    private McpStatelessAsyncServer mcpAsyncServer;
+
+    @Autowired
+    private McpToolsUpdate2NacosService mcpToolsUpdate2NacosService;
+
+    @Value("${http-mcp.folder}")
+    private String folder;
+
+    static Map<String,String> FILE_CONTENT = new HashMap<>();
+
+    @Scheduled(fixedRate = 20000)  // 单位毫秒
+    public void execute() {
+        File folderFile = new File(folder);
+        if(folderFile.exists()){
+            File[] files = folderFile.listFiles();
+            for(File file:files){
+                if(file.isFile()){
+                    String tag = file.lastModified() + "-" +file.length();
+                    String fileName = FileNameUtil.mainName(file);
+                    String tagT = FILE_CONTENT.get(fileName);
+                    if(tag.equals(tagT)){
+                        continue;
+                    }else {
+                        try {
+                            LOGGER.info("检测到有变更的文件:"+fileName);
+                            String fileContent = FileUtil.readUtf8String(file);
+                            loadTool(fileName,fileContent);
+                            FILE_CONTENT.put(fileName,tag);
+                            LOGGER.info("变更的文件解析成功:"+fileName);
+                        }catch (Exception e){
+                            LOGGER.error("文件解析报错,跳过了:"+fileName,e);
+                        }
+                    }
+                }
+            }
+        }else {
+            LOGGER.error("存放MCP文件的文件夹不存在:"+folder);
+        }
+    }
+    public void loadTool(String fileName,String fileContent) throws Exception {
+        Http2McpDto http2McpDto = strToDto(fileContent);
+        http2McpDto.setFileName(fileName);
+        Set<String> disableTools = getDisableTool(fileName,http2McpDto.getToolSpecification());
+        List<Http2McpToolDefinition> defs = parseTools(http2McpDto);
+        for(Http2McpToolDefinition def:defs){
+            String toolName = def.name();
+            if(disableTools.contains(toolName)){
+                removeTool(toolName);
+                LOGGER.info("移除工具:"+toolName);
+            }else {
+                addTool(def);
+                LOGGER.info("添加工具成功:"+toolName);
+            }
+        }
+        mcpToolsUpdate2NacosService.updateMcpTool2Nacos();
+    }
+
+    private void addTool(final Http2McpToolDefinition toolDefinition) {
+        String toolName = toolDefinition.name();// 先删除再新增
+        removeTool(toolName);
+        mcpAsyncServer.addTool(McpToolUtils.toStatelessAsyncToolSpecification(new Http2McpToolCallback(toolDefinition),null)).block();
+    }
+
+    private void removeTool(final String toolName) {
+        mcpAsyncServer.removeTool(toolName).block();
+    }
+
+
+    private List<Http2McpToolDefinition> parseTools(Http2McpDto http2McpDto) {
+        String fileName = http2McpDto.getFileName();
+        McpToolSpecification toolSpec = http2McpDto.getToolSpecification();
+        List<Http2McpToolDefinition> defs = new ArrayList<>();
+        if (toolSpec != null) {
+            List<McpTool> toolsList = toolSpec.getTools();
+            for (McpTool tool : toolsList) {
+                String toolNameRel = getRelToolName(fileName, tool.getName());
+                String toolDescription = tool.getDescription();
+                Map<String, Object> inputSchema = tool.getInputSchema();
+                List<McpEndpointInfo> endpoints = http2McpDto.getEndpoints();
+                Http2McpToolDefinition toolDefinition = new Http2McpToolDefinition(
+                        toolNameRel, toolDescription,inputSchema,endpoints,
+                        http2McpDto.getRequestBodyTemplateForHttp(),
+                        http2McpDto.getResponseBodyTemplateForMcp()
+                );
+                defs.add(toolDefinition);
+            }
+        }
+        return defs;
+    }
+
+    private Set<String> getDisableTool(String fileName,McpToolSpecification toolSpec){
+        Set re = new HashSet<>();
+        if(null!=toolSpec){
+            Map<String, McpToolMeta> meta = toolSpec.getToolsMeta();
+            if(null!=meta){
+                Set<String> keys = meta.keySet();
+                for(String key:keys){
+                    McpToolMeta toolMeta = meta.get(key);
+                    if(null!=toolMeta){
+                        if(!toolMeta.isEnabled()){
+                            re.add(getRelToolName(fileName,key));
+                        }
+                    }
+                }
+            }
+        }
+        return re;
+    }
+
+    static ObjectMapper objectMapper = new ObjectMapper();
+    static {
+        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+    }
+
+    private String getRelToolName(String fileName,String toolName){
+        return fileName + "_"+toolName;
+    }
+
+    private static Http2McpDto strToDto(String jsonStr) throws Exception {
+        Http2McpDto data = objectMapper.readValue(jsonStr, Http2McpDto.class);
+        return data;
+    }
+}

+ 82 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/service/McpToolsUpdate2NacosService.java

@@ -0,0 +1,82 @@
+package cn.com.yusys.ai.http2mcp.service;
+
+import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService;
+import com.alibaba.cloud.ai.mcp.register.NacosMcpRegisterProperties;
+import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
+import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
+import com.alibaba.nacos.api.ai.model.mcp.McpTool;
+import com.alibaba.nacos.api.ai.model.mcp.McpToolSpecification;
+import com.alibaba.nacos.api.utils.StringUtils;
+import com.alibaba.nacos.common.utils.JacksonUtils;
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.server.McpStatelessAsyncServer;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+@Service
+public class McpToolsUpdate2NacosService {
+    private static final Logger log = LoggerFactory.getLogger(McpToolsUpdate2NacosService.class);
+    private McpStatelessAsyncServer mcpStatelessAsyncServer;
+    private NacosMcpOperationService nacosMcpOperationService;
+    private NacosMcpRegisterProperties nacosMcpRegistryProperties;
+    private McpSchema.Implementation serverInfo;
+
+    public McpToolsUpdate2NacosService(McpStatelessAsyncServer mcpStatelessAsyncServer,
+                                       NacosMcpOperationService nacosMcpOperationService,
+                                       NacosMcpRegisterProperties nacosMcpRegistryProperties){
+        this.mcpStatelessAsyncServer = mcpStatelessAsyncServer;
+        this.nacosMcpOperationService = nacosMcpOperationService;
+        this.nacosMcpRegistryProperties = nacosMcpRegistryProperties;
+        this.serverInfo = mcpStatelessAsyncServer.getServerInfo();
+    }
+
+    public void updateMcpTool2Nacos() throws Exception {
+
+        McpToolSpecification mcpToolSpec = new McpToolSpecification();
+
+        McpSchema.ServerCapabilities serverCapabilities = mcpStatelessAsyncServer.getServerCapabilities();
+        Field toolsField = McpStatelessAsyncServer.class.getDeclaredField("tools");
+        toolsField.setAccessible(true);
+        CopyOnWriteArrayList<McpStatelessServerFeatures.AsyncToolSpecification> tools =
+                (CopyOnWriteArrayList)toolsField.get(mcpStatelessAsyncServer);
+        if (serverCapabilities.tools() != null) {
+            List<McpSchema.Tool> toolsNeedtoRegister = tools.stream().map(McpStatelessServerFeatures.AsyncToolSpecification::tool).toList();
+            String toolsStr = JacksonUtils.toJson(toolsNeedtoRegister);
+            List<McpTool> toolsToNacosList = (List)JacksonUtils.toObj(toolsStr, new TypeReference<List<McpTool>>() {
+            });
+            mcpToolSpec.setTools(toolsToNacosList);
+        }
+
+        McpServerDetailInfo mcpServerDetailInfo = nacosMcpOperationService.getServerDetail(serverInfo.name(), serverInfo.version());
+
+        McpEndpointSpec endpointSpec = new McpEndpointSpec();
+        endpointSpec.setType("REF");
+        Map<String, String> endpointSpecData = new HashMap();
+        endpointSpecData.put("serviceName", this.getRegisterServiceName());
+        String groupName = StringUtils.isBlank(nacosMcpRegistryProperties.getServiceGroup()) ? "DEFAULT_GROUP" : nacosMcpRegistryProperties.getServiceGroup();
+        endpointSpecData.put("groupName", groupName);
+        endpointSpec.setData(endpointSpecData);
+
+        nacosMcpOperationService.updateMcpServer(serverInfo.name(), mcpServerDetailInfo, mcpToolSpec, endpointSpec);
+        log.info("更新到Nacos成功");
+    }
+
+    private String getRegisterServiceName() {
+        String serviceName = this.nacosMcpRegistryProperties.getServiceName();
+        if (StringUtils.isBlank(serviceName)) {
+            String var10000 = this.serverInfo.name();
+            return var10000 + "::" + this.serverInfo.version();
+        } else {
+            return (String) Objects.requireNonNull(serviceName);
+        }
+    }
+}

+ 75 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/test/HiRest.java

@@ -0,0 +1,75 @@
+package cn.com.yusys.ai.http2mcp.test;
+
+import cn.com.yusys.ai.http2mcp.service.Http2McpParseService;
+import com.alibaba.nacos.common.utils.JacksonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+@RestController
+public class HiRest {
+    private static final Logger LOGGER = LoggerFactory.getLogger(HiRest.class);
+
+    @Autowired
+    private Http2McpParseService mcpToolParseService;
+    @PostMapping("/userinfo")
+    public Mono<Msg> userinfo(@RequestBody Msg b, @RequestHeader Map allHeaders) throws Exception {
+        Object name = b.getAsk().getName();
+        LOGGER.info("获取用户信息"+name);
+        LOGGER.info("header:"+ JacksonUtils.toJson(allHeaders));
+        b.getAsk().setUserId("34242219970876");
+        b.getAsk().setBirth(LocalDate.now());
+        return Mono.just(b);
+    }
+
+    public static class Msg{
+        private Ask ask;
+
+        public Ask getAsk() {
+            return ask;
+        }
+
+        public void setAsk(Ask ask) {
+            this.ask = ask;
+        }
+    }
+
+    public static class Ask{
+        private String name;
+        private LocalDate birth;
+        private String userId;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public LocalDate getBirth() {
+            return birth;
+        }
+
+        public void setBirth(LocalDate birth) {
+            this.birth = birth;
+        }
+
+        public String getUserId() {
+            return userId;
+        }
+
+        public void setUserId(String userId) {
+            this.userId = userId;
+        }
+    }
+
+}

+ 45 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/test/McpClientGatewayTest.java

@@ -0,0 +1,45 @@
+package cn.com.yusys.ai.http2mcp.test;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+public class McpClientGatewayTest {
+    public static void main(String[] args) throws Exception {
+        HttpClientStreamableHttpTransport transport =
+                HttpClientStreamableHttpTransport.builder("http://127.0.0.1:6666/mcp")
+                        .httpRequestCustomizer(new McpSyncHttpClientRequestCustomizer(){
+                            @Override
+                            public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body, McpTransportContext context) {
+                                builder.header("MCPSERVER","yusys-ai-http2mcp");
+                                builder.header("token","yyy");
+                            }
+                        })
+                        .build();
+        McpSyncClient client = McpClient.sync(transport)
+                .requestTimeout(Duration.ofSeconds(30))  // 每个请求的超时时间
+                .build();
+        client.initialize();
+
+        // 列出工具
+        McpSchema.ListToolsResult tools = client.listTools();
+        System.out.println("远程 MCP 服务暴露的工具:");
+        tools.tools().forEach(tool -> System.out.println("  - " + tool.name() + ": " + tool.description()));
+
+        // 调用工具(根据实际工具名称和参数调整)
+        McpSchema.CallToolResult result = client.callTool(
+                new McpSchema.CallToolRequest("user_getUserInfo", Map.of("userName", "王菲","b",3))
+        );
+        System.out.println("结果:" + result.content());
+        client.closeGracefully();
+    }
+}

+ 35 - 0
yusys-ai-http2mcp/src/main/java/cn/com/yusys/ai/http2mcp/test/McpClientTest.java

@@ -0,0 +1,35 @@
+package cn.com.yusys.ai.http2mcp.test;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+public class McpClientTest {
+    public static void main(String[] args) throws Exception {
+        HttpClientStreamableHttpTransport transport =
+                HttpClientStreamableHttpTransport.builder("http://127.0.0.1:7777/mcp")
+                        .build();
+        McpSyncClient client = McpClient.sync(transport)
+                .requestTimeout(Duration.ofSeconds(30))  // 每个请求的超时时间
+                .build();
+        client.initialize();
+
+        // 列出工具
+        McpSchema.ListToolsResult tools = client.listTools();
+        System.out.println("远程 MCP 服务暴露的工具:");
+        tools.tools().forEach(tool -> System.out.println("  - " + tool.name() + ": " + tool.description()));
+
+        // 调用工具(根据实际工具名称和参数调整)
+        McpSchema.CallToolResult result = client.callTool(
+                new McpSchema.CallToolRequest("user_getUserInfo", Map.of("userName", "王菲","b",3))
+        );
+        System.out.println("结果:" + result.content());
+        client.closeGracefully();
+    }
+}

+ 410 - 0
yusys-ai-http2mcp/src/main/java/com/alibaba/cloud/ai/mcp/register/NacosStatelessMcpRegister.java

@@ -0,0 +1,410 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by FernFlower decompiler)
+//
+
+package com.alibaba.cloud.ai.mcp.register;
+
+import com.alibaba.cloud.ai.mcp.nacos.NacosMcpProperties;
+import com.alibaba.cloud.ai.mcp.nacos.service.NacosMcpOperationService;
+import com.alibaba.cloud.ai.mcp.register.utils.CheckCompatibleResult;
+import com.alibaba.nacos.api.ai.model.mcp.*;
+import com.alibaba.nacos.api.ai.model.mcp.registry.ServerVersionDetail;
+import com.alibaba.nacos.api.exception.NacosException;
+import com.alibaba.nacos.api.naming.pojo.Instance;
+import com.alibaba.nacos.api.utils.NetUtils;
+import com.alibaba.nacos.api.utils.StringUtils;
+import com.alibaba.nacos.common.utils.JacksonUtils;
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.server.McpStatelessAsyncServer;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
+import org.springframework.boot.web.server.context.ConfigurableWebServerApplicationContext;
+import org.springframework.boot.web.server.context.WebServerApplicationContext;
+import org.springframework.boot.web.server.context.WebServerInitializedEvent;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+public class NacosStatelessMcpRegister implements ApplicationListener<WebServerInitializedEvent> {
+    private static final Logger log = LoggerFactory.getLogger(NacosMcpRegister.class);
+    private String type;
+    private NacosMcpRegisterProperties nacosMcpRegistryProperties;
+    private NacosMcpProperties nacosMcpProperties;
+    private McpSchema.Implementation serverInfo;
+    private McpStatelessAsyncServer mcpStatelessAsyncServer;
+    private CopyOnWriteArrayList<McpStatelessServerFeatures.AsyncToolSpecification> tools;
+    private Map<String, McpToolMeta> toolsMeta;
+    private McpSchema.ServerCapabilities serverCapabilities;
+    private McpServerProperties mcpServerProperties;
+    private McpServerSseProperties mcpServerSseProperties;
+    private ApplicationContext applicationContext;
+    private McpServerStreamableHttpProperties mcpServerStreamableHttpProperties;
+    private NacosMcpOperationService nacosMcpOperationService;
+    private McpServerDetailInfo serverDetailInfo;
+    private boolean success = false;
+
+    public NacosStatelessMcpRegister(NacosMcpOperationService nacosMcpOperationService, McpStatelessAsyncServer mcpStatelessAsyncServer, NacosMcpProperties nacosMcpProperties, NacosMcpRegisterProperties nacosMcpRegistryProperties, McpServerProperties mcpServerProperties, McpServerSseProperties mcpServerSseProperties, ApplicationContext applicationContext, String type) {
+        this.mcpStatelessAsyncServer = mcpStatelessAsyncServer;
+        log.info("Mcp server type: {}", type);
+        this.type = type;
+        this.nacosMcpProperties = nacosMcpProperties;
+        this.nacosMcpRegistryProperties = nacosMcpRegistryProperties;
+        this.nacosMcpOperationService = nacosMcpOperationService;
+        this.mcpServerProperties = mcpServerProperties;
+        this.mcpServerSseProperties = mcpServerSseProperties;
+        this.applicationContext = applicationContext;
+        this.mcpServerStreamableHttpProperties = (McpServerStreamableHttpProperties)this.applicationContext.getBean(McpServerStreamableHttpProperties.class);
+
+        try {
+            if (StringUtils.isBlank(this.mcpServerProperties.getVersion())) {
+                throw new IllegalArgumentException("[Nacos MCP Register] The version number of Mcp Server is empty; you need to specify a version number.");
+            }
+
+            this.serverInfo = mcpStatelessAsyncServer.getServerInfo();
+            this.serverCapabilities = mcpStatelessAsyncServer.getServerCapabilities();
+            Field toolsField = McpStatelessAsyncServer.class.getDeclaredField("tools");
+            toolsField.setAccessible(true);
+            this.tools = (CopyOnWriteArrayList)toolsField.get(this.mcpStatelessAsyncServer);
+            this.toolsMeta = new HashMap();
+            McpServerDetailInfo serverDetailInfo = null;
+
+            try {
+                serverDetailInfo = this.nacosMcpOperationService.getServerDetail(this.serverInfo.name(), this.serverInfo.version());
+            } catch (NacosException var22) {
+                log.info("[Nacos MCP Register] Can not found McpServer {} info from nacos, try to register info of local MCP Server {}, version:{} to Nacos", new Object[]{this.serverInfo.name(), this.serverInfo.name(), this.mcpServerProperties.getVersion()});
+            }
+
+            if (serverDetailInfo != null) {
+                try {
+                    CheckCompatibleResult checkResult = this.checkCompatible(serverDetailInfo);
+                    if (!checkResult.isCompatible()) {
+                        log.error("[Nacos MCP Register] Check mcp server compatible false, caused by:{}", checkResult.getMessage());
+                        throw new Exception("[Nacos MCP Register] Check mcp server compatible false, caused by:" + checkResult.getMessage());
+                    }
+                } catch (Exception e) {
+                    log.error("[Nacos MCP Register] Check mcp server compatible failed", e);
+                    throw e;
+                }
+
+                this.serverDetailInfo = serverDetailInfo;
+                if (this.serverCapabilities.tools() != null) {
+                    this.updateTools(serverDetailInfo);
+                }
+
+                this.subscribe();
+                this.success = true;
+                return;
+            }
+
+            McpToolSpecification mcpToolSpec = new McpToolSpecification();
+            if (this.serverCapabilities.tools() != null) {
+                List<McpSchema.Tool> toolsNeedtoRegister = this.tools.stream().map(McpStatelessServerFeatures.AsyncToolSpecification::tool).toList();
+                String toolsStr = JacksonUtils.toJson(toolsNeedtoRegister);
+                List<McpTool> toolsToNacosList = (List)JacksonUtils.toObj(toolsStr, new TypeReference<List<McpTool>>() {
+                });
+                mcpToolSpec.setTools(toolsToNacosList);
+            }
+
+            ServerVersionDetail serverVersionDetail = new ServerVersionDetail();
+            serverVersionDetail.setVersion(this.serverInfo.version());
+            McpServerBasicInfo serverBasicInfo = new McpServerBasicInfo();
+            serverBasicInfo.setName(this.serverInfo.name());
+            serverBasicInfo.setVersionDetail(serverVersionDetail);
+            String description = this.mcpServerProperties.getInstructions();
+            if (StringUtils.isBlank(description)) {
+                description = this.serverInfo.name();
+            }
+
+            serverBasicInfo.setDescription(description);
+            McpEndpointSpec endpointSpec = new McpEndpointSpec();
+            if (StringUtils.equals(this.type, "stdio")) {
+                serverBasicInfo.setProtocol("stdio");
+                serverBasicInfo.setFrontProtocol("stdio");
+            } else {
+                endpointSpec.setType("REF");
+                Map<String, String> endpointSpecData = new HashMap();
+                endpointSpecData.put("serviceName", this.getRegisterServiceName());
+                String groupName = StringUtils.isBlank(this.nacosMcpRegistryProperties.getServiceGroup()) ? "DEFAULT_GROUP" : this.nacosMcpRegistryProperties.getServiceGroup();
+                endpointSpecData.put("groupName", groupName);
+                endpointSpec.setData(endpointSpecData);
+                McpServerRemoteServiceConfig remoteServerConfigInfo = new McpServerRemoteServiceConfig();
+                String contextPath = this.nacosMcpRegistryProperties.getSseExportContextPath();
+                if (StringUtils.isBlank(contextPath)) {
+                    contextPath = "";
+                }
+
+                if (StringUtils.equals(this.type, "mcp-sse")) {
+                    remoteServerConfigInfo.setExportPath(contextPath + this.mcpServerSseProperties.getSseEndpoint());
+                    serverBasicInfo.setRemoteServerConfig(remoteServerConfigInfo);
+                    serverBasicInfo.setProtocol("mcp-sse");
+                    serverBasicInfo.setFrontProtocol("mcp-sse");
+                } else {
+                    if (this.mcpServerStreamableHttpProperties != null) {
+                        remoteServerConfigInfo.setExportPath(contextPath + this.mcpServerStreamableHttpProperties.getMcpEndpoint());
+                    } else {
+                        remoteServerConfigInfo.setExportPath(contextPath + "/mcp");
+                    }
+
+                    serverBasicInfo.setRemoteServerConfig(remoteServerConfigInfo);
+                    serverBasicInfo.setProtocol("mcp-streamable");
+                    serverBasicInfo.setFrontProtocol("mcp-streamable");
+                }
+            }
+
+            try {
+                this.nacosMcpOperationService.createMcpServer(this.serverInfo.name(), serverBasicInfo, mcpToolSpec, endpointSpec);
+            } catch (NacosException var21) {
+                McpServerDetailInfo recheckServerDetailInfo = null;
+
+                try {
+                    recheckServerDetailInfo = this.nacosMcpOperationService.getServerDetail(this.serverInfo.name(), this.serverInfo.version());
+                } catch (NacosException var20) {
+                }
+
+                if (recheckServerDetailInfo == null) {
+                    log.info("Mcp server " + this.serverInfo.name() + "exist ,try to update");
+                    this.nacosMcpOperationService.updateMcpServer(this.serverInfo.name(), serverBasicInfo, mcpToolSpec, endpointSpec);
+                } else {
+                    CheckCompatibleResult checkResult = this.checkCompatible(recheckServerDetailInfo);
+                    if (!checkResult.isCompatible()) {
+                        log.error("[Nacos MCP Register] Check mcp server compatible false, caused by:{}", checkResult.getMessage());
+                        throw new Exception("[Nacos MCP Register] Check mcp server compatible false, caused by:" + checkResult.getMessage());
+                    }
+                }
+            }
+
+            this.subscribe();
+            this.success = true;
+        } catch (Exception e) {
+            log.error("[Nacos MCP Register] Failed to register mcp server to nacos", e);
+        }
+
+    }
+
+    private void subscribe() {
+        this.nacosMcpOperationService.subscribeNacosMcpServer(this.serverInfo.name() + "::" + this.serverInfo.version(), (mcpServerDetailInfo) -> {
+            log.info("[Nacos MCP Register] Received mcp server detail info update event, mcp server name:{}, mcp server version:{}", this.serverInfo.name(), this.serverInfo.version());
+            if (this.serverCapabilities.tools() != null) {
+                this.serverDetailInfo = mcpServerDetailInfo;
+                this.updateTools(mcpServerDetailInfo);
+            }
+
+        });
+    }
+
+    private void updateToolDescription(McpStatelessServerFeatures.AsyncToolSpecification localToolRegistration, McpSchema.Tool toolInNacos, List<McpStatelessServerFeatures.AsyncToolSpecification> toolsRegistrationNeedToUpdate) {
+        Boolean changed = false;
+        if (localToolRegistration.tool().description() != null && !localToolRegistration.tool().description().equals(toolInNacos.description())) {
+            changed = true;
+        }
+
+        String localInputSchemaString = JacksonUtils.toJson(localToolRegistration.tool().inputSchema());
+        Map<String, Object> localInputSchemaMap = (Map)JacksonUtils.toObj(localInputSchemaString, new TypeReference<Map<String, Object>>() {
+        });
+        Map<String, Object> localProperties = (Map)Objects.requireNonNull((Map)localInputSchemaMap.get("properties"));
+        String nacosInputSchemaString = JacksonUtils.toJson(toolInNacos.inputSchema());
+        Map<Object, Object> nacosInputSchemaMap = (Map)JacksonUtils.toObj(nacosInputSchemaString, new TypeReference<Map<Object, Object>>() {
+        });
+        Map<String, Object> nacosProperties = (Map)Objects.requireNonNull((Map)nacosInputSchemaMap.get("properties"));
+
+        for(String key : localProperties.keySet()) {
+            if (nacosProperties.containsKey(key)) {
+                Map<String, Object> localProperty = (Map)localProperties.get(key);
+                Map<String, Object> nacosProperty = (Map)nacosProperties.get(key);
+                String localDescription = (String)localProperty.get("description");
+                String nacosDescription = (String)nacosProperty.get("description");
+                if (nacosDescription != null && !nacosDescription.equals(localDescription)) {
+                    localProperty.put("description", nacosDescription);
+                    changed = true;
+                }
+            }
+        }
+
+        McpSchema.JsonSchema inputSchema = new McpSchema.JsonSchema("object", localInputSchemaMap, localToolRegistration.tool().inputSchema().required(), localToolRegistration.tool().inputSchema().additionalProperties(), localToolRegistration.tool().inputSchema().defs(), localToolRegistration.tool().inputSchema().definitions());
+        if (changed) {
+            McpSchema.Tool toolNeededUpdate = (new McpSchema.Tool.Builder()).name(localToolRegistration.tool().name()).description(toolInNacos.description()).inputSchema(inputSchema).outputSchema(localToolRegistration.tool().outputSchema()).title(localToolRegistration.tool().title()).annotations(localToolRegistration.tool().annotations()).meta(localToolRegistration.tool().meta()).build();
+            toolsRegistrationNeedToUpdate.add(new McpStatelessServerFeatures.AsyncToolSpecification(toolNeededUpdate, localToolRegistration.callHandler()));
+        }
+
+    }
+
+    private void updateTools(McpServerDetailInfo mcpServerDetailInfo) {
+        try {
+            boolean changed = false;
+            McpToolSpecification toolSpec = mcpServerDetailInfo.getToolSpec();
+            if (toolSpec == null) {
+                log.info("[Nacos MCP Register] Mcp server tools in nacos is null, skip local mcp server tools update");
+                return;
+            }
+
+            String toolsInNacosStr = JacksonUtils.toJson(toolSpec.getTools());
+            List<McpSchema.Tool> toolsInNacos = (List)JacksonUtils.toObj(toolsInNacosStr, new TypeReference<List<McpSchema.Tool>>() {
+            });
+            changed = this.compareToolsMeta(toolSpec.getToolsMeta());
+            this.toolsMeta = toolSpec.getToolsMeta();
+            List<McpStatelessServerFeatures.AsyncToolSpecification> toolsRegistrationNeedToUpdate = new ArrayList();
+            Map<String, McpSchema.Tool> toolsInNacosMap = (Map)toolsInNacos.stream().collect(Collectors.toMap(McpSchema.Tool::name, (tool) -> tool));
+
+            for(McpStatelessServerFeatures.AsyncToolSpecification toolRegistration : this.tools) {
+                String name = toolRegistration.tool().name();
+                if (toolsInNacosMap.containsKey(name)) {
+                    McpSchema.Tool toolInNacos = (McpSchema.Tool)toolsInNacosMap.get(name);
+                    this.updateToolDescription(toolRegistration, toolInNacos, toolsRegistrationNeedToUpdate);
+                }
+            }
+
+            if (toolsRegistrationNeedToUpdate.size() > 0) {
+                log.info("[Nacos MCP Register] Update tool description for {} tools", toolsRegistrationNeedToUpdate.size());
+            }
+
+            for(McpStatelessServerFeatures.AsyncToolSpecification toolRegistration : toolsRegistrationNeedToUpdate) {
+                for(int i = 0; i < this.tools.size(); ++i) {
+                    if (((McpStatelessServerFeatures.AsyncToolSpecification)this.tools.get(i)).tool().name().equals(toolRegistration.tool().name())) {
+                        this.tools.set(i, toolRegistration);
+                        log.info("[Nacos MCP Register] Update tool description for tool {}", toolRegistration.tool().name());
+                        changed = true;
+                        break;
+                    }
+                }
+            }
+
+            if (changed) {
+                log.info("[Nacos MCP Register] Update tool description finished");
+            }
+
+            if (changed && this.serverCapabilities.tools().listChanged()) {
+                this.notifyToolsChanged();
+            }
+        } catch (Exception e) {
+            log.error("[Nacos MCP Register] Failed to update local tools according to nacos", e);
+        }
+
+    }
+
+    public void notifyToolsChanged() {
+    }
+
+    public void onApplicationEvent(WebServerInitializedEvent event) {
+        if (!"stdio".equals(this.type) && this.nacosMcpRegistryProperties.isServiceRegister() && this.success) {
+            try {
+                WebServerApplicationContext context = event.getApplicationContext();
+                if (context instanceof ConfigurableWebServerApplicationContext && "management".equals(context.getServerNamespace())) {
+                    return;
+                }
+
+                String host = this.nacosMcpRegistryProperties.getHost();
+                if (StringUtils.isBlank(host)) {
+                    host = this.nacosMcpProperties.getIp();
+                }
+
+                if (StringUtils.isBlank(host)) {
+                    host = NetUtils.localIp();
+                }
+
+                int port = this.nacosMcpRegistryProperties.getPort();
+                if (port < 0 || port > 65535) {
+                    port = event.getWebServer().getPort();
+                }
+
+                Instance instance = new Instance();
+                instance.setIp(host);
+                instance.setPort(port);
+                instance.setEphemeral(this.nacosMcpRegistryProperties.isServiceEphemeral());
+                String groupName = "DEFAULT_GROUP";
+                String configuredGroupName = this.nacosMcpRegistryProperties.getServiceGroup();
+                if (!StringUtils.isBlank(configuredGroupName)) {
+                    groupName = configuredGroupName;
+                }
+
+                String serviceName = this.getRegisterServiceName();
+                if (this.serverDetailInfo != null) {
+                    serviceName = this.serverDetailInfo.getRemoteServerConfig().getServiceRef().getServiceName();
+                    String serviceGroup = this.serverDetailInfo.getRemoteServerConfig().getServiceRef().getGroupName();
+                    groupName = StringUtils.isBlank(serviceGroup) ? "DEFAULT_GROUP" : (String)Objects.requireNonNull(serviceGroup);
+                }
+
+                this.nacosMcpOperationService.registerService(serviceName, (String)Objects.requireNonNull(groupName), instance);
+                log.info("[Nacos MCP Register] Register mcp server endpoint to nacos successfully");
+            } catch (NacosException e) {
+                log.error("[Nacos MCP Register] Failed to register mcp server endpoint to nacos", e);
+            }
+
+        } else {
+            log.info("[Nacos MCP Register] Stdio mcp server , no need to register mcp server endpoint to nacos");
+        }
+    }
+
+    private CheckCompatibleResult checkToolsCompatible(McpServerDetailInfo serverDetailInfo) {
+        return new CheckCompatibleResult(true);
+    }
+
+    private CheckCompatibleResult checkCompatible(McpServerDetailInfo serverDetailInfo) {
+        log.info("[Nacos MCP Register] Checking compatible for mcp server");
+        if (!StringUtils.equals(this.serverInfo.version(), serverDetailInfo.getVersionDetail().getVersion())) {
+            return new CheckCompatibleResult(false, "Local version is not compatible with version in Nacos");
+        } else if (!StringUtils.equals(this.type, serverDetailInfo.getProtocol())) {
+            return new CheckCompatibleResult(false, "Local protocol is not compatible with protocol in Nacos");
+        } else if (StringUtils.equals(this.type, "stdio")) {
+            return new CheckCompatibleResult(true);
+        } else {
+            McpServiceRef mcpServiceRef = serverDetailInfo.getRemoteServerConfig().getServiceRef();
+            if (!this.isServiceRefSame(mcpServiceRef)) {
+                return new CheckCompatibleResult(false, "Local service ref is not compatible with service ref in Nacos");
+            } else {
+                return this.serverCapabilities.tools() != null ? this.checkToolsCompatible(serverDetailInfo) : new CheckCompatibleResult(true);
+            }
+        }
+    }
+
+    private boolean isServiceRefSame(McpServiceRef serviceRef) {
+        if (!StringUtils.isBlank(this.nacosMcpRegistryProperties.getServiceName()) && !StringUtils.equals(serviceRef.getServiceName(), this.nacosMcpRegistryProperties.getServiceName())) {
+            return false;
+        } else {
+            return !StringUtils.isBlank(this.nacosMcpRegistryProperties.getServiceGroup()) && !StringUtils.equals(serviceRef.getGroupName(), this.nacosMcpRegistryProperties.getServiceGroup()) ? false : StringUtils.equals(serviceRef.getNamespaceId(), this.nacosMcpProperties.getNamespace());
+        }
+    }
+
+    private String getRegisterServiceName() {
+        String serviceName = this.nacosMcpRegistryProperties.getServiceName();
+        if (StringUtils.isBlank(serviceName)) {
+            String var10000 = this.serverInfo.name();
+            return var10000 + "::" + this.serverInfo.version();
+        } else {
+            return (String)Objects.requireNonNull(serviceName);
+        }
+    }
+
+    private boolean compareToolsMeta(Map<String, McpToolMeta> toolsMeta) {
+        boolean changed = false;
+        if ((this.toolsMeta != null || toolsMeta == null) && (this.toolsMeta == null || toolsMeta != null)) {
+            if (this.toolsMeta == null) {
+                return false;
+            } else if (!this.toolsMeta.keySet().equals(toolsMeta.keySet())) {
+                return true;
+            } else {
+                for(String toolName : toolsMeta.keySet()) {
+                    McpToolMeta localToolMeta = (McpToolMeta)Objects.requireNonNull((McpToolMeta)this.toolsMeta.get(toolName));
+                    McpToolMeta updatedToolMeta = (McpToolMeta)Objects.requireNonNull((McpToolMeta)toolsMeta.get(toolName));
+                    if (localToolMeta.isEnabled() != updatedToolMeta.isEnabled()) {
+                        changed = true;
+                        break;
+                    }
+                }
+
+                return changed;
+            }
+        } else {
+            return true;
+        }
+    }
+}

+ 46 - 0
yusys-ai-http2mcp/src/main/resources/application.yml

@@ -0,0 +1,46 @@
+server:
+  port: 7777
+management:
+  defaults:
+    metrics:
+      export:
+        enabled: false
+  tracing:
+    sampling:
+      probability: 1.0
+  opentelemetry:
+    tracing:
+      export:
+        otlp:
+          endpoint: http://127.0.0.1:9200/_otlp/v1/traces  # OTLP 追踪端点
+          transport: http
+spring:
+  reactor:
+    context-propagation: auto
+  application:
+    name: yusys-ai-http2mcp
+  ai:
+    mcp:
+      server:
+        name: ${spring.application.name}
+        version: 1.0
+        protocol: STATELESS
+        type: ASYNC
+        instructions: "HTTP转MCP"
+        sse-message-endpoint: /mcp
+    alibaba:
+      mcp:
+        nacos:
+          server-addr: 127.0.0.1:8848
+          namespace: public
+          # username: nacos
+          # password: nacos
+          register:
+            enabled: true
+logging:
+  pattern:
+    correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
+  include-application-name: false
+http-mcp:
+  removeHeaders: Content-Length,Accept,Content-Type,host
+  folder: D:\xds\yusys-ai-platform\yusys-ai-http2mcp\mcp