mobile-debug-mcp 0.24.1 → 0.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interact/index.js +124 -17
- package/dist/manage/android.js +24 -4
- package/dist/manage/ios.js +27 -3
- package/dist/server/common.js +32 -4
- package/dist/server/tool-definitions.js +8 -8
- package/dist/server/tool-handlers.js +22 -6
- package/dist/system/index.js +53 -2
- package/docs/CHANGELOG.md +8 -0
- package/docs/specs/baseline-spec-v0.md +312 -0
- package/docs/specs/mcp-tooling-spec-v1.md +272 -0
- package/docs/tools/interact.md +76 -10
- package/docs/tools/manage.md +17 -2
- package/docs/tools/system.md +23 -1
- package/package.json +1 -1
- package/src/interact/index.ts +126 -18
- package/src/manage/android.ts +24 -4
- package/src/manage/ios.ts +27 -3
- package/src/server/common.ts +36 -4
- package/src/server/tool-definitions.ts +8 -8
- package/src/server/tool-handlers.ts +23 -6
- package/src/system/index.ts +57 -2
- package/src/types.ts +37 -1
- package/test/unit/interact/expect_tools.test.ts +29 -2
- package/test/unit/interact/wait_for_screen_change.test.ts +7 -3
- package/test/unit/interact/wait_for_ui_selector_matching.test.ts +19 -0
- package/test/unit/server/response_shapes.test.ts +48 -4
- package/test/unit/system/get_system_status.test.ts +2 -0
- package/test/unit/system/system_status.test.ts +6 -0
package/dist/interact/index.js
CHANGED
|
@@ -185,9 +185,10 @@ export class ToolsInteract {
|
|
|
185
185
|
return await interact.tap(x, y, resolved.id);
|
|
186
186
|
}
|
|
187
187
|
static async tapElementHandler({ elementId }) {
|
|
188
|
-
const
|
|
188
|
+
const timestampMs = Date.now();
|
|
189
|
+
const timestamp = new Date(timestampMs).toISOString();
|
|
189
190
|
const actionType = 'tap_element';
|
|
190
|
-
const actionId = nextActionId(actionType,
|
|
191
|
+
const actionId = nextActionId(actionType, timestampMs);
|
|
191
192
|
const selector = { elementId };
|
|
192
193
|
const resolved = ToolsInteract._resolvedUiElements.get(elementId);
|
|
193
194
|
if (!resolved) {
|
|
@@ -225,6 +226,7 @@ export class ToolsInteract {
|
|
|
225
226
|
action_id: actionId,
|
|
226
227
|
timestamp,
|
|
227
228
|
action_type: actionType,
|
|
229
|
+
...(tree?.device ? { device: tree.device } : {}),
|
|
228
230
|
target: {
|
|
229
231
|
selector,
|
|
230
232
|
resolved: resolvedTarget
|
|
@@ -437,6 +439,12 @@ export class ToolsInteract {
|
|
|
437
439
|
}
|
|
438
440
|
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
|
|
439
441
|
const overallStart = Date.now();
|
|
442
|
+
const requestedIndex = typeof match?.index === 'number' ? match.index : null;
|
|
443
|
+
const requested = {
|
|
444
|
+
selector: selector ?? {},
|
|
445
|
+
condition,
|
|
446
|
+
match: requestedIndex === null ? null : { index: requestedIndex }
|
|
447
|
+
};
|
|
440
448
|
// Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
|
|
441
449
|
const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
|
|
442
450
|
const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
|
|
@@ -448,22 +456,27 @@ export class ToolsInteract {
|
|
|
448
456
|
code: 'INVALID_SELECTOR',
|
|
449
457
|
message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
|
|
450
458
|
},
|
|
451
|
-
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
|
|
459
|
+
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 },
|
|
460
|
+
requested,
|
|
461
|
+
observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null }
|
|
452
462
|
};
|
|
453
463
|
}
|
|
454
464
|
// Validate condition
|
|
455
465
|
if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) {
|
|
456
|
-
return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
|
|
466
|
+
return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } };
|
|
457
467
|
}
|
|
458
468
|
// Platform check
|
|
459
469
|
if (platform && !['android', 'ios'].includes(platform)) {
|
|
460
|
-
return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
|
|
470
|
+
return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } };
|
|
461
471
|
}
|
|
462
472
|
const effectivePoll = Math.max(50, Math.min(poll_interval_ms || 300, 2000));
|
|
463
473
|
const maxAttempts = (retry && retry.max_attempts) ? Math.max(1, retry.max_attempts) : 1;
|
|
464
474
|
const backoff = (retry && retry.backoff_ms) ? Math.max(0, retry.backoff_ms) : 0;
|
|
465
475
|
let attempts = 0;
|
|
466
476
|
let totalPollCount = 0;
|
|
477
|
+
let lastMatchedCount = 0;
|
|
478
|
+
let lastMatchedElement = null;
|
|
479
|
+
let lastConditionSatisfied = false;
|
|
467
480
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
468
481
|
const normalize = ToolsInteract._normalize;
|
|
469
482
|
const containsFlag = !!selector?.contains;
|
|
@@ -573,18 +586,27 @@ export class ToolsInteract {
|
|
|
573
586
|
else
|
|
574
587
|
conditionMet = false;
|
|
575
588
|
}
|
|
589
|
+
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
590
|
+
const resolvedDeviceId = tree?.device?.id || deviceId;
|
|
591
|
+
lastMatchedCount = matchedCount;
|
|
592
|
+
lastConditionSatisfied = conditionMet;
|
|
593
|
+
lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
|
|
576
594
|
if (conditionMet) {
|
|
577
595
|
const now = Date.now();
|
|
578
596
|
const latency_ms = now - overallStart;
|
|
579
|
-
|
|
580
|
-
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
581
|
-
const resolvedDeviceId = tree?.device?.id || deviceId;
|
|
582
|
-
const outEl = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
|
|
597
|
+
const outEl = lastMatchedElement;
|
|
583
598
|
return {
|
|
584
599
|
status: 'success',
|
|
585
600
|
matched: matchedCount,
|
|
586
601
|
element: outEl,
|
|
587
|
-
metrics: { latency_ms, poll_count: totalPollCount, attempts }
|
|
602
|
+
metrics: { latency_ms, poll_count: totalPollCount, attempts },
|
|
603
|
+
requested,
|
|
604
|
+
observed: {
|
|
605
|
+
matched_count: matchedCount,
|
|
606
|
+
condition_satisfied: true,
|
|
607
|
+
selected_index: outEl?.index ?? null,
|
|
608
|
+
last_matched_element: outEl
|
|
609
|
+
}
|
|
588
610
|
};
|
|
589
611
|
}
|
|
590
612
|
}
|
|
@@ -603,16 +625,38 @@ export class ToolsInteract {
|
|
|
603
625
|
}
|
|
604
626
|
// Final failure for this call
|
|
605
627
|
const elapsed = Date.now() - overallStart;
|
|
628
|
+
const observed = {
|
|
629
|
+
matched_count: lastMatchedCount,
|
|
630
|
+
condition_satisfied: lastConditionSatisfied,
|
|
631
|
+
selected_index: lastMatchedElement?.index ?? null,
|
|
632
|
+
last_matched_element: lastMatchedElement
|
|
633
|
+
};
|
|
634
|
+
const matchNote = requestedIndex !== null && lastMatchedCount <= requestedIndex
|
|
635
|
+
? ` requested match.index=${requestedIndex} but observed ${lastMatchedCount} match(es)`
|
|
636
|
+
: ` observed ${lastMatchedCount} match(es)`;
|
|
606
637
|
return {
|
|
607
638
|
status: 'timeout',
|
|
608
|
-
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout` },
|
|
609
|
-
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts }
|
|
639
|
+
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
640
|
+
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
641
|
+
requested,
|
|
642
|
+
observed
|
|
610
643
|
};
|
|
611
644
|
}
|
|
612
645
|
}
|
|
613
646
|
catch (err) {
|
|
614
647
|
const elapsed = Date.now() - overallStart;
|
|
615
|
-
return {
|
|
648
|
+
return {
|
|
649
|
+
status: 'timeout',
|
|
650
|
+
error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
|
|
651
|
+
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
652
|
+
requested,
|
|
653
|
+
observed: {
|
|
654
|
+
matched_count: lastMatchedCount,
|
|
655
|
+
condition_satisfied: false,
|
|
656
|
+
selected_index: lastMatchedElement?.index ?? null,
|
|
657
|
+
last_matched_element: lastMatchedElement
|
|
658
|
+
}
|
|
659
|
+
};
|
|
616
660
|
}
|
|
617
661
|
}
|
|
618
662
|
// Helper: normalize various log objects into plain message strings for comparison
|
|
@@ -635,10 +679,12 @@ export class ToolsInteract {
|
|
|
635
679
|
static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }) {
|
|
636
680
|
const start = Date.now();
|
|
637
681
|
let lastFingerprint = null;
|
|
682
|
+
let lastActivity = null;
|
|
638
683
|
while (Date.now() - start < timeoutMs) {
|
|
639
684
|
try {
|
|
640
685
|
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
641
686
|
const fp = res?.fingerprint ?? null;
|
|
687
|
+
lastActivity = res?.activity ?? lastActivity;
|
|
642
688
|
if (fp === null || fp === undefined) {
|
|
643
689
|
lastFingerprint = null;
|
|
644
690
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
@@ -651,8 +697,18 @@ export class ToolsInteract {
|
|
|
651
697
|
try {
|
|
652
698
|
const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
653
699
|
const confirmFp = confirmRes?.fingerprint ?? null;
|
|
700
|
+
lastActivity = confirmRes?.activity ?? lastActivity;
|
|
654
701
|
if (confirmFp === fp) {
|
|
655
|
-
return {
|
|
702
|
+
return {
|
|
703
|
+
success: true,
|
|
704
|
+
previousFingerprint,
|
|
705
|
+
newFingerprint: fp,
|
|
706
|
+
elapsedMs: Date.now() - start,
|
|
707
|
+
observed_screen: {
|
|
708
|
+
fingerprint: fp,
|
|
709
|
+
activity: lastActivity
|
|
710
|
+
}
|
|
711
|
+
};
|
|
656
712
|
}
|
|
657
713
|
lastFingerprint = confirmFp;
|
|
658
714
|
continue;
|
|
@@ -668,7 +724,17 @@ export class ToolsInteract {
|
|
|
668
724
|
}
|
|
669
725
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
670
726
|
}
|
|
671
|
-
return {
|
|
727
|
+
return {
|
|
728
|
+
success: false,
|
|
729
|
+
reason: 'timeout',
|
|
730
|
+
previousFingerprint,
|
|
731
|
+
lastFingerprint,
|
|
732
|
+
elapsedMs: Date.now() - start,
|
|
733
|
+
observed_screen: {
|
|
734
|
+
fingerprint: lastFingerprint,
|
|
735
|
+
activity: lastActivity
|
|
736
|
+
}
|
|
737
|
+
};
|
|
672
738
|
}
|
|
673
739
|
static async expectScreenHandler({ platform, fingerprint, screen, deviceId }) {
|
|
674
740
|
const observedFingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
@@ -691,16 +757,26 @@ export class ToolsInteract {
|
|
|
691
757
|
screen: screen ?? null
|
|
692
758
|
};
|
|
693
759
|
let success = false;
|
|
760
|
+
let basis = 'none';
|
|
761
|
+
let reason = 'No fingerprint or screen expectation provided';
|
|
694
762
|
if (fingerprint) {
|
|
763
|
+
basis = 'fingerprint';
|
|
695
764
|
success = observedScreen.fingerprint === fingerprint;
|
|
765
|
+
reason = success
|
|
766
|
+
? `observed fingerprint matches expected fingerprint ${fingerprint}`
|
|
767
|
+
: `expected fingerprint ${fingerprint} but observed ${observedScreen.fingerprint ?? 'null'}`;
|
|
696
768
|
}
|
|
697
769
|
else if (screen) {
|
|
770
|
+
basis = 'screen';
|
|
698
771
|
const candidates = new Set();
|
|
699
772
|
if (observedScreen.screen)
|
|
700
773
|
candidates.add(observedScreen.screen);
|
|
701
774
|
if (observedScreenLabel)
|
|
702
775
|
candidates.add(observedScreenLabel);
|
|
703
776
|
success = candidates.has(screen);
|
|
777
|
+
reason = success
|
|
778
|
+
? `observed screen matches expected screen ${screen}`
|
|
779
|
+
: `expected screen ${screen} but observed ${observedScreenLabel ?? observedScreen.screen ?? 'null'}`;
|
|
704
780
|
}
|
|
705
781
|
return {
|
|
706
782
|
success,
|
|
@@ -709,7 +785,12 @@ export class ToolsInteract {
|
|
|
709
785
|
screen: observedScreenLabel
|
|
710
786
|
},
|
|
711
787
|
expected_screen: expectedScreen,
|
|
712
|
-
confidence: success ? 1 : 0
|
|
788
|
+
confidence: success ? 1 : 0,
|
|
789
|
+
comparison: {
|
|
790
|
+
basis,
|
|
791
|
+
matched: success,
|
|
792
|
+
reason
|
|
793
|
+
}
|
|
713
794
|
};
|
|
714
795
|
}
|
|
715
796
|
static async expectElementVisibleHandler({ selector, element_id, timeout_ms = 5000, poll_interval_ms = 300, platform, deviceId }) {
|
|
@@ -726,6 +807,7 @@ export class ToolsInteract {
|
|
|
726
807
|
success: true,
|
|
727
808
|
selector,
|
|
728
809
|
element_id: result.element.elementId ?? element_id ?? null,
|
|
810
|
+
expected_condition: 'visible',
|
|
729
811
|
element: {
|
|
730
812
|
elementId: result.element.elementId ?? null,
|
|
731
813
|
text: result.element.text ?? null,
|
|
@@ -734,7 +816,23 @@ export class ToolsInteract {
|
|
|
734
816
|
class: result.element.class ?? null,
|
|
735
817
|
bounds: result.element.bounds ?? null,
|
|
736
818
|
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
737
|
-
}
|
|
819
|
+
},
|
|
820
|
+
observed: {
|
|
821
|
+
status: result.status,
|
|
822
|
+
matched_count: typeof result.matched === 'number' ? result.matched : result?.observed?.matched_count ?? null,
|
|
823
|
+
condition_satisfied: true,
|
|
824
|
+
selected_index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
825
|
+
last_matched_element: {
|
|
826
|
+
elementId: result.element.elementId ?? null,
|
|
827
|
+
text: result.element.text ?? null,
|
|
828
|
+
resource_id: result.element.resource_id ?? null,
|
|
829
|
+
accessibility_id: result.element.accessibility_id ?? null,
|
|
830
|
+
class: result.element.class ?? null,
|
|
831
|
+
bounds: result.element.bounds ?? null,
|
|
832
|
+
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
reason: 'selector is visible'
|
|
738
836
|
};
|
|
739
837
|
}
|
|
740
838
|
const errorCode = result?.error?.code === 'INTERNAL_ERROR' ? 'UNKNOWN' : 'TIMEOUT';
|
|
@@ -742,6 +840,15 @@ export class ToolsInteract {
|
|
|
742
840
|
success: false,
|
|
743
841
|
selector,
|
|
744
842
|
element_id: element_id ?? null,
|
|
843
|
+
expected_condition: 'visible',
|
|
844
|
+
observed: {
|
|
845
|
+
status: result?.status,
|
|
846
|
+
matched_count: result?.observed?.matched_count,
|
|
847
|
+
condition_satisfied: result?.observed?.condition_satisfied ?? false,
|
|
848
|
+
selected_index: result?.observed?.selected_index ?? null,
|
|
849
|
+
last_matched_element: result?.observed?.last_matched_element ?? null
|
|
850
|
+
},
|
|
851
|
+
reason: result?.error?.message ?? 'selector is not visible',
|
|
745
852
|
failure_code: errorCode,
|
|
746
853
|
retryable: errorCode === 'TIMEOUT'
|
|
747
854
|
};
|
package/dist/manage/android.js
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync } from 'fs';
|
|
|
5
5
|
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js';
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js';
|
|
8
|
+
import { AndroidObserve } from '../observe/android.js';
|
|
8
9
|
export class AndroidManage {
|
|
9
10
|
isTestOnlyInstallFailure(output) {
|
|
10
11
|
return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY');
|
|
@@ -144,8 +145,21 @@ export class AndroidManage {
|
|
|
144
145
|
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
145
146
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
146
147
|
try {
|
|
147
|
-
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
148
|
-
|
|
148
|
+
const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
149
|
+
const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null);
|
|
150
|
+
return {
|
|
151
|
+
device: deviceInfo,
|
|
152
|
+
appStarted: true,
|
|
153
|
+
launchTimeMs: 1000,
|
|
154
|
+
output,
|
|
155
|
+
observedApp: {
|
|
156
|
+
appId,
|
|
157
|
+
package: current?.package ?? null,
|
|
158
|
+
activity: current?.activity ?? null,
|
|
159
|
+
screen: current?.shortActivity ?? current?.activity ?? null,
|
|
160
|
+
matchedTarget: current ? current.package === appId : null
|
|
161
|
+
}
|
|
162
|
+
};
|
|
149
163
|
}
|
|
150
164
|
catch (e) {
|
|
151
165
|
const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
@@ -165,12 +179,18 @@ export class AndroidManage {
|
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
async restartApp(appId, deviceId) {
|
|
168
|
-
await this.terminateApp(appId, deviceId);
|
|
182
|
+
const terminateResult = await this.terminateApp(appId, deviceId);
|
|
169
183
|
const startResult = await this.startApp(appId, deviceId);
|
|
170
184
|
return {
|
|
171
185
|
device: startResult.device,
|
|
172
186
|
appRestarted: startResult.appStarted,
|
|
173
|
-
launchTimeMs: startResult.launchTimeMs
|
|
187
|
+
launchTimeMs: startResult.launchTimeMs,
|
|
188
|
+
output: startResult.output,
|
|
189
|
+
observedApp: startResult.observedApp,
|
|
190
|
+
terminatedBeforeRestart: terminateResult.appTerminated,
|
|
191
|
+
...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
|
|
192
|
+
...(startResult.error ? { error: startResult.error } : {}),
|
|
193
|
+
...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
|
|
174
194
|
};
|
|
175
195
|
}
|
|
176
196
|
async resetAppData(appId, deviceId) {
|
package/dist/manage/ios.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
import { spawn, spawnSync } from "child_process";
|
|
3
3
|
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "../utils/ios/utils.js";
|
|
4
|
+
import { iOSObserve } from "../observe/ios.js";
|
|
4
5
|
import path from "path";
|
|
5
6
|
export class iOSManage {
|
|
6
7
|
async build(projectPath, optsOrVariant) {
|
|
@@ -294,7 +295,20 @@ export class iOSManage {
|
|
|
294
295
|
try {
|
|
295
296
|
const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
296
297
|
const device = await getIOSDeviceMetadata(deviceId);
|
|
297
|
-
|
|
298
|
+
const fingerprint = await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null);
|
|
299
|
+
const pidMatch = result.output.match(/:\s*(\d+)\s*$/);
|
|
300
|
+
return {
|
|
301
|
+
device,
|
|
302
|
+
appStarted: !!result.output,
|
|
303
|
+
launchTimeMs: 1000,
|
|
304
|
+
output: result.output,
|
|
305
|
+
observedApp: {
|
|
306
|
+
appId: bundleId,
|
|
307
|
+
pid: pidMatch ? Number(pidMatch[1]) : null,
|
|
308
|
+
screen: fingerprint?.activity ?? null,
|
|
309
|
+
matchedTarget: null
|
|
310
|
+
}
|
|
311
|
+
};
|
|
298
312
|
}
|
|
299
313
|
catch (e) {
|
|
300
314
|
const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
@@ -316,9 +330,19 @@ export class iOSManage {
|
|
|
316
330
|
}
|
|
317
331
|
}
|
|
318
332
|
async restartApp(bundleId, deviceId = "booted") {
|
|
319
|
-
await this.terminateApp(bundleId, deviceId);
|
|
333
|
+
const terminateResult = await this.terminateApp(bundleId, deviceId);
|
|
320
334
|
const startResult = await this.startApp(bundleId, deviceId);
|
|
321
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
device: startResult.device,
|
|
337
|
+
appRestarted: startResult.appStarted,
|
|
338
|
+
launchTimeMs: startResult.launchTimeMs,
|
|
339
|
+
output: startResult.output,
|
|
340
|
+
observedApp: startResult.observedApp,
|
|
341
|
+
terminatedBeforeRestart: terminateResult.appTerminated,
|
|
342
|
+
...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
|
|
343
|
+
...(startResult.error ? { error: startResult.error } : {}),
|
|
344
|
+
...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
|
|
345
|
+
};
|
|
322
346
|
}
|
|
323
347
|
async resetAppData(bundleId, deviceId = "booted") {
|
|
324
348
|
validateBundleId(bundleId);
|
package/dist/server/common.js
CHANGED
|
@@ -48,12 +48,14 @@ export function inferScrollFailure(message) {
|
|
|
48
48
|
return { failureCode: 'TIMEOUT', retryable: true };
|
|
49
49
|
return { failureCode: 'UNKNOWN', retryable: false };
|
|
50
50
|
}
|
|
51
|
-
export function buildActionExecutionResult({ actionType, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure }) {
|
|
52
|
-
const
|
|
51
|
+
export function buildActionExecutionResult({ actionType, device, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure, details }) {
|
|
52
|
+
const timestampMs = Date.now();
|
|
53
|
+
const timestamp = new Date(timestampMs).toISOString();
|
|
53
54
|
return {
|
|
54
|
-
action_id: nextActionId(actionType,
|
|
55
|
+
action_id: nextActionId(actionType, timestampMs),
|
|
55
56
|
timestamp,
|
|
56
57
|
action_type: actionType,
|
|
58
|
+
...(device ? { device } : {}),
|
|
57
59
|
target: {
|
|
58
60
|
selector,
|
|
59
61
|
resolved: normalizeResolvedTarget(resolved)
|
|
@@ -61,6 +63,32 @@ export function buildActionExecutionResult({ actionType, selector, resolved, suc
|
|
|
61
63
|
success,
|
|
62
64
|
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
|
|
63
65
|
ui_fingerprint_before: uiFingerprintBefore,
|
|
64
|
-
ui_fingerprint_after: uiFingerprintAfter
|
|
66
|
+
ui_fingerprint_after: uiFingerprintAfter,
|
|
67
|
+
...(details ? { details } : {})
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function wrapToolError(name, error) {
|
|
71
|
+
const message = error instanceof Error
|
|
72
|
+
? error.message
|
|
73
|
+
: typeof error === 'object' && error !== null
|
|
74
|
+
? (() => {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.stringify(error, null, 2);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return '[unserializable error object]';
|
|
80
|
+
}
|
|
81
|
+
})()
|
|
82
|
+
: String(error);
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: JSON.stringify({
|
|
87
|
+
error: {
|
|
88
|
+
tool: name,
|
|
89
|
+
message
|
|
90
|
+
}
|
|
91
|
+
}, null, 2)
|
|
92
|
+
}]
|
|
65
93
|
};
|
|
66
94
|
}
|
|
@@ -10,7 +10,7 @@ Inputs:
|
|
|
10
10
|
- deviceId (optional)
|
|
11
11
|
|
|
12
12
|
Output Structure:
|
|
13
|
-
- action_id, timestamp, action_type
|
|
13
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
14
14
|
- target.selector = { appId }
|
|
15
15
|
- success = true when launch was dispatched successfully
|
|
16
16
|
- failure_code/retryable when launch dispatch fails
|
|
@@ -83,7 +83,7 @@ Inputs:
|
|
|
83
83
|
- deviceId (optional)
|
|
84
84
|
|
|
85
85
|
Output Structure:
|
|
86
|
-
- action_id, timestamp, action_type
|
|
86
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
87
87
|
- target.selector = { appId }
|
|
88
88
|
- success = true when the restart command completed
|
|
89
89
|
- failure_code/retryable when restart dispatch fails
|
|
@@ -532,7 +532,7 @@ Inputs:
|
|
|
532
532
|
- deviceId (optional)
|
|
533
533
|
|
|
534
534
|
Output Structure:
|
|
535
|
-
- action_id, timestamp, action_type
|
|
535
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
536
536
|
- target.selector = { x, y }
|
|
537
537
|
- success = true when the tap was dispatched
|
|
538
538
|
- failure_code/retryable when dispatch fails
|
|
@@ -587,7 +587,7 @@ Inputs:
|
|
|
587
587
|
|
|
588
588
|
Output Structure:
|
|
589
589
|
- action_id: unique timestamp-based action identifier
|
|
590
|
-
- timestamp:
|
|
590
|
+
- timestamp: ISO 8601 timestamp for the action attempt
|
|
591
591
|
- action_type: "tap_element"
|
|
592
592
|
- target.selector: original target handle ({ elementId })
|
|
593
593
|
- target.resolved: minimal resolved element info used for the tap
|
|
@@ -640,7 +640,7 @@ Inputs:
|
|
|
640
640
|
- platform/deviceId (optional)
|
|
641
641
|
|
|
642
642
|
Output Structure:
|
|
643
|
-
- action_id, timestamp, action_type
|
|
643
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
644
644
|
- target.selector = { x1, y1, x2, y2, duration }
|
|
645
645
|
- success = true when the swipe was dispatched
|
|
646
646
|
- failure_code/retryable when dispatch fails
|
|
@@ -692,7 +692,7 @@ Inputs:
|
|
|
692
692
|
- direction, maxScrolls, scrollAmount, deviceId (optional)
|
|
693
693
|
|
|
694
694
|
Output Structure:
|
|
695
|
-
- action_id, timestamp, action_type
|
|
695
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
696
696
|
- target.selector = original selector
|
|
697
697
|
- target.resolved = minimal resolved element info when found
|
|
698
698
|
- success = true when scrolling produced a visible target element
|
|
@@ -746,7 +746,7 @@ Inputs:
|
|
|
746
746
|
- platform/deviceId (optional)
|
|
747
747
|
|
|
748
748
|
Output Structure:
|
|
749
|
-
- action_id, timestamp, action_type
|
|
749
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
750
750
|
- target.selector = { text }
|
|
751
751
|
- success = true when text input was dispatched
|
|
752
752
|
- failure_code/retryable when dispatch fails
|
|
@@ -795,7 +795,7 @@ Inputs:
|
|
|
795
795
|
- platform/deviceId (optional)
|
|
796
796
|
|
|
797
797
|
Output Structure:
|
|
798
|
-
- action_id, timestamp, action_type
|
|
798
|
+
- action_id, timestamp (ISO 8601), action_type
|
|
799
799
|
- target.selector = { key: "back" }
|
|
800
800
|
- success = true when the back action was dispatched
|
|
801
801
|
- failure_code/retryable when dispatch fails
|
|
@@ -4,7 +4,7 @@ import { ToolsObserve } from '../observe/index.js';
|
|
|
4
4
|
import { classifyActionOutcome } from '../interact/classify.js';
|
|
5
5
|
import { ToolsNetwork } from '../network/index.js';
|
|
6
6
|
import { getSystemStatus } from '../system/index.js';
|
|
7
|
-
import { buildActionExecutionResult, captureActionFingerprint, inferGenericFailure, inferScrollFailure, wrapResponse } from './common.js';
|
|
7
|
+
import { buildActionExecutionResult, captureActionFingerprint, inferGenericFailure, inferScrollFailure, wrapResponse, wrapToolError } from './common.js';
|
|
8
8
|
async function handleStartApp(args) {
|
|
9
9
|
const { platform, appId, deviceId } = args;
|
|
10
10
|
const uiFingerprintBefore = await captureActionFingerprint(platform, deviceId);
|
|
@@ -13,11 +13,19 @@ async function handleStartApp(args) {
|
|
|
13
13
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
14
14
|
return wrapResponse(buildActionExecutionResult({
|
|
15
15
|
actionType: 'start_app',
|
|
16
|
+
device: res.device,
|
|
16
17
|
selector: { appId },
|
|
17
18
|
success: !!res.appStarted,
|
|
18
19
|
uiFingerprintBefore,
|
|
19
20
|
uiFingerprintAfter,
|
|
20
|
-
failure: res.appStarted ? undefined : inferGenericFailure(res.error)
|
|
21
|
+
failure: res.appStarted ? undefined : inferGenericFailure(res.error),
|
|
22
|
+
details: {
|
|
23
|
+
launch_time_ms: res.launchTimeMs,
|
|
24
|
+
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
25
|
+
...(res.device ? { device_id: res.device.id } : {}),
|
|
26
|
+
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
27
|
+
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
28
|
+
}
|
|
21
29
|
}));
|
|
22
30
|
}
|
|
23
31
|
async function handleTerminateApp(args) {
|
|
@@ -34,11 +42,20 @@ async function handleRestartApp(args) {
|
|
|
34
42
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
35
43
|
return wrapResponse(buildActionExecutionResult({
|
|
36
44
|
actionType: 'restart_app',
|
|
45
|
+
device: res.device,
|
|
37
46
|
selector: { appId },
|
|
38
47
|
success: !!res.appRestarted,
|
|
39
48
|
uiFingerprintBefore,
|
|
40
49
|
uiFingerprintAfter,
|
|
41
|
-
failure: res.appRestarted ? undefined : inferGenericFailure(res.error)
|
|
50
|
+
failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
|
|
51
|
+
details: {
|
|
52
|
+
launch_time_ms: res.launchTimeMs,
|
|
53
|
+
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
54
|
+
...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
|
|
55
|
+
...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
|
|
56
|
+
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
57
|
+
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
58
|
+
}
|
|
42
59
|
}));
|
|
43
60
|
}
|
|
44
61
|
async function handleResetAppData(args) {
|
|
@@ -313,8 +330,7 @@ export async function handleToolCall(name, args = {}) {
|
|
|
313
330
|
return await handler(args);
|
|
314
331
|
}
|
|
315
332
|
catch (error) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
};
|
|
333
|
+
console.error(`Error executing tool ${name}:`, error);
|
|
334
|
+
return wrapToolError(name, error);
|
|
319
335
|
}
|
|
320
336
|
}
|
package/dist/system/index.js
CHANGED
|
@@ -8,8 +8,30 @@ export async function getSystemStatus() {
|
|
|
8
8
|
const gradle = await checkGradle();
|
|
9
9
|
const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])];
|
|
10
10
|
const success = issues.length === 0;
|
|
11
|
+
const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue));
|
|
12
|
+
const iosReady = ios.iosAvailable && ios.iosDevices > 0;
|
|
13
|
+
const gradleReady = (gradle.issues || []).length === 0;
|
|
14
|
+
const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked');
|
|
15
|
+
const androidSummary = !android.adbAvailable
|
|
16
|
+
? 'ADB unavailable'
|
|
17
|
+
: android.devices === 0
|
|
18
|
+
? 'ADB available but no Android devices connected'
|
|
19
|
+
: android.logsAvailable
|
|
20
|
+
? `${android.devices} Android device(s) connected; log access available`
|
|
21
|
+
: `${android.devices} Android device(s) connected; log access unavailable`;
|
|
22
|
+
const iosSummary = !ios.iosAvailable
|
|
23
|
+
? 'xcrun unavailable'
|
|
24
|
+
: ios.iosDevices === 0
|
|
25
|
+
? 'xcrun available but no iOS simulators booted'
|
|
26
|
+
: `${ios.iosDevices} iOS simulator(s) booted`;
|
|
27
|
+
const gradleSummary = !gradle.gradleJavaHome
|
|
28
|
+
? 'No explicit Gradle JDK override detected'
|
|
29
|
+
: gradleReady
|
|
30
|
+
? `Gradle JDK configured at ${gradle.gradleJavaHome}`
|
|
31
|
+
: `Gradle JDK override invalid: ${gradle.gradleJavaHome}`;
|
|
11
32
|
return {
|
|
12
33
|
success,
|
|
34
|
+
status: overallStatus,
|
|
13
35
|
adbAvailable: android.adbAvailable,
|
|
14
36
|
adbVersion: android.adbVersion,
|
|
15
37
|
devices: android.devices,
|
|
@@ -23,10 +45,39 @@ export async function getSystemStatus() {
|
|
|
23
45
|
gradleJavaHome: gradle.gradleJavaHome,
|
|
24
46
|
gradleValid: gradle.gradleValid,
|
|
25
47
|
gradleFilesChecked: gradle.filesChecked,
|
|
26
|
-
gradleSuggestedFixes: gradle.suggestedFixes
|
|
48
|
+
gradleSuggestedFixes: gradle.suggestedFixes,
|
|
49
|
+
summary: {
|
|
50
|
+
overall: overallStatus,
|
|
51
|
+
android: {
|
|
52
|
+
ready: androidReady,
|
|
53
|
+
summary: androidSummary,
|
|
54
|
+
blockers: android.issues
|
|
55
|
+
},
|
|
56
|
+
ios: {
|
|
57
|
+
ready: iosReady,
|
|
58
|
+
summary: iosSummary,
|
|
59
|
+
blockers: ios.issues
|
|
60
|
+
},
|
|
61
|
+
gradle: {
|
|
62
|
+
ready: gradleReady,
|
|
63
|
+
summary: gradleSummary,
|
|
64
|
+
blockers: gradle.issues || [],
|
|
65
|
+
suggestedFixes: gradle.suggestedFixes || []
|
|
66
|
+
}
|
|
67
|
+
}
|
|
27
68
|
};
|
|
28
69
|
}
|
|
29
70
|
catch (e) {
|
|
30
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
status: 'blocked',
|
|
74
|
+
issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))],
|
|
75
|
+
summary: {
|
|
76
|
+
overall: 'blocked',
|
|
77
|
+
android: { ready: false, summary: 'Android status unavailable', blockers: [] },
|
|
78
|
+
ios: { ready: false, summary: 'iOS status unavailable', blockers: [] },
|
|
79
|
+
gradle: { ready: false, summary: 'Gradle status unavailable', blockers: [], suggestedFixes: [] }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
31
82
|
}
|
|
32
83
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.24.3]
|
|
6
|
+
- Improved output consistency
|
|
7
|
+
|
|
8
|
+
## [0.24.2]
|
|
9
|
+
- Fixed Android install issue
|
|
10
|
+
- Updated tools to have more detailed responses
|
|
11
|
+
|
|
5
12
|
## [0.24.1]
|
|
6
13
|
- Fixed Android install issue
|
|
14
|
+
- Updated tools to have more detailed responses
|
|
7
15
|
|
|
8
16
|
## [0.24.0]
|
|
9
17
|
- Improved execution loop
|