@testdriverai/mcp 7.11.283-canary → 7.11.284-test

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.
@@ -255,6 +255,51 @@ function storeImage(data, type) {
255
255
  function getStoredImage(id) {
256
256
  return imageStoreState().images.get(id);
257
257
  }
258
+ /**
259
+ * `_meta` key carrying the image inline in a tool result, for the MCP App.
260
+ *
261
+ * The app used to get every image with a follow-up `resources/read` against the
262
+ * per-connection store above. That second round trip is fragile on hosted
263
+ * clients: when it fails the app has nothing to show and the preview collapses.
264
+ * Sending the bytes with the result removes the round trip. `_meta` is passed
265
+ * through to the app but, unlike `content`/`structuredContent`, is not shown to
266
+ * the model, so the base64 stays out of the agent's context. The resource URI
267
+ * is still set as a fallback (and for `check`'s `referenceImageUri`).
268
+ */
269
+ const INLINE_IMAGE_META_KEY = "testdriver/image";
270
+ /** Above this (base64 chars) the image is left to the resource fallback, so one
271
+ * huge frame can't push a tool result past a host's size limit. */
272
+ const MAX_INLINE_IMAGE_BASE64 = 2_000_000;
273
+ function inlineImageMeta(base64) {
274
+ if (!base64)
275
+ return undefined;
276
+ if (base64.length > MAX_INLINE_IMAGE_BASE64) {
277
+ logger.debug("inlineImageMeta: Image too large to inline, relying on resource URI", { length: base64.length });
278
+ return undefined;
279
+ }
280
+ return { [INLINE_IMAGE_META_KEY]: { mimeType: "image/png", data: base64 } };
281
+ }
282
+ /**
283
+ * Store a result's images and record their resource URIs on `data`. Returns the
284
+ * base64 of the image the app displays (full screenshot wins over the crop,
285
+ * matching the app's own preference) so the caller can inline it.
286
+ */
287
+ function attachImages(data, images) {
288
+ let screenshot;
289
+ let cropped;
290
+ for (const img of images ?? []) {
291
+ const uri = storeImage(img.base64, img.kind);
292
+ if (img.kind === "cropped") {
293
+ data.croppedImageResourceUri = uri;
294
+ cropped = img.base64;
295
+ }
296
+ else {
297
+ data.screenshotResourceUri = uri;
298
+ screenshot = img.base64;
299
+ }
300
+ }
301
+ return screenshot ?? cropped;
302
+ }
258
303
  /** Diagnostics for the active connection's image store (used in debug logs). */
259
304
  function imageStoreDiagnostics() {
260
305
  const images = imageStoreState().images;
@@ -297,14 +342,8 @@ function noSessionResult(err) {
297
342
  */
298
343
  function resultToMcp(r) {
299
344
  const data = { ...r.data };
300
- for (const img of r.images ?? []) {
301
- const uri = storeImage(img.base64, img.kind);
302
- if (img.kind === "cropped")
303
- data.croppedImageResourceUri = uri;
304
- else
305
- data.screenshotResourceUri = uri;
306
- }
307
- return createToolResult(r.ok, r.text, data, r.code);
345
+ const image = attachImages(data, r.images);
346
+ return createToolResult(r.ok, r.text, data, r.code, image);
308
347
  }
309
348
  const DEFAULT_HEARTBEAT_MS = 3000;
310
349
  function makeProgressReporter(extra) {
@@ -410,7 +449,7 @@ function cancelledResultOrNull(error, tool) {
410
449
  * If generatedCode is provided, it's appended to the text response with instructions
411
450
  * for the agent to write it to the test file.
412
451
  */
413
- function createToolResult(success, textContent, structuredData, generatedCode) {
452
+ function createToolResult(success, textContent, structuredData, generatedCode, inlineImageBase64) {
414
453
  // Build text content - append generated code if provided with directive instructions
415
454
  let fullText = textContent;
416
455
  if (generatedCode && success) {
@@ -444,6 +483,7 @@ function createToolResult(success, textContent, structuredData, generatedCode) {
444
483
  generatedCode: generatedCode && success ? generatedCode : undefined,
445
484
  testFile: session?.testFile || undefined,
446
485
  },
486
+ _meta: inlineImageMeta(inlineImageBase64),
447
487
  };
448
488
  }
449
489
  /**
@@ -631,13 +671,7 @@ Debug mode (connect to existing sandbox):
631
671
  // Reproduce the server's exact success output (text + data) which differs
632
672
  // from the core's neutral text. Images are mapped to resource URIs.
633
673
  const data = { ...result.data, duration };
634
- for (const img of result.images ?? []) {
635
- const uri = storeImage(img.base64, img.kind);
636
- if (img.kind === "cropped")
637
- data.croppedImageResourceUri = uri;
638
- else
639
- data.screenshotResourceUri = uri;
640
- }
674
+ const image = attachImages(data, result.images);
641
675
  if (result.data.debugMode) {
642
676
  // Debug (existing-sandbox) success text — preserve original wording.
643
677
  const text = `Connected to existing sandbox (debug mode)
@@ -646,7 +680,7 @@ Sandbox: ${result.data.sandboxId}
646
680
  Expires in: ${Math.round(params.keepAlive / 1000)}s
647
681
 
648
682
  You are now connected to the sandbox in its current state. Use find, click, type, etc. to interact.`;
649
- return createToolResult(true, text, data, result.code);
683
+ return createToolResult(true, text, data, result.code, image);
650
684
  }
651
685
  // Normal provisioning success — append the EXACT dependency guidance block.
652
686
  const text = `${result.text}
@@ -662,7 +696,7 @@ IMPORTANT - If creating a new test project, use these EXACT dependencies in pack
662
696
  "test": "vitest"
663
697
  }
664
698
  }`;
665
- return createToolResult(true, text, data, result.code);
699
+ return createToolResult(true, text, data, result.code, image);
666
700
  }
667
701
  catch (error) {
668
702
  // On client cancellation, tear down the half-provisioned session so we
@@ -1502,6 +1536,7 @@ Useful for:
1502
1536
  screenshotResourceUri,
1503
1537
  duration
1504
1538
  },
1539
+ _meta: inlineImageMeta(imageBase64),
1505
1540
  };
1506
1541
  }
1507
1542
  catch (error) {
@@ -1533,17 +1568,19 @@ Only use 'screenshot' when you explicitly want to show something to the human us
1533
1568
  logger.info("screenshot: Starting");
1534
1569
  try {
1535
1570
  const result = await core.screenshot();
1536
- // Store the captured image as a resource URI (kept OUT of AI context — the
1537
- // MCP app fetches it via resources/read). Preserve the original user-facing
1538
- // text rather than the core's neutral text.
1571
+ // The image goes to the MCP app only (inline in `_meta`, with a resource
1572
+ // URI as fallback) and stays OUT of AI context. Preserve the original
1573
+ // user-facing text rather than the core's neutral text.
1539
1574
  const data = { action: "screenshot" };
1575
+ let image;
1540
1576
  for (const img of result.images ?? []) {
1541
1577
  data.screenshotResourceUri = storeImage(img.base64, "screenshot");
1578
+ image = img.base64;
1542
1579
  }
1543
1580
  const duration = Date.now() - startTime;
1544
1581
  data.duration = duration;
1545
1582
  logger.info("screenshot: Completed", { duration, hasImage: (result.images?.length ?? 0) > 0 });
1546
- return createToolResult(true, "Screenshot captured and displayed to user", data);
1583
+ return createToolResult(true, "Screenshot captured and displayed to user", data, undefined, image);
1547
1584
  }
1548
1585
  catch (error) {
1549
1586
  if (error instanceof NoActiveSessionError) {
@@ -66,6 +66,20 @@ interface ToolResultData {
66
66
  debuggerUrl?: string;
67
67
  sessionId?: string;
68
68
  duration?: number;
69
+ imageError?: string; // Why the image could not be shown (set client-side)
70
+ }
71
+
72
+ // `_meta` key the server uses to send the image inline with a tool result.
73
+ const INLINE_IMAGE_META_KEY = "testdriver/image";
74
+
75
+ /**
76
+ * Read the image the server inlined in the tool result's `_meta`, as a data URL.
77
+ * Null when the result carries none (older server, or an image too large to inline).
78
+ */
79
+ function inlineImageUrl(result: CallToolResult): string | null {
80
+ const image = result._meta?.[INLINE_IMAGE_META_KEY] as { mimeType?: string; data?: string } | undefined;
81
+ if (!image || typeof image.data !== "string" || !image.data) return null;
82
+ return `data:${image.mimeType || "image/png"};base64,${image.data}`;
69
83
  }
70
84
 
71
85
  // Store session info globally for display
@@ -345,6 +359,9 @@ function renderResult(data: ToolResultData) {
345
359
  if (data.error) {
346
360
  statusText += ` - ${data.error}`;
347
361
  }
362
+ if (data.imageError) {
363
+ statusText += ` - ${data.imageError}`;
364
+ }
348
365
 
349
366
  actionStatusEl.textContent = statusText;
350
367
  actionStatusEl.className = statusClass;
@@ -450,28 +467,22 @@ const app = new App({ name: "TestDriver Screenshot", version: "1.0.0" });
450
467
  /**
451
468
  * Fetch screenshot blob from resource URI and convert to data URL
452
469
  */
453
- async function fetchScreenshotFromResource(resourceUri: string): Promise<string | null> {
454
- try {
455
- console.info("Fetching screenshot from resource:", resourceUri);
456
- const result = await app.request(
457
- { method: "resources/read", params: { uri: resourceUri } },
458
- ReadResourceResultSchema,
459
- );
460
-
461
- const content = result.contents[0];
462
- if (!content || !("blob" in content)) {
463
- console.error("Resource did not contain blob data");
464
- return null;
465
- }
466
-
467
- // Convert base64 blob to data URL
468
- const dataUrl = `data:${content.mimeType || "image/png"};base64,${content.blob}`;
469
- console.info("Screenshot fetched successfully, blob length:", content.blob.length);
470
- return dataUrl;
471
- } catch (error) {
472
- console.error("Failed to fetch screenshot resource:", error);
473
- return null;
470
+ async function fetchScreenshotFromResource(resourceUri: string): Promise<string> {
471
+ console.info("Fetching screenshot from resource:", resourceUri);
472
+ const result = await app.request(
473
+ { method: "resources/read", params: { uri: resourceUri } },
474
+ ReadResourceResultSchema,
475
+ );
476
+
477
+ const content = result.contents[0];
478
+ if (!content || !("blob" in content)) {
479
+ throw new Error("Resource did not contain image data");
474
480
  }
481
+
482
+ // Convert base64 blob to data URL
483
+ const dataUrl = `data:${content.mimeType || "image/png"};base64,${content.blob}`;
484
+ console.info("Screenshot fetched successfully, blob length:", content.blob.length);
485
+ return dataUrl;
475
486
  }
476
487
 
477
488
  // 2. Register handlers BEFORE connecting
@@ -517,13 +528,22 @@ app.ontoolresult = async (result) => {
517
528
  console.info("Has screenshotResourceUri:", !!data.screenshotResourceUri);
518
529
  console.info("Has croppedImageResourceUri:", !!data.croppedImageResourceUri);
519
530
 
520
- // If a screenshot or cropped image resource URI is provided, fetch the image from it
531
+ // Prefer the image the server sent inline with the result: no extra request.
532
+ if (!data.imageUrl) {
533
+ data.imageUrl = inlineImageUrl(result) ?? undefined;
534
+ }
535
+
536
+ // Otherwise fall back to fetching it by resource URI. If that fails, say so in
537
+ // the status bar instead of silently rendering an empty frame.
521
538
  const resourceUri = data.screenshotResourceUri || data.croppedImageResourceUri;
522
539
  if (resourceUri && !data.imageUrl) {
523
540
  showLoading("Fetching image...");
524
- const imageUrl = await fetchScreenshotFromResource(resourceUri);
525
- if (imageUrl) {
526
- data.imageUrl = imageUrl;
541
+ try {
542
+ data.imageUrl = await fetchScreenshotFromResource(resourceUri);
543
+ } catch (error) {
544
+ console.error("Failed to fetch screenshot resource:", error);
545
+ const reason = error instanceof Error ? error.message : String(error);
546
+ data.imageError = `Screenshot unavailable: ${reason}`;
527
547
  }
528
548
  }
529
549
 
@@ -326,6 +326,53 @@ function getStoredImage(id: string): StoredImage | undefined {
326
326
  return imageStoreState().images.get(id);
327
327
  }
328
328
 
329
+ /**
330
+ * `_meta` key carrying the image inline in a tool result, for the MCP App.
331
+ *
332
+ * The app used to get every image with a follow-up `resources/read` against the
333
+ * per-connection store above. That second round trip is fragile on hosted
334
+ * clients: when it fails the app has nothing to show and the preview collapses.
335
+ * Sending the bytes with the result removes the round trip. `_meta` is passed
336
+ * through to the app but, unlike `content`/`structuredContent`, is not shown to
337
+ * the model, so the base64 stays out of the agent's context. The resource URI
338
+ * is still set as a fallback (and for `check`'s `referenceImageUri`).
339
+ */
340
+ const INLINE_IMAGE_META_KEY = "testdriver/image";
341
+
342
+ /** Above this (base64 chars) the image is left to the resource fallback, so one
343
+ * huge frame can't push a tool result past a host's size limit. */
344
+ const MAX_INLINE_IMAGE_BASE64 = 2_000_000;
345
+
346
+ function inlineImageMeta(base64: string | undefined): CallToolResult["_meta"] | undefined {
347
+ if (!base64) return undefined;
348
+ if (base64.length > MAX_INLINE_IMAGE_BASE64) {
349
+ logger.debug("inlineImageMeta: Image too large to inline, relying on resource URI", { length: base64.length });
350
+ return undefined;
351
+ }
352
+ return { [INLINE_IMAGE_META_KEY]: { mimeType: "image/png", data: base64 } };
353
+ }
354
+
355
+ /**
356
+ * Store a result's images and record their resource URIs on `data`. Returns the
357
+ * base64 of the image the app displays (full screenshot wins over the crop,
358
+ * matching the app's own preference) so the caller can inline it.
359
+ */
360
+ function attachImages(data: Record<string, unknown>, images: ActionResult["images"]): string | undefined {
361
+ let screenshot: string | undefined;
362
+ let cropped: string | undefined;
363
+ for (const img of images ?? []) {
364
+ const uri = storeImage(img.base64, img.kind);
365
+ if (img.kind === "cropped") {
366
+ data.croppedImageResourceUri = uri;
367
+ cropped = img.base64;
368
+ } else {
369
+ data.screenshotResourceUri = uri;
370
+ screenshot = img.base64;
371
+ }
372
+ }
373
+ return screenshot ?? cropped;
374
+ }
375
+
329
376
  /** Diagnostics for the active connection's image store (used in debug logs). */
330
377
  function imageStoreDiagnostics(): { imageStoreSize: number; availableKeys: string[] } {
331
378
  const images = imageStoreState().images;
@@ -378,12 +425,8 @@ function noSessionResult(err: NoActiveSessionError): CallToolResult {
378
425
  */
379
426
  function resultToMcp(r: ActionResult): CallToolResult {
380
427
  const data: Record<string, unknown> = { ...r.data };
381
- for (const img of r.images ?? []) {
382
- const uri = storeImage(img.base64, img.kind);
383
- if (img.kind === "cropped") data.croppedImageResourceUri = uri;
384
- else data.screenshotResourceUri = uri;
385
- }
386
- return createToolResult(r.ok, r.text, data, r.code);
428
+ const image = attachImages(data, r.images);
429
+ return createToolResult(r.ok, r.text, data, r.code, image);
387
430
  }
388
431
 
389
432
  // =============================================================================
@@ -534,7 +577,8 @@ function createToolResult(
534
577
  success: boolean,
535
578
  textContent: string,
536
579
  structuredData: Record<string, unknown>,
537
- generatedCode?: string
580
+ generatedCode?: string,
581
+ inlineImageBase64?: string
538
582
  ): CallToolResult {
539
583
  // Build text content - append generated code if provided with directive instructions
540
584
  let fullText = textContent;
@@ -572,6 +616,7 @@ function createToolResult(
572
616
  generatedCode: generatedCode && success ? generatedCode : undefined,
573
617
  testFile: session?.testFile || undefined,
574
618
  },
619
+ _meta: inlineImageMeta(inlineImageBase64),
575
620
  };
576
621
  }
577
622
 
@@ -814,11 +859,7 @@ Debug mode (connect to existing sandbox):
814
859
  // Reproduce the server's exact success output (text + data) which differs
815
860
  // from the core's neutral text. Images are mapped to resource URIs.
816
861
  const data: Record<string, unknown> = { ...result.data, duration };
817
- for (const img of result.images ?? []) {
818
- const uri = storeImage(img.base64, img.kind);
819
- if (img.kind === "cropped") data.croppedImageResourceUri = uri;
820
- else data.screenshotResourceUri = uri;
821
- }
862
+ const image = attachImages(data, result.images);
822
863
 
823
864
  if (result.data.debugMode) {
824
865
  // Debug (existing-sandbox) success text — preserve original wording.
@@ -828,7 +869,7 @@ Sandbox: ${result.data.sandboxId}
828
869
  Expires in: ${Math.round(params.keepAlive / 1000)}s
829
870
 
830
871
  You are now connected to the sandbox in its current state. Use find, click, type, etc. to interact.`;
831
- return createToolResult(true, text, data, result.code);
872
+ return createToolResult(true, text, data, result.code, image);
832
873
  }
833
874
 
834
875
  // Normal provisioning success — append the EXACT dependency guidance block.
@@ -845,7 +886,7 @@ IMPORTANT - If creating a new test project, use these EXACT dependencies in pack
845
886
  "test": "vitest"
846
887
  }
847
888
  }`;
848
- return createToolResult(true, text, data, result.code);
889
+ return createToolResult(true, text, data, result.code, image);
849
890
  } catch (error) {
850
891
  // On client cancellation, tear down the half-provisioned session so we
851
892
  // don't leak a connected sandbox. The underlying SDK call may still be
@@ -1829,6 +1870,7 @@ Useful for:
1829
1870
  screenshotResourceUri,
1830
1871
  duration
1831
1872
  },
1873
+ _meta: inlineImageMeta(imageBase64),
1832
1874
  };
1833
1875
  } catch (error) {
1834
1876
  logger.error("view_local_screenshot: Failed", { error: String(error), path: params.path });
@@ -1867,19 +1909,21 @@ Only use 'screenshot' when you explicitly want to show something to the human us
1867
1909
  try {
1868
1910
  const result = await core.screenshot();
1869
1911
 
1870
- // Store the captured image as a resource URI (kept OUT of AI context — the
1871
- // MCP app fetches it via resources/read). Preserve the original user-facing
1872
- // text rather than the core's neutral text.
1912
+ // The image goes to the MCP app only (inline in `_meta`, with a resource
1913
+ // URI as fallback) and stays OUT of AI context. Preserve the original
1914
+ // user-facing text rather than the core's neutral text.
1873
1915
  const data: Record<string, unknown> = { action: "screenshot" };
1916
+ let image: string | undefined;
1874
1917
  for (const img of result.images ?? []) {
1875
1918
  data.screenshotResourceUri = storeImage(img.base64, "screenshot");
1919
+ image = img.base64;
1876
1920
  }
1877
1921
 
1878
1922
  const duration = Date.now() - startTime;
1879
1923
  data.duration = duration;
1880
1924
  logger.info("screenshot: Completed", { duration, hasImage: (result.images?.length ?? 0) > 0 });
1881
1925
 
1882
- return createToolResult(true, "Screenshot captured and displayed to user", data);
1926
+ return createToolResult(true, "Screenshot captured and displayed to user", data, undefined, image);
1883
1927
  } catch (error) {
1884
1928
  if (error instanceof NoActiveSessionError) {
1885
1929
  logger.warn("screenshot: No active session");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/mcp",
3
- "version": "7.11.283-canary",
3
+ "version": "7.11.284-test",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",