@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera
214 lines (181 loc) • 6.22 kB
text/typescript
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import type { MCPToolInfo, MCPServerConfig } from '../types';
import type { MCPClientManager } from '../MCPClientManager';
import { ContentStoreService, shouldUseReference } from '@hashgraphonline/standards-sdk';
import type { ContentSource } from '../../types/content-reference';
/**
* Convert an MCP tool to a LangChain DynamicStructuredTool
*/
export function convertMCPToolToLangChain(
tool: MCPToolInfo,
mcpManager: MCPClientManager,
serverConfig?: MCPServerConfig
): DynamicStructuredTool {
const zodSchema = jsonSchemaToZod(tool.inputSchema);
const sanitizedName = `${tool.serverName}_${tool.name}`.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
let description = tool.description || `MCP tool ${tool.name} from ${tool.serverName}`;
if (serverConfig?.toolDescriptions?.[tool.name]) {
description = `${description}\n\n${serverConfig.toolDescriptions[tool.name]}`;
}
if (serverConfig?.additionalContext) {
description = `${description}\n\nContext: ${serverConfig.additionalContext}`;
}
return new DynamicStructuredTool({
name: sanitizedName,
description,
schema: zodSchema,
func: async (input) => {
try {
const result = await mcpManager.executeTool(
tool.serverName,
tool.name,
input
);
let responseText = '';
if (typeof result === 'string') {
responseText = result;
} else if (
result &&
typeof result === 'object' &&
'content' in result
) {
const content = (result as { content: unknown }).content;
if (Array.isArray(content)) {
const textParts = content
.filter(
(item): item is { type: string; text: string } =>
typeof item === 'object' &&
item !== null &&
'type' in item &&
item.type === 'text' &&
'text' in item
)
.map((item) => item.text);
responseText = textParts.join('\n');
} else {
responseText = JSON.stringify(content);
}
} else {
responseText = JSON.stringify(result);
}
const responseBuffer = Buffer.from(responseText, 'utf8');
const MCP_REFERENCE_THRESHOLD = 10 * 1024;
const shouldStoreMCPContent = responseBuffer.length > MCP_REFERENCE_THRESHOLD;
if (shouldStoreMCPContent || shouldUseReference(responseBuffer)) {
const contentStore = ContentStoreService.getInstance();
if (contentStore) {
try {
const referenceId = await contentStore.storeContent(responseBuffer, {
contentType: 'text' as ContentSource,
source: 'mcp',
mcpToolName: `${tool.serverName}_${tool.name}`,
originalSize: responseBuffer.length
});
return `content-ref:${referenceId}`;
} catch (storeError) {
}
}
}
return responseText;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
return `Error executing MCP tool ${tool.name}: ${errorMessage}`;
}
},
});
}
/**
* Convert JSON Schema to Zod schema
* This is a simplified converter that handles common cases
*/
function jsonSchemaToZod(schema: unknown): z.ZodTypeAny {
if (!schema || typeof schema !== 'object') {
return z.object({});
}
const schemaObj = schema as Record<string, unknown>;
if (schemaObj.type && schemaObj.type !== 'object') {
return convertType(schemaObj);
}
if (!schemaObj.properties || typeof schemaObj.properties !== 'object') {
return z.object({});
}
const shape: Record<string, z.ZodTypeAny> = {};
for (const [key, value] of Object.entries(schemaObj.properties)) {
let zodType = convertType(value);
const isRequired =
Array.isArray(schemaObj.required) && schemaObj.required.includes(key);
if (!isRequired) {
zodType = zodType.optional();
}
shape[key] = zodType;
}
return z.object(shape);
}
/**
* Convert a single JSON Schema type to Zod
*/
function convertType(schema: unknown): z.ZodTypeAny {
if (!schema || typeof schema !== 'object' || !('type' in schema)) {
return z.unknown();
}
const schemaObj = schema as {
type: string;
enum?: unknown[];
items?: unknown;
};
let zodType: z.ZodTypeAny;
switch (schemaObj.type) {
case 'string':
zodType = z.string();
if (schemaObj.enum && Array.isArray(schemaObj.enum)) {
zodType = z.enum(schemaObj.enum as [string, ...string[]]);
}
break;
case 'number':
zodType = z.number();
if ('minimum' in schemaObj && typeof schemaObj.minimum === 'number') {
zodType = (zodType as z.ZodNumber).min(schemaObj.minimum);
}
if ('maximum' in schemaObj && typeof schemaObj.maximum === 'number') {
zodType = (zodType as z.ZodNumber).max(schemaObj.maximum);
}
break;
case 'integer':
zodType = z.number().int();
if ('minimum' in schemaObj && typeof schemaObj.minimum === 'number') {
zodType = (zodType as z.ZodNumber).min(schemaObj.minimum);
}
if ('maximum' in schemaObj && typeof schemaObj.maximum === 'number') {
zodType = (zodType as z.ZodNumber).max(schemaObj.maximum);
}
break;
case 'boolean':
zodType = z.boolean();
break;
case 'array':
if (schemaObj.items) {
zodType = z.array(convertType(schemaObj.items));
} else {
zodType = z.array(z.unknown());
}
break;
case 'object':
if ('properties' in schemaObj) {
zodType = jsonSchemaToZod(schemaObj);
} else {
zodType = z.object({}).passthrough();
}
break;
default:
zodType = z.unknown();
}
if ('description' in schemaObj && typeof schemaObj.description === 'string') {
zodType = zodType.describe(schemaObj.description);
}
return zodType;
}