package com.claudecode.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* MCP 管理器 —— 管理多个 MCP 服务器连接的统一入口。
*
* 职责:
*
* - 从配置文件加载 MCP 服务器定义
* - 管理服务器连接的生命周期(连接、断开、重连)
* - 聚合所有服务器的工具和资源供上层使用
* - 路由工具调用到正确的服务器
*
*
* 配置文件格式({@code mcp.json}):
*
{@code
* {
* "servers": {
* "server-name": {
* "command": "npx",
* "args": ["-y", "@modelcontextprotocol/server-filesystem"],
* "env": { "KEY": "VALUE" }
* }
* }
* }
* }
*
* @see McpClient
* @see StdioTransport
*/
public class McpManager implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(McpManager.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
/** 全局配置文件路径:~/.claude-code-java/mcp.json */
private static final String GLOBAL_CONFIG = ".claude-code-java/mcp.json";
/** 项目级配置文件名 */
private static final String PROJECT_CONFIG = ".mcp.json";
/** 已连接的 MCP 客户端:serverName -> McpClient */
private final Map clients = new ConcurrentHashMap<>();
/** 工具名称到服务器名称的映射:toolName -> serverName(用于路由调用) */
private final Map toolToServer = new ConcurrentHashMap<>();
/**
* 从配置文件加载并连接所有 MCP 服务器。
*
* 优先级:
*
* - 项目级配置文件:当前工作目录下的 {@code .mcp.json}
* - 全局配置文件:{@code ~/.claude-code-java/mcp.json}
*
* 两个配置文件中的服务器会合并加载。
*/
public void loadFromConfig() {
// 项目级配置
Path projectConfig = Path.of(System.getProperty("user.dir"), PROJECT_CONFIG);
if (Files.exists(projectConfig)) {
loadConfigFile(projectConfig, "项目级");
}
// 全局配置
Path globalConfig = Path.of(System.getProperty("user.home"), GLOBAL_CONFIG);
if (Files.exists(globalConfig)) {
loadConfigFile(globalConfig, "全局");
}
if (clients.isEmpty()) {
log.debug("未找到 MCP 配置文件或无服务器定义");
}
}
/**
* 加载单个配置文件中的 MCP 服务器定义。
*/
private void loadConfigFile(Path configPath, String label) {
log.info("加载 {} MCP 配置: {}", label, configPath);
try {
String content = Files.readString(configPath);
JsonNode root = MAPPER.readTree(content);
JsonNode serversNode = root.get("servers");
if (serversNode == null || !serversNode.isObject()) {
log.warn("{} 配置文件缺少 'servers' 字段: {}", label, configPath);
return;
}
Iterator> fields = serversNode.fields();
while (fields.hasNext()) {
Map.Entry entry = fields.next();
String name = entry.getKey();
JsonNode serverDef = entry.getValue();
// 跳过已存在的服务器(项目级优先于全局)
if (clients.containsKey(name)) {
log.debug("MCP 服务器 '{}' 已连接,跳过 {} 配置中的重复定义", name, label);
continue;
}
try {
String command = serverDef.get("command").asText();
List args = new ArrayList<>();
if (serverDef.has("args") && serverDef.get("args").isArray()) {
for (JsonNode arg : serverDef.get("args")) {
args.add(arg.asText());
}
}
Map env = new HashMap<>();
if (serverDef.has("env") && serverDef.get("env").isObject()) {
Iterator> envFields = serverDef.get("env").fields();
while (envFields.hasNext()) {
Map.Entry envEntry = envFields.next();
env.put(envEntry.getKey(), envEntry.getValue().asText());
}
}
connect(name, command, args, env);
} catch (Exception e) {
log.error("从配置连接 MCP 服务器 '{}' 失败: {}", name, e.getMessage());
}
}
} catch (IOException e) {
log.error("读取 MCP 配置文件失败: {}", configPath, e);
}
}
/**
* 连接单个 MCP 服务器。
*
* @param name 服务器名称标识
* @param command 服务器可执行命令
* @param args 命令参数列表
* @param env 环境变量(可为 {@code null})
* @return 已初始化的 MCP 客户端
* @throws McpException 连接或初始化失败
*/
public McpClient connect(String name, String command, List args, Map env)
throws McpException {
// 如果已存在,先断开
if (clients.containsKey(name)) {
log.info("MCP 服务器 '{}' 已存在,先断开旧连接", name);
try {
disconnect(name);
} catch (Exception e) {
log.warn("断开旧 MCP 连接 '{}' 时异常: {}", name, e.getMessage());
}
}
log.info("连接 MCP 服务器 '{}': {} {}", name, command, String.join(" ", args));
// 创建传输层并启动(确保初始化失败时清理资源)
StdioTransport transport = new StdioTransport(command, args, env);
McpClient client;
try {
transport.start();
client = new McpClient(name, transport);
client.initialize();
} catch (Exception e) {
// 初始化失败时必须关闭传输层,防止子进程泄漏
try {
transport.close();
} catch (Exception suppressed) {
e.addSuppressed(suppressed);
}
throw (e instanceof McpException mcp) ? mcp
: new McpException("连接 MCP 服务器 '" + name + "' 失败: " + e.getMessage(), e);
}
// 注册客户端
clients.put(name, client);
// 建立工具 -> 服务器的映射
for (McpClient.McpTool tool : client.getTools()) {
String existingServer = toolToServer.get(tool.name());
if (existingServer != null) {
log.warn("MCP 工具名称冲突: '{}' 同时存在于服务器 '{}' 和 '{}',使用后者",
tool.name(), existingServer, name);
}
toolToServer.put(tool.name(), name);
}
log.info("MCP 服务器 '{}' 连接成功", name);
return client;
}
/**
* 断开 MCP 服务器连接。
*
* @param name 服务器名称
* @throws McpException 断开失败
*/
public void disconnect(String name) throws McpException {
McpClient client = clients.remove(name);
if (client == null) {
throw new McpException("MCP 服务器 '" + name + "' 不存在");
}
// 清理工具映射
toolToServer.entrySet().removeIf(entry -> entry.getValue().equals(name));
try {
client.close();
log.info("MCP 服务器 '{}' 已断开", name);
} catch (Exception e) {
throw new McpException("断开 MCP 服务器 '" + name + "' 时异常: " + e.getMessage(), e);
}
}
/**
* 获取所有已连接的客户端(不可变视图)。
*/
public Map getClients() {
return Collections.unmodifiableMap(clients);
}
/**
* 获取指定服务器的客户端。
*
* @param name 服务器名称
* @return 客户端实例,若不存在则返回 {@link Optional#empty()}
*/
public Optional getClient(String name) {
return Optional.ofNullable(clients.get(name));
}
/**
* 获取所有 MCP 工具(合并所有服务器的工具)。
*
* @return 所有已发现的工具列表
*/
public List getAllTools() {
return clients.values().stream()
.filter(McpClient::isInitialized)
.flatMap(client -> client.getTools().stream())
.toList();
}
/**
* 获取指定服务器的工具。
*
* @param serverName 服务器名称
* @return 工具列表,若服务器不存在则返回空列表
*/
public List getServerTools(String serverName) {
McpClient client = clients.get(serverName);
if (client == null || !client.isInitialized()) {
return List.of();
}
return List.copyOf(client.getTools());
}
/**
* 获取所有 MCP 资源(合并所有服务器的资源)。
*
* @return 所有已发现的资源列表
*/
public List getAllResources() {
return clients.values().stream()
.filter(McpClient::isInitialized)
.flatMap(client -> client.getResources().stream())
.toList();
}
/**
* 获取指定服务器的资源。
*
* @param serverName 服务器名称
* @return 资源列表,若服务器不存在则返回空列表
*/
public List getServerResources(String serverName) {
McpClient client = clients.get(serverName);
if (client == null || !client.isInitialized()) {
return List.of();
}
return List.copyOf(client.getResources());
}
/**
* 调用 MCP 工具 —— 自动路由到拥有该工具的服务器。
*
* @param toolName 工具名称
* @param args 工具参数
* @return 工具执行结果
* @throws McpException 工具不存在或调用失败
*/
public String callTool(String toolName, Map args) throws McpException {
String serverName = toolToServer.get(toolName);
if (serverName == null) {
throw new McpException("未找到 MCP 工具: " + toolName);
}
return callTool(serverName, toolName, args);
}
/**
* 调用指定服务器的 MCP 工具。
*
* @param serverName 服务器名称
* @param toolName 工具名称
* @param args 工具参数
* @return 工具执行结果
* @throws McpException 服务器不存在或调用失败
*/
public String callTool(String serverName, String toolName, Map args)
throws McpException {
McpClient client = clients.get(serverName);
if (client == null) {
throw new McpException("MCP 服务器 '" + serverName + "' 不存在");
}
if (!client.isInitialized()) {
throw new McpException("MCP 服务器 '" + serverName + "' 尚未初始化");
}
return client.callTool(toolName, args);
}
/**
* 查找工具所属的服务器名称。
*
* @param toolName 工具名称
* @return 服务器名称,若不存在则返回 {@link Optional#empty()}
*/
public Optional findServerForTool(String toolName) {
return Optional.ofNullable(toolToServer.get(toolName));
}
/**
* 重新加载配置文件并重连所有服务器。
*
* 先断开所有已有连接,再重新加载配置。
*/
public void reload() {
log.info("重新加载 MCP 配置...");
// 断开所有现有连接
List serverNames = new ArrayList<>(clients.keySet());
for (String name : serverNames) {
try {
disconnect(name);
} catch (Exception e) {
log.warn("重载时断开 MCP 服务器 '{}' 失败: {}", name, e.getMessage());
}
}
// 重新加载
loadFromConfig();
log.info("MCP 配置重载完成: {} 个服务器已连接", clients.size());
}
/**
* 获取状态摘要(用于 /mcp 命令或状态显示)。
*
* @return 格式化的状态摘要文本
*/
public String getSummary() {
if (clients.isEmpty()) {
return " 无已连接的 MCP 服务器";
}
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : clients.entrySet()) {
String name = entry.getKey();
McpClient client = entry.getValue();
String status;
if (client.isConnected() && client.isInitialized()) {
status = "✅ 已连接";
} else if (client.isConnected()) {
status = "🔄 连接中";
} else {
status = "❌ 已断开";
}
sb.append(String.format(" %-20s %s (%d 工具, %d 资源)%n",
name, status, client.getTools().size(), client.getResources().size()));
}
return sb.toString().stripTrailing();
}
@Override
public void close() throws Exception {
log.info("关闭所有 MCP 连接...");
List errors = new ArrayList<>();
for (Map.Entry entry : clients.entrySet()) {
try {
entry.getValue().close();
} catch (Exception e) {
errors.add(e);
log.error("关闭 MCP 服务器 '{}' 时异常: {}", entry.getKey(), e.getMessage());
}
}
clients.clear();
toolToServer.clear();
if (!errors.isEmpty()) {
McpException ex = new McpException("关闭 MCP 管理器时有 " + errors.size() + " 个错误");
errors.forEach(ex::addSuppressed);
throw ex;
}
log.info("所有 MCP 连接已关闭");
}
}