sandbox connected and streaming
This commit is contained in:
7
gateway/src/tools/mcp/index.ts
Normal file
7
gateway/src/tools/mcp/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// MCP tool wrappers exports
|
||||
|
||||
export {
|
||||
createMCPToolWrapper,
|
||||
createMCPToolWrappers,
|
||||
type MCPToolInfo,
|
||||
} from './mcp-tool-wrapper.js';
|
||||
186
gateway/src/tools/mcp/mcp-tool-wrapper.ts
Normal file
186
gateway/src/tools/mcp/mcp-tool-wrapper.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user