Files
claude-code/packages/mcp-client/src/manager.ts
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

242 lines
7.5 KiB
TypeScript

// McpManager — imperative API for MCP protocol client
// Factory function that creates a manager instance with event-based notifications
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
import type {
ListToolsResult,
} from '@modelcontextprotocol/sdk/types.js'
import memoize from 'lodash-es/memoize.js'
import { buildMcpToolName } from './strings.js'
import type { CoreTool } from '@claude-code-best/agent-tools'
import type {
McpServerConfig,
ScopedMcpServerConfig,
MCPServerConnection,
ConnectedMCPServer,
FailedMCPServer,
NeedsAuthMCPServer,
} from './types.js'
import type { McpClientDependencies } from './interfaces.js'
import {
McpConnectionError,
McpAuthError,
McpTimeoutError,
} from './errors.js'
import { memoizeWithLRU } from './cache.js'
import { discoverTools } from './discovery.js'
import { callMcpTool } from './execution.js'
// ============================================================================
// Event types
// ============================================================================
export type McpManagerEvents = {
connected: (name: string) => void
disconnected: (name: string, error?: Error) => void
toolsChanged: (serverName: string, tools: CoreTool[]) => void
error: (name: string, error: Error) => void
authRequired: (name: string) => void
}
type EventHandler = (...args: any[]) => void
// ============================================================================
// Manager interface
// ============================================================================
export interface McpManager {
connect(name: string, config: McpServerConfig): Promise<MCPServerConnection>
disconnect(name: string): Promise<void>
disconnectAll(): Promise<void>
getConnections(): Map<string, MCPServerConnection>
getTools(serverName: string): CoreTool[]
getAllTools(): CoreTool[]
callTool(serverName: string, toolName: string, args: unknown): Promise<unknown>
on<E extends keyof McpManagerEvents>(event: E, handler: McpManagerEvents[E]): void
off(event: string, handler: EventHandler): void
}
// ============================================================================
// Default timeout
// ============================================================================
const MCP_TIMEOUT_MS = 30_000
const MCP_REQUEST_TIMEOUT_MS = 60_000
// ============================================================================
// Manager implementation
// ============================================================================
class McpManagerImpl implements McpManager {
private connections = new Map<string, MCPServerConnection>()
private toolsCache = new Map<string, CoreTool[]>()
private listeners = new Map<string, Set<EventHandler>>()
private deps: McpClientDependencies
private connectFn: ((name: string, config: ScopedMcpServerConfig) => Promise<MCPServerConnection>) | null = null
constructor(deps: McpClientDependencies) {
this.deps = deps
}
/** Set the connect function — the host provides this with all transport logic */
setConnectFn(fn: (name: string, config: ScopedMcpServerConfig) => Promise<MCPServerConnection>): void {
this.connectFn = fn
}
async connect(name: string, config: McpServerConfig): Promise<MCPServerConnection> {
if (!this.connectFn) {
throw new Error('McpManager: connectFn not set. Call setConnectFn() first.')
}
const scopedConfig: ScopedMcpServerConfig = { ...config, scope: 'dynamic' }
try {
const connection = await this.connectFn(name, scopedConfig)
this.connections.set(name, connection)
if (connection.type === 'connected') {
this.emit('connected', name)
// Fetch tools for this server
await this.refreshTools(name, connection)
} else if (connection.type === 'needs-auth') {
this.emit('authRequired', name)
}
return connection
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err))
this.emit('error', name, error)
throw err
}
}
async disconnect(name: string): Promise<void> {
const conn = this.connections.get(name)
if (!conn) return
if (conn.type === 'connected') {
try {
await conn.cleanup()
} catch (err) {
this.deps.logger.warn(`Error disconnecting ${name}:`, err)
}
}
this.connections.delete(name)
this.toolsCache.delete(name)
this.emit('disconnected', name)
}
async disconnectAll(): Promise<void> {
const names = [...this.connections.keys()]
await Promise.all(names.map(name => this.disconnect(name)))
}
getConnections(): Map<string, MCPServerConnection> {
return new Map(this.connections)
}
getTools(serverName: string): CoreTool[] {
return this.toolsCache.get(serverName) ?? []
}
getAllTools(): CoreTool[] {
const all: CoreTool[] = []
for (const tools of this.toolsCache.values()) {
all.push(...tools)
}
return all
}
async callTool(serverName: string, toolName: string, args: unknown): Promise<unknown> {
const conn = this.connections.get(serverName)
if (!conn || conn.type !== 'connected') {
throw new McpConnectionError(serverName, `Server ${serverName} is not connected`)
}
return callMcpTool(
{
client: conn,
tool: toolName,
args: args as Record<string, unknown>,
signal: new AbortController().signal,
},
this.deps,
)
}
on<E extends keyof McpManagerEvents>(event: E, handler: McpManagerEvents[E]): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(handler)
}
off(event: string, handler: EventHandler): void {
this.listeners.get(event)?.delete(handler)
}
// ── Private ──
private emit(event: string, ...args: unknown[]): void {
this.listeners.get(event)?.forEach(handler => {
try {
handler(...args)
} catch (err) {
this.deps.logger.error(`Error in ${event} handler:`, err)
}
})
}
private async refreshTools(name: string, conn: ConnectedMCPServer): Promise<void> {
try {
const tools = await discoverTools({
serverName: name,
client: conn.client,
capabilities: conn.capabilities ?? {},
deps: this.deps,
})
this.toolsCache.set(name, tools)
this.emit('toolsChanged', name, tools)
} catch (err) {
this.deps.logger.warn(`Failed to fetch tools for ${name}:`, err)
}
}
}
// ============================================================================
// Factory function
// ============================================================================
/**
* Creates a new MCP manager instance.
*
* The manager handles connection lifecycle, tool discovery, and event notification.
* The host must call `setConnectFn()` to provide the transport-level connection logic.
*
* @param deps Host dependency injections (logger, auth, proxy, etc.)
* @returns McpManager instance
*
* @example
* ```typescript
* const manager = createMcpManager({
* logger: console,
* httpConfig: { getUserAgent: () => 'my-app/1.0' },
* })
*
* manager.setConnectFn(async (name, config) => {
* // Transport-level connection logic here
* })
*
* manager.on('connected', (name) => console.log(`Connected to ${name}`))
* manager.on('toolsChanged', (name, tools) => console.log(`${name}: ${tools.length} tools`))
*
* await manager.connect('my-server', { command: 'npx', args: ['my-mcp-server'] })
* const tools = manager.getAllTools()
* ```
*/
export function createMcpManager(deps: McpClientDependencies): McpManager {
return new McpManagerImpl(deps)
}