Skip to main content
Glama
run-maestro-flow.ts6.48 kB
/** * run_maestro_flow Tool Handler * MCP tool for running Maestro E2E test flows */ import { isPlatform, Platform } from '../../models/constants.js'; import { FlowResult } from '../../models/failure-bundle.js'; import { Errors } from '../../models/errors.js'; import { runMaestroFlow, isMaestroAvailable, MaestroRunOptions } from './maestro-executor.js'; import { generateFailureBundle, getFailureBundleSummary } from './failure-bundle.js'; import { getToolRegistry, createInputSchema } from '../register.js'; import { listDevices as listAndroidDevices } from '../../platforms/android/adb.js'; import { getBootedDevice } from '../../platforms/ios/simctl.js'; /** * Input arguments for run_maestro_flow tool */ export interface RunMaestroFlowArgs { /** Path to the Maestro flow YAML file */ flowPath: string; /** Target platform */ platform: string; /** Device ID (optional, uses first available) */ deviceId?: string; /** App package/bundle ID */ appId?: string; /** Timeout in milliseconds */ timeoutMs?: number; /** Generate failure bundle on failure */ generateFailureBundle?: boolean; /** Environment variables for the flow */ env?: Record<string, string>; } /** * Result structure for run_maestro_flow */ export interface RunMaestroFlowResult { /** Flow execution result */ flowResult: FlowResult; /** Failure bundle if test failed and generateFailureBundle is true */ failureBundle?: ReturnType<typeof getFailureBundleSummary>; /** Summary for AI consumption */ summary: string; } /** * Run Maestro flow tool handler */ export async function runMaestroFlowTool(args: RunMaestroFlowArgs): Promise<RunMaestroFlowResult> { const { flowPath, platform, deviceId, appId, timeoutMs = 300000, generateFailureBundle: shouldGenerateBundle = true, env = {}, } = args; // Validate platform if (!isPlatform(platform)) { throw Errors.invalidArguments(`Invalid platform: ${platform}. Must be 'android' or 'ios'`); } // Check if Maestro is available const maestroAvailable = await isMaestroAvailable(); if (!maestroAvailable) { throw Errors.invalidArguments( 'Maestro CLI not found. Install from https://maestro.mobile.dev/' ); } // Get target device const resolvedDeviceId = await resolveDevice(platform as Platform, deviceId); if (!resolvedDeviceId) { throw Errors.invalidArguments(`No ${platform} device found`); } // Run the flow const maestroOptions: MaestroRunOptions = { flowPath, platform: platform as Platform, deviceId: resolvedDeviceId, appId, timeoutMs, env, }; const flowResult = await runMaestroFlow(maestroOptions); // Build result const result: RunMaestroFlowResult = { flowResult, summary: createFlowSummary(flowResult), }; // Generate failure bundle if flow failed if (!flowResult.success && shouldGenerateBundle) { try { const bundle = await generateFailureBundle({ flowResult, platform: platform as Platform, deviceId: resolvedDeviceId, appIdentifier: appId, includeScreenshot: true, includeLogs: platform === 'android', }); result.failureBundle = getFailureBundleSummary(bundle); } catch (error) { console.error('[run_maestro_flow] Failed to generate failure bundle:', error); } } return result; } /** * Resolve device ID for the target platform */ async function resolveDevice(platform: Platform, deviceId?: string): Promise<string | null> { if (platform === 'android') { const devices = await listAndroidDevices(); if (deviceId) { const found = devices.find( (d) => d.id === deviceId || d.name === deviceId || d.model === deviceId ); return found?.id || null; } const booted = devices.find((d) => d.status === 'booted'); return booted?.id || null; } else { if (deviceId) { // Assume deviceId is a UDID return deviceId; } const booted = await getBootedDevice(); return booted?.id || null; } } /** * Create summary for AI consumption */ function createFlowSummary(flowResult: FlowResult): string { const lines: string[] = [ `Flow: ${flowResult.flowName}`, `Status: ${flowResult.success ? 'PASSED' : 'FAILED'}`, `Steps: ${flowResult.passedSteps}/${flowResult.totalSteps} passed`, `Duration: ${(flowResult.durationMs / 1000).toFixed(2)}s`, ]; if (!flowResult.success) { if (flowResult.failedAtStep >= 0 && flowResult.steps[flowResult.failedAtStep]) { const failedStep = flowResult.steps[flowResult.failedAtStep]; lines.push(`Failed at step ${failedStep.index + 1}: ${failedStep.command}`); if (failedStep.error) { lines.push(`Error: ${failedStep.error}`); } } else if (flowResult.error) { lines.push(`Error: ${flowResult.error}`); } } return lines.join('\n'); } /** * Register the run_maestro_flow tool */ export function registerRunMaestroFlowTool(): void { getToolRegistry().register( 'run_maestro_flow', { description: 'Run a Maestro E2E test flow. Returns structured results with step-by-step status. On failure, generates a failure bundle with screenshot and logs for debugging.', inputSchema: createInputSchema( { flowPath: { type: 'string', description: 'Path to the Maestro flow YAML file', }, platform: { type: 'string', enum: ['android', 'ios'], description: 'Target platform', }, deviceId: { type: 'string', description: 'Device ID or name (optional, uses first available)', }, appId: { type: 'string', description: 'App package (Android) or bundle ID (iOS)', }, timeoutMs: { type: 'number', description: 'Timeout in milliseconds (default: 300000)', }, generateFailureBundle: { type: 'boolean', description: 'Generate failure bundle with screenshot and logs on failure (default: true)', }, env: { type: 'object', description: 'Environment variables for the flow', additionalProperties: { type: 'string' }, }, }, ['flowPath', 'platform'] ), }, (args) => runMaestroFlowTool(args as unknown as RunMaestroFlowArgs) ); }

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/abd3lraouf/specter-mcp'

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