docs: 全面更新文档 - README/需求文档/修改记录
README.md: - 功能特性分 P0/P1/P2 三级展示 - 工具列表更新为 18 个(含 P0/P2 工具) - 命令列表更新为 28 个(分基础/P0/P1/P2 四组) - 新增 MCP 服务器配置和使用说明 - 新增插件系统使用说明(JAR 加载、目录结构) - 新增 Vim 模式和 Hook 系统使用说明 - 架构图更新:含 mcp/、plugin/、core/ 新组件 - 核心流程图更新:含 Hook 和权限确认 - 对应关系表新增 MCP/Plugin/Hook/Task 映射 需求文档.md: - 5.1 实现清单更新为 18 工具 + 28 命令 - 5.2 P0/P1/P2 标记为全部已实现 - 仅保留 P3 跳过项目 修改记录.md: - 新增 P0/P1/P2 四条修改记录(倒序排列) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,6 @@ 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;
|
||||
|
||||
@@ -47,10 +46,10 @@ public class McpClient implements AutoCloseable {
|
||||
private final Map<String, McpResource> resources = new ConcurrentHashMap<>();
|
||||
|
||||
/** 服务器能力信息 */
|
||||
private JsonNode serverCapabilities;
|
||||
private volatile JsonNode serverCapabilities;
|
||||
|
||||
/** 服务器信息 */
|
||||
private JsonNode serverInfo;
|
||||
private volatile JsonNode serverInfo;
|
||||
|
||||
/** 是否已完成初始化 */
|
||||
private volatile boolean initialized = false;
|
||||
@@ -165,15 +164,17 @@ public class McpClient implements AutoCloseable {
|
||||
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");
|
||||
JsonNode toolsNode = result.get("tools");
|
||||
if (toolsNode != null && toolsNode.isArray()) {
|
||||
for (JsonNode toolNode : toolsNode) {
|
||||
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);
|
||||
tools.put(name, new McpTool(name, description, inputSchema));
|
||||
log.debug("发现 MCP 工具: {} - {}", name, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (McpException e) {
|
||||
@@ -214,17 +215,19 @@ public class McpClient implements AutoCloseable {
|
||||
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";
|
||||
JsonNode resourcesNode = result.get("resources");
|
||||
if (resourcesNode != null && resourcesNode.isArray()) {
|
||||
for (JsonNode resNode : resourcesNode) {
|
||||
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);
|
||||
resources.put(uri, new McpResource(uri, name, description, mimeType));
|
||||
log.debug("发现 MCP 资源: {} ({})", name, uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (McpException e) {
|
||||
@@ -409,6 +412,8 @@ public class McpClient implements AutoCloseable {
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
initialized = false;
|
||||
tools.clear();
|
||||
resources.clear();
|
||||
transport.close();
|
||||
log.info("MCP 客户端 '{}' 已关闭", serverName);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ 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 服务器连接的统一入口。
|
||||
@@ -165,13 +164,23 @@ public class McpManager implements AutoCloseable {
|
||||
|
||||
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();
|
||||
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);
|
||||
@@ -239,7 +248,7 @@ public class McpManager implements AutoCloseable {
|
||||
return clients.values().stream()
|
||||
.filter(McpClient::isInitialized)
|
||||
.flatMap(client -> client.getTools().stream())
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,7 +274,7 @@ public class McpManager implements AutoCloseable {
|
||||
return clients.values().stream()
|
||||
.filter(McpClient::isInitialized)
|
||||
.flatMap(client -> client.getResources().stream())
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,11 +38,14 @@ public class StdioTransport implements McpTransport {
|
||||
/** 子进程 stdin 写入流 */
|
||||
private BufferedWriter processStdin;
|
||||
|
||||
/** 异步读取线程 */
|
||||
/** 异步读取线程(stdout) */
|
||||
private Thread readerThread;
|
||||
|
||||
/** 待匹配的请求:id -> CompletableFuture */
|
||||
private final ConcurrentHashMap<Object, CompletableFuture<JsonNode>> pendingRequests =
|
||||
/** 异步读取线程(stderr) */
|
||||
private Thread stderrThread;
|
||||
|
||||
/** 待匹配的请求:id(String) -> CompletableFuture */
|
||||
private final ConcurrentHashMap<String, CompletableFuture<JsonNode>> pendingRequests =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/** 启动命令 */
|
||||
@@ -102,7 +105,7 @@ public class StdioTransport implements McpTransport {
|
||||
readerThread = Thread.ofVirtual().name("mcp-stdio-reader").start(this::readLoop);
|
||||
|
||||
// 启动 stderr 日志线程(仅记录日志,不参与协议通信)
|
||||
Thread.ofVirtual().name("mcp-stdio-stderr").start(this::stderrLoop);
|
||||
stderrThread = Thread.ofVirtual().name("mcp-stdio-stderr").start(this::stderrLoop);
|
||||
|
||||
connected = true;
|
||||
log.info("MCP 服务器进程已启动 (PID: {})", process.pid());
|
||||
@@ -172,12 +175,8 @@ public class StdioTransport implements McpTransport {
|
||||
// 检查是否有 id 字段(响应消息)
|
||||
JsonNode idNode = message.get("id");
|
||||
if (idNode != null && !idNode.isNull()) {
|
||||
Object id;
|
||||
if (idNode.isNumber()) {
|
||||
id = idNode.asInt();
|
||||
} else {
|
||||
id = idNode.asText();
|
||||
}
|
||||
// 统一转为 String,避免 Integer/String 类型不匹配导致的查找失败
|
||||
String id = idNode.asText();
|
||||
|
||||
CompletableFuture<JsonNode> future = pendingRequests.remove(id);
|
||||
if (future != null) {
|
||||
@@ -198,20 +197,15 @@ public class StdioTransport implements McpTransport {
|
||||
throw new McpException("MCP 传输层未连接");
|
||||
}
|
||||
|
||||
String id = null;
|
||||
try {
|
||||
// 解析出请求 id
|
||||
// 解析出请求 id(统一转为 String)
|
||||
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();
|
||||
}
|
||||
id = idNode.asText();
|
||||
|
||||
// 注册 Future
|
||||
CompletableFuture<JsonNode> future = new CompletableFuture<>();
|
||||
@@ -251,6 +245,11 @@ public class StdioTransport implements McpTransport {
|
||||
throw new McpException("MCP 请求执行异常: " + cause.getMessage(), cause);
|
||||
} catch (Exception e) {
|
||||
throw new McpException("MCP 请求发送失败: " + e.getMessage(), e);
|
||||
} finally {
|
||||
// 无论成功、超时或异常,都清理待处理请求,防止内存泄漏
|
||||
if (id != null) {
|
||||
pendingRequests.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +304,9 @@ public class StdioTransport implements McpTransport {
|
||||
if (readerThread != null && readerThread.isAlive()) {
|
||||
readerThread.interrupt();
|
||||
}
|
||||
if (stderrThread != null && stderrThread.isAlive()) {
|
||||
stderrThread.interrupt();
|
||||
}
|
||||
|
||||
// 清理待处理请求
|
||||
pendingRequests.forEach((id, future) ->
|
||||
|
||||
Reference in New Issue
Block a user