UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org

216 lines (188 loc) 6.37 kB
import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import type { MCPServerConfig, MCPToolInfo, MCPConnectionStatus } from './types'; import { Logger } from '@hashgraphonline/standards-sdk'; import type { ContentStorage } from '../memory/content-storage'; import { MCPContentProcessor } from './content-processor'; /** * Manages connections to MCP servers and tool discovery */ export class MCPClientManager { private clients: Map<string, Client> = new Map(); private tools: Map<string, MCPToolInfo[]> = new Map(); private logger: Logger; private contentProcessor?: MCPContentProcessor; constructor(logger: Logger, contentStorage?: ContentStorage) { this.logger = logger; if (contentStorage) { this.contentProcessor = new MCPContentProcessor(contentStorage, logger); } } /** * Connect to an MCP server and discover its tools */ async connectServer(config: MCPServerConfig): Promise<MCPConnectionStatus> { try { if (this.isServerConnected(config.name)) { return { serverName: config.name, connected: false, error: `Server ${config.name} is already connected`, tools: [], }; } if (config.transport && config.transport !== 'stdio') { throw new Error(`Transport ${config.transport} not yet supported`); } const transport = new StdioClientTransport({ command: config.command, args: config.args, ...(config.env && { env: config.env }), }); const client = new Client({ name: `conversational-agent-${config.name}`, version: '1.0.0', }, { capabilities: {}, }); await client.connect(transport); this.clients.set(config.name, client); const toolsResponse = await client.listTools(); const toolsWithServer: MCPToolInfo[] = toolsResponse.tools.map((tool: unknown) => { const t = tool as { description?: string } & Record<string, unknown>; const { description, ...rest } = t; const base = description !== undefined && typeof description === 'string' ? { ...rest, description } : { ...rest }; return { ...(base as Omit<MCPToolInfo, 'serverName'>), serverName: config.name, } as MCPToolInfo; }); this.tools.set(config.name, toolsWithServer); this.logger.info(`Connected to MCP server ${config.name} with ${toolsWithServer.length} tools`); return { serverName: config.name, connected: true, tools: toolsWithServer, }; } catch (error) { this.logger.error(`Failed to connect to MCP server ${config.name}:`, error); return { serverName: config.name, connected: false, error: error instanceof Error ? error.message : 'Unknown error', tools: [], }; } } /** * Execute a tool on a specific MCP server */ async executeTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<unknown> { const client = this.clients.get(serverName); if (!client) { throw new Error(`MCP server ${serverName} not connected`); } this.logger.debug(`Executing MCP tool ${toolName} on server ${serverName}`, args); try { const result = await client.callTool({ name: toolName, arguments: args, }); if (this.contentProcessor) { const processed = await this.contentProcessor.processResponse(result, serverName, toolName); if (processed.wasProcessed) { this.logger.debug( `Processed MCP response from ${serverName}::${toolName}`, { referenceCreated: processed.referenceCreated, originalSize: processed.originalSize, errors: processed.errors } ); if (processed.errors && processed.errors.length > 0) { this.logger.warn(`Content processing warnings for ${serverName}::${toolName}:`, processed.errors); } } return processed.content; } return result; } catch (error) { this.logger.error(`Error executing MCP tool ${toolName}:`, error); throw error; } } /** * Disconnect all MCP servers */ async disconnectAll(): Promise<void> { for (const [name, client] of this.clients) { try { await client.close(); this.logger.info(`Disconnected from MCP server ${name}`); } catch (error) { this.logger.error(`Error disconnecting MCP server ${name}:`, error); } } this.clients.clear(); this.tools.clear(); } /** * Get all discovered tools from all connected servers */ getAllTools(): MCPToolInfo[] { const allTools: MCPToolInfo[] = []; for (const tools of this.tools.values()) { allTools.push(...tools); } return allTools; } /** * Get tools from a specific server */ getServerTools(serverName: string): MCPToolInfo[] { return this.tools.get(serverName) || []; } /** * Check if a server is connected */ isServerConnected(serverName: string): boolean { return this.clients.has(serverName); } /** * Get list of connected server names */ getConnectedServers(): string[] { return Array.from(this.clients.keys()); } /** * Enable content processing with content storage */ enableContentProcessing(contentStorage: ContentStorage): void { this.contentProcessor = new MCPContentProcessor(contentStorage, this.logger); this.logger.info('Content processing enabled for MCP responses'); } /** * Disable content processing */ disableContentProcessing(): void { delete this.contentProcessor; this.logger.info('Content processing disabled for MCP responses'); } /** * Check if content processing is enabled */ isContentProcessingEnabled(): boolean { return this.contentProcessor !== undefined; } /** * Analyze a response without processing it (for testing/debugging) */ analyzeResponseContent(response: unknown): unknown { if (!this.contentProcessor) { throw new Error('Content processing is not enabled'); } return this.contentProcessor.analyzeResponse(response); } }