@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
text/typescript
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);
}
}