Skip to main content
Glama
andreahaku

Expo iOS Development MCP Server

by andreahaku

flow.run

Execute sequential tool calls to automate iOS development workflows for Expo/React Native applications, including simulator control, server management, and testing.

Instructions

Execute a sequence of tool calls (macro flow)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
stepsYesSteps to execute in sequence.
stopOnErrorNoStop flow on first error.

Implementation Reference

  • Core handler function that executes a sequence of FlowSteps using a provided ToolExecutor, with error handling, logging, and optional screenshots on failure.
    export async function runFlow(
      steps: FlowStep[],
      executor: ToolExecutor,
      options: {
        stopOnError?: boolean;
        screenshotOnError?: boolean;
      } = {}
    ): Promise<FlowResult> {
      const { stopOnError = true, screenshotOnError = true } = options;
      const startTime = Date.now();
      const results: StepResult[] = [];
      const evidence: string[] = [];
    
      logger.info("expo", `Starting flow with ${steps.length} steps`);
    
      for (let i = 0; i < steps.length; i++) {
        const step = steps[i];
        const stepNumber = i + 1;
        const stepStart = Date.now();
    
        logger.info("expo", `Step ${stepNumber}/${steps.length}: ${step.tool}`, {
          description: step.description,
        });
    
        try {
          const { success, result, error } = await executor(
            step.tool,
            step.input as Record<string, unknown>
          );
    
          const stepResult: StepResult = {
            step: stepNumber,
            tool: step.tool,
            success,
            elapsedMs: Date.now() - stepStart,
            result,
            error,
          };
    
          results.push(stepResult);
    
          if (!success) {
            logger.error("expo", `Step ${stepNumber} failed: ${error}`);
    
            if (screenshotOnError) {
              try {
                const screenshot = await takeScreenshot(`flow-error-step-${stepNumber}`);
                evidence.push(screenshot.path);
              } catch {
                logger.warn("expo", "Failed to capture error screenshot");
              }
            }
    
            if (stopOnError) {
              return {
                success: false,
                totalSteps: steps.length,
                completedSteps: stepNumber - 1,
                failedStep: stepNumber,
                results,
                totalElapsedMs: Date.now() - startTime,
                evidence: evidence.length > 0 ? evidence : undefined,
              };
            }
          } else {
            logger.info("expo", `Step ${stepNumber} completed in ${stepResult.elapsedMs}ms`);
          }
        } catch (err) {
          const stepResult: StepResult = {
            step: stepNumber,
            tool: step.tool,
            success: false,
            elapsedMs: Date.now() - stepStart,
            error: err instanceof Error ? err.message : "Unknown error",
          };
    
          results.push(stepResult);
    
          if (screenshotOnError) {
            try {
              const screenshot = await takeScreenshot(`flow-error-step-${stepNumber}`);
              evidence.push(screenshot.path);
            } catch {
              // Ignore screenshot errors
            }
          }
    
          if (stopOnError) {
            return {
              success: false,
              totalSteps: steps.length,
              completedSteps: stepNumber - 1,
              failedStep: stepNumber,
              results,
              totalElapsedMs: Date.now() - startTime,
              evidence: evidence.length > 0 ? evidence : undefined,
            };
          }
        }
      }
    
      const allSuccess = results.every((r) => r.success);
    
      logger.info("expo", `Flow completed: ${allSuccess ? "SUCCESS" : "FAILED"}`, {
        totalSteps: steps.length,
        completedSteps: results.filter((r) => r.success).length,
        totalElapsedMs: Date.now() - startTime,
      });
    
      return {
        success: allSuccess,
        totalSteps: steps.length,
        completedSteps: results.filter((r) => r.success).length,
        failedStep: allSuccess ? undefined : results.findIndex((r) => !r.success) + 1,
        results,
        totalElapsedMs: Date.now() - startTime,
        evidence: evidence.length > 0 ? evidence : undefined,
      };
    }
  • MCP server.tool registration for the 'flow.run' tool, delegating to runFlow with a custom toolExecutor.
    server.tool(
      "flow.run",
      "Execute a sequence of tool calls (macro flow)",
      FlowRunInputSchema.shape,
      async (args) => {
        try {
          const result = await runFlow(args.steps, toolExecutor, {
            stopOnError: args.stopOnError,
          });
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(result, null, 2),
              },
            ],
            isError: !result.success,
          };
        } catch (error) {
          return handleToolError(error);
        }
      }
    );
  • Zod schema defining the input parameters for the flow.run tool, including steps array and stopOnError option.
    export const FlowRunInputSchema = z.object({
      steps: z.array(FlowStepSchema).describe("Steps to execute in sequence."),
      stopOnError: z.boolean().optional().default(true).describe("Stop flow on first error."),
    });
  • Custom ToolExecutor implementation used by flow.run to execute individual UI and simulator tools via Detox actions.
    const toolExecutor: ToolExecutor = async (toolName, input) => {
      // This is a simplified executor - in production you might want to
      // route through the actual MCP tool handlers
      try {
        switch (toolName) {
          case "ui.tap":
            const tapSnippet = generateTapSnippet({
              selector: input.selector as { by: "id" | "text" | "label"; value: string },
              x: input.x as number | undefined,
              y: input.y as number | undefined,
            });
            const tapResult = await runDetoxAction({
              actionName: `tap:${(input.selector as { value: string }).value}`,
              actionSnippet: tapSnippet,
            });
            return { success: tapResult.success, result: tapResult, error: tapResult.error?.message };
    
          case "ui.type":
            const typeSnippet = generateTypeSnippet({
              selector: input.selector as { by: "id" | "text" | "label"; value: string },
              text: input.text as string,
              replace: input.replace as boolean | undefined,
            });
            const typeResult = await runDetoxAction({
              actionName: `type:${(input.selector as { value: string }).value}`,
              actionSnippet: typeSnippet,
            });
            return { success: typeResult.success, result: typeResult, error: typeResult.error?.message };
    
          case "ui.wait_for":
            const waitSnippet = generateWaitForSnippet({
              selector: input.selector as { by: "id" | "text" | "label"; value: string },
              visible: input.visible as boolean | undefined,
              timeout: input.timeout as number | undefined,
            });
            const waitResult = await runDetoxAction({
              actionName: `waitFor:${(input.selector as { value: string }).value}`,
              actionSnippet: waitSnippet,
            });
            return { success: waitResult.success, result: waitResult, error: waitResult.error?.message };
    
          case "simulator.screenshot":
            const screenshot = await takeScreenshot(input.name as string | undefined);
            return { success: true, result: screenshot };
    
          default:
            return { success: false, error: `Unknown tool: ${toolName}` };
        }
      } catch (error) {
        return {
          success: false,
          error: error instanceof Error ? error.message : "Unknown error",
        };
      }
    };

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/andreahaku/expo_ios_development_mcp'

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