| |
| |
| |
| |
|
|
| export type MCPTransportType = 'stdio' | 'sse' | 'http' |
|
|
| export interface MCPConfig { |
| name: string |
| command?: string |
| args?: string[] |
| env?: Record<string, string> |
| url?: string |
| transport?: MCPTransportType |
| } |
|
|
| export interface MCPTool { |
| name: string |
| description: string |
| inputSchema: Record<string, unknown> |
| } |
|
|
| export interface MCPResource { |
| uri: string |
| name: string |
| description?: string |
| mimeType?: string |
| } |
|
|
| export interface MCP_SERVER_CONFIG { |
| name: string |
| transport: 'stdio' | 'sse' | 'http' |
| command?: string |
| args?: string[] |
| env?: Record<string, string> |
| url?: string |
| } |
|
|
| interface MCPRequest { |
| jsonrpc: '2.0' |
| id: number | string |
| method: string |
| params?: Record<string, unknown> |
| } |
|
|
| interface MCPResponse { |
| jsonrpc: '2.0' |
| id: number | string |
| result?: unknown |
| error?: { |
| code: number |
| message: string |
| data?: unknown |
| } |
| } |
|
|
| |
|
|
| export class MCPClient { |
| private config: MCP_SERVER_CONFIG |
| private requestId = 0 |
| private pendingRequests: Map<number | string, { |
| resolve: (value: unknown) => void |
| reject: (error: Error) => void |
| }> = new Map() |
|
|
| constructor(config: MCP_SERVER_CONFIG) { |
| this.config = config |
| } |
|
|
| get name(): string { |
| return this.config.name |
| } |
|
|
| get transport(): string { |
| return this.config.transport |
| } |
|
|
| |
| async sendRequest(method: string, params?: Record<string, unknown>): Promise<unknown> { |
| const id = ++this.requestId |
| const request: MCPRequest = { |
| jsonrpc: '2.0', |
| id, |
| method, |
| params, |
| } |
|
|
| return new Promise((resolve, reject) => { |
| this.pendingRequests.set(id, { resolve, reject }) |
|
|
| if (this.config.transport === 'stdio') { |
| this.sendStdioRequest(request) |
| } else if (this.config.transport === 'http' || this.config.transport === 'sse') { |
| this.sendHttpRequest(request) |
| } |
| }) |
| } |
|
|
| private async sendStdioRequest(request: MCPRequest): Promise<void> { |
| |
| console.log('[mcp] Stdio request:', request) |
| } |
|
|
| private async sendHttpRequest(request: MCPRequest): Promise<void> { |
| const url = this.config.url |
| if (!url) { |
| throw new Error('MCP HTTP client requires URL') |
| } |
|
|
| try { |
| const response = await fetch(url, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(request), |
| }) |
|
|
| if (!response.ok) { |
| throw new Error(`MCP request failed: ${response.status}`) |
| } |
|
|
| const data = await response.json() as MCPResponse |
| const pending = this.pendingRequests.get(data.id) |
| if (pending) { |
| if (data.error) { |
| pending.reject(new Error(data.error.message)) |
| } else { |
| pending.resolve(data.result) |
| } |
| this.pendingRequests.delete(data.id) |
| } |
| } catch (error) { |
| |
| for (const [, pending] of this.pendingRequests) { |
| pending.reject(error as Error) |
| } |
| this.pendingRequests.clear() |
| } |
| } |
|
|
| |
| async listTools(): Promise<MCPTool[]> { |
| try { |
| const result = await this.sendRequest('tools/list') as { |
| tools: Array<{ |
| name: string |
| description?: string |
| inputSchema?: Record<string, unknown> |
| }> |
| } |
| return (result.tools ?? []).map(t => ({ |
| name: t.name, |
| description: t.description ?? '', |
| inputSchema: t.inputSchema ?? {}, |
| })) |
| } catch { |
| return [] |
| } |
| } |
|
|
| |
| async callTool(name: string, args: Record<string, unknown>): Promise<unknown> { |
| return this.sendRequest('tools/call', { name, arguments: args }) |
| } |
|
|
| |
| async listResources(): Promise<MCPResource[]> { |
| try { |
| const result = await this.sendRequest('resources/list') as { |
| resources: Array<{ |
| uri: string |
| name: string |
| description?: string |
| mimeType?: string |
| }> |
| } |
| return (result.resources ?? []).map(r => ({ |
| uri: r.uri, |
| name: r.name, |
| description: r.description, |
| mimeType: r.mimeType, |
| })) |
| } catch { |
| return [] |
| } |
| } |
|
|
| |
| async readResource(uri: string): Promise<unknown> { |
| return this.sendRequest('resources/read', { uri }) |
| } |
| } |
|
|
| |
|
|
| export class MCPConnectionManager { |
| private connections: Map<string, MCPClient> = new Map() |
|
|
| async addServer(config: MCP_SERVER_CONFIG): Promise<MCPClient> { |
| const client = new MCPClient(config) |
| this.connections.set(config.name, client) |
|
|
| |
| try { |
| await client.sendRequest('initialize', { |
| protocolVersion: '2024-11-05', |
| capabilities: {}, |
| clientInfo: { |
| name: 'stack-2.9', |
| version: '1.0.0', |
| }, |
| }) |
| console.log(`[mcp] Connected to ${config.name}`) |
| } catch (error) { |
| console.error(`[mcp] Failed to connect to ${config.name}:`, error) |
| } |
|
|
| return client |
| } |
|
|
| getServer(name: string): MCPClient | undefined { |
| return this.connections.get(name) |
| } |
|
|
| removeServer(name: string): void { |
| this.connections.delete(name) |
| } |
|
|
| listServers(): string[] { |
| return Array.from(this.connections.keys()) |
| } |
|
|
| async closeAll(): Promise<void> { |
| for (const [name, client] of this.connections) { |
| try { |
| await client.sendRequest('shutdown') |
| } catch { |
| |
| } |
| console.log(`[mcp] Disconnected from ${name}`) |
| } |
| this.connections.clear() |
| } |
| } |
|
|
| |
|
|
| export function createMCPClient(config: MCPConfig): MCPClient { |
| const serverConfig: MCP_SERVER_CONFIG = { |
| name: config.name, |
| transport: config.transport ?? 'stdio', |
| command: config.command, |
| args: config.args, |
| env: config.env, |
| url: config.url, |
| } |
|
|
| return new MCPClient(serverConfig) |
| } |
|
|
| export default { |
| MCPClient, |
| MCPConnectionManager, |
| createMCPClient, |
| } |