飞书原文档链接地址:https://ik3te1knhq.feishu.cn/wiki/D8kSwC9tFi61CMkRdd8cMxNTnpg

企业级分布式 MCP 方案

背景:现阶段 MCP Client 和 MCP Server 是一对一的连接方式,若当前 MCP Server 挂掉了,那么 MCP Client 便不能使用 MCP Server 提供的工具能力。工具稳定性的提供得不到保证

解决:做了一些分布式 Client 连接的探索,一个 MCP Client 端可以连接多个 MCP Server(分布式部署),目前采用的方案如下:

  1. 新建一个包含服务名和对应连接的类

  2. 另外实现监听机制,可以动态的应对 MCP Server 节点上下线,去动态调整 mcpAsyncClientList

  3. (读操作)获取 MCP Server 相关信息的,采用从 mcpAsyncClientList 列表中随机中获取一个去发起请求,比如获取工具列表信息

  4. (写操作)对应 MCP Server 需要更改的信息,由 MCP Client 端发起,需要修改所有的 MCP Server

public class LoadbalancedAsyncClient implements EventListener { private String serviceName; private List<McpAsyncClient> mcpAsyncClientList;

} 给社区贡献代码:https://github.com/alibaba/spring-ai-alibaba/pull/755

模块代码解析

yaml 文件

 
 
 
 
 
 
 
spring:
  ai:
    mcp:
      client:
        enabled: true
        name: mcp-client-webflux
        version: 1.0.0
        type: SYNC
        nacos-enabled: true # 开启nacos-client配置,启动分布式
        
    alibaba:
      mcp:
        nacos: ## nacos的基础配置信息
          enabled: true
          server-addr: <nacos-sever-addr>
          service-namespace: <nacos-namespace>  
          service-group: <nacos-group>
        client:
          sse:
            connections: // 注册在nacos的MCP Server服务,这里mcp-server1代表多节点
              nacos-server1: mcp-server1
              nacos-server2: mcp-server2
 

自动注入部分

NacosMcpSseClientProperties(配置类)

@ConfigurationProperties("spring.ai.alibaba.mcp.client.sse")
public class NacosMcpSseClientProperties {

public static final String _CONFIG_PREFIX _= "spring.ai.alibaba.mcp.client.sse";

private final Map<String, String> connections = new HashMap<>();

public Map<String, String> getConnections() {
return connections;
}

}

NacosMcpSseClientAutoConfiguration

提供 Map<String, List<NamedClientMcpTransport>> 的 bean

  • 键代表服务名

  • 值为对应的后续连接的 WebFluxSseClientTransport 列表

@AutoConfiguration
@EnableConfigurationProperties({ NacosMcpSseClientProperties.class, NacosMcpRegistryProperties.class })
public class NacosMcpSseClientAutoConfiguration {

private static final Logger _logger _= LoggerFactory._getLogger_(NacosMcpSseClientAutoConfiguration.class);

public NacosMcpSseClientAutoConfiguration() {
}

@Bean
public NamingService nacosNamingService(NacosMcpRegistryProperties nacosMcpRegistryProperties) {
Properties nacosProperties = nacosMcpRegistryProperties.getNacosProperties();
try {
return NamingFactory._createNamingService_(nacosProperties);
}
catch (NacosException e) {
throw new RuntimeException(e);
}
}

@Bean(name = "server2NamedTransport")
public Map<String, List<NamedClientMcpTransport>> server2NamedTransport(
NacosMcpSseClientProperties nacosMcpSseClientProperties, NamingService namingService,
ObjectProvider<WebClient.Builder> webClientBuilderProvider,
ObjectProvider<ObjectMapper> objectMapperProvider) {
Map<String, List<NamedClientMcpTransport>> server2NamedTransport = new HashMap<>();
WebClient.Builder webClientBuilderTemplate = (WebClient.Builder) webClientBuilderProvider
.getIfAvailable(WebClient::_builder_);
ObjectMapper objectMapper = (ObjectMapper) objectMapperProvider.getIfAvailable(ObjectMapper::new);

Map<String, String> connections = nacosMcpSseClientProperties.getConnections();
connections.forEach((serviceKey, serviceName) -> {
try {
List<Instance> instances = namingService.selectInstances(serviceName, true);
List<NamedClientMcpTransport> namedTransports = new ArrayList<>();
for (Instance instance : instances) {
String url = instance.getMetadata().getOrDefault("scheme", "http") + "://" + instance.getIp() + ":"
+ instance.getPort();

WebClient.Builder webClientBuilder = webClientBuilderTemplate.clone().baseUrl(url);
WebFluxSseClientTransport transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
namedTransports
.add(new NamedClientMcpTransport(serviceName + "-" + instance.getInstanceId(), transport));
}

server2NamedTransport.put(serviceName, namedTransports);
}
catch (NacosException e) {
_logger_.error("nacos naming service: {} error", serviceName, e);
}
});
return server2NamedTransport;
}

}

NacosMcpClientAutoConfiguration

提供和 MCP Server 进行交互的客户端

  • List<LoadbalancedMcpAsyncClient>

  • List<LoadbalancedMcpSyncClient>

@AutoConfiguration(after = { NacosMcpSseClientAutoConfiguration.class, McpClientAutoConfiguration.class })
@ConditionalOnClass({ McpSchema.class })
@EnableConfigurationProperties({ McpClientCommonProperties.class })
@ConditionalOnProperty(prefix = "spring.ai.mcp.client", name = { "nacos-enabled" }, havingValue = "true",
matchIfMissing = false)
public class NacosMcpClientAutoConfiguration {

public NacosMcpClientAutoConfiguration() {
}

private String connectedClientName(String clientName, String serverConnectionName) {
return clientName + " - " + serverConnectionName;
}

@Bean
@ConditionalOnProperty(prefix = "spring.ai.mcp.client", name = { "type" }, havingValue = "SYNC",
matchIfMissing = true)
public List<LoadbalancedMcpSyncClient> loadbalancedMcpSyncClientList(
ObjectProvider<McpSyncClientConfigurer> mcpSyncClientConfigurerProvider,
McpClientCommonProperties commonProperties,
@Qualifier("server2NamedTransport") ObjectProvider<Map<String, List<NamedClientMcpTransport>>> server2NamedTransportProvider,
ObjectProvider<NamingService> namingServiceProvider) {
NamingService namingService = namingServiceProvider.getObject();
McpSyncClientConfigurer mcpSyncClientConfigurer = mcpSyncClientConfigurerProvider.getObject();

List<LoadbalancedMcpSyncClient> loadbalancedMcpSyncClients = new ArrayList<>();
Map<String, List<NamedClientMcpTransport>> server2NamedTransport = server2NamedTransportProvider.getObject();
for (Map.Entry<String, List<NamedClientMcpTransport>> entry : server2NamedTransport.entrySet()) {
String serviceName = entry.getKey();
List<NamedClientMcpTransport> namedTransports = entry.getValue();
List<McpSyncClient> mcpSyncClients = new ArrayList<>();

McpSyncClient syncClient;
for (NamedClientMcpTransport namedTransport : namedTransports) {
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
commonProperties.getVersion());
McpClient.SyncSpec syncSpec = McpClient._sync_(namedTransport.transport())
.clientInfo(clientInfo)
.requestTimeout(commonProperties.getRequestTimeout());
syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec);
syncClient = syncSpec.build();
if (commonProperties.isInitialized()) {
syncClient.initialize();
}
mcpSyncClients.add(syncClient);
}

LoadbalancedMcpSyncClient loadbalancedMcpSyncClient = LoadbalancedMcpSyncClient._builder_()
.serviceName(serviceName)
.mcpSyncClientList(mcpSyncClients)
.namingService(namingService)
.build();
loadbalancedMcpSyncClient.subscribe();

loadbalancedMcpSyncClients.add(loadbalancedMcpSyncClient);
}

return loadbalancedMcpSyncClients;

}

@Bean
@ConditionalOnProperty(prefix = "spring.ai.mcp.client", name = { "type" }, havingValue = "ASYNC")
public List<LoadbalancedMcpAsyncClient> loadbalancedMcpAsyncClientList(
ObjectProvider<McpAsyncClientConfigurer> mcpAsyncClientConfigurerProvider,
McpClientCommonProperties commonProperties,
@Qualifier("server2NamedTransport") ObjectProvider<Map<String, List<NamedClientMcpTransport>>> server2NamedTransportProvider,
ObjectProvider<NamingService> namingServiceProvider) {
NamingService namingService = namingServiceProvider.getObject();
McpAsyncClientConfigurer mcpAsyncClientConfigurer = mcpAsyncClientConfigurerProvider.getObject();

List<LoadbalancedMcpAsyncClient> loadbalancedMcpAsyncClients = new ArrayList<>();
Map<String, List<NamedClientMcpTransport>> server2NamedTransport = server2NamedTransportProvider.getObject();
for (Map.Entry<String, List<NamedClientMcpTransport>> entry : server2NamedTransport.entrySet()) {
String serviceName = entry.getKey();
List<NamedClientMcpTransport> namedTransports = entry.getValue();
List<McpAsyncClient> mcpAsyncClients = new ArrayList<>();

McpAsyncClient asyncClient;
for (NamedClientMcpTransport namedTransport : namedTransports) {
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
commonProperties.getVersion());
McpClient.AsyncSpec asyncSpec = McpClient._async_(namedTransport.transport())
.clientInfo(clientInfo)
.requestTimeout(commonProperties.getRequestTimeout());
asyncSpec = mcpAsyncClientConfigurer.configure(namedTransport.name(), asyncSpec);
asyncClient = asyncSpec.build();
if (commonProperties.isInitialized()) {
asyncClient.initialize().block();
}

mcpAsyncClients.add(asyncClient);
}

LoadbalancedMcpAsyncClient loadbalancedMcpAsyncClient = LoadbalancedMcpAsyncClient._builder_()
.serviceName(serviceName)
.mcpAsyncClientList(mcpAsyncClients)
.namingService(namingService)
.build();

loadbalancedMcpAsyncClient.subscribe();

loadbalancedMcpAsyncClients.add(loadbalancedMcpAsyncClient);
}
return loadbalancedMcpAsyncClients;
}

}

Client 端部分

LoadbalancedMcpAsyncClient

各字段含义:

  • String serviceName:MCP Server 注册的服务名称

  • List<McpAsyncClient> mcpAsyncClientList:对应的多节点客户端

  • NamingService namingService:Nacos 服务

  • List<Instance> instances:Nacos 中 MCP Server 的实例列表

其余方法的使用和 McpAsyncClient 保持一致,已经全面封装好了

  1. 读操作:通过 getMcpAsyncClient()方法轮询得到 McpAsyncClient 列表

  2. 写操作:对所有 List<McpAsyncClient> 进行操作

通过实现 EventListener 接口,动态增加 or 减少 McpAsyncClient

public class LoadbalancedMcpAsyncClient implements EventListener {

private static final Logger _logger _= LoggerFactory._getLogger_(LoadbalancedMcpAsyncClient.class);

private final String serviceName;

private final List<McpAsyncClient> mcpAsyncClientList;

private final AtomicInteger currentIndex = new AtomicInteger(0);

private final NamingService namingService;

private List<Instance> instances;

public LoadbalancedMcpAsyncClient(String serviceName, List<McpAsyncClient> mcpAsyncClientList,
NamingService namingService) {
Assert._notNull_(serviceName, "serviceName cannot be null");
Assert._notNull_(mcpAsyncClientList, "mcpAsyncClientList cannot be null");
Assert._notNull_(namingService, "namingService cannot be null");
this.serviceName = serviceName;
this.mcpAsyncClientList = mcpAsyncClientList;

try {
this.namingService = namingService;
this.instances = namingService.selectInstances(serviceName, true);
}
catch (NacosException e) {
throw new RuntimeException(String._format_("Failed to get instances for service: %s", serviceName));
}
}

public void subscribe() {
try {
this.namingService.subscribe(this.serviceName, this);
}
catch (NacosException e) {
throw new RuntimeException(String._format_("Failed to subscribe to service: %s", this.serviceName));
}
}

public String getServiceName() {
return serviceName;
}

public List<McpAsyncClient> getMcpAsyncClientList() {
return mcpAsyncClientList;
}

public NamingService getNamingService() {
return this.namingService;
}

public List<Instance> getInstances() {
return this.instances;
}

private McpAsyncClient getMcpAsyncClient() {
if (mcpAsyncClientList.isEmpty()) {
throw new IllegalStateException("No McpAsyncClient available");
}
int index = currentIndex.getAndIncrement() % mcpAsyncClientList.size();
return mcpAsyncClientList.get(index);
}

// ------------------------------------------------------------------------------------------------------------------------------------------------

public McpSchema.ServerCapabilities getServerCapabilities() {
return getMcpAsyncClient().getServerCapabilities();
}

public McpSchema.Implementation getServerInfo() {
return getMcpAsyncClient().getServerInfo();
}

public boolean isInitialized() {
return getMcpAsyncClient().isInitialized();
}

public McpSchema.ClientCapabilities getClientCapabilities() {
return getMcpAsyncClient().getClientCapabilities();
}

public McpSchema.Implementation getClientInfo() {
return getMcpAsyncClient().getClientInfo();
}

public void close() {
Iterator<McpAsyncClient> iterator = mcpAsyncClientList.iterator();
while (iterator.hasNext()) {
McpAsyncClient mcpAsyncClient = iterator.next();
mcpAsyncClient.close();
iterator.remove();
_logger_.info("Closed and removed McpSyncClient: {}", mcpAsyncClient.getClientInfo().name());
}
}

public Mono<Void> closeGracefully() {
Iterator<McpAsyncClient> iterator = mcpAsyncClientList.iterator();
List<Mono<Void>> closeMonos = new ArrayList<>();
while (iterator.hasNext()) {
McpAsyncClient mcpAsyncClient = iterator.next();
Mono<Void> voidMono = mcpAsyncClient.closeGracefully().doOnSuccess(v -> {
iterator.remove();
_logger_.info("Closed and removed McpAsyncClient: {}", mcpAsyncClient.getClientInfo().name());
});
closeMonos.add(voidMono);
}
return Mono._when_(closeMonos);
}

public Mono<Object> ping() {
return getMcpAsyncClient().ping();
}

public Mono<Void> addRoot(McpSchema.Root root) {
return Mono._when_(mcpAsyncClientList.stream()
.map(mcpAsyncClient -> mcpAsyncClient.addRoot(root))
.collect(Collectors._toList_()));
}

public Mono<Void> removeRoot(String rootUri) {
return Mono._when_(mcpAsyncClientList.stream()
.map(mcpAsyncClient -> mcpAsyncClient.removeRoot(rootUri))
.collect(Collectors._toList_()));
}

public Mono<Void> rootsListChangedNotification() {
return Mono._when_(mcpAsyncClientList.stream()
.map(McpAsyncClient::rootsListChangedNotification)
.collect(Collectors._toList_()));
}

public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {
return getMcpAsyncClient().callTool(callToolRequest);
}

public Mono<McpSchema.ListToolsResult> listTools() {
return getMcpAsyncClient().listTools();
}

public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
return getMcpAsyncClient().listTools(cursor);
}

public Mono<McpSchema.ListResourcesResult> listResources() {
return getMcpAsyncClient().listResources();
}

public Mono<McpSchema.ListResourcesResult> listResources(String cursor) {
return getMcpAsyncClient().listResources(cursor);
}

public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.Resource resource) {
return getMcpAsyncClient().readResource(resource);
}

public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) {
return getMcpAsyncClient().readResource(readResourceRequest);
}

public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() {
return getMcpAsyncClient().listResourceTemplates();
}

public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) {
return getMcpAsyncClient().listResourceTemplates(cursor);
}

public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
return Mono._when_(mcpAsyncClientList.stream()
.map(mcpAsyncClient -> mcpAsyncClient.subscribeResource(subscribeRequest))
.collect(Collectors._toList_()));
}

public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
return Mono._when_(mcpAsyncClientList.stream()
.map(mcpAsyncClient -> mcpAsyncClient.unsubscribeResource(unsubscribeRequest))
.collect(Collectors._toList_()));
}

public Mono<McpSchema.ListPromptsResult> listPrompts() {
return getMcpAsyncClient().listPrompts();
}

public Mono<McpSchema.ListPromptsResult> listPrompts(String cursor) {
return getMcpAsyncClient().listPrompts(cursor);
}

public Mono<McpSchema.GetPromptResult> getPrompt(McpSchema.GetPromptRequest getPromptRequest) {
return getMcpAsyncClient().getPrompt(getPromptRequest);
}

public Mono<Void> setLoggingLevel(McpSchema.LoggingLevel loggingLevel) {
return Mono._when_(mcpAsyncClientList.stream()
.map(mcpAsyncClient -> mcpAsyncClient.setLoggingLevel(loggingLevel))
.collect(Collectors._toList_()));
}

// ------------------------------------------------------------------------------------------------------------------------------------------------

@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent namingEvent) {
if (this.serviceName.equals(namingEvent.getServiceName())) {
_logger_.info("Received service instance change event for service: {}", namingEvent.getServiceName());
List<Instance> instances = namingEvent.getInstances();
_logger_.info("Updated instances count: {}", instances.size());
// 打印每个实例的详细信息
instances.forEach(instance -> {
_logger_.info("Instance: {}:{} (Healthy: {}, Enabled: {}, Metadata: {})", instance.getIp(),
instance.getPort(), instance.isHealthy(), instance.isEnabled(),
JacksonUtils._toJson_(instance.getMetadata()));
});
updateClientList(instances);
}
}
}

private void updateClientList(List<Instance> currentInstances) {
McpClientCommonProperties commonProperties = ApplicationContextHolder._getBean_(McpClientCommonProperties.class);
McpAsyncClientConfigurer mcpSyncClientConfigurer = ApplicationContextHolder
._getBean_(McpAsyncClientConfigurer.class);
ObjectMapper objectMapper = ApplicationContextHolder._getBean_(ObjectMapper.class);
WebClient.Builder webClientBuilderTemplate = ApplicationContextHolder._getBean_(WebClient.Builder.class);

// 移除的实例列表
List<Instance> removeInstances = instances.stream()
.filter(instance -> !currentInstances.contains(instance))
.collect(Collectors._toList_());

// 新增的实例列表
List<Instance> addInstances = currentInstances.stream()
.filter(instance -> !instances.contains(instance))
.collect(Collectors._toList_());

// 删除McpAsyncClient实例
List<String> clientInfoNames = removeInstances.stream()
.map(instance -> connectedClientName(commonProperties.getName(),
this.serviceName + "-" + instance.getInstanceId()))
.toList();
Iterator<McpAsyncClient> iterator = mcpAsyncClientList.iterator();
while (iterator.hasNext()) {
McpAsyncClient mcpAsyncClient = iterator.next();
McpSchema.Implementation clientInfo = mcpAsyncClient.getClientInfo();
if (clientInfoNames.contains(clientInfo.name())) {
_logger_.info("Removing McpAsyncClient: {}", clientInfo.name());
mcpAsyncClient.closeGracefully().subscribe(v -> {
iterator.remove();
}, e -> _logger_.error("Failed to remove McpAsyncClient: {}", clientInfo.name(), e));
}
}

// 新增McpAsyncClient实例
McpAsyncClient asyncClient;
for (Instance instance : addInstances) {
String baseUrl = instance.getMetadata().getOrDefault("scheme", "http") + "://" + instance.getIp() + ":"
+ instance.getPort();
WebClient.Builder webClientBuilder = webClientBuilderTemplate.clone().baseUrl(baseUrl);
WebFluxSseClientTransport transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
NamedClientMcpTransport namedTransport = new NamedClientMcpTransport(
serviceName + "-" + instance.getInstanceId(), transport);

McpSchema.Implementation clientInfo = new McpSchema.Implementation(
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
commonProperties.getVersion());
McpClient.AsyncSpec asyncSpec = McpClient._async_(namedTransport.transport())
.clientInfo(clientInfo)
.requestTimeout(commonProperties.getRequestTimeout());
asyncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), asyncSpec);
asyncClient = asyncSpec.build();
if (commonProperties.isInitialized()) {
asyncClient.initialize().block();
}
_logger_.info("Added McpAsyncClient: {}", clientInfo.name());
mcpAsyncClientList.add(asyncClient);
}

private String connectedClientName(String clientName, String serverConnectionName) {
return clientName + " - " + serverConnectionName;
}

public static Builder builder() {
return new Builder();
}

public static class Builder {

private String serviceName;

private List<McpAsyncClient> mcpAsyncClientList;

private NamingService namingService;

public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}

public Builder mcpAsyncClientList(List<McpAsyncClient> mcpAsyncClientList) {
this.mcpAsyncClientList = mcpAsyncClientList;
return this;
}

public Builder namingService(NamingService namingService) {
this.namingService = namingService;
return this;
}

public LoadbalancedMcpAsyncClient build() {
return new LoadbalancedMcpAsyncClient(this.serviceName, this.mcpAsyncClientList, this.namingService);
}

}

}

LoadbalancedMcpSyncClient

同上

public class LoadbalancedMcpSyncClient implements EventListener {

private static final Logger _logger _= LoggerFactory._getLogger_(LoadbalancedMcpAsyncClient.class);

private final String serviceName;

private final List<McpSyncClient> mcpSyncClientList;

private final AtomicInteger currentIndex = new AtomicInteger(0);

private final NamingService namingService;

private List<Instance> instances;

public LoadbalancedMcpSyncClient(String serviceName, List<McpSyncClient> mcpSyncClientList,
NamingService namingService) {
Assert._notNull_(serviceName, "Service name must not be null");
Assert._notNull_(mcpSyncClientList, "McpSyncClient list must not be null");
Assert._notNull_(namingService, "NamingService must not be null");

this.serviceName = serviceName;
this.mcpSyncClientList = mcpSyncClientList;

try {
this.namingService = namingService;
this.instances = namingService.selectInstances(serviceName, true);
}
catch (NacosException e) {
throw new RuntimeException(String._format_("Failed to get instances for service: %s", serviceName));
}
}

public void subscribe() {
try {
this.namingService.subscribe(this.serviceName, this);
}
catch (NacosException e) {
throw new RuntimeException(String._format_("Failed to subscribe to service: %s", this.serviceName));
}
}

public String getServiceName() {
return this.serviceName;
}

public List<McpSyncClient> getMcpSyncClientList() {
return this.mcpSyncClientList;
}

public NamingService getNamingService() {
return this.namingService;
}

public List<Instance> getInstances() {
return this.instances;
}

private McpSyncClient getMcpSyncClient() {
if (mcpSyncClientList.isEmpty()) {
throw new IllegalStateException("No McpAsyncClient available");
}
int index = currentIndex.getAndIncrement() % mcpSyncClientList.size();
return mcpSyncClientList.get(index);
}

// ------------------------------------------------------------------------------------------------------------------------------------------------
public McpSchema.ServerCapabilities getServerCapabilities() {
return getMcpSyncClient().getServerCapabilities();
}

public McpSchema.Implementation getServerInfo() {
return getMcpSyncClient().getServerInfo();
}

public McpSchema.ClientCapabilities getClientCapabilities() {
return getMcpSyncClient().getClientCapabilities();
}

public McpSchema.Implementation getClientInfo() {
return getMcpSyncClient().getClientInfo();
}

public void close() {
Iterator<McpSyncClient> iterator = mcpSyncClientList.iterator();
while (iterator.hasNext()) {
McpSyncClient mcpSyncClient = iterator.next();
mcpSyncClient.close();
iterator.remove();
_logger_.info("Closed and removed McpSyncClient: {}", mcpSyncClient.getClientInfo().name());
}
}

public boolean closeGracefully() {
List<Boolean> flagList = new ArrayList<>();
Iterator<McpSyncClient> iterator = mcpSyncClientList.iterator();
while (iterator.hasNext()) {
McpSyncClient mcpSyncClient = iterator.next();
boolean flag = mcpSyncClient.closeGracefully();
flagList.add(flag);
if (flag) {
iterator.remove();
_logger_.info("Closed and removed McpSyncClient: {}", mcpSyncClient.getClientInfo().name());
}
}
return !flagList.stream().allMatch(flag -> flag);
}

public Object ping() {
return getMcpSyncClient().ping();
}

public void addRoot(McpSchema.Root root) {
for (McpSyncClient mcpSyncClient : mcpSyncClientList) {
mcpSyncClient.addRoot(root);
}
}

public void removeRoot(String rootUri) {
for (McpSyncClient mcpSyncClient : mcpSyncClientList) {
mcpSyncClient.removeRoot(rootUri);
}
}

public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolRequest) {
return getMcpSyncClient().callTool(callToolRequest);
}

public McpSchema.ListToolsResult listTools() {
return getMcpSyncClient().listTools();
}

public McpSchema.ListToolsResult listTools(String cursor) {
return getMcpSyncClient().listTools(cursor);
}

public McpSchema.ListResourcesResult listResources(String cursor) {
return getMcpSyncClient().listResources(cursor);
}

public McpSchema.ListResourcesResult listResources() {
return getMcpSyncClient().listResources();
}

public McpSchema.ReadResourceResult readResource(McpSchema.Resource resource) {
return getMcpSyncClient().readResource(resource);
}

public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest readResourceRequest) {
return getMcpSyncClient().readResource(readResourceRequest);
}

public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor) {
return getMcpSyncClient().listResourceTemplates(cursor);
}

public McpSchema.ListResourceTemplatesResult listResourceTemplates() {
return getMcpSyncClient().listResourceTemplates();
}

public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
for (McpSyncClient mcpSyncClient : mcpSyncClientList) {
mcpSyncClient.subscribeResource(subscribeRequest);
}
}

public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
for (McpSyncClient mcpSyncClient : mcpSyncClientList) {
mcpSyncClient.unsubscribeResource(unsubscribeRequest);
}
}

public McpSchema.ListPromptsResult listPrompts(String cursor) {
return getMcpSyncClient().listPrompts(cursor);
}

public McpSchema.ListPromptsResult listPrompts() {
return getMcpSyncClient().listPrompts();
}

public McpSchema.GetPromptResult getPrompt(McpSchema.GetPromptRequest getPromptRequest) {
return getMcpSyncClient().getPrompt(getPromptRequest);
}

public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) {
for (McpSyncClient mcpSyncClient : mcpSyncClientList) {
mcpSyncClient.setLoggingLevel(loggingLevel);
}
}

// ------------------------------------------------------------------------------------------------------------------------------------------------

@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent namingEvent) {
if (this.serviceName.equals(namingEvent.getServiceName())) {
_logger_.info("Received service instance change event for service: {}", namingEvent.getServiceName());
List<Instance> instances = namingEvent.getInstances();
_logger_.info("Updated instances count: {}", instances.size());
// 打印每个实例的详细信息
instances.forEach(instance -> {
_logger_.info("Instance: {}:{} (Healthy: {}, Enabled: {}, Metadata: {})", instance.getIp(),
instance.getPort(), instance.isHealthy(), instance.isEnabled(),
JacksonUtils._toJson_(instance.getMetadata()));
});
updateClientList(instances);
}
}
}

private void updateClientList(List<Instance> currentInstances) {
McpClientCommonProperties commonProperties = ApplicationContextHolder._getBean_(McpClientCommonProperties.class);
McpSyncClientConfigurer mcpSyncClientConfigurer = ApplicationContextHolder
._getBean_(McpSyncClientConfigurer.class);
ObjectMapper objectMapper = ApplicationContextHolder._getBean_(ObjectMapper.class);
WebClient.Builder webClientBuilderTemplate = ApplicationContextHolder._getBean_(WebClient.Builder.class);

// 移除的实例列表
List<Instance> removeInstances = instances.stream()
.filter(instance -> !currentInstances.contains(instance))
.collect(Collectors._toList_());

// 新增的实例列表
List<Instance> addInstances = currentInstances.stream()
.filter(instance -> !instances.contains(instance))
.collect(Collectors._toList_());

// 删除McpSyncClient实例
List<String> clientInfoNames = removeInstances.stream()
.map(instance -> connectedClientName(commonProperties.getName(),
this.serviceName + "-" + instance.getInstanceId()))
.toList();
Iterator<McpSyncClient> iterator = mcpSyncClientList.iterator();
while (iterator.hasNext()) {
McpSyncClient mcpSyncClient = iterator.next();
McpSchema.Implementation clientInfo = mcpSyncClient.getClientInfo();
if (clientInfoNames.contains(clientInfo.name())) {
_logger_.info("Removing McpsyncClient: {}", clientInfo.name());
if (mcpSyncClient.closeGracefully()) {
iterator.remove();
}
else {
_logger_.warn("Failed to remove mcpSyncClient: {}", clientInfo.name());
}
}
}

// 新增McpSyncClient实例
McpSyncClient syncClient;
for (Instance instance : addInstances) {
String baseUrl = instance.getMetadata().getOrDefault("scheme", "http") + "://" + instance.getIp() + ":"
+ instance.getPort();
WebClient.Builder webClientBuilder = webClientBuilderTemplate.clone().baseUrl(baseUrl);
WebFluxSseClientTransport transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
NamedClientMcpTransport namedTransport = new NamedClientMcpTransport(
serviceName + "-" + instance.getInstanceId(), transport);

McpSchema.Implementation clientInfo = new McpSchema.Implementation(
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
commonProperties.getVersion());
McpClient.SyncSpec syncSpec = McpClient._sync_(namedTransport.transport())
.clientInfo(clientInfo)
.requestTimeout(commonProperties.getRequestTimeout());
syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec);
syncClient = syncSpec.build();
if (commonProperties.isInitialized()) {
syncClient.initialize();
}

_logger_.info("Added McpAsyncClient: {}", clientInfo.name());
mcpSyncClientList.add(syncClient);
}

this.instances = currentInstances;
}

private String connectedClientName(String clientName, String serverConnectionName) {
return clientName + " - " + serverConnectionName;
}

public static Builder builder() {
return new Builder();
}

public static class Builder {

private String serviceName;

private List<McpSyncClient> mcpSyncClientList;

private NamingService namingService;

public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}

public Builder mcpSyncClientList(List<McpSyncClient> mcpSyncClientList) {
this.mcpSyncClientList = mcpSyncClientList;
return this;
}

public Builder namingService(NamingService namingService) {
this.namingService = namingService;
return this;
}

public LoadbalancedMcpSyncClient build() {
return new LoadbalancedMcpSyncClient(this.serviceName, this.mcpSyncClientList, this.namingService);
}

}

}

工具类

ApplicationContextHolder

@Component
public class ApplicationContextHolder implements ApplicationContextAware {

private static ApplicationContext _applicationContext_;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder._applicationContext _= applicationContext;
}

public static <T> T getBean(Class<T> clazz) {
return _applicationContext_.getBean(clazz);
}

}

效果演示

我在 nacos 中,注册了 MCP Server 服务,部署两个节点

  • 同一台机器以不同端口号启动的 MCP Server 服务,分别是 19000、19001,注册在 Nacos 中以 mcp-server-provider 为服务名

yml 配置如下

server:
port: 8080

spring:
application:
name: mcp-client-webflux

_ _ai:
alibaba:
mcp:
nacos:
enabled: true
server-addr: 127.0.0.1:8848
username: nacos
password: nacos

client:
sse:
connections:
nacos-server1: mcp-server-provider

mcp:
client:
enabled: true
name: mcp-client-webflux
version: 0.0.1
initialized: true
request-timeout: 600s

nacos-enabled: true

我们能发现已经成功注入 LoadbalancedMcpSyncClient 类,其中 mcp-server-provider 有两个实例,对应的两个 McpSyncClient

我们停掉其中的 MCP Server19001 端口的服务,通过 removeInstances 获取移除的实例列表,同步在 mcpSyncClientList 移除对应的 McpSyncClient

我们再新启动 MCP Server19001 端口的服务,通过 addInstances 获取新增的实例列表,同步在 mcpSyncClientList 新增对应的 McpSyncClient

企业级分布式MCP方案的更多相关文章

  1. Hadoop生态圈-构建企业级平台安全方案

    Hadoop生态圈-构建企业级平台安全方案 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 能看到这篇文章的小伙伴,估计你对大数据集群的部署对于你来说应该是手到擒来了吧.我之前分享过 ...

  2. Memcached常规应用与分布式部署方案

    1.Memcached常规应用 $mc = new Memcache(); $mc->conncet('127.0.0.1', 11211); $sql = sprintf("SELE ...

  3. Window Redis分布式部署方案 java

    Redis分布式部署方案 Window 1.    基本介绍 首先redis官方是没有提供window下的版本, 是window配合发布的.因现阶段项目需求,所以研究部署的是window版本的,其实都 ...

  4. 企业级分布式应用服务EDAS _Dubbo商业版_微服务PaaS平台 【EDAS Serverless 运维 创业】

    企业级分布式应用服务EDAS _Dubbo商业版_微服务PaaS平台_分布式框架 - 阿里云https://www.aliyun.com/product/edas?source_type=yqzb_e ...

  5. 基于Solr和Zookeeper的分布式搜索方案的配置

    1.1 什么是SolrCloud SolrCloud(solr 云)是Solr提供的分布式搜索方案,当你需要大规模,容错,分布式索引和检索能力时使用 SolrCloud.当一个系统的索引数据量少的时候 ...

  6. ebay分布式事务方案中文版

    http://cailin.iteye.com/blog/2268428 不使用分布式事务实现目的  -- ibm https://www.ibm.com/developerworks/cn/clou ...

  7. [Apache Pulsar] 企业级分布式消息系统-Pulsar快速上手

    Pulsar快速上手 前言 如果你还不了解Pulsar消息系统,可以先看上一篇文章 企业级分布式消息系统-Pulsar入门基础 Pulsar客户端支持多个语言,包括Java,Go,Pytho和C++, ...

  8. Dubbo学习系列之十五(Seata分布式事务方案TCC模式)

    上篇的续集. 工具: Idea201902/JDK11/Gradle5.6.2/Mysql8.0.11/Lombok0.27/Postman7.5.0/SpringBoot2.1.9/Nacos1.1 ...

  9. Dubbo学习系列之十四(Seata分布式事务方案AT模式)

    一直说写有关最新技术的文章,但前面似乎都有点偏了,只能说算主流技术,今天这个主题,我觉得应该名副其实.分布式微服务的深水区并不是单个微服务的设计,而是服务间的数据一致性问题!解决了这个问题,才算是把分 ...

  10. redis生成分布式id方案

    分布式Id - redis方式   本篇分享内容是关于生成分布式Id的其中之一方案,除了redis方案之外还有如:数据库,雪花算法,mogodb(object_id也是数据库)等方案,对于redis来 ...

随机推荐

  1. 闲话 6.19/CF1938M

    CF1938M 计数以下序列 \(\lang a\rang\) 的个数: \[\sum_{i=1}^m a_i=n\\ \forall 1<i<m,(a_i-a_{i-1})(a_i-a_ ...

  2. .NET最佳实践:避免同步读取HttpRequest

    为什么要避免同步读取 ASP.NET Core 中的所有 I/O 操作都是异步的.服务器实现了 Stream 接口,该接口同时具备同步和异步的方法. 在进行 I/O 操作时,应优先使用异步方法,以避免 ...

  3. 问题-ifconfig

    在运行centos7 运行ifconfig命令时出现: [root@kvm1 ~]# ifconfig -bash: ifconfig: command not found 原来是这样: 1.ifco ...

  4. 库卡机器人KR3R540电源模块常见故障维修解决方法

            库卡机器人KR3R540电源模块的常见故障及维修解决方法包括:           电源模块无法正常启动:应检查电源模块的电源连接是否正常,以及电源开关是否开启.如果电源连接正常,但驱 ...

  5. 泰山派(Ubuntu 20.0)更换软件源

    泰山派更换软件源 1.编辑apt软件包源获取文件 vim /etc/apt/sources.list 2.更换为下面的源 deb http://mirrors.ustc.edu.cn/ubuntu-p ...

  6. Android:如何在后台启动Activity

    通常我用这段代码开始一个活动: Intent i = new Intent(context, MyActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_NE ...

  7. java 8 lamdba 表达式list集合的BigDecimal求和操作

  8. SpringBoot实现HandlerInterceptor拦截器的接口没有需要重写的方法也不报错是怎么回事

    以前实现HandlerInterceptor接口,总会提示需要实现3个方法(preHandle.postHandle.afterCompletion).现在没有出现提示.原因:这是Java8的新特性- ...

  9. 【渗透测试】Vulnhub Corrosion 1

    渗透环境 攻击机:   IP: 192.168.226.129(Kali) 靶机:     IP:192.168.226.128 靶机下载地址:https://www.vulnhub.com/entr ...

  10. NCS开发学习笔记-基础篇-前言

    nRF5 SDK 与 nRF Connect SDK 目前Nordic有2套并存的SDK:老的nRF5 SDK和新的nRF Connect SDK(简称NCS),两套SDK相互独立. nRF5 SDK ...