From 81efba0de5d7a83d95fb38f16a112feac476b7c7 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Wed, 17 Dec 2025 20:52:19 +1000 Subject: [PATCH] fix: add schema extraction to manual tools sync - Try Railway executor first for dynamic schema extraction - Fall back to converting parameters from manual-tools.ts to JSON Schema - This ensures Vercel AI registry tools get schemas during sync --- sync-manual-tools.ts | 128 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/sync-manual-tools.ts b/sync-manual-tools.ts index 3fc34cc..6c2b63b 100644 --- a/sync-manual-tools.ts +++ b/sync-manual-tools.ts @@ -1,6 +1,91 @@ import { manualTools } from './manual-tools.js'; import { prisma } from './packages/db/src/index.js'; import { fetchLatestPackageWithMetadata } from './packages/npm-client/src/package.js'; +// Schema extraction is done via HTTP call to Railway executor +const RAILWAY_EXECUTOR_URL = + process.env.RAILWAY_SERVICE_URL || 'https://endearing-commitment-production.up.railway.app'; + +async function extractToolSchema( + packageName: string, + toolName: string, + version: string, + _importUrl: string | null +): Promise< + | { success: true; inputSchema: Record; description?: string } + | { success: false; error: string } +> { + try { + const response = await fetch(`${RAILWAY_EXECUTOR_URL}/load-and-describe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ packageName, name: toolName, version, env: {} }), + }); + const data = await response.json(); + if (!response.ok || !data.success) { + return { success: false, error: data.error || 'Failed to extract schema' }; + } + if (!data.tool?.inputSchema) { + return { success: false, error: 'No inputSchema returned from executor' }; + } + return { + success: true, + inputSchema: data.tool.inputSchema, + description: data.tool.description, + }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } +} + +function convertJsonSchemaToParameters(inputSchema: Record): Array<{ + name: string; + type: string; + description: string; + required: boolean; +}> { + const properties = + (inputSchema.properties as Record) || {}; + const required = (inputSchema.required as string[]) || []; + return Object.entries(properties).map(([name, prop]) => ({ + name, + type: prop.type || 'unknown', + description: prop.description || '', + required: required.includes(name), + })); +} + +function convertParametersToJsonSchema( + parameters: Array<{ + name: string; + type: string; + description: string; + required: boolean; + default?: string; + }> +): Record { + const properties: Record = {}; + const required: string[] = []; + + for (const param of parameters) { + properties[param.name] = { + type: param.type || 'string', + description: param.description || '', + }; + if (param.default !== undefined) { + properties[param.name].default = param.default; + } + if (param.required) { + required.push(param.name); + } + } + + return { + type: 'object', + properties, + required, + additionalProperties: false, + }; +} async function syncManualTools() { console.log('\nšŸ”§ Starting manual tools sync...\n'); @@ -111,6 +196,49 @@ async function syncManualTools() { }); console.log(` āœ… Tool upserted: ${tool.name} (${tool.id})`); + + // Try to extract schema from the Railway executor first + const schemaResult = await extractToolSchema( + manualTool.npmPackageName, + manualTool.name, + version, + null + ); + + if (schemaResult.success) { + await prisma.tool.update({ + where: { id: tool.id }, + data: { + // biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility + inputSchema: schemaResult.inputSchema as any, + // biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility + parameters: convertJsonSchemaToParameters(schemaResult.inputSchema) as any, + schemaSource: 'extracted', + schemaExtractedAt: new Date(), + // Update description if not provided manually + ...(!manualTool.description && schemaResult.description + ? { description: schemaResult.description } + : {}), + }, + }); + console.log(` āœ… Schema extracted for ${manualTool.name}`); + } else if (manualTool.parameters && manualTool.parameters.length > 0) { + // Fallback: Convert parameters from manual-tools.ts to inputSchema format + const inputSchema = convertParametersToJsonSchema(manualTool.parameters); + await prisma.tool.update({ + where: { id: tool.id }, + data: { + // biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility + inputSchema: inputSchema as any, + schemaSource: 'author', + schemaExtractedAt: new Date(), + }, + }); + console.log(` āœ… Schema generated from parameters for ${manualTool.name}`); + } else { + console.log(` āš ļø No schema available for ${manualTool.name}`); + } + processed++; } catch (error) { console.error(