mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
- Added new `SdkMcpController` to manage communication between CLI MCP clients and SDK MCP servers. - Introduced `createSdkMcpServer` function for creating SDK-embedded MCP servers. - Updated configuration options to support both external and SDK MCP servers. - Enhanced timeout settings for various SDK operations, including MCP requests. - Refactored existing control request handling to accommodate new SDK MCP server functionality. - Updated tests to cover new SDK MCP server features and ensure proper integration.
141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type { Config, MCPServerConfig } from '../config/config.js';
|
|
import { isSdkMcpServerConfig } from '../config/config.js';
|
|
import type { ToolRegistry } from './tool-registry.js';
|
|
import type { PromptRegistry } from '../prompts/prompt-registry.js';
|
|
import {
|
|
McpClient,
|
|
MCPDiscoveryState,
|
|
populateMcpServerCommand,
|
|
} from './mcp-client.js';
|
|
import type { SendSdkMcpMessage } from './mcp-client.js';
|
|
import { getErrorMessage } from '../utils/errors.js';
|
|
import type { EventEmitter } from 'node:events';
|
|
import type { WorkspaceContext } from '../utils/workspaceContext.js';
|
|
|
|
/**
|
|
* Manages the lifecycle of multiple MCP clients, including local child processes.
|
|
* This class is responsible for starting, stopping, and discovering tools from
|
|
* a collection of MCP servers defined in the configuration.
|
|
*/
|
|
export class McpClientManager {
|
|
private clients: Map<string, McpClient> = new Map();
|
|
private readonly mcpServers: Record<string, MCPServerConfig>;
|
|
private readonly mcpServerCommand: string | undefined;
|
|
private readonly toolRegistry: ToolRegistry;
|
|
private readonly promptRegistry: PromptRegistry;
|
|
private readonly debugMode: boolean;
|
|
private readonly workspaceContext: WorkspaceContext;
|
|
private discoveryState: MCPDiscoveryState = MCPDiscoveryState.NOT_STARTED;
|
|
private readonly eventEmitter?: EventEmitter;
|
|
private readonly sendSdkMcpMessage?: SendSdkMcpMessage;
|
|
|
|
constructor(
|
|
mcpServers: Record<string, MCPServerConfig>,
|
|
mcpServerCommand: string | undefined,
|
|
toolRegistry: ToolRegistry,
|
|
promptRegistry: PromptRegistry,
|
|
debugMode: boolean,
|
|
workspaceContext: WorkspaceContext,
|
|
eventEmitter?: EventEmitter,
|
|
sendSdkMcpMessage?: SendSdkMcpMessage,
|
|
) {
|
|
this.mcpServers = mcpServers;
|
|
this.mcpServerCommand = mcpServerCommand;
|
|
this.toolRegistry = toolRegistry;
|
|
this.promptRegistry = promptRegistry;
|
|
this.debugMode = debugMode;
|
|
this.workspaceContext = workspaceContext;
|
|
this.eventEmitter = eventEmitter;
|
|
this.sendSdkMcpMessage = sendSdkMcpMessage;
|
|
}
|
|
|
|
/**
|
|
* Initiates the tool discovery process for all configured MCP servers.
|
|
* It connects to each server, discovers its available tools, and registers
|
|
* them with the `ToolRegistry`.
|
|
*/
|
|
async discoverAllMcpTools(cliConfig: Config): Promise<void> {
|
|
if (!cliConfig.isTrustedFolder()) {
|
|
return;
|
|
}
|
|
await this.stop();
|
|
|
|
const servers = populateMcpServerCommand(
|
|
this.mcpServers,
|
|
this.mcpServerCommand,
|
|
);
|
|
|
|
this.discoveryState = MCPDiscoveryState.IN_PROGRESS;
|
|
|
|
this.eventEmitter?.emit('mcp-client-update', this.clients);
|
|
const discoveryPromises = Object.entries(servers).map(
|
|
async ([name, config]) => {
|
|
// For SDK MCP servers, pass the sendSdkMcpMessage callback
|
|
const sdkCallback = isSdkMcpServerConfig(config)
|
|
? this.sendSdkMcpMessage
|
|
: undefined;
|
|
|
|
const client = new McpClient(
|
|
name,
|
|
config,
|
|
this.toolRegistry,
|
|
this.promptRegistry,
|
|
this.workspaceContext,
|
|
this.debugMode,
|
|
sdkCallback,
|
|
);
|
|
this.clients.set(name, client);
|
|
|
|
this.eventEmitter?.emit('mcp-client-update', this.clients);
|
|
try {
|
|
await client.connect();
|
|
await client.discover(cliConfig);
|
|
this.eventEmitter?.emit('mcp-client-update', this.clients);
|
|
} catch (error) {
|
|
this.eventEmitter?.emit('mcp-client-update', this.clients);
|
|
// Log the error but don't let a single failed server stop the others
|
|
console.error(
|
|
`Error during discovery for server '${name}': ${getErrorMessage(
|
|
error,
|
|
)}`,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
await Promise.all(discoveryPromises);
|
|
this.discoveryState = MCPDiscoveryState.COMPLETED;
|
|
}
|
|
|
|
/**
|
|
* Stops all running local MCP servers and closes all client connections.
|
|
* This is the cleanup method to be called on application exit.
|
|
*/
|
|
async stop(): Promise<void> {
|
|
const disconnectionPromises = Array.from(this.clients.entries()).map(
|
|
async ([name, client]) => {
|
|
try {
|
|
await client.disconnect();
|
|
} catch (error) {
|
|
console.error(
|
|
`Error stopping client '${name}': ${getErrorMessage(error)}`,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
await Promise.all(disconnectionPromises);
|
|
this.clients.clear();
|
|
}
|
|
|
|
getDiscoveryState(): MCPDiscoveryState {
|
|
return this.discoveryState;
|
|
}
|
|
}
|