McpManager 与 McpToolAdapter
这两个类构成了 MCP 集成的最上层——McpManager 负责编排整个 MCP 子系统的生命周期,McpToolAdapter 负责将远程工具"伪装"成本地工具。
McpManager — MCP 子系统的总指挥
📄 claude-code-java/src/main/java/com/claudecode/mcp/McpManager.java
核心职责
McpManager 是 MCP 子系统的入口,负责三件事:
- 连接:为每个配置的 MCP Server 创建 Transport 和 Client
- 发现:通过
tools/list获取每个 Server 暴露的工具 - 注册:为每个工具创建 McpToolAdapter 并注册到 ToolRegistry
类结构
public class McpManager implements Closeable {
private final ObjectMapper mapper;
private final Map<String, McpClient> clients = new LinkedHashMap<>();
// 入口方法:初始化所有 MCP Server 并注册工具
public void initializeAndRegister(Map<String, McpServerConfig> configs,
ToolRegistry registry);
// 资源清理:关闭所有 Client(销毁子进程)
public void close() throws IOException;
}initializeAndRegister — 启动流程
这是整个 MCP 子系统的启动入口,在 ClaudeCode.main() 中被调用:
public void initializeAndRegister(Map<String, McpServerConfig> configs,
ToolRegistry registry) {
if (configs == null || configs.isEmpty()) {
return; // 没配置 MCP Server,直接返回
}
for (Map.Entry<String, McpServerConfig> entry : configs.entrySet()) {
String serverName = entry.getKey();
McpServerConfig config = entry.getValue();
try {
connectAndRegister(serverName, config, registry);
} catch (Exception e) {
// ⚡ 单个 Server 失败不影响其他 Server
System.err.println("[MCP] Failed to connect to server '"
+ serverName + "': " + e.getMessage());
}
}
}容错设计:每个 Server 的连接都在独立的 try-catch 中。如果配置了 3 个 Server,其中 1 个的 command 写错了,另外 2 个照常连接并注册工具。
connectAndRegister — 单个 Server 的连接流程
private void connectAndRegister(String serverName, McpServerConfig config,
ToolRegistry registry) throws IOException {
// 1. 创建 Transport(启动子进程的准备)
StdioTransport transport = new StdioTransport(
serverName, config.getCommand(), config.getArgs(),
config.getEnv(), mapper);
// 2. 创建 Client 并握手
McpClient client = new McpClient(serverName, transport, mapper);
client.initialize(); // 启动子进程 + initialize 握手
clients.put(serverName, client); // 记录连接
// 3. 发现工具
List<McpToolDefinition> tools = client.listTools();
// 4. 为每个工具创建适配器并注册
for (McpToolDefinition toolDef : tools) {
McpToolAdapter adapter = new McpToolAdapter(
serverName,
toolDef.getName(),
toolDef.getDescription(),
toolDef.getInputSchema(),
client);
try {
registry.register(adapter);
} catch (IllegalArgumentException e) {
// 工具名重复,跳过
System.err.println("[MCP] Skipping duplicate tool: " + adapter.name());
}
}
}四步流程,每一步都可能抛异常,整体在上层 catch 处理。
close — 资源清理
@Override
public void close() throws IOException {
for (Map.Entry<String, McpClient> entry : clients.entrySet()) {
try {
entry.getValue().close(); // 关闭每个 Client(销毁子进程)
} catch (IOException e) {
System.err.println("[MCP] Error closing server '"
+ entry.getKey() + "': " + e.getMessage());
}
}
clients.clear();
}关闭也是容错的——一个 Client 关闭失败不影响关闭其他 Client。
在 ClaudeCode.main() 中的集成点
// ClaudeCode.java 中的 MCP 初始化代码
// 1. 注册内置工具
ToolRegistry toolRegistry = new ToolRegistry(workingDirectory);
toolRegistry.registerBuiltinTools();
// 2. 加载 MCP 配置
ObjectMapper objectMapper = new ObjectMapper();
McpConfigLoader configLoader = new McpConfigLoader(objectMapper);
Map<String, McpServerConfig> mcpConfigs = configLoader.load(workingDirectory);
// 3. 连接 MCP Server 并注册工具
McpManager mcpManager = new McpManager(objectMapper);
if (!mcpConfigs.isEmpty()) {
mcpManager.initializeAndRegister(mcpConfigs, toolRegistry);
}
// 4. 创建 AgentLoop(此时 ToolRegistry 已包含内置 + MCP 工具)
AgentLoop agentLoop = new AgentLoop(apiClient, toolRegistry, permissionManager, systemPrompt);
// 5. 注册 ShutdownHook 清理 MCP 子进程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { mcpManager.close(); } catch (IOException ignored) {}
}));这段代码体现了一个重要原则:MCP 是可选的。如果 settings.json 中没有 mcpServers 配置,mcpConfigs 为空,MCP 子系统完全不启动,不影响内置工具的正常使用。
McpToolAdapter — 远程工具的本地外衣
📄 claude-code-java/src/main/java/com/claudecode/mcp/McpToolAdapter.java
设计思路
McpToolAdapter 是经典的适配器模式:
MCP Server 暴露的工具 McpToolAdapter Tool 接口
┌───────────────────┐ ┌─────────────────┐ ┌──────────┐
│ name: "query-docs"│ wrap │ Tool 接口实现 │ impl │ name() │
│ desc: "..." │ ───────→ │ execute() 内部 │ ─────→ │ execute()│
│ schema: {...} │ │ 调 McpClient. │ │ ... │
│ │ │ callTool() │ │ │
└───────────────────┘ └─────────────────┘ └──────────┘命名规范
private static final String SEPARATOR = "__";
private static final String PREFIX = "mcp";
// 构造函数中:
this.qualifiedName = PREFIX + SEPARATOR + serverName + SEPARATOR + originalToolName;
// 例如:mcp__context7__query-docs这个命名格式与 Claude Code 官方保持一致。三段式命名确保:
mcp__前缀不会和内置工具(Bash、Read等)冲突serverName区分不同 Server 的同名工具
Tool 接口的五个方法实现
@Override
public String name() {
return qualifiedName; // "mcp__context7__query-docs"
}
@Override
public String description() {
return description; // 直接透传 MCP Server 提供的描述
}
@Override
public Map<String, Object> inputSchema() {
return inputSchema; // 直接透传 MCP Server 提供的 JSON Schema
}
@Override
public boolean requiresPermission() {
return true; // 外部工具一律需要用户确认
}前四个方法都是简单的属性返回。关键在第五个:
execute — 核心桥接逻辑
@Override
public ToolResult execute(Map<String, Object> input) {
try {
// 通过 McpClient 发送 JSON-RPC tools/call 请求
McpToolCallResult result = mcpClient.callTool(originalToolName, input);
// 转换结果类型
return result.isError()
? ToolResult.error(result.getText())
: ToolResult.success(result.getText());
} catch (Exception e) {
return ToolResult.error("MCP tool '" + qualifiedName + "' failed: " + e.getMessage());
}
}这个方法做了三件事:
- 委托调用:把
input参数原封不动传给McpClient.callTool() - 结果转换:把
McpToolCallResult转为ToolResult(两个不同的类型体系) - 异常兜底:网络超时、子进程崩溃等异常都被 catch 并转为 error 结果
注意这里用的是 originalToolName(如 query-docs)而不是 qualifiedName(如 mcp__context7__query-docs)。MCP Server 只认识自己原始的工具名。
数据流全景
LLM 返回: tool_use { name: "mcp__context7__query-docs", input: {...} }
↓
AgentLoop → ToolRegistry.execute("mcp__context7__query-docs", input)
↓
ToolRegistry → McpToolAdapter.execute(input) ← 查到适配器
↓
McpToolAdapter → McpClient.callTool("query-docs", input) ← 用原始名
↓
McpClient → StdioTransport.send(JSON-RPC request)
↓
StdioTransport → 子进程 stdin → MCP Server 处理 → stdout → JsonRpcResponse
↓
McpClient → parseToolCallResult() → McpToolCallResult
↓
McpToolAdapter → ToolResult.success/error(text)
↓
ToolRegistry → AgentLoop → 追加到对话历史 → 继续调用 API思考题
- 如果 MCP Server 返回的工具
description写得很差(比如空字符串),对 LLM 选择工具会有什么影响?能否在McpToolAdapter层面做优化? McpToolAdapter.execute()中为什么要 catchException而不是只 catchIOException?- 如果需要实现 MCP 工具的"只读"判定(某些 MCP 工具不需要权限审批),你会怎么设计
requiresPermission()的逻辑?
下一步
接下来了解底层通信的实现:McpClient 与 StdioTransport。