Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/mcp/test/auto-screenshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe("shouldAutoScreenshot", () => {
});

it("returns false for excluded tools", () => {
expect(shouldAutoScreenshot("list-simulators")).toBe(false);
expect(shouldAutoScreenshot("list-devices")).toBe(false);
expect(shouldAutoScreenshot("boot-simulator")).toBe(false);
expect(shouldAutoScreenshot("simulator-server")).toBe(false);
expect(shouldAutoScreenshot("activate-sso")).toBe(false);
Expand Down
15 changes: 11 additions & 4 deletions packages/skills/skills/argent-ios-profiler/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,20 @@ After presenting findings, ask the user whether to investigate further, implemen

**Complete all steps in order — do not break mid-flow.**

### Step 0: Ensure the target app is running
### Step 0: Choose device and ensure the target app is running

The `ios-profiler-start` tool **auto-detects** the running app on the simulator.
**Simulator vs physical device:** Call `list-devices` to find a target device.

- **Prefer simulators** for fast iteration, CI, and most development workflows.
- **Use a physical device** (`include_physical_devices: true`) when the user explicitly asks for device profiling, or when you need accurate real-world data: CPU/GPU timings, thermal throttling, real memory behavior, or hardware-dependent features (camera, GPS, NFC, push notifications).

> Physical devices do **not** support automated interaction (taps, swipes, screenshots, describe) — only profiling and debugging tools work. The user must navigate the device by hand.

The `ios-profiler-start` tool **auto-detects** the running app on the simulator or device.
You do not need to derive `app_process` manually — just make sure the app is launched.

1. If the app is already running on the simulator, skip to Step 1 (do not pass `app_process`).
2. If the app is not running, use `launch-app` with the correct bundle ID first.
1. If the app is already running, skip to Step 1 (do not pass `app_process`).
2. If the app is not running on a simulator, use `launch-app` with the correct bundle ID first. On a physical device, ask the user to launch the app.
3. Only pass `app_process` explicitly if the tool reports multiple running user apps and you need to disambiguate.

> **Note**: If multiple build flavors are installed (dev, staging, prod), the tool will detect whichever one is currently running. If both are running, it will ask you to specify.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Once you discover the correct build/run workflow for a project, **save it to pro

| Action | Tool / Command |
| -------------------------- | -------------------------------------------------- |
| List devices | `list-simulators` tool |
| List devices | `list-devices` tool |
| Boot a simulator | `boot-simulator` tool (pass UDID) |
| Launch an app | `launch-app` tool (pass UDID + bundle ID) |
| Restart an app | `restart-app` tool (pass UDID + bundle ID) |
Expand Down Expand Up @@ -210,7 +210,7 @@ If the user's intent is ambiguous (run existing tests, write new tests, or find
| Start Metro | `npx react-native start` |
| Start Metro (reset cache) | `npx react-native start --reset-cache` |
| Run iOS app | `npx react-native run-ios` |
| List simulators | `list-simulators` tool |
| List devices | `list-devices` tool |
| Boot simulator | `boot-simulator` tool |
| Take screenshot | `screenshot` tool |
| Describe screen (a11y tree) | `describe` tool for normal app screens and in-app modals; use `screenshot` only when permission/system overlays are not exposed reliably |
Expand Down
4 changes: 3 additions & 1 deletion packages/skills/skills/argent-simulator-interact/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ description: Interact with an iOS simulator using argent MCP tools. Use when tap

If you delegate simulator tasks to sub-agents, make sure they have MCP permissions.

Use `list-simulators` to find available simulators. **Pick the first result** if specific not specified by user — booted iPhones are listed first. If none are booted, use `boot-simulator` first.
Use `list-devices` to find available simulators. **Pick the first result** if specific not specified by user — booted iPhones are listed first. If none are booted, use `boot-simulator` first.

> **Physical devices** do not support interaction tools (taps, swipes, screenshots, describe). For physical device workflows, use profiling and debugging tools only — see `argent-ios-profiler` skill.

**Load tool schemas before first use.** Gesture tools (`gesture-tap`, `gesture-swipe`, `gesture-pinch`, `gesture-rotate`, `gesture-custom`) may be deferred — their parameter schemas are not loaded until fetched. Always use ToolSearch to load the schemas of all gesture tools you plan to use **before** calling any of them. If you skip this step, parameters may be coerced to strings instead of numbers, causing validation errors.

Expand Down
2 changes: 1 addition & 1 deletion packages/skills/skills/argent-simulator-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: Set up and connect to an iOS simulator using argent MCP tools. Use
If you delegate simulator tasks to sub-agents, make sure they have MCP permissions.

1. **Find a booted simulator**
Use `list-simulators`. Pick the first result — booted iPhones are listed first.
Use `list-devices`. Pick the first result — booted iPhones are listed first.
If none are booted, use `boot-simulator` with the desired UDID.

2. **Verify connection**
Expand Down
4 changes: 3 additions & 1 deletion packages/tool-server/src/blueprints/js-runtime-debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ export const jsRuntimeDebuggerBlueprint: ServiceBlueprint<JsRuntimeDebuggerApi,

await cdp.evaluate(DISABLE_LOGBOX_SCRIPT).catch(warnOnError("DISABLE_LOGBOX_SCRIPT"));

await sourceMaps.waitForPending();
// Let source maps load in the background – consumers that need them
// (e.g. debugger-set-breakpoint) already call waitForPending() themselves.
sourceMaps.waitForPending().catch(ignore);

const sourceResolver = createSourceResolver(port, metro.projectRoot);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { z } from "zod";
import { spawn, execSync } from "child_process";
import { spawn } from "child_process";
import * as path from "path";
import type { ToolDefinition } from "@argent/registry";
import {
IOS_PROFILER_SESSION_NAMESPACE,
type IosProfilerSessionApi,
} from "../../../blueprints/ios-profiler-session";
import { getDebugDir } from "../../../utils/react-profiler/debug/dump";
import {
checkIsSimulator,
detectRunningAppOnSimulator,
detectRunningAppOnDevice,
} from "../../../utils/ios-device";

const DEFAULT_TEMPLATE_PATH = path.resolve(__dirname, "Argent.tracetemplate");

Expand All @@ -16,77 +21,14 @@ const zodSchema = z.object({
.string()
.optional()
.describe(
"The exact CFBundleExecutable of the app to profile. If omitted, auto-detects the currently running foreground app on the simulator. Only provide this if auto-detection picks the wrong app (e.g. multiple apps running)."
"The exact CFBundleExecutable of the app to profile. If omitted, auto-detects the currently running foreground app on the simulator or device. Only provide this if auto-detection picks the wrong app (e.g. multiple apps running)."
),
template_path: z
.string()
.optional()
.describe("Path to an Instruments .tracetemplate file (defaults to bundled Argent template)"),
});

interface AppInfo {
CFBundleExecutable: string;
CFBundleIdentifier: string;
CFBundleDisplayName?: string;
ApplicationType: string;
}

function detectRunningApp(udid: string): string {
// 1. Get running UIKitApplication processes
const launchctlOutput = execSync(`xcrun simctl spawn ${udid} launchctl list`, {
encoding: "utf-8",
});

const runningBundleIds = new Set<string>();
for (const line of launchctlOutput.split("\n")) {
const match = line.match(/UIKitApplication:([^\[]+)/);
if (match) {
runningBundleIds.add(match[1]);
}
}

if (runningBundleIds.size === 0) {
throw new Error(
"No running apps detected on the simulator. Launch the app first using `launch-app`, then retry."
);
}

// 2. Get installed app metadata
const listAppsOutput = execSync(`xcrun simctl listapps ${udid} | plutil -convert json -o - -`, {
encoding: "utf-8",
});

const installedApps: Record<string, AppInfo> = JSON.parse(listAppsOutput);

// 3. Cross-reference: running user apps
const runningUserApps: AppInfo[] = [];
for (const [, appInfo] of Object.entries(installedApps)) {
if (appInfo.ApplicationType === "User" && runningBundleIds.has(appInfo.CFBundleIdentifier)) {
runningUserApps.push(appInfo);
}
}

if (runningUserApps.length === 0) {
throw new Error(
"No running user apps detected on the simulator (only system apps are running). Launch the app first using `launch-app`, then retry."
);
}

if (runningUserApps.length > 1) {
const appList = runningUserApps
.map(
(a) =>
` - ${a.CFBundleExecutable} (${a.CFBundleIdentifier}${a.CFBundleDisplayName ? `, "${a.CFBundleDisplayName}"` : ""})`
)
.join("\n");
throw new Error(
`Multiple user apps are running on the simulator:\n${appList}\nSpecify \`app_process\` with the CFBundleExecutable of the app you want to profile.`
);
}

return runningUserApps[0].CFBundleExecutable;
}

export const iosInstrumentsStartTool: ToolDefinition<
z.infer<typeof zodSchema>,
{ status: "recording"; pid: number; traceFile: string }
Expand All @@ -110,7 +52,14 @@ Fails if no app is running on the simulator or xctrace cannot attach to the proc
}

const templatePath = params.template_path ?? DEFAULT_TEMPLATE_PATH;
const appProcess = params.app_process ?? detectRunningApp(params.device_id);

let appProcess = params.app_process;
if (!appProcess) {
const isSimulator = await checkIsSimulator(params.device_id);
appProcess = isSimulator
? detectRunningAppOnSimulator(params.device_id)
: await detectRunningAppOnDevice(params.device_id);
}

const debugDir = await getDebugDir();
const timestamp = new Date()
Expand Down
104 changes: 104 additions & 0 deletions packages/tool-server/src/tools/simulator/list-devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { z } from "zod";
import type { ToolDefinition } from "@argent/registry";
import { listPhysicalDevices } from "../../utils/ios-device";

const execFileAsync = promisify(execFile);

interface SimctlDevice {
udid: string;
name: string;
state: string;
deviceTypeIdentifier: string;
isAvailable: boolean;
}

interface SimctlOutput {
devices: Record<string, SimctlDevice[]>;
}

const zodSchema = z.object({
include_physical_devices: z
.boolean()
.optional()
.default(false)
.describe(
"Also scan for physical iOS devices connected via USB or Wi-Fi. Slower (~5s), requires Xcode 15+."
),
});

export const listDevicesTool: ToolDefinition = {
id: "list-devices",
description: [
"List available iOS devices (simulators and optionally physical devices). Use when you need a UDID or want to see which simulators are Booted vs Shutdown.",
"By default returns only simulators (fast) — each with udid, name, state, runtime, and isAvailable. Set `include_physical_devices: true` to also scan for physical devices connected via USB or Wi-Fi (slower, requires Xcode 15+); physical-device entries additionally include model, osVersion, and connectionType.",
"",
"WHEN TO INCLUDE PHYSICAL DEVICES:",
"- User explicitly asks to run/test on a real device",
"- Task requires device-only hardware: camera, GPS, NFC, Bluetooth, push notifications",
"- Profiling real-world performance (thermal throttling, actual CPU/GPU)",
"- Testing on a specific OS version not available in simulators",
"",
"IMPORTANT LIMITATION: Physical devices do NOT support automated interaction (taps, swipes, screenshots, describe, etc.) — the user must navigate the device by hand. Only profiling and debugging tools work on physical devices.",
"",
"PREFER SIMULATORS for: fast iteration, CI, UI testing, automated interaction, and most development workflows.",
"",
"Fails if Xcode command-line tools are not installed.",
].join("\n"),
zodSchema,
services: () => ({}),
async execute(_services, params) {
const { include_physical_devices } = params as unknown as z.infer<typeof zodSchema>;

// Always list simulators
const { stdout } = await execFileAsync("xcrun", ["simctl", "list", "devices", "--json"]);
const data: SimctlOutput = JSON.parse(stdout);

const devices: Record<string, unknown>[] = [];

for (const [runtimeId, runtimeDevices] of Object.entries(data.devices)) {
if (!runtimeId.includes("iOS")) continue;
for (const device of runtimeDevices) {
if (!device.isAvailable) continue;
devices.push({
type: "simulator",
udid: device.udid,
name: device.name,
state: device.state,
runtime: runtimeId,
isAvailable: device.isAvailable,
});
}
}

// Sort simulators: booted first, iPhones before iPads
devices.sort((a, b) => {
const aBooted = a.state === "Booted" ? 0 : 1;
const bBooted = b.state === "Booted" ? 0 : 1;
if (aBooted !== bBooted) return aBooted - bBooted;
const aIpad = (a.name as string).includes("iPad") ? 1 : 0;
const bIpad = (b.name as string).includes("iPad") ? 1 : 0;
return aIpad - bIpad;
});

// Optionally scan for physical devices
let physicalDevicesError: string | undefined;
if (include_physical_devices) {
const { devices: physicalDevices, error } = await listPhysicalDevices();
physicalDevicesError = error;
for (const pd of physicalDevices) {
devices.push({
type: "physical_device",
udid: pd.udid,
name: pd.name,
model: pd.model,
osVersion: pd.osVersion,
connectionType: pd.connectionType,
});
}
}

return physicalDevicesError ? { devices, physicalDevicesError } : { devices };
},
};
64 changes: 0 additions & 64 deletions packages/tool-server/src/tools/simulator/list-simulators.ts

This file was deleted.

Loading
Loading