feat: P2全部功能 - MCP协议+插件系统+任务管理+CLI增强

A组 CLI增强:
- DiffRenderer: 彩色diff渲染(行号/stat摘要/unified格式)
- /hooks: 查看已注册Hook列表
- /review: AI代码审查(支持--staged/文件路径)
- /stats: 会话使用统计(tokens/费用/API调用/运行时长)
- /branch: 对话分支管理(save/load/list/delete)
- /rewind: 回退对话历史
- /tag: 对话位置标签
- /security-review: AI安全审查

B组 任务系统:
- TaskManager: 后台任务管理(线程安全/自动执行/手动管理)
- TaskCreate/TaskGet/TaskList/TaskUpdate 4个工具
- ConfigTool: 配置读写工具

C组 MCP协议:
- McpClient: MCP客户端(JSON-RPC 2.0/工具发现/资源读取)
- McpTransport: 传输层接口
- StdioTransport: StdIO传输实现(子进程/异步读写)
- McpManager: 多MCP服务器管理(配置加载/生命周期)
- McpToolBridge: MCP工具桥接为本地Tool
- /mcp: MCP服务器管理命令

D组 插件系统:
- Plugin接口 + PluginContext + PluginManager
- JAR插件加载(ClassLoader隔离/Manifest读取)
- OutputStylePlugin: 内置输出样式插件
- /plugin: 插件管理命令

集成: AppConfig注册全部18工具+28命令

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
liuzh
2026-04-01 22:56:35 +08:00
co-authored by Copilot
parent da26f02498
commit 79736cf16f
27 changed files with 5592 additions and 9 deletions
@@ -0,0 +1,443 @@
package com.claudecode.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* MCP 客户端 —— 对应 claude-code 中的 mcp/ 模块。
* <p>
* 负责与单个 MCP 服务器的完整生命周期管理:
* <ol>
* <li>通过 {@link McpTransport} 建立连接</li>
* <li>发送 {@code initialize} 握手请求</li>
* <li>发现服务器提供的工具({@code tools/list})和资源({@code resources/list}</li>
* <li>调用工具({@code tools/call})和读取资源({@code resources/read}</li>
* </ol>
* <p>
* MCP 协议使用 JSON-RPC 2.0 格式通信。
*
* @see McpTransport
* @see McpManager
*/
public class McpClient implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(McpClient.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
/** JSON-RPC 请求 ID 生成器 */
private final AtomicInteger idCounter = new AtomicInteger(1);
/** 服务器名称标识 */
private final String serverName;
/** 底层传输层 */
private final McpTransport transport;
/** 已发现的工具集合:toolName -> McpTool */
private final Map<String, McpTool> tools = new ConcurrentHashMap<>();
/** 已发现的资源集合:uri -> McpResource */
private final Map<String, McpResource> resources = new ConcurrentHashMap<>();
/** 服务器能力信息 */
private JsonNode serverCapabilities;
/** 服务器信息 */
private JsonNode serverInfo;
/** 是否已完成初始化 */
private volatile boolean initialized = false;
/**
* 创建 MCP 客户端。
*
* @param serverName 服务器标识名称
* @param transport 传输层实现
*/
public McpClient(String serverName, McpTransport transport) {
this.serverName = Objects.requireNonNull(serverName, "服务器名称不能为空");
this.transport = Objects.requireNonNull(transport, "传输层不能为空");
}
/**
* 初始化连接 —— MCP 协议握手流程。
* <p>
* 步骤:
* <ol>
* <li>发送 {@code initialize} 请求,声明客户端能力和协议版本</li>
* <li>解析服务器返回的能力信息</li>
* <li>发送 {@code notifications/initialized} 通知</li>
* <li>发现服务器提供的工具和资源</li>
* </ol>
*
* @throws McpException 初始化失败
*/
public void initialize() throws McpException {
log.info("正在初始化 MCP 服务器 '{}'...", serverName);
// 1. 发送 initialize 请求
int initId = nextId();
var initRequest = Map.of(
"jsonrpc", "2.0",
"id", initId,
"method", "initialize",
"params", Map.of(
"protocolVersion", "2024-11-05",
"capabilities", Map.of(),
"clientInfo", Map.of(
"name", "claude-code-java",
"version", "1.0.0"
)
)
);
JsonNode response;
try {
response = transport.sendRequest(MAPPER.writeValueAsString(initRequest));
} catch (Exception e) {
throw new McpException("MCP initialize 请求失败: " + e.getMessage(), e);
}
// 2. 解析服务器能力
JsonNode result = response.get("result");
if (result != null) {
serverCapabilities = result.get("capabilities");
serverInfo = result.get("serverInfo");
String serverVersion = result.has("protocolVersion")
? result.get("protocolVersion").asText() : "unknown";
log.info("MCP 服务器 '{}' 协议版本: {}", serverName, serverVersion);
if (serverInfo != null) {
log.info("MCP 服务器信息: {}", serverInfo);
}
}
// 3. 发送 initialized 通知
var initializedNotif = Map.of(
"jsonrpc", "2.0",
"method", "notifications/initialized"
);
try {
transport.sendNotification(MAPPER.writeValueAsString(initializedNotif));
} catch (Exception e) {
throw new McpException("发送 initialized 通知失败: " + e.getMessage(), e);
}
// 4. 发现工具
discoverTools();
// 5. 发现资源
discoverResources();
initialized = true;
log.info("MCP 服务器 '{}' 初始化完成: {} 个工具, {} 个资源",
serverName, tools.size(), resources.size());
}
/**
* 发现服务器提供的工具 —— 发送 {@code tools/list} 请求。
*/
private void discoverTools() throws McpException {
// 检查服务器是否支持 tools 能力
if (serverCapabilities != null
&& serverCapabilities.has("tools")
&& serverCapabilities.get("tools").isObject()) {
// 服务器声明支持工具
} else if (serverCapabilities != null && !serverCapabilities.has("tools")) {
log.debug("MCP 服务器 '{}' 未声明 tools 能力,尝试发现工具", serverName);
}
int id = nextId();
var request = Map.of(
"jsonrpc", "2.0",
"id", id,
"method", "tools/list",
"params", Map.of()
);
try {
JsonNode response = transport.sendRequest(MAPPER.writeValueAsString(request));
JsonNode result = response.get("result");
if (result != null && result.has("tools")) {
ArrayNode toolsArray = (ArrayNode) result.get("tools");
for (JsonNode toolNode : toolsArray) {
String name = toolNode.get("name").asText();
String description = toolNode.has("description")
? toolNode.get("description").asText() : "";
JsonNode inputSchema = toolNode.get("inputSchema");
tools.put(name, new McpTool(name, description, inputSchema));
log.debug("发现 MCP 工具: {} - {}", name, description);
}
}
} catch (McpException e) {
// tools/list 可能不被支持,记录警告但不中断初始化
if (e.isJsonRpcError() && e.getErrorCode() == -32601) {
log.debug("MCP 服务器 '{}' 不支持 tools/list", serverName);
} else {
log.warn("发现 MCP 工具失败: {}", e.getMessage());
}
} catch (Exception e) {
log.warn("发现 MCP 工具时序列化异常: {}", e.getMessage());
}
}
/**
* 发现服务器提供的资源 —— 发送 {@code resources/list} 请求。
*/
private void discoverResources() throws McpException {
// 检查服务器是否支持 resources 能力
if (serverCapabilities != null
&& serverCapabilities.has("resources")
&& serverCapabilities.get("resources").isObject()) {
// 服务器声明支持资源
} else if (serverCapabilities != null && !serverCapabilities.has("resources")) {
log.debug("MCP 服务器 '{}' 未声明 resources 能力,跳过资源发现", serverName);
return;
}
int id = nextId();
var request = Map.of(
"jsonrpc", "2.0",
"id", id,
"method", "resources/list",
"params", Map.of()
);
try {
JsonNode response = transport.sendRequest(MAPPER.writeValueAsString(request));
JsonNode result = response.get("result");
if (result != null && result.has("resources")) {
ArrayNode resourcesArray = (ArrayNode) result.get("resources");
for (JsonNode resNode : resourcesArray) {
String uri = resNode.get("uri").asText();
String name = resNode.has("name") ? resNode.get("name").asText() : uri;
String description = resNode.has("description")
? resNode.get("description").asText() : "";
String mimeType = resNode.has("mimeType")
? resNode.get("mimeType").asText() : "text/plain";
resources.put(uri, new McpResource(uri, name, description, mimeType));
log.debug("发现 MCP 资源: {} ({})", name, uri);
}
}
} catch (McpException e) {
if (e.isJsonRpcError() && e.getErrorCode() == -32601) {
log.debug("MCP 服务器 '{}' 不支持 resources/list", serverName);
} else {
log.warn("发现 MCP 资源失败: {}", e.getMessage());
}
} catch (Exception e) {
log.warn("发现 MCP 资源时序列化异常: {}", e.getMessage());
}
}
/**
* 调用 MCP 工具 —— 发送 {@code tools/call} 请求。
*
* @param toolName 工具名称
* @param arguments 工具参数(键值对)
* @return 工具执行结果文本
* @throws McpException 调用失败或工具不存在
*/
public String callTool(String toolName, Map<String, Object> arguments) throws McpException {
if (!initialized) {
throw new McpException("MCP 客户端尚未初始化");
}
if (!tools.containsKey(toolName)) {
throw new McpException("MCP 工具不存在: " + toolName);
}
int id = nextId();
var request = Map.of(
"jsonrpc", "2.0",
"id", id,
"method", "tools/call",
"params", Map.of(
"name", toolName,
"arguments", arguments != null ? arguments : Map.of()
)
);
try {
log.debug("调用 MCP 工具: {} (参数: {})", toolName, arguments);
JsonNode response = transport.sendRequest(MAPPER.writeValueAsString(request));
JsonNode result = response.get("result");
if (result == null) {
return "";
}
// MCP tools/call 返回 { content: [{ type: "text", text: "..." }, ...] }
if (result.has("content")) {
StringBuilder sb = new StringBuilder();
for (JsonNode contentItem : result.get("content")) {
String type = contentItem.has("type") ? contentItem.get("type").asText() : "text";
if ("text".equals(type) && contentItem.has("text")) {
if (!sb.isEmpty()) {
sb.append("\n");
}
sb.append(contentItem.get("text").asText());
}
}
// 检查 isError 标志
if (result.has("isError") && result.get("isError").asBoolean()) {
throw new McpException("MCP 工具 '" + toolName + "' 执行出错: " + sb);
}
return sb.toString();
}
// 兜底:直接返回 result 的文本形式
return result.toString();
} catch (McpException e) {
throw e;
} catch (Exception e) {
throw new McpException("调用 MCP 工具 '" + toolName + "' 失败: " + e.getMessage(), e);
}
}
/**
* 读取 MCP 资源 —— 发送 {@code resources/read} 请求。
*
* @param uri 资源 URI
* @return 资源内容文本
* @throws McpException 读取失败或资源不存在
*/
public String readResource(String uri) throws McpException {
if (!initialized) {
throw new McpException("MCP 客户端尚未初始化");
}
int id = nextId();
var request = Map.of(
"jsonrpc", "2.0",
"id", id,
"method", "resources/read",
"params", Map.of("uri", uri)
);
try {
log.debug("读取 MCP 资源: {}", uri);
JsonNode response = transport.sendRequest(MAPPER.writeValueAsString(request));
JsonNode result = response.get("result");
if (result == null) {
return "";
}
// MCP resources/read 返回 { contents: [{ uri, text/blob }] }
if (result.has("contents")) {
StringBuilder sb = new StringBuilder();
for (JsonNode contentItem : result.get("contents")) {
if (contentItem.has("text")) {
if (!sb.isEmpty()) {
sb.append("\n");
}
sb.append(contentItem.get("text").asText());
}
}
return sb.toString();
}
return result.toString();
} catch (McpException e) {
throw e;
} catch (Exception e) {
throw new McpException("读取 MCP 资源 '" + uri + "' 失败: " + e.getMessage(), e);
}
}
/**
* 获取所有已发现的工具(不可变视图)。
*/
public Collection<McpTool> getTools() {
return Collections.unmodifiableCollection(tools.values());
}
/**
* 获取所有已发现的资源(不可变视图)。
*/
public Collection<McpResource> getResources() {
return Collections.unmodifiableCollection(resources.values());
}
/**
* 按名称查找工具。
*
* @param toolName 工具名称
* @return 工具定义,若不存在则返回 {@link Optional#empty()}
*/
public Optional<McpTool> findTool(String toolName) {
return Optional.ofNullable(tools.get(toolName));
}
/** 获取服务器名称标识 */
public String getServerName() {
return serverName;
}
/** 是否已完成初始化 */
public boolean isInitialized() {
return initialized;
}
/** 传输层是否仍然连接 */
public boolean isConnected() {
return transport.isConnected();
}
/** 获取服务器能力信息 */
public JsonNode getServerCapabilities() {
return serverCapabilities;
}
/** 获取服务器信息 */
public JsonNode getServerInfo() {
return serverInfo;
}
@Override
public void close() throws Exception {
initialized = false;
transport.close();
log.info("MCP 客户端 '{}' 已关闭", serverName);
}
/** 生成下一个 JSON-RPC 请求 ID */
private int nextId() {
return idCounter.getAndIncrement();
}
// ========== 内部记录类型 ==========
/**
* MCP 工具定义 —— 服务器暴露的可调用工具。
*
* @param name 工具名称
* @param description 工具描述
* @param inputSchema 输入参数的 JSON Schema
*/
public record McpTool(String name, String description, JsonNode inputSchema) {
}
/**
* MCP 资源定义 —— 服务器暴露的可读取资源。
*
* @param uri 资源 URI
* @param name 资源名称
* @param description 资源描述
* @param mimeType MIME 类型
*/
public record McpResource(String uri, String name, String description, String mimeType) {
}
}
@@ -0,0 +1,46 @@
package com.claudecode.mcp;
/**
* MCP 相关异常 —— 统一封装 MCP 通信、协议解析和工具调用中的错误。
*/
public class McpException extends Exception {
/** JSON-RPC 错误码(若源自 JSON-RPC error 响应) */
private final int errorCode;
public McpException(String message) {
super(message);
this.errorCode = -1;
}
public McpException(String message, Throwable cause) {
super(message, cause);
this.errorCode = -1;
}
public McpException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
public McpException(String message, int errorCode, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
/**
* 获取 JSON-RPC 错误码。
*
* @return 错误码,若非 JSON-RPC 错误则返回 {@code -1}
*/
public int getErrorCode() {
return errorCode;
}
/**
* 是否为 JSON-RPC 协议级错误。
*/
public boolean isJsonRpcError() {
return errorCode != -1;
}
}
@@ -0,0 +1,409 @@
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;
import java.util.stream.Collectors;
/**
* MCP 管理器 —— 管理多个 MCP 服务器连接的统一入口。
* <p>
* 职责:
* <ul>
* <li>从配置文件加载 MCP 服务器定义</li>
* <li>管理服务器连接的生命周期(连接、断开、重连)</li>
* <li>聚合所有服务器的工具和资源供上层使用</li>
* <li>路由工具调用到正确的服务器</li>
* </ul>
* <p>
* 配置文件格式({@code mcp.json}):
* <pre>{@code
* {
* "servers": {
* "server-name": {
* "command": "npx",
* "args": ["-y", "@modelcontextprotocol/server-filesystem"],
* "env": { "KEY": "VALUE" }
* }
* }
* }
* }</pre>
*
* @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<String, McpClient> clients = new ConcurrentHashMap<>();
/** 工具名称到服务器名称的映射:toolName -> serverName(用于路由调用) */
private final Map<String, String> toolToServer = new ConcurrentHashMap<>();
/**
* 从配置文件加载并连接所有 MCP 服务器。
* <p>
* 优先级:
* <ol>
* <li>项目级配置文件:当前工作目录下的 {@code .mcp.json}</li>
* <li>全局配置文件:{@code ~/.claude-code-java/mcp.json}</li>
* </ol>
* 两个配置文件中的服务器会合并加载。
*/
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<Map.Entry<String, JsonNode>> fields = serversNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> 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<String> args = new ArrayList<>();
if (serverDef.has("args") && serverDef.get("args").isArray()) {
for (JsonNode arg : serverDef.get("args")) {
args.add(arg.asText());
}
}
Map<String, String> env = new HashMap<>();
if (serverDef.has("env") && serverDef.get("env").isObject()) {
Iterator<Map.Entry<String, JsonNode>> envFields = serverDef.get("env").fields();
while (envFields.hasNext()) {
Map.Entry<String, JsonNode> 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<String> args, Map<String, String> 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);
transport.start();
// 创建客户端并初始化
McpClient client = new McpClient(name, transport);
client.initialize();
// 注册客户端
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<String, McpClient> getClients() {
return Collections.unmodifiableMap(clients);
}
/**
* 获取指定服务器的客户端。
*
* @param name 服务器名称
* @return 客户端实例,若不存在则返回 {@link Optional#empty()}
*/
public Optional<McpClient> getClient(String name) {
return Optional.ofNullable(clients.get(name));
}
/**
* 获取所有 MCP 工具(合并所有服务器的工具)。
*
* @return 所有已发现的工具列表
*/
public List<McpClient.McpTool> getAllTools() {
return clients.values().stream()
.filter(McpClient::isInitialized)
.flatMap(client -> client.getTools().stream())
.collect(Collectors.toList());
}
/**
* 获取指定服务器的工具。
*
* @param serverName 服务器名称
* @return 工具列表,若服务器不存在则返回空列表
*/
public List<McpClient.McpTool> 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<McpClient.McpResource> getAllResources() {
return clients.values().stream()
.filter(McpClient::isInitialized)
.flatMap(client -> client.getResources().stream())
.collect(Collectors.toList());
}
/**
* 获取指定服务器的资源。
*
* @param serverName 服务器名称
* @return 资源列表,若服务器不存在则返回空列表
*/
public List<McpClient.McpResource> 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<String, Object> 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<String, Object> 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<String> findServerForTool(String toolName) {
return Optional.ofNullable(toolToServer.get(toolName));
}
/**
* 重新加载配置文件并重连所有服务器。
* <p>
* 先断开所有已有连接,再重新加载配置。
*/
public void reload() {
log.info("重新加载 MCP 配置...");
// 断开所有现有连接
List<String> 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<String, McpClient> 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<Exception> errors = new ArrayList<>();
for (Map.Entry<String, McpClient> 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 连接已关闭");
}
}
@@ -0,0 +1,43 @@
package com.claudecode.mcp;
import com.fasterxml.jackson.databind.JsonNode;
/**
* MCP 传输层接口 —— 抽象不同的通信方式(StdIO / SSE / HTTP)。
* <p>
* MCP 协议底层基于 JSON-RPC 2.0,传输层负责将 JSON-RPC 消息
* 发送给 MCP 服务器并接收响应。不同传输实现(如 StdIO 子进程、
* HTTP+SSE 远程连接)均通过此接口统一暴露。
*
* @see StdioTransport
*/
public interface McpTransport extends AutoCloseable {
/**
* 发送 JSON-RPC 请求并等待响应。
* <p>
* 实现应根据请求中的 {@code id} 字段将响应与请求关联。
*
* @param jsonRpcRequest 完整的 JSON-RPC 2.0 请求字符串
* @return 服务器返回的 JSON-RPC 响应节点
* @throws McpException 通信异常或超时
*/
JsonNode sendRequest(String jsonRpcRequest) throws McpException;
/**
* 发送 JSON-RPC 通知(无需响应)。
* <p>
* 通知消息不包含 {@code id} 字段,服务器无需回复。
*
* @param jsonRpcNotification 完整的 JSON-RPC 2.0 通知字符串
* @throws McpException 通信异常
*/
void sendNotification(String jsonRpcNotification) throws McpException;
/**
* 传输层是否已连接且可用。
*
* @return 如果底层连接仍然活跃则返回 {@code true}
*/
boolean isConnected();
}
@@ -0,0 +1,324 @@
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.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
* 基于 StdIO 的 MCP 传输实现 —— 通过子进程的 stdin/stdout 进行 JSON-RPC 通信。
* <p>
* 工作原理:
* <ol>
* <li>启动外部 MCP 服务器进程(如 {@code npx -y @modelcontextprotocol/server-filesystem}</li>
* <li>通过进程的 stdin 发送 JSON-RPC 消息(每行一条 JSON</li>
* <li>通过独立读线程从 stdout 异步读取响应</li>
* <li>使用 {@link CompletableFuture} 按 {@code id} 字段进行请求-响应关联</li>
* </ol>
* <p>
* 消息分隔:每条 JSON-RPC 消息占一行,以 {@code \n} 分隔。
*/
public class StdioTransport implements McpTransport {
private static final Logger log = LoggerFactory.getLogger(StdioTransport.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
/** 请求默认超时时间(秒) */
private static final int DEFAULT_TIMEOUT_SECONDS = 30;
/** MCP 服务器子进程 */
private Process process;
/** 子进程 stdin 写入流 */
private BufferedWriter processStdin;
/** 异步读取线程 */
private Thread readerThread;
/** 待匹配的请求:id -> CompletableFuture */
private final ConcurrentHashMap<Object, CompletableFuture<JsonNode>> pendingRequests =
new ConcurrentHashMap<>();
/** 启动命令 */
private final String command;
/** 启动参数 */
private final List<String> args;
/** 环境变量(可选) */
private final Map<String, String> env;
/** 连接状态 */
private volatile boolean connected = false;
/**
* 创建 StdIO 传输实例。
*
* @param command 服务器可执行命令(如 "npx"
* @param args 命令参数列表
* @param env 额外的环境变量,可为 {@code null}
*/
public StdioTransport(String command, List<String> args, Map<String, String> env) {
this.command = command;
this.args = args != null ? List.copyOf(args) : List.of();
this.env = env != null ? Map.copyOf(env) : Map.of();
}
/**
* 启动 MCP 服务器子进程并开始监听 stdout。
*
* @throws McpException 进程启动失败
*/
public void start() throws McpException {
try {
// 构建进程命令行
var cmdList = new java.util.ArrayList<String>();
cmdList.add(command);
cmdList.addAll(args);
log.info("启动 MCP 服务器进程: {}", String.join(" ", cmdList));
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.redirectErrorStream(false); // stderr 单独处理
// 设置环境变量
if (!env.isEmpty()) {
pb.environment().putAll(env);
}
process = pb.start();
// 初始化 stdin 写入器
processStdin = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
// 启动 stdout 异步读取线程
readerThread = Thread.ofVirtual().name("mcp-stdio-reader").start(this::readLoop);
// 启动 stderr 日志线程(仅记录日志,不参与协议通信)
Thread.ofVirtual().name("mcp-stdio-stderr").start(this::stderrLoop);
connected = true;
log.info("MCP 服务器进程已启动 (PID: {})", process.pid());
} catch (IOException e) {
throw new McpException("启动 MCP 服务器进程失败: " + e.getMessage(), e);
}
}
/**
* stdout 读取循环 —— 逐行读取 JSON-RPC 响应并分发到对应的 Future。
*/
private void readLoop() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
try {
JsonNode message = MAPPER.readTree(line);
handleMessage(message);
} catch (Exception e) {
log.warn("解析 MCP 响应失败: {}", line, e);
}
}
} catch (IOException e) {
if (connected) {
log.warn("MCP stdout 读取中断: {}", e.getMessage());
}
} finally {
connected = false;
// 清理所有等待中的请求
pendingRequests.forEach((id, future) ->
future.completeExceptionally(new McpException("MCP 连接已断开")));
pendingRequests.clear();
}
}
/**
* stderr 读取循环 —— 将服务器 stderr 输出记录为日志。
*/
private void stderrLoop() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
log.debug("[MCP stderr] {}", line);
}
} catch (IOException e) {
// stderr 读取结束,忽略
}
}
/**
* 处理从 stdout 读取的 JSON-RPC 消息。
* <p>
* 若消息包含 {@code id} 字段,则匹配到对应的待处理请求;
* 若消息为通知(无 {@code id}),则记录日志。
*/
private void handleMessage(JsonNode message) {
// 检查是否有 id 字段(响应消息)
JsonNode idNode = message.get("id");
if (idNode != null && !idNode.isNull()) {
Object id;
if (idNode.isNumber()) {
id = idNode.asInt();
} else {
id = idNode.asText();
}
CompletableFuture<JsonNode> future = pendingRequests.remove(id);
if (future != null) {
future.complete(message);
} else {
log.warn("收到未匹配的 MCP 响应 (id={}): {}", id, message);
}
} else {
// 服务器主动通知(如 notifications/tools/list_changed
String method = message.has("method") ? message.get("method").asText() : "unknown";
log.debug("收到 MCP 服务器通知: {}", method);
}
}
@Override
public JsonNode sendRequest(String jsonRpcRequest) throws McpException {
if (!connected) {
throw new McpException("MCP 传输层未连接");
}
try {
// 解析出请求 id
JsonNode requestNode = MAPPER.readTree(jsonRpcRequest);
JsonNode idNode = requestNode.get("id");
if (idNode == null || idNode.isNull()) {
throw new McpException("JSON-RPC 请求缺少 id 字段");
}
Object id;
if (idNode.isNumber()) {
id = idNode.asInt();
} else {
id = idNode.asText();
}
// 注册 Future
CompletableFuture<JsonNode> future = new CompletableFuture<>();
pendingRequests.put(id, future);
// 写入 stdin(一行 JSON + 换行符)
synchronized (processStdin) {
processStdin.write(jsonRpcRequest);
processStdin.newLine();
processStdin.flush();
}
log.debug("发送 MCP 请求 (id={}): {}", id, truncate(jsonRpcRequest, 200));
// 等待响应
JsonNode response = future.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
// 检查 JSON-RPC error
JsonNode errorNode = response.get("error");
if (errorNode != null && !errorNode.isNull()) {
int code = errorNode.has("code") ? errorNode.get("code").asInt() : -1;
String msg = errorNode.has("message") ? errorNode.get("message").asText() : "未知错误";
throw new McpException("MCP 服务器返回错误: " + msg, code);
}
return response;
} catch (McpException e) {
throw e;
} catch (TimeoutException e) {
throw new McpException("MCP 请求超时 (" + DEFAULT_TIMEOUT_SECONDS + "s)", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof McpException mcp) {
throw mcp;
}
throw new McpException("MCP 请求执行异常: " + cause.getMessage(), cause);
} catch (Exception e) {
throw new McpException("MCP 请求发送失败: " + e.getMessage(), e);
}
}
@Override
public void sendNotification(String jsonRpcNotification) throws McpException {
if (!connected) {
throw new McpException("MCP 传输层未连接");
}
try {
synchronized (processStdin) {
processStdin.write(jsonRpcNotification);
processStdin.newLine();
processStdin.flush();
}
log.debug("发送 MCP 通知: {}", truncate(jsonRpcNotification, 200));
} catch (IOException e) {
throw new McpException("MCP 通知发送失败: " + e.getMessage(), e);
}
}
@Override
public boolean isConnected() {
return connected && process != null && process.isAlive();
}
@Override
public void close() throws Exception {
connected = false;
log.info("关闭 MCP StdIO 传输...");
// 关闭 stdin(通知服务器退出)
if (processStdin != null) {
try {
processStdin.close();
} catch (IOException e) {
log.debug("关闭 stdin 时异常: {}", e.getMessage());
}
}
// 等待进程退出
if (process != null && process.isAlive()) {
boolean exited = process.waitFor(5, TimeUnit.SECONDS);
if (!exited) {
log.warn("MCP 服务器进程未在 5s 内退出,强制终止");
process.destroyForcibly();
process.waitFor(3, TimeUnit.SECONDS);
}
}
// 中断读取线程
if (readerThread != null && readerThread.isAlive()) {
readerThread.interrupt();
}
// 清理待处理请求
pendingRequests.forEach((id, future) ->
future.completeExceptionally(new McpException("MCP 传输已关闭")));
pendingRequests.clear();
log.info("MCP StdIO 传输已关闭");
}
/**
* 截断字符串用于日志输出。
*/
private static String truncate(String s, int maxLen) {
if (s == null) return "null";
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
}
}