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:
liuzh
2026-04-01 23:04:01 +08:00
co-authored by Copilot
parent 79736cf16f
commit 1fc7a6a495
6 changed files with 411 additions and 112 deletions
+26 -21
View File
@@ -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) ->