浏览代码

mcp 配置

Grizzly 5 月之前
父节点
当前提交
edd21fa27b

+ 16 - 11
pom.xml

@@ -38,7 +38,7 @@
             <dependency>
                 <groupId>com.alibaba.cloud.ai</groupId>
                 <artifactId>spring-ai-alibaba-bom</artifactId>
-                <version>1.0.0.2</version>
+                <version>1.1.2.0</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
@@ -88,32 +88,26 @@
 <!--        </dependency>-->
 
         <!-- Spring AI MCP Client -->
-        <dependency>
-            <groupId>org.springframework.ai</groupId>
-            <artifactId>spring-ai-starter-mcp-client</artifactId>
-        </dependency>
-
         <dependency>
             <groupId>com.alibaba.cloud.ai</groupId>
             <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
+            <version>1.1.2.0</version>
         </dependency>
-
         <dependency>
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
         </dependency>
+        <!-- 注释掉 Ollama 依赖,解决 bean 冲突问题 -->
+        <!--
         <dependency>
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-starter-model-ollama</artifactId>
         </dependency>
+        -->
         <dependency>
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-starter-mcp-client</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.springframework.ai</groupId>
-            <artifactId>spring-ai-mcp</artifactId>
-        </dependency>
 
         <!-- Third-party Libraries -->
         <dependency>
@@ -162,6 +156,17 @@
     </dependencies>
 
     <repositories>
+        <repository>
+            <id>aliyunmaven</id>
+            <name>阿里云公共仓库</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
         <repository>
             <id>spring-milestones</id>
             <name>Spring Milestones</name>

+ 41 - 18
src/main/java/edu/nju/software/aipaasagent/agent/config/AgentConfiguration.java

@@ -28,8 +28,7 @@ public class AgentConfiguration {
     private String description;
 
     /**
-     * Agent 类型
-     * 可选值: base, react
+     * Agent 类型(base/react/tool)
      */
     private String type = "base";
 
@@ -39,45 +38,69 @@ public class AgentConfiguration {
     private ModelConfig model;
 
     /**
-     * 记忆策略配置
+     * 系统提示词
      */
-    private MemoryConfig memory;
+    private String systemPrompt;
 
     /**
-     * 系统提示词
+     * 记忆配置
      */
-    private String systemPrompt;
+    private MemoryConfig memory;
 
     /**
-     * 工具列表
+     * RAG 配置
      */
-    private List<String> tools;
+    private RagConfig rag;
 
     /**
-     * ReAct 特定配置
+     * ReAct 配置
      */
     private ReActConfig react;
 
     /**
-     * MCP 外部工具配置列表
-     * 用于配置来自 MCP Hub 的外部工具
+     * 工具列表
      */
-   // private List<McpToolConfig> mcpTools;
+    private List<String> tools;
 
     /**
-     * 新的 MCP 工具配置结构
+     * MCP 配置
      */
     private McpConfig mcp;
 
     /**
-     * 额外参数(扩展用)
+     * 其他动态参数
      */
     private Map<String, Object> parameters;
 
     /**
-     * RAG 配置
+     * MCP 配置(从 app.mcp.custom.nodes 读取)
      */
-    private RagConfig rag;
+    private AppMcpConfig app;
+
+    /**
+     * MCP 配置类(用于 app.mcp.custom.nodes)
+     */
+    @Data
+    public static class AppMcpConfig {
+        private McpSection mcp;
+
+        @Data
+        public static class McpSection {
+            private CustomConfig custom;
+
+            @Data
+            public static class CustomConfig {
+                private Map<String, NodeConfig> nodes;
+            }
+        }
+
+        @Data
+        public static class NodeConfig {
+            private String url;
+            private String endpoint;
+            private Map<String, String> headers;
+        }
+    }
 
     /**
      * 模型配置
@@ -86,7 +109,7 @@ public class AgentConfiguration {
     public static class ModelConfig {
         /**
          * 模型提供商
-         * 可选值: ollama, dashscope, openai
+         * 可选值ollama, dashscope, openai
          */
         private String provider = "ollama";
 
@@ -118,7 +141,7 @@ public class AgentConfiguration {
     public static class MemoryConfig {
         /**
          * 记忆策略类型
-         * 可选值: in-memory, redis, database
+         * 可选值in-memory, redis, database
          */
         private String strategy = "in-memory";
 

+ 2 - 2
src/main/java/edu/nju/software/aipaasagent/agent/core/base/BaseAgent.java

@@ -15,7 +15,7 @@ import org.springframework.ai.chat.memory.ChatMemory;
 import org.springframework.ai.chat.messages.Message;
 import org.springframework.ai.chat.model.ChatModel;
 import org.springframework.ai.chat.prompt.ChatOptions;
-import org.springframework.ai.ollama.api.OllamaChatOptions;
+//import org.springframework.ai.ollama.api.OllamaChatOptions;
 import org.springframework.beans.factory.annotation.Autowired;
 import reactor.core.publisher.Flux;
 
@@ -134,7 +134,7 @@ public abstract class BaseAgent {
                         .build();
             } else if ("ollama".equals(provider)) {
                 // 对于 ollama 模型,使用 OllamaOptions
-                return OllamaChatOptions.builder()
+                return ChatOptions.builder()
                         .temperature(temperature)
                         .model( modelName)
                         .build();

+ 4 - 4
src/main/java/edu/nju/software/aipaasagent/agent/core/reactagent/ToolCallAgent.java

@@ -1,7 +1,7 @@
 package edu.nju.software.aipaasagent.agent.core.reactagent;
 
 import cn.hutool.core.collection.CollUtil;
-import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions;
+
 import edu.nju.software.aipaasagent.agent.config.AgentConfiguration;
 import edu.nju.software.aipaasagent.agent.manager.AgentState;
 import edu.nju.software.aipaasagent.mcp.manage.ToolRegister;
@@ -194,7 +194,7 @@ public class ToolCallAgent extends ReActAgent {
             // 调用大模型,传入工具定义(模型通过工具描述判断是否需要调用)
             ChatResponse chatResponse = chatClient.prompt(prompt)
                     .system(enhancedSystemPrompt)
-                    .tools(toolRegister.getTools().toArray(new ToolCallback[0]))
+                    .toolCallbacks(toolRegister.getTools().toArray(new ToolCallback[0]))
                     .call()
                     .chatResponse();
 
@@ -402,7 +402,7 @@ public class ToolCallAgent extends ReActAgent {
                 .prompt()
                 .user(message)
                 .options(chatOptions)
-                .tools(toolRegister.getTools().toArray(new ToolCallback[0]))
+                .toolCallbacks(toolRegister.getTools().toArray(new ToolCallback[0]))
                 .advisors(spec -> spec
                         .param(ChatMemory.CONVERSATION_ID, chatId))
                 .call()
@@ -417,7 +417,7 @@ public class ToolCallAgent extends ReActAgent {
                 .prompt()
                 .user(message)
                 .options(chatOptions)
-                .tools(toolRegister.getTools().toArray(new ToolCallback[0]))
+                .toolCallbacks(toolRegister.getTools().toArray(new ToolCallback[0]))
                 .advisors(spec -> spec
                         .param(ChatMemory.CONVERSATION_ID, chatId))
                 .stream()

+ 31 - 11
src/main/java/edu/nju/software/aipaasagent/mcp/manage/McpClientService.java

@@ -1,5 +1,7 @@
 package edu.nju.software.aipaasagent.mcp.manage;
 
+import edu.nju.software.aipaasagent.agent.config.AgentConfigRegistry;
+import edu.nju.software.aipaasagent.agent.config.AgentConfiguration;
 import edu.nju.software.aipaasagent.mcp.config.CustomMcpProperties;
 import edu.nju.software.aipaasagent.mcp.config.DefaultMcpJsonMapper;
 import io.modelcontextprotocol.client.McpAsyncClient;
@@ -26,9 +28,11 @@ import java.util.concurrent.ConcurrentHashMap;
 @Slf4j
 public class McpClientService {
 
-    private final CustomMcpProperties properties;
     private final Builder webClientBuilder;
 
+    @Resource
+    private AgentConfigRegistry agentConfigRegistry;
+
     // 使用 Map 存储 MCP 客户端,key 为节点名称
     private final Map<String, McpAsyncClient> mcpAsyncClientMap = new ConcurrentHashMap<>();
 
@@ -36,9 +40,7 @@ public class McpClientService {
     @Resource
     DefaultMcpJsonMapper defaultMcpJsonMapper;
 
-    public McpClientService(CustomMcpProperties properties,
-                            Builder webClientBuilder) {
-        this.properties = properties;
+    public McpClientService(Builder webClientBuilder) {
         this.webClientBuilder = webClientBuilder;
     }
 
@@ -54,7 +56,7 @@ public class McpClientService {
      * 通过 SSE 连接获取服务端分配的 sessionId
      * 如果获取失败,返回空字符串,不影响后续连接
      */
-    private String getMCPSessionId(String url, String authorization) {
+    private String getMCPSessionId(String url, String authorization,String endpoint) {
         try {
             WebClient webClient = webClientBuilder.clone()
                     .baseUrl(url)
@@ -67,7 +69,7 @@ public class McpClientService {
 
             // 使用更灵活的方式处理 SSE 流
             String sessionId = webClient.get()
-                    .uri("/mcp/airouting")
+                    .uri(endpoint)
                     .retrieve()
                     .bodyToFlux(String.class)
                     .doOnNext(line -> {
@@ -91,6 +93,7 @@ public class McpClientService {
                     .blockFirst();
 
             if (sessionId == null || sessionId.isEmpty()) {
+                log.warn("⚠️ 无法从 SSE 响应中提取 Session ID,SSE 数据:{}", sseData.toString());
                 log.warn("⚠️ 无法从 SSE 响应中提取 Session ID,将继续使用空 sessionId");
                 return "";
             }
@@ -127,22 +130,39 @@ public class McpClientService {
 
         mcpAsyncClientMap.clear();
 
+        // 从 AgentConfiguration 中读取 MCP 配置
+        AgentConfiguration config = agentConfigRegistry.getConfig();
+        if (config == null || config.getApp() == null || config.getApp().getMcp() == null 
+            || config.getApp().getMcp().getCustom() == null 
+            || config.getApp().getMcp().getCustom().getNodes() == null) {
+            log.warn("未找到 MCP 配置,跳过 MCP 节点注册");
+            return;
+        }
+
+        Map<String, AgentConfiguration.AppMcpConfig.NodeConfig> nodes = 
+            config.getApp().getMcp().getCustom().getNodes();
+
         List<String> failedNodes = new ArrayList<>();
 
         // 从配置中动态读取 MCP 节点
-        properties.getNodes().forEach((name, config) -> {
+        nodes.forEach((name, nodeConfig) -> {
             try {
                 // 获取 session id(失败时返回空字符串)
-                String sessionId = getMCPSessionId(config.getUrl(), config.getHeaders().getOrDefault("Authorization", ""));
+                String sessionId = getMCPSessionId(
+                    nodeConfig.getUrl(), 
+                    nodeConfig.getHeaders() != null ? nodeConfig.getHeaders().getOrDefault("Authorization", "") : "",
+                        nodeConfig.getEndpoint()
+                );
 
                 Builder dedicatedBuilder = webClientBuilder.clone()
-                        .baseUrl(config.getUrl())
-                        .defaultHeader(HttpHeaders.AUTHORIZATION, config.getHeaders().getOrDefault("Authorization", ""))
+                        .baseUrl(nodeConfig.getUrl())
+                        .defaultHeader(HttpHeaders.AUTHORIZATION, 
+                            nodeConfig.getHeaders() != null ? nodeConfig.getHeaders().getOrDefault("Authorization", "") : "")
                         .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                         .defaultHeader("mcp-session-id", sessionId);
 
                 // 获取 endpoint 配置,默认为 /mcp/airouting
-                String endpoint = config.getEndpoint() != null ? config.getEndpoint() : "/mcp/airouting";
+                String endpoint = nodeConfig.getEndpoint() ;
 
                 // 5. 创建 SSE Transport(会在构造时立即发起 SSE 连接)
                 // 4. 补全参数 resumableStreams(是否支持可恢复流,解决断连重连)

+ 125 - 3
src/main/java/edu/nju/software/aipaasagent/mcp/manage/ToolRegister.java

@@ -3,6 +3,7 @@ package edu.nju.software.aipaasagent.mcp.manage;
 import cn.hutool.core.collection.CollUtil;
 
 import cn.hutool.core.lang.TypeReference;
+import edu.nju.software.aipaasagent.agent.config.AgentConfigRegistry;
 import edu.nju.software.aipaasagent.agent.config.AgentConfiguration;
 import edu.nju.software.aipaasagent.util.McpToolUtils;
 import io.modelcontextprotocol.client.McpAsyncClient;
@@ -29,16 +30,29 @@ public class ToolRegister {
     @Resource
     private McpClientService mcpClientService;
 
+    @Resource
+    private AgentConfigRegistry agentConfigRegistry;
+
     /**
      * 根据配置获取在线工具列表
      * 利用 Spring AI 官方 McpToolCallbackProvider 实现自动转换
      */
     private ToolCallback createMcpToolCallback(McpSchema.Tool tool, McpAsyncClient client) {
-        // 1. 正确构建工具定义(inputSchema 不要直接 toString,用原始值)
+        // 1. 将 inputSchema 转换为正确的 JSON 字符串格式
+        String inputSchemaJson;
+        try {
+            // 使用 ObjectMapper 将 JsonSchema 对象转换为 JSON 字符串
+            inputSchemaJson = OBJECT_MAPPER.writeValueAsString(tool.inputSchema());
+        } catch (Exception e) {
+            log.error("转换 inputSchema 失败:{}", e.getMessage());
+            inputSchemaJson = "{}";
+        }
+
+        // 2. 正确构建工具定义
         ToolDefinition toolDefinition = ToolDefinition.builder()
                 .name(tool.name())
                 .description(tool.description())
-                .inputSchema(tool.inputSchema().toString()) // 旧版本通常接受 JsonNode/Map,不要 toString
+                .inputSchema(inputSchemaJson)
                 .build();
 
         // 2. 匿名内部类实现抽象类,所有抽象方法必须返回正确值!
@@ -87,8 +101,116 @@ public class ToolRegister {
         };
     }
 
-    public List<ToolCallback > getTools(){
+    /**
+     * 获取所有可用工具
+     * 每次调用时动态获取在线工具(因为 MCP client 可能失效)
+     * 
+     * @return 工具回调列表
+     */
+    public List<ToolCallback> getTools() {
+        List<ToolCallback> allTools = new ArrayList<>();
+
+        // 获取当前 Agent 配置
+        AgentConfiguration config = agentConfigRegistry.getConfig();
+        if (config == null) {
+            log.warn("Agent 配置为空,返回空工具列表");
+            return allTools;
+        }
+
+        AgentConfiguration.McpConfig mcpConfig = config.getMcp();
+        
+        // 检查是否为 disable 模式
+        if (mcpConfig != null && mcpConfig.getPolicy() != null && 
+            "disable".equals(mcpConfig.getPolicy().getMode())) {
+            log.info("MCP 模式为 disable,跳过在线工具加载");
+            // disable 模式下只加载本地工具(目前本地工具未实现,返回空列表)
+            return allTools;
+        }
+
+        // 加载在线 MCP 工具
+        List<ToolCallback> onlineTools = loadOnlineTools(mcpConfig);
+        allTools.addAll(onlineTools);
+        log.info("加载在线工具 {} 个", onlineTools.size());
+
+        log.info("工具加载完成,共 {} 个工具", allTools.size());
+        return allTools;
+    }
+
+    /**
+     * 加载在线 MCP 工具
+     */
+    private List<ToolCallback> loadOnlineTools(AgentConfiguration.McpConfig mcpConfig) {
+        List<ToolCallback> toolCallbacks = new ArrayList<>();
+        
+        if (mcpClientService.getMcpClientMap().isEmpty()) {
+            log.warn("MCP 客户端为空,无法加载在线工具");
+            return toolCallbacks;
+        }
+
+        Map<String, McpAsyncClient> clientMap = mcpClientService.getMcpClientMap();
+        Map<String, List<String>> onlineToolConfig = mcpConfig != null ? mcpConfig.getOnlineTool() : null;
+
+        // 如果配置了 onlineTool,按配置筛选
+        if (onlineToolConfig != null && !onlineToolConfig.isEmpty()) {
+            log.info("根据配置筛选在线工具,配置节点数:{}", onlineToolConfig.size());
+            
+            onlineToolConfig.forEach((nodeName, toolNames) -> {
+                McpAsyncClient client = clientMap.get(nodeName);
+                if (client == null) {
+                    log.warn("未找到 MCP 节点:{},跳过该节点的工具加载", nodeName);
+                    return;
+                }
+
+                // 获取该节点的所有工具
+                try {
+                    McpSchema.ListToolsResult toolsResult = client.listTools().block();
+                    if (toolsResult != null && toolsResult.tools() != null) {
+                        List<McpSchema.Tool> allTools = toolsResult.tools();
+                        log.info("节点 [{}] 共有 {} 个工具,配置筛选 {} 个", nodeName, allTools.size(), toolNames != null ? toolNames.size() : 0);
+
+                        // 如果配置了工具列表,只加载指定的工具
+                        if (toolNames != null && !toolNames.isEmpty()) {
+                            for (McpSchema.Tool tool : allTools) {
+                                if (toolNames.contains(tool.name())) {
+                                    ToolCallback callback = createMcpToolCallback(tool, client);
+                                    toolCallbacks.add(callback);
+                                    log.info("✅ 加载工具:{} (节点:{})", tool.name(), nodeName);
+                                }
+                            }
+                        } else {
+                            // 没有配置工具列表,加载该节点所有工具
+                            for (McpSchema.Tool tool : allTools) {
+                                ToolCallback callback = createMcpToolCallback(tool, client);
+                                toolCallbacks.add(callback);
+                                log.info("✅ 加载工具:{} (节点:{})", tool.name(), nodeName);
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("获取节点 [{}] 工具列表失败:{}", nodeName, e.getMessage(), e);
+                }
+            });
+        } else {
+            // 没有配置 onlineTool,加载所有节点的所有工具
+            log.info("未配置 onlineTool,加载所有 MCP 节点的所有工具");
+            
+            clientMap.forEach((nodeName, client) -> {
+                try {
+                    McpSchema.ListToolsResult toolsResult = client.listTools().block();
+                    if (toolsResult != null && toolsResult.tools() != null) {
+                        for (McpSchema.Tool tool : toolsResult.tools()) {
+                            ToolCallback callback = createMcpToolCallback(tool, client);
+                            toolCallbacks.add(callback);
+                            log.info("✅ 加载工具:{} (节点:{})", tool.name(), nodeName);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("获取节点 [{}] 工具列表失败:{}", nodeName, e.getMessage(), e);
+                }
+            });
+        }
 
+        return toolCallbacks;
     }
     private boolean shouldLoadTool(String nodeName, String toolName, Map<String, List<String>> config) {
         if (config == null || config.isEmpty()) return true;

+ 3 - 3
src/main/java/edu/nju/software/aipaasagent/service/RagService.java

@@ -31,7 +31,7 @@ public class RagService {
     private RagClient ragClient;
 
     @Autowired
-    private ChatModel ollamaChatModel;
+    private ChatModel dashscopeModel;
 
     @Autowired
     private RedisBasedChatMemory redisBasedChatMemory;
@@ -104,7 +104,7 @@ public class RagService {
 
         try {
             // 使用本地模型生成查询词
-            ChatResponse response = ollamaChatModel.call(new Prompt(prompt));
+            ChatResponse response = dashscopeModel.call(new Prompt(prompt));
             String query = response.getResult().getOutput().getText().trim();
             
             log.info("[RAG-Query] 生成的检索查询词:{}", query);
@@ -182,7 +182,7 @@ public class RagService {
         log.debug("[RAG-Extract] 提示词:\n{}", prompt);
 
         try {
-            ChatResponse response = ollamaChatModel.call(new Prompt(prompt));
+            ChatResponse response = dashscopeModel.call(new Prompt(prompt));
             String keyInfoJson = response.getResult().getOutput().getText().trim();
             
             log.info("[RAG-Extract] 总结的关键信息:{}", keyInfoJson);

+ 12 - 9
src/main/resources/agent-config.yml

@@ -40,13 +40,10 @@ mcp:
   onlineTool:
     SeeCoder-mcp:
       - everything_get_sum
-      - everything_echo
       - chat_improve_prompt
-    xxx-mcp:
-      - xxx-tool
   #本地的
-  localTools:
-    - weather
+  localTools: #plugin
+    - terminate
   # MCP 策略配置
   policy:
     # 模式:auto(自动)、force(强制)、intelligence(智能),disable(关闭)
@@ -57,11 +54,17 @@ app:
   mcp:
     custom:
       nodes:
-        xxx-mcp:
-          url:  https://xxxx
-          endpoint: /mcp
+        SeeCoder-mcp:
+          url: https://ai-paas-mcp-endpoint.njuu.top
+          endpoint: /mcp/airouting
           headers:
-            Authorization: "xxx"
+            Authorization: "sqGYuMvKgdxmzmTM5lNBgLdVpl6XNnPX"
+#        map-mcp:
+#          url: https://dashscope.aliyuncs.com/api/v1/mcps/amap-maps
+#          endpoint: /mcp
+#          headers:
+#            Authorization: "sk-63f4b5a5f7ab42e78843c26a89c377ec"
+
 # RAG 配置
 rag:
   enabled: false

+ 4 - 26
src/main/resources/application.yml

@@ -10,17 +10,7 @@ spring:
         enabled: true
         options:
           model: qwen3-max-2026-01-23
-    ollama:
-      base-url: ${OLLAMA_BASE_URL:http://localhost:11434}
-      chat:
-        enabled: true
-        options:
-          model: gemma3:4b
-          timeout: 300000
-      init:
-        timeout: 120000
-      embedding:
-        timeout: 120000
+      # Agent 侧默认最大结果数(防止 RAG 接口返回过多结果)
     rag:
       base-url: ${RAG_BASE_URL:http://127.0.0.1:8000}
       # Agent 侧默认最大结果数(防止 RAG 接口返回过多结果)
@@ -77,18 +67,6 @@ file:
     max-file-size: 10
     # 支持的文件类型
     allowed-types: txt,md,json,xml,csv,log,jpg,jpeg,png,gif,bmp,webp,pdf
-
-app:
-  mcp:
-    custom:
-      nodes:
-        SeeCoder-mcp:
-          url: https://ai-paas-mcp-endpoint.njuu.top
-          endpoint: /mcp
-          headers:
-            Authorization: "sqGYuMvKgdxmzmTM5lNBgLdVpl6XNnPX"
-        SeeCoder-Intelligence-mcp:
-          url: https://ai-paas-mcp-endpoint.njuu.top
-          endpoint: /mcp
-          headers:
-            Authorization: "sqGYuMvKgdxmzmTM5lNBgLdVpl6XNnPX"
+logging:
+  level:
+    io.mcp.client.LifecycleInitializer: ERROR