mobile-debug-mcp 0.16.0 → 0.17.0
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/observe/index.js +103 -0
- package/dist/server.js +21 -0
- package/docs/CHANGELOG.md +3 -0
- package/docs/tools/observe.md +44 -0
- package/package.json +1 -1
- package/src/interact/index.ts +7 -5
- package/src/observe/index.ts +88 -0
- package/src/server.ts +23 -0
- package/test/observe/unit/capture_debug_snapshot.test.ts +89 -0
- package/test/unit/index.ts +1 -0
package/dist/observe/index.js
CHANGED
|
@@ -82,4 +82,107 @@ export class ToolsObserve {
|
|
|
82
82
|
// Both observes implement getScreenFingerprint
|
|
83
83
|
return await observe.getScreenFingerprint(resolved.id);
|
|
84
84
|
}
|
|
85
|
+
static async captureDebugSnapshotHandler({ reason, includeLogs = true, logLines = 200, platform, appId, deviceId, sessionId } = {}) {
|
|
86
|
+
const timestamp = Date.now();
|
|
87
|
+
const out = { timestamp, reason: reason || '', activity: null, fingerprint: null, screenshot: null, ui_tree: null, logs: [] };
|
|
88
|
+
// Parallel fetches for performance: screenshot, current screen, fingerprint, ui tree, and log stream/get logs
|
|
89
|
+
const sid = sessionId || 'default';
|
|
90
|
+
const tasks = {
|
|
91
|
+
screenshot: ToolsObserve.captureScreenshotHandler({ platform, deviceId }),
|
|
92
|
+
currentScreen: (!platform || platform === 'android') ? ToolsObserve.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
|
|
93
|
+
fingerprint: ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }),
|
|
94
|
+
uiTree: ToolsObserve.getUITreeHandler({ platform, deviceId }),
|
|
95
|
+
readLogStream: includeLogs ? ToolsObserve.readLogStreamHandler({ platform, sessionId: sid, limit: logLines }) : Promise.resolve({ entries: [] }),
|
|
96
|
+
};
|
|
97
|
+
const results = await Promise.allSettled(Object.values(tasks));
|
|
98
|
+
const keys = Object.keys(tasks);
|
|
99
|
+
// Map results back to keys
|
|
100
|
+
for (let i = 0; i < results.length; i++) {
|
|
101
|
+
const key = keys[i];
|
|
102
|
+
const res = results[i];
|
|
103
|
+
if (res.status === 'fulfilled') {
|
|
104
|
+
const val = res.value;
|
|
105
|
+
if (key === 'screenshot') {
|
|
106
|
+
out.screenshot = val && val.screenshot ? val.screenshot : null;
|
|
107
|
+
}
|
|
108
|
+
else if (key === 'currentScreen') {
|
|
109
|
+
out.activity = val && ((val.activity || val.shortActivity)) ? (val.activity || val.shortActivity) : out.activity || '';
|
|
110
|
+
}
|
|
111
|
+
else if (key === 'fingerprint') {
|
|
112
|
+
if (val && val.fingerprint)
|
|
113
|
+
out.fingerprint = val.fingerprint;
|
|
114
|
+
if (val && val.activity)
|
|
115
|
+
out.activity = out.activity || val.activity;
|
|
116
|
+
if (val && val.error)
|
|
117
|
+
out.fingerprint_error = val.error;
|
|
118
|
+
}
|
|
119
|
+
else if (key === 'uiTree') {
|
|
120
|
+
out.ui_tree = val;
|
|
121
|
+
if (val && val.error)
|
|
122
|
+
out.ui_tree_error = val.error;
|
|
123
|
+
}
|
|
124
|
+
else if (key === 'readLogStream') {
|
|
125
|
+
// handle below after evaluating fallback
|
|
126
|
+
// temporarily attach to out._streamEntries
|
|
127
|
+
out._streamEntries = val && val.entries ? val.entries : [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const errMsg = res.reason instanceof Error ? res.reason.message : String(res.reason);
|
|
132
|
+
if (key === 'screenshot')
|
|
133
|
+
out.screenshot_error = errMsg;
|
|
134
|
+
if (key === 'currentScreen')
|
|
135
|
+
out.activity_error = errMsg;
|
|
136
|
+
if (key === 'fingerprint') {
|
|
137
|
+
out.fingerprint = null;
|
|
138
|
+
out.fingerprint_error = errMsg;
|
|
139
|
+
}
|
|
140
|
+
if (key === 'uiTree') {
|
|
141
|
+
out.ui_tree = null;
|
|
142
|
+
out.ui_tree_error = errMsg;
|
|
143
|
+
}
|
|
144
|
+
if (key === 'readLogStream') {
|
|
145
|
+
out._streamEntries = [];
|
|
146
|
+
out.logs_error = errMsg;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Logs: prefer stream entries, fallback to snapshot logs when empty
|
|
151
|
+
if (includeLogs) {
|
|
152
|
+
try {
|
|
153
|
+
let entries = Array.isArray(out._streamEntries) ? out._streamEntries : [];
|
|
154
|
+
if (!entries || entries.length === 0) {
|
|
155
|
+
const gl = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines: logLines });
|
|
156
|
+
const raw = (gl && gl.logs) ? gl.logs : [];
|
|
157
|
+
entries = raw.slice(-Math.max(0, logLines)).map(line => {
|
|
158
|
+
const level = /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(line) ? 'ERROR' : /\b(WARN| W )\b/i.test(line) ? 'WARN' : 'INFO';
|
|
159
|
+
return { timestamp: null, level, message: line };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
entries = entries.map(ent => {
|
|
164
|
+
const msg = (ent && (ent.message || ent.msg)) ? (ent.message || ent.msg) : (typeof ent === 'string' ? ent : JSON.stringify(ent));
|
|
165
|
+
const levelRaw = (ent && (ent.level || ent.levelName || ent._level)) ? (ent.level || ent.levelName || ent._level) : '';
|
|
166
|
+
const level = (levelRaw && String(levelRaw)).toString().toUpperCase() || (/\bERROR\b/i.test(msg) ? 'ERROR' : /\bWARN\b/i.test(msg) ? 'WARN' : 'INFO');
|
|
167
|
+
let tsNum = null;
|
|
168
|
+
const maybeIso = ent && (ent._iso || ent.timestamp);
|
|
169
|
+
if (maybeIso && typeof maybeIso === 'string') {
|
|
170
|
+
const d = new Date(maybeIso);
|
|
171
|
+
if (!isNaN(d.getTime()))
|
|
172
|
+
tsNum = d.getTime();
|
|
173
|
+
}
|
|
174
|
+
return { timestamp: tsNum, level, message: msg };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
out.logs = entries;
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
out.logs = [];
|
|
181
|
+
out.logs_error = e instanceof Error ? e.message : String(e);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Clean up internal temporary field
|
|
185
|
+
delete out._streamEntries;
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
85
188
|
}
|
package/dist/server.js
CHANGED
|
@@ -195,6 +195,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
195
195
|
required: ["platform"]
|
|
196
196
|
}
|
|
197
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: "capture_debug_snapshot",
|
|
200
|
+
description: "Capture a complete debug snapshot (screenshot, ui tree, activity, fingerprint, logs). Returns structured JSON.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
reason: { type: "string", description: "Optional reason for snapshot" },
|
|
205
|
+
includeLogs: { type: "boolean", description: "Whether to include logs", default: true },
|
|
206
|
+
logLines: { type: "number", description: "Maximum number of log lines to include", default: 200 },
|
|
207
|
+
platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override" },
|
|
208
|
+
appId: { type: "string", description: "Optional appId to scope logs (package/bundle id)" },
|
|
209
|
+
deviceId: { type: "string", description: "Optional device serial/udid" },
|
|
210
|
+
sessionId: { type: "string", description: "Optional log stream session id to prefer" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
198
214
|
{
|
|
199
215
|
name: "start_log_stream",
|
|
200
216
|
description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
|
|
@@ -556,6 +572,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
556
572
|
]
|
|
557
573
|
};
|
|
558
574
|
}
|
|
575
|
+
if (name === "capture_debug_snapshot") {
|
|
576
|
+
const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args;
|
|
577
|
+
const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId });
|
|
578
|
+
return wrapResponse(res);
|
|
579
|
+
}
|
|
559
580
|
if (name === "get_ui_tree") {
|
|
560
581
|
const { platform, deviceId } = args;
|
|
561
582
|
const res = await ToolsObserve.getUITreeHandler({ platform, deviceId });
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.17.0]
|
|
6
|
+
- Added `capture_debug_snapshot` observe tool: captures a full debugging snapshot including screenshot (base64), UI tree, current activity (Android), screen fingerprint, and recent logs (prefers active log stream, falls back to snapshot logs). Returns a single structured JSON object and includes per-part error fields for partial failures. Implemented as `ToolsObserve.captureDebugSnapshotHandler` and registered in the server.
|
|
7
|
+
|
|
5
8
|
## [0.16.0]
|
|
6
9
|
- Added `wait_for_screen_change` interact tool: polls the platform-specific `get_screen_fingerprint` until it differs from a provided `previousFingerprint`, with configurable `timeoutMs` and `pollIntervalMs` and an optional stability confirmation poll to avoid reacting to transient UI flickers. Implemented at the interact layer and delegates fingerprinting to the observe implementations (Android/iOS).
|
|
7
10
|
- Added unit tests covering immediate change, transient null fingerprints, stability confirmation and timeout behavior: `test/interact/unit/wait_for_screen_change.test.ts`.
|
package/docs/tools/observe.md
CHANGED
|
@@ -76,6 +76,50 @@ Response:
|
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
|
79
|
+
## capture_debug_snapshot
|
|
80
|
+
Capture a complete debug snapshot of the app state for diagnostics and post-mortem analysis.
|
|
81
|
+
|
|
82
|
+
Input:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"reason": "optional string describing why snapshot is taken",
|
|
87
|
+
"includeLogs": true,
|
|
88
|
+
"logLines": 200,
|
|
89
|
+
"platform": "android | ios",
|
|
90
|
+
"appId": "optional package/bundle id to scope logs",
|
|
91
|
+
"deviceId": "optional device serial/udid",
|
|
92
|
+
"sessionId": "optional log stream session id to prefer"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Behavior:
|
|
97
|
+
- Captures screenshot (base64), current activity (Android), screen fingerprint, full UI tree, and recent logs.
|
|
98
|
+
- Prefers active log stream entries (read_log_stream) and falls back to get_logs when no active stream is available.
|
|
99
|
+
- Returns partial data when components fail and includes per-part error fields (e.g. `screenshot_error`, `ui_tree_error`).
|
|
100
|
+
- Caps logs to `logLines` entries and prefers recent entries.
|
|
101
|
+
- Fast by default: does not wait for new logs and avoids long blocking operations.
|
|
102
|
+
|
|
103
|
+
Response (example):
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"timestamp": 1710000000,
|
|
108
|
+
"reason": "Crash after tapping checkout",
|
|
109
|
+
"activity": "CheckoutActivity",
|
|
110
|
+
"fingerprint": "abc123",
|
|
111
|
+
"screenshot": "<base64 PNG string>",
|
|
112
|
+
"ui_tree": { ... },
|
|
113
|
+
"logs": [ { "timestamp": 1710000000, "level": "ERROR", "message": "NullPointerException at CheckoutViewModel" } ]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Notes:
|
|
118
|
+
- Useful immediately after detecting crashes or unexpected UI behaviour.
|
|
119
|
+
- Do not expect perfect data during a crash; tool is designed to return best-effort context and include errors for failed parts.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
79
123
|
## get_screen_fingerprint
|
|
80
124
|
Generate a stable fingerprint representing the visible screen. Useful for detecting navigation changes, preventing loops, and synchronisation.
|
|
81
125
|
|
package/package.json
CHANGED
package/src/interact/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { AndroidInteract, iOSInteract };
|
|
|
5
5
|
import { resolveTargetDevice } from '../utils/resolve-device.js'
|
|
6
6
|
import { ToolsObserve } from '../observe/index.js'
|
|
7
7
|
|
|
8
|
+
interface ScreenFingerprintResponse { fingerprint: string | null }
|
|
9
|
+
|
|
8
10
|
export class ToolsInteract {
|
|
9
11
|
|
|
10
12
|
private static async getInteractionService(platform?: 'android' | 'ios', deviceId?: string) {
|
|
@@ -51,8 +53,8 @@ export class ToolsInteract {
|
|
|
51
53
|
|
|
52
54
|
while (Date.now() - start < timeoutMs) {
|
|
53
55
|
try {
|
|
54
|
-
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
|
|
55
|
-
const fp =
|
|
56
|
+
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
57
|
+
const fp = res?.fingerprint ?? null
|
|
56
58
|
if (fp === null || fp === undefined) {
|
|
57
59
|
lastFingerprint = null
|
|
58
60
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
@@ -64,9 +66,9 @@ export class ToolsInteract {
|
|
|
64
66
|
if (fp !== previousFingerprint) {
|
|
65
67
|
// Stability confirmation
|
|
66
68
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
67
|
-
|
|
68
|
-
const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
|
|
69
|
-
const confirmFp =
|
|
69
|
+
try {
|
|
70
|
+
const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
71
|
+
const confirmFp = confirmRes?.fingerprint ?? null
|
|
70
72
|
if (confirmFp === fp) {
|
|
71
73
|
return { success: true, newFingerprint: fp, elapsedMs: Date.now() - start }
|
|
72
74
|
}
|
package/src/observe/index.ts
CHANGED
|
@@ -89,4 +89,92 @@ export class ToolsObserve {
|
|
|
89
89
|
// Both observes implement getScreenFingerprint
|
|
90
90
|
return await (observe as any).getScreenFingerprint(resolved.id)
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
static async captureDebugSnapshotHandler({ reason, includeLogs = true, logLines = 200, platform, appId, deviceId, sessionId }: { reason?: string; includeLogs?: boolean; logLines?: number; platform?: 'android' | 'ios'; appId?: string; deviceId?: string; sessionId?: string } = {}) {
|
|
94
|
+
const timestamp = Date.now()
|
|
95
|
+
const out: any = { timestamp, reason: reason || '', activity: null, fingerprint: null, screenshot: null, ui_tree: null, logs: [] }
|
|
96
|
+
|
|
97
|
+
// Parallel fetches for performance: screenshot, current screen, fingerprint, ui tree, and log stream/get logs
|
|
98
|
+
const sid = sessionId || 'default'
|
|
99
|
+
const tasks = {
|
|
100
|
+
screenshot: ToolsObserve.captureScreenshotHandler({ platform, deviceId }),
|
|
101
|
+
currentScreen: (!platform || platform === 'android') ? ToolsObserve.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
|
|
102
|
+
fingerprint: ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }),
|
|
103
|
+
uiTree: ToolsObserve.getUITreeHandler({ platform, deviceId }),
|
|
104
|
+
readLogStream: includeLogs ? ToolsObserve.readLogStreamHandler({ platform, sessionId: sid, limit: logLines }) : Promise.resolve({ entries: [] }),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const results = await Promise.allSettled(Object.values(tasks))
|
|
108
|
+
const keys = Object.keys(tasks)
|
|
109
|
+
|
|
110
|
+
// Map results back to keys
|
|
111
|
+
for (let i = 0; i < results.length; i++) {
|
|
112
|
+
const key = keys[i]
|
|
113
|
+
const res = results[i] as PromiseSettledResult<any>
|
|
114
|
+
if (res.status === 'fulfilled') {
|
|
115
|
+
const val = res.value
|
|
116
|
+
if (key === 'screenshot') {
|
|
117
|
+
out.screenshot = val && val.screenshot ? val.screenshot : null
|
|
118
|
+
} else if (key === 'currentScreen') {
|
|
119
|
+
out.activity = val && ((val.activity || val.shortActivity)) ? (val.activity || val.shortActivity) : out.activity || ''
|
|
120
|
+
} else if (key === 'fingerprint') {
|
|
121
|
+
if (val && val.fingerprint) out.fingerprint = val.fingerprint
|
|
122
|
+
if (val && val.activity) out.activity = out.activity || val.activity
|
|
123
|
+
if (val && val.error) out.fingerprint_error = val.error
|
|
124
|
+
} else if (key === 'uiTree') {
|
|
125
|
+
out.ui_tree = val
|
|
126
|
+
if (val && val.error) out.ui_tree_error = val.error
|
|
127
|
+
} else if (key === 'readLogStream') {
|
|
128
|
+
// handle below after evaluating fallback
|
|
129
|
+
// temporarily attach to out._streamEntries
|
|
130
|
+
out._streamEntries = val && val.entries ? val.entries : []
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
const errMsg = res.reason instanceof Error ? res.reason.message : String(res.reason)
|
|
134
|
+
if (key === 'screenshot') out.screenshot_error = errMsg
|
|
135
|
+
if (key === 'currentScreen') out.activity_error = errMsg
|
|
136
|
+
if (key === 'fingerprint') { out.fingerprint = null; out.fingerprint_error = errMsg }
|
|
137
|
+
if (key === 'uiTree') { out.ui_tree = null; out.ui_tree_error = errMsg }
|
|
138
|
+
if (key === 'readLogStream') { out._streamEntries = [] ; out.logs_error = errMsg }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Logs: prefer stream entries, fallback to snapshot logs when empty
|
|
143
|
+
if (includeLogs) {
|
|
144
|
+
try {
|
|
145
|
+
let entries: any[] = Array.isArray(out._streamEntries) ? out._streamEntries : []
|
|
146
|
+
if (!entries || entries.length === 0) {
|
|
147
|
+
const gl = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines: logLines })
|
|
148
|
+
const raw: string[] = (gl && (gl as any).logs) ? (gl as any).logs : []
|
|
149
|
+
entries = raw.slice(-Math.max(0, logLines)).map(line => {
|
|
150
|
+
const level = /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(line) ? 'ERROR' : /\b(WARN| W )\b/i.test(line) ? 'WARN' : 'INFO'
|
|
151
|
+
return { timestamp: null, level, message: line }
|
|
152
|
+
})
|
|
153
|
+
} else {
|
|
154
|
+
entries = entries.map(ent => {
|
|
155
|
+
const msg = (ent && (ent.message || ent.msg)) ? (ent.message || ent.msg) : (typeof ent === 'string' ? ent : JSON.stringify(ent))
|
|
156
|
+
const levelRaw = (ent && (ent.level || ent.levelName || ent._level)) ? (ent.level || ent.levelName || ent._level) : ''
|
|
157
|
+
const level = (levelRaw && String(levelRaw)).toString().toUpperCase() || (/\bERROR\b/i.test(msg) ? 'ERROR' : /\bWARN\b/i.test(msg) ? 'WARN' : 'INFO')
|
|
158
|
+
let tsNum: number | null = null
|
|
159
|
+
const maybeIso = ent && ((ent._iso || ent.timestamp) as any)
|
|
160
|
+
if (maybeIso && typeof maybeIso === 'string') {
|
|
161
|
+
const d = new Date(maybeIso)
|
|
162
|
+
if (!isNaN(d.getTime())) tsNum = d.getTime()
|
|
163
|
+
}
|
|
164
|
+
return { timestamp: tsNum, level, message: msg }
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
out.logs = entries
|
|
169
|
+
} catch (e) {
|
|
170
|
+
out.logs = []
|
|
171
|
+
out.logs_error = e instanceof Error ? e.message : String(e)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Clean up internal temporary field
|
|
176
|
+
delete out._streamEntries
|
|
177
|
+
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
92
180
|
}
|
package/src/server.ts
CHANGED
|
@@ -215,6 +215,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
215
215
|
required: ["platform"]
|
|
216
216
|
}
|
|
217
217
|
},
|
|
218
|
+
{
|
|
219
|
+
name: "capture_debug_snapshot",
|
|
220
|
+
description: "Capture a complete debug snapshot (screenshot, ui tree, activity, fingerprint, logs). Returns structured JSON."
|
|
221
|
+
,
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
reason: { type: "string", description: "Optional reason for snapshot" },
|
|
226
|
+
includeLogs: { type: "boolean", description: "Whether to include logs", default: true },
|
|
227
|
+
logLines: { type: "number", description: "Maximum number of log lines to include", default: 200 },
|
|
228
|
+
platform: { type: "string", enum: ["android","ios"], description: "Optional platform override" },
|
|
229
|
+
appId: { type: "string", description: "Optional appId to scope logs (package/bundle id)" },
|
|
230
|
+
deviceId: { type: "string", description: "Optional device serial/udid" },
|
|
231
|
+
sessionId: { type: "string", description: "Optional log stream session id to prefer" }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
218
235
|
{
|
|
219
236
|
name: "start_log_stream",
|
|
220
237
|
description: "Start streaming logs for a target application on Android or iOS. For Android this uses adb logcat --pid=<pid>; for iOS it streams `xcrun simctl spawn <device> log stream` with a predicate.",
|
|
@@ -594,6 +611,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
594
611
|
}
|
|
595
612
|
}
|
|
596
613
|
|
|
614
|
+
if (name === "capture_debug_snapshot") {
|
|
615
|
+
const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args as any
|
|
616
|
+
const res = await ToolsObserve.captureDebugSnapshotHandler({ reason, includeLogs, logLines, platform, appId, deviceId, sessionId })
|
|
617
|
+
return wrapResponse(res)
|
|
618
|
+
}
|
|
619
|
+
|
|
597
620
|
if (name === "get_ui_tree") {
|
|
598
621
|
const { platform, deviceId } = args as any
|
|
599
622
|
const res = await ToolsObserve.getUITreeHandler({ platform, deviceId })
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
2
|
+
|
|
3
|
+
async function run() {
|
|
4
|
+
console.log('Starting capture_debug_snapshot unit tests...')
|
|
5
|
+
|
|
6
|
+
// Save original ToolsObserve handlers
|
|
7
|
+
const origCaptureHandler = (ToolsObserve as any).captureScreenshotHandler
|
|
8
|
+
const origGetCurrentHandler = (ToolsObserve as any).getCurrentScreenHandler
|
|
9
|
+
const origGetFpHandler = (ToolsObserve as any).getScreenFingerprintHandler
|
|
10
|
+
const origGetTreeHandler = (ToolsObserve as any).getUITreeHandler
|
|
11
|
+
const origReadLogStreamHandler = (ToolsObserve as any).readLogStreamHandler
|
|
12
|
+
const origGetLogsHandler = (ToolsObserve as any).getLogsHandler
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// --- Test 1: all components succeed and logs come from stream ---
|
|
16
|
+
;(ToolsObserve as any).captureScreenshotHandler = async function() {
|
|
17
|
+
return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screenshot: 'BASE64PNG', resolution: { width: 1080, height: 1920 } }
|
|
18
|
+
}
|
|
19
|
+
;(ToolsObserve as any).getCurrentScreenHandler = async function() {
|
|
20
|
+
return { device: { platform: 'android', id: 'mock' }, package: 'com.example', activity: 'com.example.Main', shortActivity: 'Main' }
|
|
21
|
+
}
|
|
22
|
+
;(ToolsObserve as any).getScreenFingerprintHandler = async function() {
|
|
23
|
+
return { fingerprint: 'abc123', activity: 'Main' }
|
|
24
|
+
}
|
|
25
|
+
;(ToolsObserve as any).getUITreeHandler = async function() {
|
|
26
|
+
return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] }
|
|
27
|
+
}
|
|
28
|
+
;(ToolsObserve as any).readLogStreamHandler = async function() {
|
|
29
|
+
return { entries: [ { timestamp: '2026-03-23T20:00:00.000Z', level: 'ERROR', message: 'Boom' } ], crash_summary: { crash_detected: true } }
|
|
30
|
+
}
|
|
31
|
+
;(ToolsObserve as any).getLogsHandler = async function() {
|
|
32
|
+
return { device: { platform: 'android', id: 'mock' }, logs: [], logCount: 0 }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const res1: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 50, sessionId: 's1' })
|
|
36
|
+
console.log('res1:', JSON.stringify(res1, null, 2))
|
|
37
|
+
const pass1 = res1 && res1.screenshot === 'BASE64PNG' && res1.activity && res1.fingerprint === 'abc123' && Array.isArray(res1.logs) && res1.logs.length === 1
|
|
38
|
+
console.log('Test 1:', pass1 ? 'PASS' : 'FAIL')
|
|
39
|
+
|
|
40
|
+
// Restore handlers before next test
|
|
41
|
+
;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
|
|
42
|
+
;(ToolsObserve as any).getCurrentScreenHandler = origGetCurrentHandler
|
|
43
|
+
;(ToolsObserve as any).getScreenFingerprintHandler = origGetFpHandler
|
|
44
|
+
;(ToolsObserve as any).getUITreeHandler = origGetTreeHandler
|
|
45
|
+
;(ToolsObserve as any).readLogStreamHandler = origReadLogStreamHandler
|
|
46
|
+
;(ToolsObserve as any).getLogsHandler = origGetLogsHandler
|
|
47
|
+
|
|
48
|
+
// --- Test 2: screenshot and ui tree fail; logs fallback to getLogs ---
|
|
49
|
+
;(ToolsObserve as any).captureScreenshotHandler = async function() { throw new Error('screencap failed') }
|
|
50
|
+
;(ToolsObserve as any).getUITreeHandler = async function() { throw new Error('uie_error') }
|
|
51
|
+
;(ToolsObserve as any).readLogStreamHandler = async function() { return { entries: [] } }
|
|
52
|
+
;(ToolsObserve as any).getLogsHandler = async function() { return { device: { platform: 'android', id: 'mock' }, logs: ['INFO starting','ERROR crash here'], logCount: 2 } }
|
|
53
|
+
|
|
54
|
+
const res2: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 10, appId: 'com.example' })
|
|
55
|
+
console.log('res2:', JSON.stringify(res2, null, 2))
|
|
56
|
+
const pass2 = res2 && res2.screenshot_error && res2.ui_tree_error && Array.isArray(res2.logs) && res2.logs.length === 2
|
|
57
|
+
console.log('Test 2:', pass2 ? 'PASS' : 'FAIL')
|
|
58
|
+
|
|
59
|
+
// Restore handlers before next test
|
|
60
|
+
;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
|
|
61
|
+
;(ToolsObserve as any).getCurrentScreenHandler = origGetCurrentHandler
|
|
62
|
+
;(ToolsObserve as any).getScreenFingerprintHandler = origGetFpHandler
|
|
63
|
+
;(ToolsObserve as any).getUITreeHandler = origGetTreeHandler
|
|
64
|
+
;(ToolsObserve as any).readLogStreamHandler = origReadLogStreamHandler
|
|
65
|
+
;(ToolsObserve as any).getLogsHandler = origGetLogsHandler
|
|
66
|
+
|
|
67
|
+
// --- Test 3: includeLogs=false should omit logs ---
|
|
68
|
+
;(ToolsObserve as any).captureScreenshotHandler = async function() { return { device: { platform: 'android', id: 'mock' }, screenshot: null } }
|
|
69
|
+
;(ToolsObserve as any).getCurrentScreenHandler = async function() { return { device: { platform: 'android', id: 'mock' }, package: '', activity: '', shortActivity: '' } }
|
|
70
|
+
;(ToolsObserve as any).getScreenFingerprintHandler = async function() { return { fingerprint: null } }
|
|
71
|
+
;(ToolsObserve as any).getUITreeHandler = async function() { return { device: { platform: 'android', id: 'mock' }, screen: '', resolution: { width: 0, height: 0 }, elements: [] } }
|
|
72
|
+
;(ToolsObserve as any).readLogStreamHandler = async function() { return { entries: [] } }
|
|
73
|
+
|
|
74
|
+
const res3: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: false })
|
|
75
|
+
console.log('res3:', JSON.stringify(res3, null, 2))
|
|
76
|
+
const pass3 = res3 && typeof res3.logs !== 'undefined' && res3.logs.length === 0
|
|
77
|
+
console.log('Test 3:', pass3 ? 'PASS' : 'FAIL')
|
|
78
|
+
|
|
79
|
+
} finally {
|
|
80
|
+
;(ToolsObserve as any).captureScreenshotHandler = origCaptureHandler
|
|
81
|
+
;(ToolsObserve as any).getCurrentScreenHandler = origGetCurrentHandler
|
|
82
|
+
;(ToolsObserve as any).getScreenFingerprintHandler = origGetFpHandler
|
|
83
|
+
;(ToolsObserve as any).getUITreeHandler = origGetTreeHandler
|
|
84
|
+
;(ToolsObserve as any).readLogStreamHandler = origReadLogStreamHandler
|
|
85
|
+
;(ToolsObserve as any).getLogsHandler = origGetLogsHandler
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
run().catch(console.error)
|
package/test/unit/index.ts
CHANGED
|
@@ -11,5 +11,6 @@ import '../manage/unit/diagnostics.test.ts'
|
|
|
11
11
|
import '../manage/unit/detection.test.ts'
|
|
12
12
|
import '../manage/unit/mcp_disable_autodetect.test.ts'
|
|
13
13
|
import '../interact/unit/wait_for_screen_change.test.ts'
|
|
14
|
+
import '../observe/unit/capture_debug_snapshot.test.ts'
|
|
14
15
|
|
|
15
16
|
console.log('Unit tests loaded.')
|