diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/DbMcpRouterAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/DbMcpRouterAutoConfiguration.java new file mode 100644 index 0000000000..4a708e1501 --- /dev/null +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/DbMcpRouterAutoConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.autoconfigure.mcp.router; + +import com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties; +import com.alibaba.cloud.ai.mcp.router.config.DbMcpProperties; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.DbMcpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscoveryFactory; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Register DbMcpServiceDiscovery to McpServiceDiscoveryFactory. + * + * @author digitzh + */ +@AutoConfiguration +@AutoConfigureAfter(McpServiceDiscoveryAutoConfiguration.class) +@EnableConfigurationProperties({ McpRouterProperties.class, DbMcpProperties.class, McpServerProperties.class }) +@ConditionalOnProperty(prefix = McpRouterProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class DbMcpRouterAutoConfiguration { + + private static final Logger log = LoggerFactory.getLogger(DbMcpRouterAutoConfiguration.class); + + @Bean + @ConditionalOnProperty(prefix = DbMcpProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") + public DbMcpServiceDiscoveryRegistrar dbMcpServiceDiscoveryRegistrar(McpServiceDiscoveryFactory discoveryFactory, + DbMcpProperties dbMcpProperties) { + log.info("Creating database MCP service discovery registrar with properties: {}", dbMcpProperties); + return new DbMcpServiceDiscoveryRegistrar(discoveryFactory, dbMcpProperties); + } + + public static class DbMcpServiceDiscoveryRegistrar { + + private final McpServiceDiscoveryFactory discoveryFactory; + + private final DbMcpProperties dbMcpProperties; + + public DbMcpServiceDiscoveryRegistrar(McpServiceDiscoveryFactory discoveryFactory, + DbMcpProperties dbMcpProperties) { + this.discoveryFactory = discoveryFactory; + this.dbMcpProperties = dbMcpProperties; + log.info("Database MCP service discovery registrar constructor called with properties: {}", + dbMcpProperties); + } + + @PostConstruct + public void init() { + log.info("Database MCP service discovery registrar initialized with properties: {}", dbMcpProperties); + log.info("Registering DB MCP service discovery with configuration: {}", dbMcpProperties); + McpServiceDiscovery dbDiscovery = new DbMcpServiceDiscovery(dbMcpProperties); + discoveryFactory.registerDiscovery("database", dbDiscovery); + } + + } + +} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/FileMcpRouterAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/FileMcpRouterAutoConfiguration.java new file mode 100644 index 0000000000..5f03b30f4d --- /dev/null +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/FileMcpRouterAutoConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.autoconfigure.mcp.router; + +import com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties; +import com.alibaba.cloud.ai.mcp.router.core.discovery.FileConfigMcpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscoveryFactory; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Register FileConfigMcpServiceDiscovery to McpServiceDiscoveryFactory. + * + * @author digitzh + */ +@AutoConfiguration +@AutoConfigureAfter(McpServiceDiscoveryAutoConfiguration.class) +@EnableConfigurationProperties({ McpRouterProperties.class, McpServerProperties.class }) +@ConditionalOnProperty(prefix = McpRouterProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class FileMcpRouterAutoConfiguration { + + private static final Logger log = LoggerFactory.getLogger(FileMcpRouterAutoConfiguration.class); + + @Bean + public FileMcpServiceDiscoveryRegistrar fileMcpServiceDiscoveryRegistrar( + McpServiceDiscoveryFactory discoveryFactory, McpRouterProperties properties) { + log.info("Creating file MCP service discovery registrar with properties: {}", properties); + return new FileMcpServiceDiscoveryRegistrar(discoveryFactory, properties); + } + + public static class FileMcpServiceDiscoveryRegistrar { + + private final McpServiceDiscoveryFactory discoveryFactory; + + private final McpRouterProperties properties; + + public FileMcpServiceDiscoveryRegistrar(McpServiceDiscoveryFactory discoveryFactory, + McpRouterProperties properties) { + this.discoveryFactory = discoveryFactory; + this.properties = properties; + log.info("File MCP service discovery registrar constructor called with properties: {}", properties); + } + + @PostConstruct + public void init() { + log.info("File MCP service discovery registrar initialized with properties: {}", properties); + log.info("Registering file config MCP service discovery with {} services", + properties.getServices() != null ? properties.getServices().size() : 0); + McpServiceDiscovery fileDiscovery = new FileConfigMcpServiceDiscovery(properties); + discoveryFactory.registerDiscovery("file", fileDiscovery); + } + + } + +} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/McpServiceDiscoveryAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/McpServiceDiscoveryAutoConfiguration.java new file mode 100644 index 0000000000..adf2376208 --- /dev/null +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/McpServiceDiscoveryAutoConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.autoconfigure.mcp.router; + +import com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties; +import com.alibaba.cloud.ai.mcp.router.core.discovery.CompositeMcpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscoveryFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +import java.util.List; + +/** + * Register McpServiceDiscovery to McpServiceDiscoveryFactory. + * + * @author digitzh + */ +@AutoConfiguration +@EnableConfigurationProperties(McpRouterProperties.class) +@ConditionalOnProperty(prefix = McpRouterProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class McpServiceDiscoveryAutoConfiguration { + + private static final Logger log = LoggerFactory.getLogger(McpServiceDiscoveryAutoConfiguration.class); + + @Bean + public McpServiceDiscoveryFactory mcpServiceDiscoveryFactory() { + log.info("Creating MCP service discovery factory"); + return new McpServiceDiscoveryFactory(); + } + + @Bean + @Primary + public McpServiceDiscovery compositeMcpServiceDiscovery(McpServiceDiscoveryFactory discoveryFactory, + McpRouterProperties properties) { + + List searchOrder = getSearchOrder(properties); + log.info("Creating composite MCP service discovery with search order: {}", searchOrder); + + return new CompositeMcpServiceDiscovery(discoveryFactory, searchOrder); + } + + private List getSearchOrder(McpRouterProperties properties) { + if (properties.getDiscoveryOrder() != null && !properties.getDiscoveryOrder().isEmpty()) { + return properties.getDiscoveryOrder(); + } + + return List.of("nacos"); + } + +} diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java index 312cdf62c9..aa8bcd0743 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/java/com/alibaba/cloud/ai/autoconfigure/mcp/router/NacosMcpRouterAutoConfiguration.java @@ -24,12 +24,14 @@ import com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties; import com.alibaba.cloud.ai.mcp.router.core.McpRouterWatcher; import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscovery; +import com.alibaba.cloud.ai.mcp.router.core.discovery.McpServiceDiscoveryFactory; import com.alibaba.cloud.ai.mcp.router.core.vectorstore.McpServerVectorStore; import com.alibaba.cloud.ai.mcp.router.core.vectorstore.SimpleMcpServerVectorStore; import com.alibaba.cloud.ai.mcp.router.nacos.NacosMcpServiceDiscovery; import com.alibaba.cloud.ai.mcp.router.service.McpProxyService; import com.alibaba.cloud.ai.mcp.router.service.McpRouterService; import com.alibaba.nacos.api.exception.NacosException; +import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.MetadataMode; @@ -38,6 +40,8 @@ import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -46,11 +50,15 @@ import java.util.Properties; /** + * Register NacosMcpServiceDiscovery to McpServiceDiscoveryFactory. + * * @author aias00 */ +@AutoConfiguration +@AutoConfigureAfter(McpServiceDiscoveryAutoConfiguration.class) @EnableConfigurationProperties({ McpRouterProperties.class, NacosMcpProperties.class, McpServerProperties.class }) @ConditionalOnProperty(prefix = McpRouterProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = false) + matchIfMissing = true) public class NacosMcpRouterAutoConfiguration { private static final Logger log = LoggerFactory.getLogger(NacosMcpRouterAutoConfiguration.class); @@ -62,7 +70,7 @@ public class NacosMcpRouterAutoConfiguration { @ConditionalOnMissingBean public EmbeddingModel embeddingModel() { if (apiKey == null || apiKey.isEmpty() || "default_api_key".equals(apiKey)) { - throw new IllegalArgumentException("Environment variable DASHSCOPE_API_KEY is not set."); + throw new IllegalArgumentException("Environment variable AI_DASHSCOPE_API_KEY is not set."); } DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); @@ -82,13 +90,11 @@ public NacosMcpOperationService nacosMcpOperationService(NacosMcpProperties naco } } - /** - * 配置 MCP 服务发现 - */ @Bean - @ConditionalOnMissingBean - public McpServiceDiscovery mcpServiceDiscovery(NacosMcpOperationService nacosMcpOperationService) { - return new NacosMcpServiceDiscovery(nacosMcpOperationService); + public NacosMcpServiceDiscoveryRegistrar nacosMcpServiceDiscoveryRegistrar( + McpServiceDiscoveryFactory discoveryFactory, NacosMcpOperationService nacosMcpOperationService) { + log.info("Creating Nacos MCP service discovery registrar"); + return new NacosMcpServiceDiscoveryRegistrar(discoveryFactory, nacosMcpOperationService); } /** @@ -138,4 +144,30 @@ public McpRouterWatcher mcpRouterWatcher(McpServiceDiscovery mcpServiceDiscovery return new McpRouterWatcher(mcpServiceDiscovery, mcpServerVectorStore, mcpRouterProperties.getServiceNames()); } + /** + * Nacos MCP服务发现注册器 + */ + public static class NacosMcpServiceDiscoveryRegistrar { + + private final McpServiceDiscoveryFactory discoveryFactory; + + private final NacosMcpOperationService nacosMcpOperationService; + + public NacosMcpServiceDiscoveryRegistrar(McpServiceDiscoveryFactory discoveryFactory, + NacosMcpOperationService nacosMcpOperationService) { + this.discoveryFactory = discoveryFactory; + this.nacosMcpOperationService = nacosMcpOperationService; + log.info("Nacos MCP service discovery registrar constructor called"); + } + + @PostConstruct + public void init() { + log.info("Nacos MCP service discovery registrar initialized"); + log.info("Registering Nacos MCP service discovery"); + McpServiceDiscovery nacosDiscovery = new NacosMcpServiceDiscovery(nacosMcpOperationService); + discoveryFactory.registerDiscovery("nacos", nacosDiscovery); + } + + } + } diff --git a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index cdd0671ca1..7ea30d2b19 100644 --- a/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/spring-ai-alibaba-autoconfigure-mcp-router/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,10 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. # + +com.alibaba.cloud.ai.autoconfigure.mcp.router.McpServiceDiscoveryAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.router.FileMcpRouterAutoConfiguration +com.alibaba.cloud.ai.autoconfigure.mcp.router.DbMcpRouterAutoConfiguration com.alibaba.cloud.ai.autoconfigure.mcp.router.NacosMcpRouterAutoConfiguration com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewayServerAutoConfiguration #com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewaySseServerAutoConfiguration #com.alibaba.cloud.ai.autoconfigure.mcp.gateway.core.McpGatewayStreamableServerAutoConfiguration com.alibaba.cloud.ai.autoconfigure.mcp.gateway.nacos.NacosMcpGatewayAutoConfiguration - diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/chat/DashScopeMultiModalChatTests.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/chat/DashScopeMultiModalChatTests.java index 5c487ef3e3..48914ffb4c 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/chat/DashScopeMultiModalChatTests.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/chat/DashScopeMultiModalChatTests.java @@ -303,7 +303,7 @@ void testStreamImageResponse() { /** * Integration test for image processing with URL This test will only run if - * DASHSCOPE_API_KEY environment variable is set + * AI_DASHSCOPE_API_KEY environment variable is set */ @Test @Tag("integration") diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/pom.xml b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/pom.xml index d8156cadea..ff9491f6c2 100644 --- a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/pom.xml +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/pom.xml @@ -120,6 +120,10 @@ + + com.zaxxer + HikariCP + org.junit.jupiter diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/DbMcpProperties.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/DbMcpProperties.java new file mode 100644 index 0000000000..e50b3a8db6 --- /dev/null +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/DbMcpProperties.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.mcp.router.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = DbMcpProperties.CONFIG_PREFIX) +public class DbMcpProperties { + + public static final String CONFIG_PREFIX = "spring.ai.alibaba.mcp.router.database"; + + private boolean enabled = false; + + private String url; + + private String username; + + private String password; + + private String driverClassName = "com.mysql.cj.jdbc.Driver"; + + private String tableName = "mcp_server_info"; + + private String querySql; + + private int maxPoolSize = 10; + + private int minIdle = 2; + + private long connectionTimeout = 30000; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getDriverClassName() { + return driverClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getQuerySql() { + return querySql; + } + + public void setQuerySql(String querySql) { + this.querySql = querySql; + } + + public int getMaxPoolSize() { + return maxPoolSize; + } + + public void setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + } + + public int getMinIdle() { + return minIdle; + } + + public void setMinIdle(int minIdle) { + this.minIdle = minIdle; + } + + public long getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/McpRouterProperties.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/McpRouterProperties.java index 1ee1804462..41700e56dc 100644 --- a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/McpRouterProperties.java +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/config/McpRouterProperties.java @@ -17,6 +17,7 @@ package com.alibaba.cloud.ai.mcp.router.config; +import com.alibaba.cloud.ai.mcp.router.model.McpServerInfo; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.ArrayList; @@ -27,13 +28,10 @@ public class McpRouterProperties { public static final String CONFIG_PREFIX = "spring.ai.alibaba.mcp.router"; - /** - * 是否启用 MCP 路由器 - */ private boolean enabled = true; /** - * MCP 路由器服务名称 + * MCP router service names */ private List serviceNames = new ArrayList<>(); @@ -53,4 +51,31 @@ public void setServiceNames(final List serviceNames) { this.serviceNames = serviceNames; } + /** + * MCP router service configurations + */ + private List services = new ArrayList<>(); + + public List getServices() { + return services; + } + + public void setServices(List services) { + this.services = services; + } + + /** + * MCP router service discovery search order 支持多种发现方式同时使用,按顺序查找服务 例如: ["file", + * "database", "nacos"] + */ + private List discoveryOrder = List.of("nacos"); + + public List getDiscoveryOrder() { + return discoveryOrder; + } + + public void setDiscoveryOrder(List discoveryOrder) { + this.discoveryOrder = discoveryOrder; + } + } diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/CompositeMcpServiceDiscovery.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/CompositeMcpServiceDiscovery.java new file mode 100644 index 0000000000..268910df15 --- /dev/null +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/CompositeMcpServiceDiscovery.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.mcp.router.core.discovery; + +import com.alibaba.cloud.ai.mcp.router.model.McpServerInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Composite McpServiceDiscovery, support multiple discovery types. Queries multiple + * McpServiceDiscovery implementations in order. Returns the first non-null result. + * + * @author digitzh + */ +public class CompositeMcpServiceDiscovery implements McpServiceDiscovery { + + private static final Logger log = LoggerFactory.getLogger(CompositeMcpServiceDiscovery.class); + + private final McpServiceDiscoveryFactory discoveryFactory; + + private final List searchOrder; + + public CompositeMcpServiceDiscovery(McpServiceDiscoveryFactory discoveryFactory, List searchOrder) { + this.discoveryFactory = discoveryFactory; + this.searchOrder = searchOrder; + if (discoveryFactory == null) { + throw new IllegalArgumentException("McpServiceDiscoveryFactory cannot be null"); + } + if (searchOrder == null || searchOrder.isEmpty()) { + throw new IllegalArgumentException("Search order cannot be null or empty"); + } + log.info("Created composite MCP service discovery with search order: {}", searchOrder); + } + + @Override + public McpServerInfo getService(String serviceName) { + if (serviceName == null || serviceName.trim().isEmpty()) { + log.warn("Service name is null or empty"); + return null; + } + + log.debug("Searching for service: {} with order: {}", serviceName, searchOrder); + + for (String discoveryType : searchOrder) { + McpServiceDiscovery discovery = discoveryFactory.getDiscovery(discoveryType); + if (discovery == null) { + log.debug("No discovery implementation found for type: {}", discoveryType); + continue; + } + + try { + McpServerInfo serverInfo = discovery.getService(serviceName); + if (serverInfo != null) { + log.info("Found service '{}' using discovery type: {}", serviceName, discoveryType); + return serverInfo; + } + else { + log.debug("Service '{}' not found in discovery type: {}", serviceName, discoveryType); + } + } + catch (Exception e) { + log.error("Error occurred while searching service '{}' in discovery type: {}", serviceName, + discoveryType, e); + } + } + + log.warn("Service '{}' not found in any registered discovery implementations", serviceName); + return null; + } + + public List getSearchOrder() { + return List.copyOf(searchOrder); + } + + public McpServiceDiscoveryFactory getDiscoveryFactory() { + return discoveryFactory; + } + +} diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/DbMcpServiceDiscovery.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/DbMcpServiceDiscovery.java new file mode 100644 index 0000000000..25ae92718a --- /dev/null +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/DbMcpServiceDiscovery.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.mcp.router.core.discovery; + +import com.alibaba.cloud.ai.mcp.router.config.DbMcpProperties; +import com.alibaba.cloud.ai.mcp.router.model.McpServerInfo; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class DbMcpServiceDiscovery implements McpServiceDiscovery { + + private static final Logger log = LoggerFactory.getLogger(DbMcpServiceDiscovery.class); + + private final DataSource dataSource; + + private final String querySql; + + public DbMcpServiceDiscovery(DbMcpProperties properties) { + this.dataSource = createDataSource(properties); + this.querySql = buildQuerySql(properties); + } + + @Override + public McpServerInfo getService(String serviceName) { + McpServerInfo serverInfo = null; + Connection connection = null; + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + + try { + connection = dataSource.getConnection(); + preparedStatement = connection.prepareStatement(querySql); + preparedStatement.setString(1, serviceName); + resultSet = preparedStatement.executeQuery(); + + if (resultSet.next()) { + serverInfo = mapResultSetToMcpServerInfo(resultSet); + } + } + catch (SQLException e) { + log.error("Failed to get service {} from database", serviceName, e); + } + finally { + closeResources(resultSet, preparedStatement, connection); + } + + return serverInfo; + } + + private DataSource createDataSource(DbMcpProperties properties) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(properties.getUrl()); + config.setUsername(properties.getUsername()); + config.setPassword(properties.getPassword()); + config.setDriverClassName(properties.getDriverClassName()); + config.setMaximumPoolSize(properties.getMaxPoolSize()); + config.setMinimumIdle(properties.getMinIdle()); + config.setConnectionTimeout(properties.getConnectionTimeout()); + + return new HikariDataSource(config); + } + + private String buildQuerySql(DbMcpProperties properties) { + if (StringUtils.hasText(properties.getQuerySql())) { + return properties.getQuerySql(); + } + + // default query SQL, assuming table structure matches McpServerInfo fields + return "SELECT name, description, protocol, version, endpoint, enabled, tags " + "FROM " + + properties.getTableName() + " " + "WHERE name = ? AND enabled = true"; + } + + private McpServerInfo mapResultSetToMcpServerInfo(ResultSet resultSet) throws SQLException { + McpServerInfo serverInfo = new McpServerInfo(); + serverInfo.setName(resultSet.getString("name")); + serverInfo.setDescription(resultSet.getString("description")); + serverInfo.setProtocol(resultSet.getString("protocol")); + serverInfo.setVersion(resultSet.getString("version")); + serverInfo.setEndpoint(resultSet.getString("endpoint")); + serverInfo.setEnabled(resultSet.getBoolean("enabled")); + + // parse tags, assuming split with ',' + String tagsStr = resultSet.getString("tags"); + if (StringUtils.hasText(tagsStr)) { + List tags = Arrays.asList(tagsStr.split(",")); + serverInfo.setTags(new ArrayList<>(tags)); + } + + return serverInfo; + } + + private void closeResources(ResultSet resultSet, PreparedStatement statement, Connection connection) { + closeQuietly(resultSet, "ResultSet"); + closeQuietly(statement, "PreparedStatement"); + closeQuietly(connection, "Connection"); + } + + private void closeQuietly(AutoCloseable resource, String name) { + if (resource != null) { + try { + if (resource instanceof Connection conn) { + if (!conn.isClosed()) { + conn.close(); + } + } + else { + resource.close(); + } + } + catch (Exception e) { + log.error("Failed to close {}", name, e); + } + } + } + +} diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/FileConfigMcpServiceDiscovery.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/FileConfigMcpServiceDiscovery.java new file mode 100644 index 0000000000..b4d32f5515 --- /dev/null +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/FileConfigMcpServiceDiscovery.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.mcp.router.core.discovery; + +import com.alibaba.cloud.ai.mcp.router.config.McpRouterProperties; +import com.alibaba.cloud.ai.mcp.router.model.McpServerInfo; + +import java.util.Objects; + +public class FileConfigMcpServiceDiscovery implements McpServiceDiscovery { + + private final McpRouterProperties properties; + + public FileConfigMcpServiceDiscovery(McpRouterProperties properties) { + this.properties = properties; + } + + @Override + public McpServerInfo getService(String serviceName) { + return properties.getServices() + .stream() + .filter(config -> Objects.equals(serviceName, config.getName())) + .findFirst() + .orElse(null); + } + +} diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscovery.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscovery.java index 8d7d995812..10cb3c10bc 100644 --- a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscovery.java +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscovery.java @@ -19,16 +19,8 @@ import com.alibaba.cloud.ai.mcp.router.model.McpServerInfo; -/** - * MCP 服务发现接口 - */ public interface McpServiceDiscovery { - /** - * 获取指定服务信息 - * @param serviceName 服务名 - * @return 服务信息 - */ McpServerInfo getService(String serviceName); } diff --git a/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscoveryFactory.java b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscoveryFactory.java new file mode 100644 index 0000000000..8fcbd63dc1 --- /dev/null +++ b/spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/router/core/discovery/McpServiceDiscoveryFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.ai.mcp.router.core.discovery; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Factory class for creating and managing {@link McpServiceDiscovery} implementations. + * + * @author digitzh + */ +public class McpServiceDiscoveryFactory { + + private static final Logger log = LoggerFactory.getLogger(McpServiceDiscoveryFactory.class); + + /** + * Stores the registered {@link McpServiceDiscovery} implementations, keyed by their + * type. + */ + private final ConcurrentMap discoveryMap = new ConcurrentHashMap<>(); + + /** + * Registers a {@link McpServiceDiscovery} implementation for the specified type. + * @param type of the service discovery (file, database, nacos等) + * @param discovery implementation of the service discovery + */ + public void registerDiscovery(String type, McpServiceDiscovery discovery) { + if (!StringUtils.hasText(type)) { + throw new IllegalArgumentException("Discovery type cannot be null or empty"); + } + if (discovery == null) { + throw new IllegalArgumentException("Discovery implementation cannot be null"); + } + + McpServiceDiscovery existing = discoveryMap.put(type, discovery); + if (existing != null) { + log.warn("Replaced existing MCP service discovery for type: {}", type); + } + else { + log.info("Registered MCP service discovery for type: {}", type); + } + } + + public McpServiceDiscovery getDiscovery(String type) { + return discoveryMap.get(type); + } + + public List getAllDiscoveries() { + return new ArrayList<>(discoveryMap.values()); + } + + public List getRegisteredTypes() { + return new ArrayList<>(discoveryMap.keySet()); + } + + public boolean hasDiscovery(String type) { + return discoveryMap.containsKey(type); + } + + public McpServiceDiscovery removeDiscovery(String type) { + McpServiceDiscovery removed = discoveryMap.remove(type); + if (removed != null) { + log.info("Removed MCP service discovery for type: {}", type); + } + return removed; + } + + public void clear() { + discoveryMap.clear(); + log.info("Cleared all MCP service discovery implementations"); + } + + public int size() { + return discoveryMap.size(); + } + +}