feat: implement SDK MCP server support and enhance control request handling

- 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.
This commit is contained in:
mingholy.lmh 2025-12-04 17:01:13 +08:00
parent a58d3f7aaf
commit 322ce80e2c
29 changed files with 2473 additions and 837 deletions

View file

@ -16,9 +16,12 @@
* Controllers:
* - SystemController: initialize, interrupt, set_model, supported_commands
* - PermissionController: can_use_tool, set_permission_mode
* - MCPController: mcp_message, mcp_server_status
* - SdkMcpController: mcp_server_status (mcp_message handled via callback)
* - HookController: hook_callback
*
* Note: mcp_message requests are NOT routed through the dispatcher. CLI MCP
* clients send messages via SdkMcpController.createSendSdkMcpMessage() callback.
*
* Note: Control request types are centrally defined in the ControlRequestType
* enum in packages/sdk/typescript/src/types/controlRequests.ts
*/
@ -27,7 +30,7 @@ import type { IControlContext } from './ControlContext.js';
import type { IPendingRequestRegistry } from './controllers/baseController.js';
import { SystemController } from './controllers/systemController.js';
import { PermissionController } from './controllers/permissionController.js';
// import { MCPController } from './controllers/mcpController.js';
import { SdkMcpController } from './controllers/sdkMcpController.js';
// import { HookController } from './controllers/hookController.js';
import type {
CLIControlRequest,
@ -65,7 +68,7 @@ export class ControlDispatcher implements IPendingRequestRegistry {
// Make controllers publicly accessible
readonly systemController: SystemController;
readonly permissionController: PermissionController;
// readonly mcpController: MCPController;
readonly sdkMcpController: SdkMcpController;
// readonly hookController: HookController;
// Central pending request registries
@ -88,7 +91,11 @@ export class ControlDispatcher implements IPendingRequestRegistry {
this,
'PermissionController',
);
// this.mcpController = new MCPController(context, this, 'MCPController');
this.sdkMcpController = new SdkMcpController(
context,
this,
'SdkMcpController',
);
// this.hookController = new HookController(context, this, 'HookController');
// Listen for main abort signal
@ -228,10 +235,10 @@ export class ControlDispatcher implements IPendingRequestRegistry {
}
this.pendingOutgoingRequests.clear();
// Cleanup controllers (MCP controller will close all clients)
// Cleanup controllers
this.systemController.cleanup();
this.permissionController.cleanup();
// this.mcpController.cleanup();
this.sdkMcpController.cleanup();
// this.hookController.cleanup();
}
@ -291,6 +298,47 @@ export class ControlDispatcher implements IPendingRequestRegistry {
}
}
/**
* Get count of pending incoming requests (for debugging)
*/
getPendingIncomingRequestCount(): number {
return this.pendingIncomingRequests.size;
}
/**
* Wait for all incoming request handlers to complete.
*
* Uses polling since we don't have direct Promise references to handlers.
* The pendingIncomingRequests map is managed by BaseController:
* - Registered when handler starts (in handleRequest)
* - Deregistered when handler completes (success or error)
*
* @param pollIntervalMs - How often to check (default 50ms)
* @param timeoutMs - Maximum wait time (default 30s)
*/
async waitForPendingIncomingRequests(
pollIntervalMs: number = 50,
timeoutMs: number = 30000,
): Promise<void> {
const startTime = Date.now();
while (this.pendingIncomingRequests.size > 0) {
if (Date.now() - startTime > timeoutMs) {
if (this.context.debugMode) {
console.error(
`[ControlDispatcher] Timeout waiting for ${this.pendingIncomingRequests.size} pending incoming requests`,
);
}
break;
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
if (this.context.debugMode && this.pendingIncomingRequests.size === 0) {
console.error('[ControlDispatcher] All incoming requests completed');
}
}
/**
* Returns the controller that handles the given request subtype
*/
@ -306,9 +354,8 @@ export class ControlDispatcher implements IPendingRequestRegistry {
case 'set_permission_mode':
return this.permissionController;
// case 'mcp_message':
// case 'mcp_server_status':
// return this.mcpController;
case 'mcp_server_status':
return this.sdkMcpController;
// case 'hook_callback':
// return this.hookController;