mobile-debug-mcp 0.21.1 → 0.21.2

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.
@@ -5,6 +5,7 @@ import { createWriteStream } from "fs";
5
5
  import { promises as fsPromises } from "fs";
6
6
  import path from "path";
7
7
  import { computeScreenFingerprint } from "../utils/ui/index.js";
8
+ import { parsePngSize } from "../utils/image.js";
8
9
  const activeLogStreams = new Map();
9
10
  export class AndroidObserve {
10
11
  async getDeviceMetadata(appId, deviceId) {
@@ -120,7 +121,7 @@ export class AndroidObserve {
120
121
  child.kill();
121
122
  reject(new Error(`ADB screencap timed out after 10s`));
122
123
  }, 10000);
123
- child.on('close', (code) => {
124
+ child.on('close', async (code) => {
124
125
  clearTimeout(timeout);
125
126
  if (code !== 0) {
126
127
  reject(new Error(stderr.trim() || `Screencap failed with code ${code}`));
@@ -128,28 +129,91 @@ export class AndroidObserve {
128
129
  }
129
130
  const screenshotBuffer = Buffer.concat(chunks);
130
131
  const screenshotBase64 = screenshotBuffer.toString('base64');
131
- execAdb(['shell', 'wm', 'size'], deviceId)
132
- .then(sizeStdout => {
133
- let width = 0;
134
- let height = 0;
135
- const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/);
136
- if (match) {
137
- width = parseInt(match[1], 10);
138
- height = parseInt(match[2], 10);
132
+ const parsed = parsePngSize(screenshotBuffer);
133
+ if (parsed.width > 0 && parsed.height > 0) {
134
+ // Attempt to convert to WebP (preferred) and provide JPEG fallback (awaited to avoid race)
135
+ try {
136
+ const sharpModule = await import('sharp');
137
+ const sharp = sharpModule && sharpModule.default ? sharpModule.default : sharpModule;
138
+ const buf = screenshotBuffer;
139
+ const img = sharp(buf);
140
+ const meta = await img.metadata().catch((err) => { console.error('sharp.metadata failed (Android):', err); return {}; });
141
+ const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
142
+ let webpBuf = null;
143
+ let jpegBuf = null;
144
+ try {
145
+ webpBuf = await img.webp({ quality: 80 }).toBuffer();
146
+ }
147
+ catch (err) {
148
+ console.error('WebP conversion failed (Android):', err instanceof Error ? err.message : String(err));
149
+ webpBuf = null;
150
+ }
151
+ try {
152
+ jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
153
+ }
154
+ catch (err) {
155
+ console.error('JPEG conversion failed (Android):', err instanceof Error ? err.message : String(err));
156
+ jpegBuf = null;
157
+ }
158
+ if (hasAlpha) {
159
+ if (webpBuf) {
160
+ const webpB64 = webpBuf.toString('base64');
161
+ const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null;
162
+ resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } });
163
+ return;
164
+ }
165
+ const pngB64 = buf.toString('base64');
166
+ resolve({ device: deviceInfo, screenshot: pngB64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } });
167
+ return;
168
+ }
169
+ if (webpBuf) {
170
+ const webpB64 = webpBuf.toString('base64');
171
+ const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null;
172
+ resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } });
173
+ return;
174
+ }
175
+ if (jpegBuf) {
176
+ resolve({ device: deviceInfo, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: parsed.width, height: parsed.height } });
177
+ return;
178
+ }
179
+ // No conversions succeeded; return original PNG
180
+ resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } });
181
+ return;
139
182
  }
140
- resolve({
141
- device: deviceInfo,
142
- screenshot: screenshotBase64,
143
- resolution: { width, height }
144
- });
145
- })
146
- .catch(() => {
147
- resolve({
148
- device: deviceInfo,
149
- screenshot: screenshotBase64,
150
- resolution: { width: 0, height: 0 }
183
+ catch (err) {
184
+ console.error('Screenshot conversion pipeline failed (Android):', err instanceof Error ? err.message : String(err));
185
+ // Conversion failed - fall back to original PNG with parsed resolution
186
+ resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } });
187
+ return;
188
+ }
189
+ }
190
+ else {
191
+ // Fallback to querying wm size if parsing failed
192
+ execAdb(['shell', 'wm', 'size'], deviceId)
193
+ .then(sizeStdout => {
194
+ let width = 0;
195
+ let height = 0;
196
+ const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/);
197
+ if (match) {
198
+ width = parseInt(match[1], 10);
199
+ height = parseInt(match[2], 10);
200
+ }
201
+ resolve({
202
+ device: deviceInfo,
203
+ screenshot: screenshotBase64,
204
+ screenshot_mime: 'image/png',
205
+ resolution: { width, height }
206
+ });
207
+ })
208
+ .catch(() => {
209
+ resolve({
210
+ device: deviceInfo,
211
+ screenshot: screenshotBase64,
212
+ screenshot_mime: 'image/png',
213
+ resolution: { width: 0, height: 0 }
214
+ });
151
215
  });
152
- });
216
+ }
153
217
  });
154
218
  child.on('error', (err) => {
155
219
  clearTimeout(timeout);
@@ -5,6 +5,7 @@ import { createWriteStream, promises as fsPromises } from 'fs';
5
5
  import path from 'path';
6
6
  import { parseLogLine } from '../utils/android/utils.js';
7
7
  import { computeScreenFingerprint } from '../utils/ui/index.js';
8
+ import { parsePngSize } from '../utils/image.js';
8
9
  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
9
10
  function parseIDBFrame(frame) {
10
11
  if (!frame)
@@ -109,11 +110,59 @@ export class iOSObserve {
109
110
  await execCommand(['simctl', 'io', deviceId, 'screenshot', tmpFile], deviceId);
110
111
  const buffer = await fs.readFile(tmpFile);
111
112
  const base64 = buffer.toString('base64');
113
+ const dims = parsePngSize(buffer);
114
+ // Try to generate WebP (preferred) and JPEG fallback using sharp (in-process, cross-platform)
115
+ try {
116
+ const sharpModule = await import('sharp');
117
+ const sharp = sharpModule && sharpModule.default ? sharpModule.default : sharpModule;
118
+ const img = sharp(buffer);
119
+ const meta = await img.metadata().catch((err) => { console.error('sharp.metadata failed:', err); return {}; });
120
+ // If image has alpha channel, prefer lossless PNG to preserve transparency
121
+ const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
122
+ // Generate WebP and JPEG buffers; log failures
123
+ let webpBuf = null;
124
+ let jpegBuf = null;
125
+ try {
126
+ webpBuf = await img.webp({ quality: 80 }).toBuffer();
127
+ }
128
+ catch (err) {
129
+ console.error('WebP conversion failed (iOS):', err instanceof Error ? err.message : String(err));
130
+ webpBuf = null;
131
+ }
132
+ try {
133
+ jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
134
+ }
135
+ catch (err) {
136
+ console.error('JPEG conversion failed (iOS):', err instanceof Error ? err.message : String(err));
137
+ jpegBuf = null;
138
+ }
139
+ await fs.rm(tmpFile).catch(() => { });
140
+ if (hasAlpha) {
141
+ // preserve alpha: return PNG if WebP not available
142
+ if (webpBuf) {
143
+ return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: base64, screenshot_fallback_mime: 'image/png', resolution: { width: dims.width, height: dims.height } };
144
+ }
145
+ // if webp unavailable, return original PNG
146
+ return { device, screenshot: base64, screenshot_mime: 'image/png', resolution: { width: dims.width, height: dims.height } };
147
+ }
148
+ // No alpha: prefer webp, fall back to jpeg
149
+ if (webpBuf) {
150
+ return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: jpegBuf ? jpegBuf.toString('base64') : undefined, screenshot_fallback_mime: jpegBuf ? 'image/jpeg' : undefined, resolution: { width: dims.width, height: dims.height } };
151
+ }
152
+ if (jpegBuf) {
153
+ return { device, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: dims.width, height: dims.height } };
154
+ }
155
+ }
156
+ catch (err) {
157
+ console.error('Screenshot conversion pipeline failed (iOS):', err instanceof Error ? err.message : String(err));
158
+ // fall through to png fallback
159
+ }
112
160
  await fs.rm(tmpFile).catch(() => { });
113
161
  return {
114
162
  device,
115
163
  screenshot: base64,
116
- resolution: { width: 0, height: 0 },
164
+ screenshot_mime: 'image/png',
165
+ resolution: { width: dims.width, height: dims.height },
117
166
  };
118
167
  }
119
168
  catch (e) {
package/dist/server.js CHANGED
@@ -584,12 +584,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
584
584
  if (name === "capture_screenshot") {
585
585
  const { platform, deviceId } = args;
586
586
  const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId });
587
- return {
588
- content: [
589
- { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution } }, null, 2) },
590
- { type: 'image', data: res.screenshot, mimeType: 'image/png' }
591
- ]
592
- };
587
+ const mime = res.screenshot_mime || 'image/png';
588
+ const content = [
589
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: res.resolution, mimeType: mime } }, null, 2) },
590
+ { type: 'image', data: res.screenshot, mimeType: mime }
591
+ ];
592
+ // If a jpeg fallback is available, include a small note and the fallback as an additional image block for compatibility
593
+ if (res.screenshot_fallback) {
594
+ content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: res.screenshot_fallback_mime || 'image/jpeg' }) });
595
+ content.push({ type: 'image', data: res.screenshot_fallback, mimeType: res.screenshot_fallback_mime || 'image/jpeg' });
596
+ }
597
+ return { content };
593
598
  }
594
599
  if (name === "capture_debug_snapshot") {
595
600
  const { reason, includeLogs, logLines, platform, appId, deviceId, sessionId } = args;
@@ -0,0 +1,18 @@
1
+ export function parsePngSize(buf) {
2
+ try {
3
+ if (!buf || buf.length < 24)
4
+ return { width: 0, height: 0 };
5
+ // PNG signature + IHDR checks
6
+ if (buf.readUInt32BE(0) !== 0x89504e47 || buf.readUInt32BE(4) !== 0x0d0a1a0a)
7
+ return { width: 0, height: 0 };
8
+ const ihdr = buf.toString('ascii', 12, 16);
9
+ if (ihdr !== 'IHDR')
10
+ return { width: 0, height: 0 };
11
+ const width = buf.readUInt32BE(16);
12
+ const height = buf.readUInt32BE(20);
13
+ return { width, height };
14
+ }
15
+ catch {
16
+ return { width: 0, height: 0 };
17
+ }
18
+ }
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.21.2]
6
+ - Fixed screenshots not working, imnproved tool
7
+
5
8
  ## [0.21.1]
6
9
  - Removed wait_for_element and renamed observe_until to wait_for_ui (obsolete references removed)
7
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,8 @@
27
27
  "dependencies": {
28
28
  "@modelcontextprotocol/sdk": "^1.0.0",
29
29
  "fast-xml-parser": "^5.5.1",
30
- "zod": "^3.22.4"
30
+ "zod": "^3.22.4",
31
+ "sharp": "^0.32.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/node": "^25.4.0",
@@ -6,6 +6,7 @@ import { createWriteStream } from "fs"
6
6
  import { promises as fsPromises } from "fs"
7
7
  import path from "path"
8
8
  import { computeScreenFingerprint } from "../utils/ui/index.js"
9
+ import { parsePngSize } from "../utils/image.js"
9
10
 
10
11
  const activeLogStreams: Map<string, { proc: any, file: string }> = new Map()
11
12
 
@@ -142,7 +143,7 @@ export class AndroidObserve {
142
143
  reject(new Error(`ADB screencap timed out after 10s`))
143
144
  }, 10000)
144
145
 
145
- child.on('close', (code) => {
146
+ child.on('close', async (code) => {
146
147
  clearTimeout(timeout)
147
148
  if (code !== 0) {
148
149
  reject(new Error(stderr.trim() || `Screencap failed with code ${code}`))
@@ -152,28 +153,91 @@ export class AndroidObserve {
152
153
  const screenshotBuffer = Buffer.concat(chunks)
153
154
  const screenshotBase64 = screenshotBuffer.toString('base64')
154
155
 
155
- execAdb(['shell', 'wm', 'size'], deviceId)
156
- .then(sizeStdout => {
157
- let width = 0
158
- let height = 0
159
- const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/)
160
- if (match) {
161
- width = parseInt(match[1], 10)
162
- height = parseInt(match[2], 10)
156
+ const parsed = parsePngSize(screenshotBuffer)
157
+ if (parsed.width > 0 && parsed.height > 0) {
158
+ // Attempt to convert to WebP (preferred) and provide JPEG fallback (awaited to avoid race)
159
+ try {
160
+ const sharpModule = await import('sharp'); const sharp = sharpModule && (sharpModule as any).default ? (sharpModule as any).default : sharpModule;
161
+ const buf = screenshotBuffer;
162
+ const img = sharp(buf);
163
+ const meta = await img.metadata().catch((err: any) => { console.error('sharp.metadata failed (Android):', err); return {} as any });
164
+ const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
165
+
166
+ let webpBuf: Buffer | null = null;
167
+ let jpegBuf: Buffer | null = null;
168
+ try {
169
+ webpBuf = await img.webp({ quality: 80 }).toBuffer();
170
+ } catch (err) {
171
+ console.error('WebP conversion failed (Android):', err instanceof Error ? err.message : String(err));
172
+ webpBuf = null;
163
173
  }
164
- resolve({
165
- device: deviceInfo,
166
- screenshot: screenshotBase64,
167
- resolution: { width, height }
174
+ try {
175
+ jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
176
+ } catch (err) {
177
+ console.error('JPEG conversion failed (Android):', err instanceof Error ? err.message : String(err));
178
+ jpegBuf = null;
179
+ }
180
+
181
+ if (hasAlpha) {
182
+ if (webpBuf) {
183
+ const webpB64 = webpBuf.toString('base64')
184
+ const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null
185
+ resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } } as any)
186
+ return
187
+ }
188
+ const pngB64 = buf.toString('base64')
189
+ resolve({ device: deviceInfo, screenshot: pngB64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
190
+ return
191
+ }
192
+
193
+ if (webpBuf) {
194
+ const webpB64 = webpBuf.toString('base64')
195
+ const jpegB64 = jpegBuf ? jpegBuf.toString('base64') : null
196
+ resolve({ device: deviceInfo, screenshot: webpB64, screenshot_mime: 'image/webp', screenshot_fallback: jpegB64, screenshot_fallback_mime: jpegB64 ? 'image/jpeg' : undefined, resolution: { width: parsed.width, height: parsed.height } } as any)
197
+ return
198
+ }
199
+
200
+ if (jpegBuf) {
201
+ resolve({ device: deviceInfo, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: parsed.width, height: parsed.height } })
202
+ return
203
+ }
204
+
205
+ // No conversions succeeded; return original PNG
206
+ resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
207
+ return
208
+ } catch (err) {
209
+ console.error('Screenshot conversion pipeline failed (Android):', err instanceof Error ? err.message : String(err));
210
+ // Conversion failed - fall back to original PNG with parsed resolution
211
+ resolve({ device: deviceInfo, screenshot: screenshotBase64, screenshot_mime: 'image/png', resolution: { width: parsed.width, height: parsed.height } })
212
+ return
213
+ }
214
+ } else {
215
+ // Fallback to querying wm size if parsing failed
216
+ execAdb(['shell', 'wm', 'size'], deviceId)
217
+ .then(sizeStdout => {
218
+ let width = 0
219
+ let height = 0
220
+ const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/)
221
+ if (match) {
222
+ width = parseInt(match[1], 10)
223
+ height = parseInt(match[2], 10)
224
+ }
225
+ resolve({
226
+ device: deviceInfo,
227
+ screenshot: screenshotBase64,
228
+ screenshot_mime: 'image/png',
229
+ resolution: { width, height }
230
+ })
168
231
  })
169
- })
170
- .catch(() => {
171
- resolve({
172
- device: deviceInfo,
173
- screenshot: screenshotBase64,
174
- resolution: { width: 0, height: 0 }
232
+ .catch(() => {
233
+ resolve({
234
+ device: deviceInfo,
235
+ screenshot: screenshotBase64,
236
+ screenshot_mime: 'image/png',
237
+ resolution: { width: 0, height: 0 }
238
+ })
175
239
  })
176
- })
240
+ }
177
241
  })
178
242
 
179
243
  child.on('error', (err) => {
@@ -6,6 +6,7 @@ import { createWriteStream, promises as fsPromises } from 'fs'
6
6
  import path from 'path'
7
7
  import { parseLogLine } from '../utils/android/utils.js'
8
8
  import { computeScreenFingerprint } from '../utils/ui/index.js'
9
+ import { parsePngSize } from '../utils/image.js'
9
10
 
10
11
  const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
11
12
 
@@ -146,13 +147,63 @@ export class iOSObserve {
146
147
 
147
148
  const buffer = await fs.readFile(tmpFile)
148
149
  const base64 = buffer.toString('base64')
149
-
150
- await fs.rm(tmpFile).catch(() => {})
151
150
 
151
+ const dims = parsePngSize(buffer)
152
+
153
+ // Try to generate WebP (preferred) and JPEG fallback using sharp (in-process, cross-platform)
154
+ try {
155
+ const sharpModule = await import('sharp'); const sharp = sharpModule && (sharpModule as any).default ? (sharpModule as any).default : sharpModule;
156
+ const img = sharp(buffer);
157
+ const meta = await img.metadata().catch((err: any) => { console.error('sharp.metadata failed:', err); return {} as any });
158
+
159
+ // If image has alpha channel, prefer lossless PNG to preserve transparency
160
+ const hasAlpha = !!meta.hasAlpha || (meta.channels && meta.channels > 3);
161
+
162
+ // Generate WebP and JPEG buffers; log failures
163
+ let webpBuf: Buffer | null = null;
164
+ let jpegBuf: Buffer | null = null;
165
+ try {
166
+ webpBuf = await img.webp({ quality: 80 }).toBuffer();
167
+ } catch (err) {
168
+ console.error('WebP conversion failed (iOS):', err instanceof Error ? err.message : String(err));
169
+ webpBuf = null;
170
+ }
171
+ try {
172
+ jpegBuf = await img.jpeg({ quality: 80 }).toBuffer();
173
+ } catch (err) {
174
+ console.error('JPEG conversion failed (iOS):', err instanceof Error ? err.message : String(err));
175
+ jpegBuf = null;
176
+ }
177
+
178
+ await fs.rm(tmpFile).catch(() => {});
179
+
180
+ if (hasAlpha) {
181
+ // preserve alpha: return PNG if WebP not available
182
+ if (webpBuf) {
183
+ return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: base64, screenshot_fallback_mime: 'image/png', resolution: { width: dims.width, height: dims.height } }
184
+ }
185
+ // if webp unavailable, return original PNG
186
+ return { device, screenshot: base64, screenshot_mime: 'image/png', resolution: { width: dims.width, height: dims.height } }
187
+ }
188
+
189
+ // No alpha: prefer webp, fall back to jpeg
190
+ if (webpBuf) {
191
+ return { device, screenshot: webpBuf.toString('base64'), screenshot_mime: 'image/webp', screenshot_fallback: jpegBuf ? jpegBuf.toString('base64') : undefined, screenshot_fallback_mime: jpegBuf ? 'image/jpeg' : undefined, resolution: { width: dims.width, height: dims.height } }
192
+ }
193
+ if (jpegBuf) {
194
+ return { device, screenshot: jpegBuf.toString('base64'), screenshot_mime: 'image/jpeg', resolution: { width: dims.width, height: dims.height } }
195
+ }
196
+ } catch (err) {
197
+ console.error('Screenshot conversion pipeline failed (iOS):', err instanceof Error ? err.message : String(err));
198
+ // fall through to png fallback
199
+ }
200
+
201
+ await fs.rm(tmpFile).catch(() => {})
152
202
  return {
153
203
  device,
154
204
  screenshot: base64,
155
- resolution: { width: 0, height: 0 },
205
+ screenshot_mime: 'image/png',
206
+ resolution: { width: dims.width, height: dims.height },
156
207
  }
157
208
  } catch (e) {
158
209
  await fs.rm(tmpFile).catch(() => {})
package/src/server.ts CHANGED
@@ -628,12 +628,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request: SchemaOutput<typ
628
628
  if (name === "capture_screenshot") {
629
629
  const { platform, deviceId } = args as any
630
630
  const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
631
- return {
632
- content: [
633
- { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution } }, null, 2) },
634
- { type: 'image', data: (res as any).screenshot, mimeType: 'image/png' }
635
- ]
631
+ const mime = (res as any).screenshot_mime || 'image/png'
632
+ const content: any[] = [
633
+ { type: 'text', text: JSON.stringify({ device: res.device, result: { resolution: (res as any).resolution, mimeType: mime } }, null, 2) },
634
+ { type: 'image', data: (res as any).screenshot, mimeType: mime }
635
+ ]
636
+ // If a jpeg fallback is available, include a small note and the fallback as an additional image block for compatibility
637
+ if ((res as any).screenshot_fallback) {
638
+ content.push({ type: 'text', text: JSON.stringify({ note: 'JPEG fallback included for compatibility', mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' }) })
639
+ content.push({ type: 'image', data: (res as any).screenshot_fallback, mimeType: (res as any).screenshot_fallback_mime || 'image/jpeg' })
636
640
  }
641
+ return { content }
637
642
  }
638
643
 
639
644
  if (name === "capture_debug_snapshot") {
package/src/types.ts CHANGED
@@ -50,6 +50,9 @@ export interface GetCrashResponse {
50
50
  export interface CaptureAndroidScreenResponse {
51
51
  device: DeviceInfo;
52
52
  screenshot: string; // base64 encoded string
53
+ screenshot_mime?: string; // e.g. image/webp, image/jpeg, image/png
54
+ screenshot_fallback?: string; // optional fallback base64 (e.g., jpeg)
55
+ screenshot_fallback_mime?: string;
53
56
  resolution: {
54
57
  width: number;
55
58
  height: number;
@@ -59,6 +62,9 @@ export interface CaptureAndroidScreenResponse {
59
62
  export interface CaptureIOSScreenshotResponse {
60
63
  device: DeviceInfo;
61
64
  screenshot: string; // base64 encoded string
65
+ screenshot_mime?: string; // e.g. image/webp, image/jpeg, image/png
66
+ screenshot_fallback?: string; // optional fallback base64 (e.g., jpeg)
67
+ screenshot_fallback_mime?: string;
62
68
  resolution: {
63
69
  width: number;
64
70
  height: number;
@@ -0,0 +1,14 @@
1
+ export function parsePngSize(buf: Buffer): { width: number; height: number } {
2
+ try {
3
+ if (!buf || buf.length < 24) return { width: 0, height: 0 };
4
+ // PNG signature + IHDR checks
5
+ if (buf.readUInt32BE(0) !== 0x89504e47 || buf.readUInt32BE(4) !== 0x0d0a1a0a) return { width: 0, height: 0 };
6
+ const ihdr = buf.toString('ascii', 12, 16);
7
+ if (ihdr !== 'IHDR') return { width: 0, height: 0 };
8
+ const width = buf.readUInt32BE(16);
9
+ const height = buf.readUInt32BE(20);
10
+ return { width, height };
11
+ } catch {
12
+ return { width: 0, height: 0 };
13
+ }
14
+ }