Repository: mobile-next/mobile-mcp

Issue:
mobile_list_apps is not detecting all apps on iOS
**Describe the bug**
mobile_list_apps is not finding the app I have installed on the simulator. I can see the app and interact with it in the simulator, but when the mpc tool calls mobile_list_apps it is not in the list. On android it worked fine. 

The interesting thing is that I installed WebDriverAgent and the app that it installs is also visible, but mine isn't. 

If I run `xcrun simctl listapps booted` I can see my app in the list. 

**Configuration (please complete the following information):**
- Agent: Cursor
- OS: Mac
- Device used:  iOS Simulator
- Device version: 18.4.1

**To Reproduce**
add an account to the "AppXYZ" in iOS

**Expected behavior**
I would expect the MCP to find the app.

**Screenshots**
If applicable, add screenshots to help explain your problem.


Discussion:
@gmegidish: @compojoom I wonder if it's a matter of regexp. Can you please run `xcrun simctl listapps booted` and paste the section that is related to your app? maybe the mcp doesn't parse it properly? thanks for helping us debug this!@compojoom: thank for looking into this. Here is the output

```
    "global.example.mobileapp" =     {
        ApplicationType = User;
        Bundle = "file:///xxx/ExampleMVP.app/";
        BundleContainer = "file:///xxx";
        CFBundleDisplayName = "Example{Something} MVP";
        CFBundleExecutable = SafeWalletMVP;
        CFBundleIdentifier = "global.example.mobileapp";
        CFBundleName = ExampleWalletMVP;
        CFBundleVersion = 1;
        DataContainer = "file:///xxx";
        GroupContainers =         {
        };
        Path = "xxx/ExampleWalletMVP.app";
        SBAppTags =         (
        );
    };
```

Maybe it is the bundle identifier because it doesn't start with com...@gmegidish: @compojoom very interesting! I'll try to reproduce this in a unittest, maybe it's related to {Wallet} inside the display name? though everything is json encoded, and regexp only cares for "doublequotes". I'll report back!@gmegidish: Yes, put this in a unittest and it indeed breaks because of the {Wallet} string. I'll re-do this code so it doesn't use regexp. It's magic and I rather do proper parsing :). Especially since there's also GroupContainers and SBAppTags objects. I'll report back later. Thank you for the bug report!@compojoom: Thanks for looking into this. It's fun playing around with this MCP! @gmegidish: Hey @compojoom . Version 0.0.17 was just released and it fixes this issue. You can see what has changed here: https://github.com/mobile-next/mobile-mcp/pull/63/commits/bf19771dcd49444ba4841ec649e3a72a03b54c74

Can you please try again and confirm?

Thank you!! 🙏 @compojoom: @gmegidish thanks for the PR. I'll try to test it when I get a chance. Just of curiousity - I looked into your PR and wondered why are you not using a node library for plist parsing? You could convert the string to a valid json and then search for keys? @gmegidish: @compojoom insert mindblown.gif here. omg. plutil actually reads this listapps output. I've never seen this textual representation being supported in any of the node libraries I looked at. I will update the code so I just pipe it through plutil. Since simulators require xcrun anyway, it's safe to assume it won't run on linux :P. Thanks for letting me know! I'll remove all the parsing code and replace with a json. 

Regardless, 0.0.17 can be used and it should be fixed for you. Let me know and I'll close the issue on github 🙏 @compojoom: @gmegidish thanks. I just tested it and it works! Now the app is visible to the agent :)@gmegidish: @compojoom Thanks for the update!

Solution patch:
diff --git a/src/iphone-simulator.ts b/src/iphone-simulator.ts
index e1bd935..fde9d52 100644
--- a/src/iphone-simulator.ts
+++ b/src/iphone-simulator.ts
@@ -75,42 +75,75 @@ export class Simctl implements Robot {
 		this.simctl("terminate", this.simulatorUuid, packageName);
 	}
 
-	private parseIOSAppData(inputText: string): Array<AppInfo> {
+	public static parseIOSAppData(inputText: string): Array<AppInfo> {
 		const result: Array<AppInfo> = [];
 
-		// Remove leading and trailing characters if needed
-		const cleanText = inputText.trim();
-
-		// Extract each app section
-		const appRegex = /"([^"]+)"\s+=\s+\{([^}]+)\};/g;
-		let appMatch;
-
-		while ((appMatch = appRegex.exec(cleanText)) !== null) {
-			// const bundleId = appMatch[1];
-			const appContent = appMatch[2];
-
-			const appInfo: Partial<AppInfo> = {
-			};
-
-			// parse simple key-value pairs
-			const keyValueRegex = /\s+(\w+)\s+=\s+([^;]+);/g;
-			let keyValueMatch;
-
-			while ((keyValueMatch = keyValueRegex.exec(appContent)) !== null) {
-				const key = keyValueMatch[1];
-				let value = keyValueMatch[2].trim();
+		enum ParseState {
+			LOOKING_FOR_APP,
+			IN_APP,
+			IN_PROPERTY
+		}
 
-				// Handle quoted string values
-				if (value.startsWith('"') && value.endsWith('"')) {
-					value = value.substring(1, value.length - 1);
-				}
+		let state = ParseState.LOOKING_FOR_APP;
+		let currentApp: Partial<AppInfo> = {};
+		let appIdentifier = "";
 
-				if (key !== "GroupContainers" && key !== "SBAppTags") {
-					(appInfo as any)[key] = value;
-				}
+		const lines = inputText.split("\n");
+		for (let line of lines) {
+			line = line.trim();
+			if (line === "") {
+				continue;
 			}
 
-			result.push(appInfo as AppInfo);
+			switch (state) {
+				case ParseState.LOOKING_FOR_APP:
+					// look for app identifier pattern: "com.example.app" = {
+					const appMatch = line.match(/^"?([^"=]+)"?\s*=\s*\{/);
+					if (appMatch) {
+						appIdentifier = appMatch[1].trim();
+						currentApp = {
+							CFBundleIdentifier: appIdentifier,
+						};
+
+						state = ParseState.IN_APP;
+					}
+					break;
+
+				case ParseState.IN_APP:
+					if (line === "};") {
+						result.push(currentApp as AppInfo);
+						currentApp = {};
+						state = ParseState.LOOKING_FOR_APP;
+					} else {
+						// look for property: PropertyName = Value;
+						const propertyMatch = line.match(/^([^=]+)\s*=\s*(.+?);\s*$/);
+						if (propertyMatch) {
+							const propName = propertyMatch[1].trim();
+							let propValue = propertyMatch[2].trim();
+
+							// remove quotes if present (they're optional)
+							if (propValue.startsWith('"') && propValue.endsWith('"')) {
+								propValue = propValue.substring(1, propValue.length - 1);
+							}
+
+							// add property to current app
+							(currentApp as any)[propName] = propValue;
+						} else if (line.endsWith("{")) {
+							// nested property like GroupContainers = {
+							state = ParseState.IN_PROPERTY;
+						}
+					}
+					break;
+
+				case ParseState.IN_PROPERTY:
+					if (line === "};") {
+						// end of nested property
+						state = ParseState.IN_APP;
+					}
+
+					// skip content of nested properties, we don't care of those right now
+					break;
+			}
 		}
 
 		return result;
@@ -118,7 +151,7 @@ export class Simctl implements Robot {
 
 	public async listApps(): Promise<InstalledApp[]> {
 		const text = this.simctl("listapps", this.simulatorUuid).toString();
-		const apps = this.parseIOSAppData(text);
+		const apps = Simctl.parseIOSAppData(text);
 		return apps.map(app => ({
 			packageName: app.CFBundleIdentifier,
 			appName: app.CFBundleDisplayName,



Test patch:
diff --git a/test/iphone-simulator.ts b/test/iphone-simulator.ts
index 646914b..1c6c473 100644
--- a/test/iphone-simulator.ts
+++ b/test/iphone-simulator.ts
@@ -1,7 +1,7 @@
 import assert from "assert";
 
 import { PNG } from "../src/png";
-import { SimctlManager } from "../src/iphone-simulator";
+import { Simctl, SimctlManager } from "../src/iphone-simulator";
 import { randomBytes } from "crypto";
 
 describe("iphone-simulator", () => {
@@ -94,7 +94,7 @@ describe("iphone-simulator", () => {
 
 		// must be a valid png image that matches the screen size
 		const image = new PNG(screenshot);
-		const pngSize =	image.getDimensions();
+		const pngSize = image.getDimensions();
 		const screenSize = await simctl.getScreenSize();
 		assert.equal(pngSize.width, screenSize.width * screenSize.scale);
 		assert.equal(pngSize.height, screenSize.height * screenSize.scale);
@@ -162,4 +162,56 @@ describe("iphone-simulator", () => {
 			assert.ok(error.message.includes("Button \"NOT_A_BUTTON\" is not supported"));
 		}
 	});
+
+	// this test exists for regression testing, see issue #59
+	it("should parse listapps output properly", () => {
+		const text = `
+		{
+			".xctrunner" =     {
+				ApplicationType = User;
+				Bundle = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Bundle/Application/8EB6C622-A298-4BB6-8E29-AA2A5CE062EF/WebDriverAgentRunner-Runner.app/";
+				BundleContainer = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Bundle/Application/8EB6C622-A298-4BB6-8E29-AA2A5CE062EF/";
+				CFBundleDisplayName = "WebDriverAgentRunner-Runner";
+				CFBundleExecutable = "WebDriverAgentRunner-Runner";
+				CFBundleIdentifier = ".xctrunner";
+				CFBundleName = "WebDriverAgentRunner-Runner";
+				CFBundleVersion = 1;
+				DataContainer = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Data/Application/AE7D36A1-AACA-4171-A070-645992DEAEB9/";
+				GroupContainers =         {
+				};
+				Path = "/Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Bundle/Application/8EB6C622-A298-4BB6-8E29-AA2A5CE062EF/WebDriverAgentRunner-Runner.app";
+				SBAppTags =         (
+				);
+			};
+			"com.mobilenext.sample1" =     {
+				ApplicationType = System;
+				Bundle = "file:///Library/Developer/CoreSimulator/Volumes/iOS_22D8075/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS%2018.3.simruntime/Contents/Resources/RuntimeRoot/Applications/Bridge.app/";
+				CFBundleDisplayName = Sample1;
+				CFBundleExecutable = Sample1;
+				CFBundleIdentifier = "com.mobilenext.sample1";
+				CFBundleName = "Sample{1}App";
+				CFBundleVersion = "1.0";
+				DataContainer = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Data/Application/0D5C84C1-044C-4C03-B443-A1416DC1A296/";
+				GroupContainers =         {
+					"243LU875E5.groups.com.apple.podcasts" = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Shared/AppGroup/8B2DA97D-2308-4B65-B87F-1E71493477E5/";
+					"group.com.apple.bridge" = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Shared/AppGroup/F6E42206-B548-4F83-AB13-6E5BD7D69AB0/";
+					"group.com.apple.iBooks" = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Shared/AppGroup/EEBEC72A-6673-446A-AB31-E154AB850B69/";
+					"group.com.apple.mail" = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Shared/AppGroup/C3339457-EB6A-46D1-92A3-AE398DA8CAC5/";
+					"group.com.apple.stocks" = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Shared/AppGroup/50E3741D-E249-421F-83E8-24A896A1245B/";
+					"group.com.apple.weather" = "file:///Library/Developer/CoreSimulator/Devices/FB9D9985-8FD0-493D-9B09-58FD3AA4BE65/data/Containers/Shared/AppGroup/28D40933-57F1-4B65-864F-1F6538B3ADF9/";
+				};
+				Path = "/Library/Developer/CoreSimulator/Volumes/iOS_22D8075/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 18.3.simruntime/Contents/Resources/RuntimeRoot/Applications/Bridge.app";
+				SBAppTags =         (
+					"watch-companion"
+				);
+			};
+		}
+		`;
+
+		const apps = Simctl.parseIOSAppData(text);
+		assert.equal(apps.length, 2);
+		assert.equal(apps[0].CFBundleDisplayName, "WebDriverAgentRunner-Runner");
+		assert.equal(apps[1].CFBundleDisplayName, "Sample1");
+		assert.equal(apps[1].CFBundleName, "Sample{1}App");
+	});
 });