/** * Railway Dynamic Tool Executor (Deno) * Uses Deno's native HTTP import support */ // Import zod-to-json-schema for Zod v3 support import { zodToJsonSchema } from 'https://esm.sh/zod-to-json-schema@3.25.0'; // Cache for imported tool modules const moduleCache = new Map(); /** * Load and describe a tool from esm.sh */ async function loadAndDescribe(req: Request): Promise { try { const body = await req.json(); const { packageName, exportName, version, importUrl } = body; if (!packageName || !exportName || !version) { return Response.json( { success: false, error: 'Missing required fields: packageName, exportName, version', }, { status: 400 } ); } const cacheKey = `${packageName}::${exportName}`; let toolModule; // Check cache first if (moduleCache.has(cacheKey)) { console.log(`✅ Cache hit: ${cacheKey}`); toolModule = moduleCache.get(cacheKey); } else { // Dynamic import from esm.sh (Deno supports this natively!) const url = importUrl || `https://esm.sh/${packageName}@${version}`; console.log(`📦 Importing: ${url}`); const module = await import(url); toolModule = module[exportName]; if (!toolModule) { console.error(`❌ Export "${exportName}" not found. Available:`, Object.keys(module)); return Response.json( { success: false, error: `Export "${exportName}" not found in module`, availableExports: Object.keys(module), }, { status: 404 } ); } // Validate it's an AI SDK tool if (!toolModule.description || !toolModule.execute) { console.error(`❌ Invalid AI SDK tool structure:`, { hasDescription: !!toolModule.description, hasExecute: !!toolModule.execute, hasInputSchema: !!toolModule.inputSchema, keys: Object.keys(toolModule), }); return Response.json( { success: false, error: 'Invalid AI SDK tool structure (missing description or execute)', toolKeys: Object.keys(toolModule), }, { status: 400 } ); } // Cache it moduleCache.set(cacheKey, toolModule); console.log(`✅ Cached: ${cacheKey}`); } // Extract tool definition - try multiple schema formats let rawJsonSchema = null; if (toolModule.inputSchema) { // Strategy 1: Try Zod v4 native JSON Schema export if (typeof toolModule.inputSchema.toJSONSchema === 'function') { console.log(`📋 Using Zod v4 toJSONSchema() for ${cacheKey}`); try { rawJsonSchema = toolModule.inputSchema.toJSONSchema(); } catch (error) { console.warn(`⚠️ Zod toJSONSchema() failed for ${cacheKey}:`, error); } } else if (typeof toolModule.inputSchema.jsonSchema === 'function') { console.log(`📋 Using Zod v4 jsonSchema() for ${cacheKey}`); try { rawJsonSchema = toolModule.inputSchema.jsonSchema(); } catch (error) { console.warn(`⚠️ Zod jsonSchema() failed for ${cacheKey}:`, error); } } // Strategy 2: Try AI SDK v6 jsonSchema() wrapper (has .schema property) if (!rawJsonSchema && toolModule.inputSchema.schema) { console.log(`📋 Using AI SDK jsonSchema.schema for ${cacheKey}`); rawJsonSchema = toolModule.inputSchema.schema; } // Strategy 3: Try Zod v3 schema (detect via _def property and convert) if (!rawJsonSchema && toolModule.inputSchema._def) { console.log( `📋 Detected Zod schema (v3), converting with zod-to-json-schema for ${cacheKey}` ); try { rawJsonSchema = zodToJsonSchema(toolModule.inputSchema); console.log(`✅ Successfully converted Zod schema for ${cacheKey}`); } catch (error) { console.warn(`⚠️ zod-to-json-schema conversion failed for ${cacheKey}:`, error); } } } // If no schema found, fail with helpful error if (!rawJsonSchema) { console.error(`❌ No valid schema found for ${cacheKey}`, { hasInputSchema: !!toolModule.inputSchema, inputSchemaType: typeof toolModule.inputSchema, hasToJSONSchema: typeof toolModule.inputSchema?.toJSONSchema === 'function', hasJsonSchema: typeof toolModule.inputSchema?.jsonSchema === 'function', hasSchema: !!toolModule.inputSchema?.schema, keys: toolModule.inputSchema ? Object.keys(toolModule.inputSchema) : [], }); return Response.json( { success: false, error: `Tool "${exportName}" has no valid inputSchema. Tools must use AI SDK jsonSchema(), Zod v4 toJSONSchema(), or Zod v3 schemas.`, debug: { hasInputSchema: !!toolModule.inputSchema, availableMethods: toolModule.inputSchema ? Object.keys(toolModule.inputSchema) : [], hasZodDef: !!toolModule.inputSchema?._def, }, }, { status: 400 } ); } console.log(`✅ Extracted schema for ${cacheKey}`); return Response.json({ success: true, tool: { exportName, description: toolModule.description, inputSchema: rawJsonSchema, // Plain JSON Schema - fully serializable }, }); } catch (error) { console.error('❌ Failed to load tool:', error); return Response.json( { success: false, error: error.message, }, { status: 500 } ); } } /** * Execute a tool with parameters */ async function executeTool(req: Request): Promise { try { const body = await req.json(); const { packageName, exportName, version, importUrl, params, env } = body; console.log(`📥 Execute request:`, { packageName, exportName, version, envKeys: env ? Object.keys(env) : [], envValues: env || {}, }); if (!packageName || !exportName || !version) { return Response.json( { success: false, error: 'Missing required fields: packageName, exportName, version', }, { status: 400 } ); } const cacheKey = `${packageName}::${exportName}`; const startTime = Date.now(); let toolModule; // Check cache or import if (moduleCache.has(cacheKey)) { console.log(`✅ Using cached tool: ${cacheKey}`); toolModule = moduleCache.get(cacheKey); } else { const url = importUrl || `https://esm.sh/${packageName}@${version}`; console.log(`📦 Importing for execution: ${url}`); const module = await import(url); toolModule = module[exportName]; if (!toolModule || !toolModule.execute) { return Response.json( { success: false, error: 'Tool not found or invalid', executionTimeMs: Date.now() - startTime, }, { status: 404 } ); } moduleCache.set(cacheKey, toolModule); } // Inject environment variables from client if (env && typeof env === 'object') { const envKeys = Object.keys(env); if (envKeys.length > 0) { console.log(`🔐 Injecting ${envKeys.length} environment variables:`, envKeys); for (const [key, value] of Object.entries(env)) { Deno.env.set(key, String(value)); console.log(` ✅ Set ${key} = ${String(value).substring(0, 10)}...`); } // Verify they're set console.log(`🔍 Verification - Deno.env has:`, envKeys.map(k => `${k}=${Deno.env.get(k)?.substring(0, 10)}...`)); } else { console.log(`⚠️ No env vars provided in request`); } } else { console.log(`⚠️ No env object in request body`); } // Execute the tool console.log(`🚀 Executing ${cacheKey} with params:`, params); const result = await toolModule.execute(params || {}); const executionTimeMs = Date.now() - startTime; console.log(`✅ Execution complete in ${executionTimeMs}ms`); return Response.json({ success: true, output: result, executionTimeMs, }); } catch (error) { const executionTimeMs = Date.now() - Date.now(); console.error('❌ Tool execution failed:', error); return Response.json( { success: false, error: error.message, executionTimeMs, }, { status: 500 } ); } } /** * Health check */ function health(): Response { return Response.json({ status: 'ok', timestamp: new Date().toISOString(), cacheSize: moduleCache.size, denoVersion: Deno.version.deno, v8Version: Deno.version.v8, httpImports: true, }); } /** * Cache stats */ function cacheStats(): Response { const entries = Array.from(moduleCache.keys()); return Response.json({ success: true, cacheSize: moduleCache.size, cachedTools: entries, }); } /** * Clear cache */ function clearCache(): Response { const size = moduleCache.size; moduleCache.clear(); console.log(`🗑️ Cleared cache (${size} entries)`); return Response.json({ success: true, message: `Cleared ${size} cached modules`, }); } /** * Main request handler */ async function handler(req: Request): Promise { const url = new URL(req.url); // Add CORS headers const headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; if (req.method === 'OPTIONS') { return new Response(null, { headers }); } try { let response: Response; if (url.pathname === '/health' && req.method === 'GET') { response = health(); } else if (url.pathname === '/load-and-describe' && req.method === 'POST') { response = await loadAndDescribe(req); } else if (url.pathname === '/execute-tool' && req.method === 'POST') { response = await executeTool(req); } else if (url.pathname === '/cache/stats' && req.method === 'GET') { response = cacheStats(); } else if (url.pathname === '/cache/clear' && req.method === 'POST') { response = clearCache(); } else { response = Response.json({ error: 'Not found' }, { status: 404 }); } // Add CORS headers to response Object.entries(headers).forEach(([key, value]) => { response.headers.set(key, value); }); return response; } catch (error) { console.error('Request handler error:', error); return Response.json( { success: false, error: error.message, }, { status: 500, headers } ); } } // Start server const port = Number.parseInt(Deno.env.get('PORT') || '3002'); console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`); console.log(`📦 HTTP imports: ENABLED`); console.log(`🔗 Health check: http://localhost:${port}/health`); console.log(`🛠️ Endpoints:`); console.log(` POST /load-and-describe - Load tool and get schema`); console.log(` POST /execute-tool - Execute a tool with params`); console.log(` POST /cache/clear - Clear module cache`); console.log(` GET /cache/stats - Get cache statistics`); Deno.serve({ port }, handler);