mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
* 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>
242 lines
7.5 KiB
TypeScript
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)
|
|
}
|