Skip to main content
Glama
tool-builder.ts12 kB
import { HttpMethod, Integration, Workflow as Tool } from "@superglue/client"; import { convertRequiredToArray, Metadata, toJsonSchema } from "@superglue/shared"; import { JSONSchema } from "openai/lib/jsonschema.mjs"; import { getToolBuilderContext } from "../context/context-builders.js"; import { BUILD_TOOL_SYSTEM_PROMPT } from "../context/context-prompts.js"; import { LanguageModel, LLMMessage } from "../llm/llm-base-model.js"; import { logMessage } from "../utils/logs.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import z from "zod"; import { getWebSearchTool } from "../llm/llm-tools.js"; export class ToolBuilder { private integrations: Record<string, Integration>; private instruction: string; private initialPayload: Record<string, unknown>; private metadata: Metadata; private responseSchema: JSONSchema; private inputSchema: JSONSchema; private toolSchema: any; constructor( instruction: string, integrations: Integration[], initialPayload: Record<string, unknown>, responseSchema: JSONSchema, metadata: Metadata ) { this.integrations = integrations.reduce((acc, int) => { acc[int.id] = int; return acc; }, {} as Record<string, Integration>); this.instruction = instruction; this.initialPayload = initialPayload || {}; this.metadata = metadata; this.responseSchema = responseSchema; this.toolSchema = zodToJsonSchema(toolSchema); try { const credentials = Object.values(integrations).reduce((acc, int) => { return { ...acc, ...Object.entries(int.credentials || {}).reduce((obj, [name, value]) => ({ ...obj, [`${int.id}_${name}`]: value }), {}) }; }, {}); const rawSchema = toJsonSchema( { payload: this.initialPayload, credentials: credentials }, { arrays: { mode: 'all' }, required: true, requiredDepth: 2 } ); this.inputSchema = convertRequiredToArray(rawSchema) as JSONSchema; } catch (error) { logMessage('error', `Error during payload parsing: ${error}`, this.metadata); throw new Error(`Error during payload parsing: ${error}`); } } private prepareBuildingContext(): LLMMessage[] { const buildingPromptForAgent = getToolBuilderContext({ integrations: Object.values(this.integrations), payload: this.initialPayload, userInstruction: this.instruction, responseSchema: this.responseSchema }, { characterBudget: 120000, include: { integrationContext: true, availableVariablesContext: true, payloadContext: true, userInstruction: true } }); return [ { role: "system", content: BUILD_TOOL_SYSTEM_PROMPT }, { role: "user", content: buildingPromptForAgent } ]; } private validateTool(tool: Tool): { valid: boolean; errors: string[] } { const errors: string[] = []; const availableIntegrationIds = Object.keys(this.integrations); const hasSteps = tool.steps && tool.steps.length > 0; const hasFinalTransform = tool.finalTransform && tool.finalTransform !== "$" && tool.finalTransform !== "(sourceData) => sourceData"; if (!hasSteps && !hasFinalTransform) { errors.push("Tool must have either steps or a finalTransform to process data"); } if (hasSteps && availableIntegrationIds.length === 0) { errors.push("Tool has steps but no integrations are available. Either provide integrations or use a transform-only tool."); } if (hasSteps) { tool.steps?.forEach((step, index) => { if (!step.integrationId) { errors.push(`Step ${index + 1} (${step.id}): Missing integrationId`); } else if (!availableIntegrationIds.includes(step.integrationId)) { errors.push(`Step ${index + 1} (${step.id}): Invalid integrationId '${step.integrationId}'. Available integrations: ${availableIntegrationIds.join(', ')}`); } if (!step.apiConfig?.urlHost) { errors.push(`Step ${index + 1} (${step.id}): Missing URL configuration (urlHost: '${step.apiConfig?.urlHost || 'undefined'}'). Please ensure that all steps correspond to a single API call, or merge this step with the previous one.`); } }); } return { valid: errors.length === 0, errors }; } public async buildTool(): Promise<Tool> { const maxRetries = 3; let messages = this.prepareBuildingContext(); let lastError: string | null = null; const webSearchTool = getWebSearchTool(); const tools = webSearchTool ? [{ toolDefinition: { web_search: webSearchTool }, toolContext: {} }] : []; for (let attempt = 0; attempt < maxRetries; attempt++) { try { logMessage('info', `Building tool${attempt > 0 ? ` (attempt ${attempt + 1}/${maxRetries})` : ''}`, this.metadata); if (attempt > 0 && lastError) { messages.push({ role: "user", content: `The previous attempt failed with: "${lastError}". Please fix this issue in your new attempt.` } as LLMMessage); } const generateToolResult = await LanguageModel.generateObject<Tool>({ messages, schema: this.toolSchema, temperature: 0.0, tools }); messages = generateToolResult.messages; if (!generateToolResult.success) { throw new Error(`Error generating tool: ${generateToolResult.response}`); } const generatedTool = generateToolResult.response; if (!Array.isArray(generatedTool.steps)) { throw new Error(`LLM returned invalid tool structure: steps must be an array, got ${typeof generatedTool.steps}: ${typeof generatedTool.steps === 'object' ? JSON.stringify(generatedTool.steps, null, 2) : generatedTool.steps}`); } generatedTool.steps = generatedTool.steps.map(step => ({ ...step, modify: step.modify || false, apiConfig: { ...step.apiConfig, queryParams: step.apiConfig.queryParams ? Object.fromEntries(step.apiConfig.queryParams.map((p: any) => [p.key, p.value])) : undefined, headers: step.apiConfig.headers ? Object.fromEntries(step.apiConfig.headers.map((h: any) => [h.key, h.value])) : undefined, } })); const validation = this.validateTool(generatedTool); if (!validation.valid) { const errorDetails = validation.errors.join('\n'); const toolSummary = JSON.stringify({ id: generatedTool.id, steps: generatedTool.steps?.map(s => ({ id: s.id, integrationId: s.integrationId, urlHost: s.apiConfig?.urlHost, urlPath: s.apiConfig?.urlPath })) }, null, 2); throw new Error(`Tool validation failed:\n${errorDetails}\n\nGenerated tool:\n${toolSummary}`); } generatedTool.instruction = this.instruction; generatedTool.responseSchema = this.responseSchema; return { id: generatedTool.id, steps: generatedTool.steps, integrationIds: Object.keys(this.integrations), instruction: this.instruction, finalTransform: generatedTool.finalTransform, responseSchema: this.responseSchema, inputSchema: this.inputSchema, createdAt: generatedTool.createdAt || new Date(), updatedAt: generatedTool.updatedAt || new Date(), }; } catch (error: any) { lastError = error.message; logMessage('error', `Error during tool build attempt ${attempt + 1}: ${error.message}`, this.metadata); } } const finalErrorMsg = `Tool build failed after ${maxRetries} attempts. Last error: ${lastError}`; logMessage('error', finalErrorMsg, this.metadata); throw new Error(finalErrorMsg); } } const toolSchema = z.object({ id: z.string().describe("The tool ID (e.g., 'stripe-create-order')"), steps: z.array(z.object({ id: z.string().describe("Unique camelCase identifier for the step (e.g., 'fetchCustomerDetails')"), integrationId: z.string().describe("REQUIRED: The integration ID for this step (must match one of the available integration IDs)"), loopSelector: z.string().describe("JavaScript function that returns OBJECT for direct execution or ARRAY for loop execution. If returns OBJECT (including {}), step executes once with object as currentItem. If returns ARRAY, step executes once per array item. Examples: (sourceData) => ({ userId: sourceData.userId }) OR (sourceData) => sourceData.getContacts.data.filter(c => c.active)"), modify: z.boolean().optional().describe("Marks whether this operation modifies data on the system it operates on (writes, updates, deletes). Read-only operations should be false. Default is false."), apiConfig: z.object({ id: z.string().describe("Same as the step ID"), instruction: z.string().describe("A concise instruction describing WHAT data this API call should retrieve or what action it should perform."), urlHost: z.string().describe("The base URL host (e.g., https://api.example.com). Must not be empty."), urlPath: z.string().describe("The API endpoint path (e.g., /v1/users)."), method: z.enum(Object.values(HttpMethod) as [string, ...string[]]).describe("HTTP method (MUST be a literal value, not a variable or expression): GET, POST, PUT, DELETE, or PATCH"), queryParams: z.array(z.object({ key: z.string(), value: z.string() })).optional().describe("Query parameters as key-value pairs. If pagination is configured, ensure you have included the right pagination parameters here or in the body."), headers: z.array(z.object({ key: z.string(), value: z.string() })).optional().describe("HTTP headers as key-value pairs. Use <<variable>> syntax for dynamic values or JavaScript expressions"), body: z.string().optional().describe("Request body. Use <<variable>> syntax for dynamic values. If pagination is configured, ensure you have included the right pagination parameters here or in the queryParams."), pagination: z.object({ type: z.enum(["OFFSET_BASED", "PAGE_BASED", "CURSOR_BASED"]), pageSize: z.string().describe("Number of items per page (e.g., '50', '100'). Once set, this becomes available as <<limit>> (same as pageSize)."), cursorPath: z.string().describe("If cursor_based: The JSONPath to the cursor in the response. If not, set this to \"\""), stopCondition: z.string().describe("REQUIRED: JavaScript function that determines when to stop pagination. This is the primary control for pagination. Format: (response, pageInfo) => boolean. The pageInfo object contains: page (number), offset (number), cursor (any), totalFetched (number). response is the axios response object, access response data via response.data. Return true to STOP. E.g. (response, pageInfo) => !response.data.pagination.has_more") }).optional().describe("OPTIONAL: Only configure if you are using pagination variables in the URL, headers, or body. For OFFSET_BASED, ALWAYS use <<offset>>. If PAGE_BASED, ALWAYS use <<page>>. If CURSOR_BASED, ALWAYS use <<cursor>>.") }).describe("Complete API configuration for this step") })).describe("Array of workflow steps. Can be empty ([]) for transform-only workflows that just process the input payload without API calls"), finalTransform: z.string().describe("JavaScript function to transform the final workflow output to match responseSchema. Check if result is object or array: if object use sourceData.stepId.data, if array use sourceData.stepId.map(item => item.data). Example: (sourceData) => {return { result: Array.isArray(sourceData.stepId) ? sourceData.stepId.map(item => item.data) : sourceData.stepId.data }}"), });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/superglue-ai/superglue'

If you have feedback or need assistance with the MCP directory API, please join our Discord server