Skip to main content
Glama
client-utils.ts12.3 kB
import { ExecutionStep, Integration, SelfHealingMode, SuperglueClient, Workflow as Tool } from "@superglue/client"; import { tokenRegistry } from "./token-registry"; const BASE62_REGEX = /^[a-zA-Z0-9_-]*$/; export function isValidToolName(name: string): boolean { return BASE62_REGEX.test(name); } export function validateToolName(name: string): string | null { if (!name) { return "Tool name cannot be empty"; } if (name.length > 100) { return "Tool name cannot be longer than 100 characters"; } if (!isValidToolName(name)) { return "Tool name can only contain letters, numbers, hyphens, and underscores"; } return null; } export interface StepExecutionResult { stepId: string; success: boolean; data?: any; error?: string; updatedStep?: any; } export interface FinalTransformExecutionResult { success: boolean; data?: any; error?: string; updatedTransform?: string; updatedResponseSchema?: any; } export interface ToolExecutionState { originalTool: Tool; currentTool: Tool; stepResults: Record<string, StepExecutionResult>; completedSteps: string[]; failedSteps: string[]; isExecuting: boolean; currentStepIndex: number; interrupted?: boolean; } export async function executeSingleStep({ client, step, toolId, payload, previousResults, selfHealing }: { client: SuperglueClient; step: ExecutionStep; toolId: string; payload: any; previousResults: Record<string, any>; selfHealing: boolean; }): Promise<StepExecutionResult> { try { const singleStepTool: Tool = { id: `${toolId}_step_${step.id}`, steps: [step], finalTransform: '' } as any; const executionPayload = { ...payload, ...previousResults }; const result = await client.executeWorkflow({ workflow: singleStepTool, payload: executionPayload, options: { testMode: selfHealing, selfHealing: selfHealing ? SelfHealingMode.REQUEST_ONLY : SelfHealingMode.DISABLED, timeout: 3_600_000 // 1 hour } }); const stepResult = result.stepResults[0]; return { stepId: step.id, success: result.success, data: stepResult.data, error: result.error, updatedStep: result.config?.steps?.[0] }; } catch (error: any) { return { stepId: step.id, success: false, error: error.message || 'Step execution failed' }; } } export async function executeToolStepByStep( client: SuperglueClient, tool: Tool, payload: any, onStepComplete?: (stepIndex: number, result: StepExecutionResult) => void, selfHealing: boolean = false, shouldStop?: () => boolean, onBeforeStep?: (stepIndex: number, step: any) => Promise<boolean> ): Promise<ToolExecutionState> { const state: ToolExecutionState = { originalTool: tool, currentTool: { ...tool }, stepResults: {}, completedSteps: [], failedSteps: [], isExecuting: true, currentStepIndex: 0, interrupted: false }; const previousResults: Record<string, any> = {}; for (let i = 0; i < tool.steps.length; i++) { if (shouldStop && shouldStop()) { state.isExecuting = false; state.interrupted = true; return state; } state.currentStepIndex = i; const step = tool.steps[i]; // Check if we should pause before executing this step if (onBeforeStep) { const shouldContinue = await onBeforeStep(i, step); if (!shouldContinue) { state.isExecuting = false; state.interrupted = true; return state; } } const result = await executeSingleStep( { client, step, toolId: state.currentTool.id, payload, previousResults, selfHealing } ); state.stepResults[step.id] = result; if (result.success) { state.completedSteps.push(step.id); previousResults[step.id] = result.data; // Update the tool with any returned step configuration (normalization, self-healing, etc.) if (result.updatedStep) { state.currentTool = { ...state.currentTool, steps: state.currentTool.steps.map((s, idx) => idx === i ? result.updatedStep : s ) }; } } else { state.failedSteps.push(step.id); state.isExecuting = false; // Stop execution on failure if (onStepComplete) { onStepComplete(i, result); } return state; } if (onStepComplete) { onStepComplete(i, result); } if (shouldStop && shouldStop()) { state.isExecuting = false; state.interrupted = true; return state; } } if (tool.finalTransform && state.failedSteps.length === 0) { // Final guard before executing transform if (shouldStop && shouldStop()) { state.isExecuting = false; state.interrupted = true; return state; } const finalResult = await executeFinalTransform( client, tool.id || 'tool', state.currentTool.finalTransform || tool.finalTransform, tool.responseSchema, tool.inputSchema, payload, previousResults, selfHealing ); state.stepResults['__final_transform__'] = { stepId: '__final_transform__', success: finalResult.success, data: finalResult.data, error: finalResult.error }; if (finalResult.success) { state.completedSteps.push('__final_transform__'); if (finalResult.updatedTransform && selfHealing) { state.currentTool = { ...state.currentTool, finalTransform: finalResult.updatedTransform }; } } else { state.failedSteps.push('__final_transform__'); } } state.isExecuting = false; return state; } export async function executeFinalTransform( client: SuperglueClient, toolId: string, finalTransform: string, responseSchema: any, inputSchema: any, payload: any, previousResults: Record<string, any>, selfHealing: boolean = false ): Promise<FinalTransformExecutionResult> { try { const finalPayload = { ...payload, ...previousResults }; const result = await client.executeWorkflow({ workflow: { id: `${toolId}_final_transform`, steps: [], finalTransform, responseSchema, inputSchema: inputSchema }, payload: finalPayload, options: { testMode: selfHealing, selfHealing: selfHealing ? SelfHealingMode.TRANSFORM_ONLY : SelfHealingMode.DISABLED, timeout: 600_000 // 10 minutes } }); return { success: result.success, data: result.data, error: result.error, updatedTransform: result.config?.finalTransform, updatedResponseSchema: result.config?.responseSchema }; } catch (error: any) { return { success: false, error: error.message || 'Final transform execution failed' }; } } export function canExecuteStep( stepIndex: number, completedSteps: string[], tool: Tool, stepResults?: Record<string, any> ): boolean { if (stepIndex === 0) { return true; } // Check that all previous steps are completed and have results for (let i = 0; i < stepIndex; i++) { const stepId = tool.steps[i].id; if (!completedSteps.includes(stepId)) { return false; } // If stepResults is provided, also check that the step has a result if (stepResults && !stepResults[stepId]) { return false; } } return true; } export const isJsonEmpty = (inputJson: string): boolean => { try { if (!inputJson) return true const parsedJson = JSON.parse(inputJson) return Object.keys(parsedJson).length === 0 } catch (error) { // If invalid JSON, we consider it empty return true } } export const findArraysOfObjects = (obj: any): Record<string, any[]> => { const arrays: Record<string, any[]> = {}; const traverse = (value: any, path: string = '') => { if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') { arrays[path] = value; } if (typeof value === 'object' && value !== null) { Object.entries(value).forEach(([key, val]) => { traverse(val, `${path ? `${path}.` : ''}${key}`); }); } }; traverse(obj); if (Object.keys(arrays).length === 0) { if (Object.keys(obj).length === 1) { const [key, value] = Object.entries(obj)[0]; return { [key]: [value] }; } return { response: [obj] }; } return arrays; }; export const parseCredentialsHelper = (simpleCreds: string): Record<string, string> => { try { const creds = simpleCreds?.trim() || "" if (!creds) { return {} } if (creds.startsWith('{')) { return JSON.parse(creds) } if (creds.startsWith('Bearer ')) { return { token: creds.replace('Bearer ', '') } } if (creds.startsWith('Basic ')) { return { token: creds.replace('Basic ', '') } } return { token: creds } } catch (error) { return {} } } export const splitUrl = (url: string) => { if (!url) { return { urlHost: '', urlPath: '' } } // Find the position after the protocol (://) const protocolEnd = url.indexOf('://'); // Find the first slash after the protocol const firstSlashAfterProtocol = url.indexOf('/', protocolEnd + 3); if (firstSlashAfterProtocol === -1) { // No path, entire URL is the host return { urlHost: url, urlPath: '' } } // Split at the first slash after protocol return { urlHost: url.substring(0, firstSlashAfterProtocol), urlPath: url.substring(firstSlashAfterProtocol) } } export function needsUIToTriggerDocFetch(newIntegration: Integration, oldIntegration: Integration | null): boolean { // If documentation was manually provided, no fetch needed. if (newIntegration.documentation && newIntegration.documentation.trim()) { return false; } // If it's a new integration with a doc URL, fetch is needed. if (!oldIntegration) { return true; } // If any of the relevant URLs have changed, fetch is needed. if (newIntegration.urlHost !== oldIntegration.urlHost || newIntegration.urlPath !== oldIntegration.urlPath || newIntegration.documentationUrl !== oldIntegration.documentationUrl) { return true; } return false; } export const deepMergePreferRight = (left: any, right: any): any => { if (Array.isArray(left) && Array.isArray(right)) return right; if (typeof left !== 'object' || left === null) return right ?? left; if (typeof right !== 'object' || right === null) return right ?? left; const result: Record<string, any> = { ...left }; for (const key of new Set([...Object.keys(left), ...Object.keys(right)])) { result[key] = deepMergePreferRight(left[key], right[key]); } return result; }; /** * Generate a UUID v4 with fallback for browsers that don't support crypto.randomUUID * Works in both secure and non-secure contexts */ export function generateUUID(): string { // Use native crypto.randomUUID if available (secure contexts only) if (typeof crypto !== 'undefined' && crypto.randomUUID) { try { return crypto.randomUUID(); } catch (e) { // Fall through to manual implementation } } // Fallback implementation using crypto.getRandomValues (available in more contexts) if (typeof crypto !== 'undefined' && crypto.getRandomValues) { const array = new Uint8Array(16); crypto.getRandomValues(array); // Set the version to 4 (random) array[6] = (array[6] & 0x0f) | 0x40; // Set the variant to 1 (RFC4122) array[8] = (array[8] & 0x3f) | 0x80; return [...array].map((b, i) => { const hex = b.toString(16).padStart(2, '0'); return (i === 4 || i === 6 || i === 8 || i === 10) ? `-${hex}` : hex; }).join(''); } // Final fallback for environments without crypto (should be rare) return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } export function createSuperglueClient(endpoint: string): SuperglueClient { return new SuperglueClient({ endpoint, apiKey: tokenRegistry.getToken(), }) }

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