sandbox connected and streaming

This commit is contained in:
2026-03-30 23:29:03 -04:00
parent c3a8fae132
commit 998f69fa1a
130 changed files with 7416 additions and 2123 deletions

View File

@@ -0,0 +1,7 @@
// MCP tool wrappers exports
export {
createMCPToolWrapper,
createMCPToolWrappers,
type MCPToolInfo,
} from './mcp-tool-wrapper.js';

View File

@@ -0,0 +1,186 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import type { FastifyBaseLogger } from 'fastify';
import type { MCPClientConnector } from '../../harness/mcp-client.js';
/**
* MCP Tool Wrapper
*
* Wraps remote MCP server tools as standard LangChain tools.
* Provides dynamic tool creation based on MCP tool definitions.
*/
export interface MCPToolInfo {
name: string;
description?: string;
inputSchema?: {
type: string;
properties?: Record<string, any>;
required?: string[];
};
}
/**
* Create a LangChain tool from an MCP tool definition
*/
export function createMCPToolWrapper(
toolInfo: MCPToolInfo,
mcpClient: MCPClientConnector,
logger: FastifyBaseLogger,
onImage?: (image: { data: string; mimeType: string }) => void
): DynamicStructuredTool {
// Convert MCP input schema to Zod schema
const zodSchema = mcpInputSchemaToZod(toolInfo.inputSchema);
return new DynamicStructuredTool({
name: toolInfo.name,
description: toolInfo.description || `MCP tool: ${toolInfo.name}`,
schema: zodSchema,
func: async (input: Record<string, unknown>) => {
try {
const result = await mcpClient.callTool(toolInfo.name, input);
logger.info({ tool: toolInfo.name }, 'MCP tool call completed');
// Handle different MCP result formats
if (typeof result === 'string') {
return result;
}
// Handle structured MCP responses with content arrays
if (result && typeof result === 'object') {
// Extract text content from MCP response
const textParts: string[] = [];
// Check for content array (standard MCP format)
if (Array.isArray((result as any).content)) {
logger.debug({ tool: toolInfo.name, itemCount: (result as any).content.length }, 'Processing MCP content array');
for (const item of (result as any).content) {
if (item.type === 'text' && item.text) {
textParts.push(item.text);
} else if (item.type === 'image' && item.data && item.mimeType) {
logger.info({ tool: toolInfo.name, mimeType: item.mimeType }, 'Capturing image from MCP response');
onImage?.({ data: item.data, mimeType: item.mimeType });
}
}
if (textParts.length > 0) {
return textParts.join('\n\n');
}
}
// Check for nested execution.content
if ((result as any).execution && Array.isArray((result as any).execution.content)) {
for (const item of (result as any).execution.content) {
if (item.type === 'text' && item.text) {
textParts.push(item.text);
} else if (item.type === 'image' && item.data && item.mimeType) {
onImage?.({ data: item.data, mimeType: item.mimeType });
}
}
if (textParts.length > 0) {
return textParts.join('\n\n');
}
}
// Fallback: stringify the result
return JSON.stringify(result, null, 2);
}
return String(result || '');
} catch (error) {
logger.error({ error, tool: toolInfo.name, input }, 'MCP tool call failed');
return `Error calling MCP tool ${toolInfo.name}: ${error instanceof Error ? error.message : String(error)}`;
}
},
});
}
/**
* Convert MCP input schema to Zod schema
*/
function mcpInputSchemaToZod(inputSchema?: MCPToolInfo['inputSchema']): z.ZodObject<any> {
if (!inputSchema || !inputSchema.properties) {
// Generic schema that accepts any properties
return z.object({}).passthrough();
}
const properties = inputSchema.properties;
const required = inputSchema.required || [];
const zodFields: Record<string, z.ZodTypeAny> = {};
for (const [key, prop] of Object.entries(properties)) {
let zodType: z.ZodTypeAny;
// Map JSON Schema types to Zod types
switch (prop.type) {
case 'string':
zodType = z.string().describe(prop.description || '');
break;
case 'number':
zodType = z.number().describe(prop.description || '');
break;
case 'integer':
zodType = z.number().int().describe(prop.description || '');
break;
case 'boolean':
zodType = z.boolean().describe(prop.description || '');
break;
case 'array':
// Handle array items
if (prop.items) {
const itemType = getZodTypeForProperty(prop.items);
zodType = z.array(itemType).describe(prop.description || '');
} else {
zodType = z.array(z.any()).describe(prop.description || '');
}
break;
case 'object':
zodType = z.object({}).passthrough().describe(prop.description || '');
break;
default:
zodType = z.any().describe(prop.description || '');
}
// Make optional if not required
if (!required.includes(key)) {
zodType = zodType.optional();
}
zodFields[key] = zodType;
}
return z.object(zodFields);
}
/**
* Helper to get Zod type for a property definition
*/
function getZodTypeForProperty(prop: any): z.ZodTypeAny {
switch (prop.type) {
case 'string':
return z.string();
case 'number':
return z.number();
case 'integer':
return z.number().int();
case 'boolean':
return z.boolean();
case 'object':
return z.object({}).passthrough();
default:
return z.any();
}
}
/**
* Create multiple MCP tool wrappers from tool list
*/
export function createMCPToolWrappers(
toolInfos: MCPToolInfo[],
mcpClient: MCPClientConnector,
logger: FastifyBaseLogger,
onImage?: (image: { data: string; mimeType: string }) => void
): DynamicStructuredTool[] {
return toolInfos.map(toolInfo => createMCPToolWrapper(toolInfo, mcpClient, logger, onImage));
}