testdriverai 7.2.73 → 7.2.74

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.
@@ -1,13 +1,17 @@
1
1
  ---
2
+ name: testdriver
2
3
  description: An expert at creating and refining automated tests using TestDriver.ai
3
- capabilities:
4
- [
5
- "create tests",
6
- "refine tests",
7
- "debug tests",
8
- "use MCP workflow",
9
- "visual verification",
10
- ]
4
+ tools:
5
+ - testdriver/*
6
+ mcp-servers:
7
+ testdriver:
8
+ command: npx
9
+ args:
10
+ - -p
11
+ - testdriverai@beta
12
+ - testdriverai-mcp
13
+ env:
14
+ TD_API_KEY: ${TD_API_KEY}
11
15
  ---
12
16
 
13
17
  # TestDriver Expert
@@ -35,11 +39,12 @@ Use this agent when the user asks to:
35
39
  ### Workflow
36
40
 
37
41
  1. **Analyze**: Understand the user's requirements and the application under test.
38
- 2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app.
39
- 3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot showing the result.
40
- 4. **Verify**: Use `check` after actions and `assert` for test conditions.
41
- 5. **Commit**: Use `commit` to write recorded commands to a test file.
42
- 6. **Verify Test**: Use `verify` to run the generated test from scratch.
42
+ 2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app. Specify `testFile` to track where code should be written.
43
+ 3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot AND generated code.
44
+ 4. **⚠️ WRITE CODE IMMEDIATELY**: After EVERY successful action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the end.
45
+ 5. **Verify Actions**: Use `check` after actions to verify they succeeded (for YOUR understanding only).
46
+ 6. **Add Assertions**: Use `assert` for test conditions that should be in the final test file.
47
+ 7. **⚠️ RUN THE TEST YOURSELF**: Use `npx vitest run <testFile>` to run the test - do NOT tell the user to run it. Iterate until it passes.
43
48
 
44
49
  ## Prerequisites
45
50
 
@@ -108,12 +113,16 @@ describe("My Test Suite", () => {
108
113
  await testdriver.provision.chrome({
109
114
  url: "https://example.com",
110
115
  });
116
+ await testdriver.screenshot(); // Capture initial page state
111
117
 
112
118
  // Find elements and interact
113
119
  const button = await testdriver.find("Sign In button");
120
+ await testdriver.screenshot(); // Capture before click
114
121
  await button.click();
122
+ await testdriver.screenshot(); // Capture after click
115
123
 
116
124
  // Assert using natural language
125
+ await testdriver.screenshot(); // Capture before assertion
117
126
  const result = await testdriver.assert("the dashboard is visible");
118
127
  expect(result).toBeTruthy();
119
128
  });
@@ -177,9 +186,9 @@ await element.mouseUp(); // release mouse
177
186
  element.found(); // check if found (boolean)
178
187
  ```
179
188
 
180
- ### Screenshots
189
+ ### Screenshots for Debugging
181
190
 
182
- Use `screenshot()` **only when the user explicitly asks** to see what the screen looks like. Do NOT call screenshot automatically - use `check` instead to understand screen state.
191
+ **Use `screenshot()` liberally throughout your tests** to capture the screen state at key moments. This makes debugging much easier when tests fail - you can see exactly what the screen looked like at each step.
183
192
 
184
193
  ```javascript
185
194
  // Capture a screenshot - saved to .testdriver/screenshots/<test-file>/
@@ -190,6 +199,14 @@ console.log("Screenshot saved to:", screenshotPath);
190
199
  await testdriver.screenshot(1, false, true);
191
200
  ```
192
201
 
202
+ **When to add screenshots:**
203
+ - After provisioning (initial page load)
204
+ - Before and after clicking important elements
205
+ - After typing text into fields
206
+ - Before assertions (to see what the AI is evaluating)
207
+ - After any action that changes the page state
208
+ - When debugging a flaky or failing test
209
+
193
210
  **Screenshot file organization:**
194
211
 
195
212
  ```
@@ -210,107 +227,106 @@ await testdriver.screenshot(1, false, true);
210
227
  ### Key Advantages
211
228
 
212
229
  - **No need to restart** - continue from current state
213
- - **Automatic command recording** - successful commands are logged
214
- - **Code generation** - convert recorded commands to test files
230
+ - **Generated code with every action** - each tool returns the code to add to your test
215
231
  - **Use `check` to verify** - understand screen state without explicit screenshots
216
232
 
233
+ ### ⚠️ CRITICAL: Write Code Immediately & Run Tests Yourself
234
+
235
+ **Every MCP tool response includes "ACTION REQUIRED: Append this code..." - you MUST write that code to the test file IMMEDIATELY before proceeding to the next action.**
236
+
237
+ **When ready to validate, RUN THE TEST YOURSELF using `npx vitest run`. Do NOT tell the user to run it.**
238
+
217
239
  ### Step 1: Start a Session
218
240
 
219
241
  ```
220
- session_start({ type: "chrome", url: "https://your-app.com/login" })
242
+ session_start({ type: "chrome", url: "https://your-app.com/login", testFile: "tests/login.test.mjs" })
221
243
  → Screenshot shows login page
244
+ → Response includes: "ACTION REQUIRED: Append this code..."
245
+ → ⚠️ IMMEDIATELY write to tests/login.test.mjs:
246
+ await testdriver.provision.chrome({ url: "https://your-app.com/login" });
247
+ await testdriver.screenshot(); // Capture initial page state
222
248
  ```
223
249
 
224
250
  This provisions a sandbox with Chrome and navigates to your URL. You'll see a screenshot of the initial page.
225
251
 
226
252
  ### Step 2: Interact with the App
227
253
 
228
- Find elements and interact with them:
254
+ Find elements and interact with them. **Write code to file after EACH action, including screenshots for debugging:**
229
255
 
230
256
  ```
231
- find({ description: "email input field" })
232
- → Returns: screenshot with element highlighted, coordinates, and a ref ID
233
-
234
- click({ elementRef: "el-123456" })
235
- Returns: screenshot with click marker
257
+ find_and_click({ description: "email input field" })
258
+ → Returns: screenshot with element highlighted
259
+ → ⚠️ IMMEDIATELY append to test file:
260
+ await testdriver.find("email input field").click();
261
+ await testdriver.screenshot(); // Capture after click
236
262
 
237
263
  type({ text: "user@example.com" })
238
264
  → Returns: screenshot showing typed text
265
+ → ⚠️ IMMEDIATELY append to test file:
266
+ await testdriver.type("user@example.com");
267
+ await testdriver.screenshot(); // Capture after typing
239
268
  ```
240
269
 
241
- Or combine find + click in one step:
242
-
243
- ```
244
- find_and_click({ description: "Sign In button" })
245
- ```
246
-
247
- ### Step 3: Verify Actions Succeeded
270
+ ### Step 3: Verify Actions Succeeded (For Your Understanding)
248
271
 
249
- After each action, use `check` to verify it worked:
272
+ After actions, use `check` to verify they worked. This is for YOUR understanding - does NOT generate code:
250
273
 
251
274
  ```
252
275
  check({ task: "Was the email entered into the field?" })
253
276
  → Returns: AI analysis comparing previous screenshot to current state
254
277
  ```
255
278
 
256
- ### Step 4: Add Assertions
279
+ ### Step 4: Add Assertions (Generates Code)
257
280
 
258
- Use `assert` for pass/fail conditions that get recorded in test files:
281
+ Use `assert` for pass/fail conditions. This DOES generate code for the test file:
259
282
 
260
283
  ```
261
284
  assert({ assertion: "the dashboard is visible" })
262
285
  → Returns: pass/fail with screenshot
286
+ → ⚠️ IMMEDIATELY append to test file:
287
+ await testdriver.screenshot(); // Capture before assertion
288
+ const assertResult = await testdriver.assert("the dashboard is visible");
289
+ expect(assertResult).toBeTruthy();
263
290
  ```
264
291
 
265
- ### Step 5: Commit to Test File
292
+ ### Step 5: Run the Test Yourself
266
293
 
267
- When your sequence works, save it:
294
+ **⚠️ YOU must run the test - do NOT tell the user to run it:**
268
295
 
296
+ ```bash
297
+ npx vitest run tests/login.test.mjs
269
298
  ```
270
- commit({
271
- testFile: "tests/login.test.mjs",
272
- testName: "Login Flow",
273
- testDescription: "User can log in with email and password"
274
- })
275
- ```
276
-
277
- ### Step 6: Verify the Test
278
299
 
279
- Run the generated test from scratch to ensure it works:
280
-
281
- ```
282
- verify({ testFile: "tests/login.test.mjs" })
283
- ```
300
+ Analyze the output, fix any issues, and iterate until the test passes.
284
301
 
285
302
  ### MCP Tools Reference
286
303
 
287
304
  | Tool | Description |
288
305
  |------|-------------|
289
- | `session_start` | Start sandbox with browser/app, capture initial screenshot |
290
- | `session_status` | Check session health, time remaining, command count |
306
+ | `session_start` | Start sandbox with browser/app, returns screenshot + provision code |
307
+ | `session_status` | Check session health and time remaining |
291
308
  | `session_extend` | Add more time before session expires |
292
309
  | `find` | Locate element by description, returns ref for later use |
293
- | `click` | Click on element ref or coordinates |
310
+ | `click` | Click on element ref |
294
311
  | `find_and_click` | Find and click in one action |
295
312
  | `type` | Type text into focused field |
296
313
  | `press_keys` | Press keyboard shortcuts (e.g., `["ctrl", "a"]`) |
297
314
  | `scroll` | Scroll page (up/down/left/right) |
298
- | `check` | AI analysis of whether a task completed |
299
- | `assert` | AI-powered boolean assertion (pass/fail for test files) |
315
+ | `check` | AI analysis of screen state - for YOUR understanding only, does NOT generate code |
316
+ | `assert` | AI-powered boolean assertion - GENERATES CODE for test files |
300
317
  | `exec` | Execute JavaScript, shell, or PowerShell in sandbox |
301
318
  | `screenshot` | Capture screenshot - **only use when user explicitly asks** |
302
- | `commit` | Write recorded commands to test file |
303
- | `verify` | Run test file from scratch |
304
- | `get_command_log` | View recorded commands before committing |
305
319
 
306
320
  ### Tips for MCP Workflow
307
321
 
308
- 1. **Work incrementally** - Don't try to build the entire test at once
309
- 2. **Use `check` after every action** - Verify your actions succeeded before moving on
310
- 3. **Be specific with element descriptions** - "the blue Sign In button in the header" is better than "button"
311
- 4. **Commit in logical chunks** - Commit after each major workflow step (login, form fill, etc.)
312
- 5. **Extend session proactively** - Sessions expire after 5 minutes; use `session_extend` if needed
313
- 6. **Review the command log** - Use `get_command_log` to see what will be committed
322
+ 1. **⚠️ Write code IMMEDIATELY** - After EVERY action, append generated code to test file RIGHT AWAY
323
+ 2. **⚠️ Run tests YOURSELF** - Use `npx vitest run` - do NOT tell user to run tests
324
+ 3. **⚠️ Add screenshots liberally** - Include `await testdriver.screenshot()` after every significant action for debugging
325
+ 4. **Work incrementally** - Don't try to build the entire test at once
326
+ 5. **Use `check` after actions** - Verify your actions succeeded before moving on (for YOUR understanding)
327
+ 6. **Use `assert` for test verifications** - These generate code that goes in the test file
328
+ 7. **Be specific with element descriptions** - "the blue Sign In button in the header" is better than "button"
329
+ 8. **Extend session proactively** - Sessions expire after 5 minutes; use `session_extend` if needed
314
330
 
315
331
  ## Recommended Development Workflow
316
332
 
@@ -325,17 +341,21 @@ verify({ testFile: "tests/login.test.mjs" })
325
341
  it("should incrementally build test", async (context) => {
326
342
  const testdriver = TestDriver(context);
327
343
  await testdriver.provision.chrome({ url: "https://example.com" });
344
+ await testdriver.screenshot(); // Capture initial state
328
345
 
329
346
  // Step 1: Find and inspect
330
347
  const element = await testdriver.find("Some button");
331
348
  console.log("Element found:", element.found());
332
349
  console.log("Coordinates:", element.x, element.y);
333
350
  console.log("Confidence:", element.confidence);
351
+ await testdriver.screenshot(); // Capture after find
334
352
 
335
353
  // Step 2: Interact
336
354
  await element.click();
355
+ await testdriver.screenshot(); // Capture after click
337
356
 
338
357
  // Step 3: Assert and log
358
+ await testdriver.screenshot(); // Capture before assertion
339
359
  const result = await testdriver.assert("Something happened");
340
360
  console.log("Assertion result:", result);
341
361
  expect(result).toBeTruthy();
@@ -417,33 +437,42 @@ const date = await testdriver.exec("pwsh", "Get-Date", 5000);
417
437
 
418
438
  ### Capturing Screenshots
419
439
 
440
+ **Add screenshots liberally throughout your tests** for debugging. When a test fails, you'll have a visual trail showing exactly what happened at each step.
441
+
420
442
  ```javascript
421
- // Capture a screenshot and save to file
422
- const screenshot = await testdriver.screenshot();
423
- const filepath = "screenshot.png";
424
- fs.writeFileSync(filepath, Buffer.from(screenshot, "base64"));
425
- console.log("Screenshot saved to:", filepath);
443
+ // Basic screenshot - automatically saved to .testdriver/screenshots/<test-file>/
444
+ await testdriver.screenshot();
426
445
 
427
446
  // Capture with mouse cursor visible
428
- const screenshotWithMouse = await testdriver.screenshot(1, false, true);
429
- fs.writeFileSync(
430
- "screenshot-with-mouse.png",
431
- Buffer.from(screenshotWithMouse, "base64"),
432
- );
433
- console.log("Screenshot with mouse saved to: screenshot-with-mouse.png");
447
+ await testdriver.screenshot(1, false, true);
448
+
449
+ // Recommended pattern: screenshot after every significant action
450
+ await testdriver.provision.chrome({ url: "https://example.com" });
451
+ await testdriver.screenshot(); // After page load
452
+
453
+ await testdriver.find("Login button").click();
454
+ await testdriver.screenshot(); // After click
455
+
456
+ await testdriver.type("user@example.com");
457
+ await testdriver.screenshot(); // After typing
458
+
459
+ await testdriver.screenshot(); // Before assertion
460
+ const result = await testdriver.assert("dashboard is visible");
434
461
  ```
435
462
 
436
463
  ## Tips for Agents
437
464
 
438
- 1. **Use MCP tools for development** - Don't write test files manually; use the MCP workflow to build tests interactively
439
- 2. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
440
- 3. **Look at test samples** in `node_modules/testdriverai/test` for working examples
441
- 4. **Use `check` to understand screen state** - This is how you verify what the sandbox shows. Only use `screenshot` when the user asks to see the screen.
442
- 5. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis, `assert` gives boolean pass/fail
443
- 6. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
444
- 7. **Start simple** - get one step working before adding more
445
- 8. **Commit working sequences** - Don't lose progress; use `commit` after each successful interaction sequence
446
- 9. **Always `await` async methods** - TestDriver will warn if you forget, but for TypeScript projects, add `@typescript-eslint/no-floating-promises` to your ESLint config to catch missing `await` at compile time:
465
+ 1. **⚠️ WRITE CODE IMMEDIATELY** - After EVERY successful MCP action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the session ends.
466
+ 2. **⚠️ RUN TESTS YOURSELF** - Do NOT tell the user to run tests. YOU must run the tests using `npx vitest run <testFile>`. Analyze the output and iterate until the test passes.
467
+ 3. **⚠️ ADD SCREENSHOTS LIBERALLY** - Include `await testdriver.screenshot()` throughout your tests: after provision, before/after clicks, after typing, and before assertions. This creates a visual trail that makes debugging failures much easier.
468
+ 4. **Use MCP tools for development** - Build tests interactively with visual feedback
469
+ 5. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
470
+ 6. **Look at test samples** in `node_modules/testdriverai/test` for working examples
471
+ 7. **Use `check` to understand screen state** - This is how you verify what the sandbox shows during MCP development.
472
+ 8. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis (no code), `assert` gives boolean pass/fail (generates code)
473
+ 9. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
474
+ 10. **Start simple** - get one step working before adding more
475
+ 11. **Always `await` async methods** - TestDriver will warn if you forget, but for TypeScript projects, add `@typescript-eslint/no-floating-promises` to your ESLint config to catch missing `await` at compile time:
447
476
 
448
477
  ```json
449
478
  // eslint.config.js (for TypeScript projects)
@@ -453,5 +482,3 @@ console.log("Screenshot with mouse saved to: screenshot-with-mouse.png");
453
482
  }
454
483
  }
455
484
  ```
456
-
457
- 10. **Use `verify` to validate tests** - After committing, run `verify` to ensure the generated test works from scratch.
@@ -21,6 +21,7 @@ class InitCommand extends BaseCommand {
21
21
  await this.createGitHubWorkflow();
22
22
  await this.createGitignore();
23
23
  await this.createVscodeMcpConfig();
24
+ await this.createVscodeExtensions();
24
25
  await this.installDependencies();
25
26
  await this.copySkills();
26
27
  await this.createAgents();
@@ -149,7 +150,10 @@ class InitCommand extends BaseCommand {
149
150
  try {
150
151
  execSync(`setx ${key} "${value}"`, { stdio: "ignore" });
151
152
  console.log(
152
- chalk.green(` ✓ Set ${key} as user environment variable\n`),
153
+ chalk.green(` ✓ Set ${key} as user environment variable`),
154
+ );
155
+ console.log(
156
+ chalk.gray(` Restart your terminal for changes to take effect\n`),
153
157
  );
154
158
  } catch (error) {
155
159
  console.log(
@@ -184,7 +188,10 @@ class InitCommand extends BaseCommand {
184
188
  );
185
189
  fs.writeFileSync(profilePath, updated);
186
190
  console.log(
187
- chalk.green(` ✓ Updated ${key} in ${profilePath}\n`),
191
+ chalk.green(` ✓ Updated ${key} in ${profilePath}`),
192
+ );
193
+ console.log(
194
+ chalk.gray(` Run: source ${profilePath} (or open a new terminal)\n`),
188
195
  );
189
196
  return;
190
197
  }
@@ -193,7 +200,10 @@ class InitCommand extends BaseCommand {
193
200
  // Append to profile
194
201
  fs.appendFileSync(profilePath, `\n${exportLine}\n`);
195
202
  console.log(
196
- chalk.green(` ✓ Added ${key} to ${profilePath}\n`),
203
+ chalk.green(` ✓ Added ${key} to ${profilePath}`),
204
+ );
205
+ console.log(
206
+ chalk.gray(` Run: source ${profilePath} (or open a new terminal)\n`),
197
207
  );
198
208
  }
199
209
 
@@ -478,10 +488,10 @@ jobs:
478
488
 
479
489
  if (!fs.existsSync(mcpConfigFile)) {
480
490
  const mcpConfig = {
481
- mcpServers: {
491
+ servers: {
482
492
  testdriver: {
483
493
  command: "npx",
484
- args: ["testdriverai-mcp"],
494
+ args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
485
495
  env: {
486
496
  TD_API_KEY: "${TD_API_KEY}",
487
497
  },
@@ -499,6 +509,36 @@ jobs:
499
509
  }
500
510
  }
501
511
 
512
+ /**
513
+ * Create VSCode extensions recommendations
514
+ */
515
+ async createVscodeExtensions() {
516
+ const vscodeDir = path.join(process.cwd(), ".vscode");
517
+ const extensionsFile = path.join(vscodeDir, "extensions.json");
518
+
519
+ // Create .vscode directory if it doesn't exist
520
+ if (!fs.existsSync(vscodeDir)) {
521
+ fs.mkdirSync(vscodeDir, { recursive: true });
522
+ console.log(chalk.gray(` Created directory: ${vscodeDir}`));
523
+ }
524
+
525
+ if (!fs.existsSync(extensionsFile)) {
526
+ const extensionsConfig = {
527
+ recommendations: [
528
+ "vitest.explorer",
529
+ ],
530
+ };
531
+
532
+ fs.writeFileSync(
533
+ extensionsFile,
534
+ JSON.stringify(extensionsConfig, null, 2) + "\n",
535
+ );
536
+ console.log(chalk.green(` Created extensions config: ${extensionsFile}`));
537
+ } else {
538
+ console.log(chalk.gray(" Extensions config already exists, skipping..."));
539
+ }
540
+ }
541
+
502
542
  /**
503
543
  * Copy TestDriver skills from the package to the project
504
544
  */
@@ -557,7 +597,7 @@ jobs:
557
597
  }
558
598
 
559
599
  /**
560
- * Create TestDriver agents in GitHub Copilot format
600
+ * Copy TestDriver agents to .github/agents
561
601
  */
562
602
  async createAgents() {
563
603
  const agentsDestDir = path.join(process.cwd(), ".github", "agents");
@@ -576,98 +616,36 @@ jobs:
576
616
  }
577
617
  }
578
618
 
619
+ if (!agentsSourceDir) {
620
+ console.log(chalk.yellow(" ⚠️ Agents directory not found, skipping agents copy..."));
621
+ return;
622
+ }
623
+
579
624
  // Create .github/agents directory if it doesn't exist
580
625
  if (!fs.existsSync(agentsDestDir)) {
581
626
  fs.mkdirSync(agentsDestDir, { recursive: true });
582
627
  console.log(chalk.gray(` Created directory: ${agentsDestDir}`));
583
628
  }
584
629
 
585
- // If we found source agents, convert them to .agent.md format
586
- if (agentsSourceDir) {
587
- const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
588
-
589
- for (const agentFile of agentFiles) {
590
- const sourcePath = path.join(agentsSourceDir, agentFile);
591
- const agentName = agentFile.replace(".md", "");
592
- const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
593
-
594
- if (!fs.existsSync(destPath)) {
595
- const sourceContent = fs.readFileSync(sourcePath, "utf8");
596
-
597
- // Parse the source frontmatter and body
598
- const frontmatterMatch = sourceContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
599
-
600
- if (frontmatterMatch) {
601
- const frontmatterText = frontmatterMatch[1];
602
- const body = frontmatterMatch[2];
603
-
604
- // Extract description from frontmatter
605
- const descMatch = frontmatterText.match(/description:\s*["']?(.*?)["']?$/m);
606
- const description = descMatch ? descMatch[1] : `TestDriver ${agentName} agent`;
607
-
608
- // Create GitHub Copilot agent format
609
- const agentContent = `---
610
- name: ${agentName}
611
- description: ${description}
612
- tools:
613
- - testdriver/*
614
- mcp-servers:
615
- testdriver:
616
- command: npx
617
- args:
618
- - testdriverai-mcp
619
- env:
620
- TD_API_KEY: \${TD_API_KEY}
621
- ---
622
- ${body}`;
623
-
624
- fs.writeFileSync(destPath, agentContent);
625
- console.log(chalk.green(` Created agent: ${destPath}`));
626
- }
627
- } else {
628
- console.log(chalk.gray(` Agent ${agentName}.agent.md already exists, skipping...`));
629
- }
630
+ // Copy agent files with .agent.md extension
631
+ const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
632
+ let copiedCount = 0;
633
+
634
+ for (const agentFile of agentFiles) {
635
+ const sourcePath = path.join(agentsSourceDir, agentFile);
636
+ const agentName = agentFile.replace(".md", "");
637
+ const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
638
+
639
+ if (!fs.existsSync(destPath)) {
640
+ fs.copyFileSync(sourcePath, destPath);
641
+ copiedCount++;
630
642
  }
631
- } else {
632
- // Create a default test-writer agent if no source found
633
- const defaultAgentPath = path.join(agentsDestDir, "test-writer.agent.md");
634
-
635
- if (!fs.existsSync(defaultAgentPath)) {
636
- const defaultAgentContent = `---
637
- name: test-writer
638
- description: An expert at creating and refining automated tests using TestDriver.ai
639
- tools:
640
- - testdriver/*
641
- mcp-servers:
642
- testdriver:
643
- command: npx
644
- args:
645
- - testdriverai-mcp
646
- env:
647
- TD_API_KEY: \${TD_API_KEY}
648
- ---
649
- # TestDriver Expert
650
-
651
- You are an expert at writing automated tests using the TestDriver library. Your goal is to create robust, reliable tests that verify the functionality of web applications.
652
-
653
- ## Workflow
654
-
655
- 1. **Start Session**: Use \`session_start\` to provision a sandbox with browser
656
- 2. **Interact**: Use \`find\`, \`click\`, \`type\` etc. - each returns a screenshot
657
- 3. **Verify**: Use \`check\` after actions and \`assert\` for test conditions
658
- 4. **Build Test**: Append generated code to your test file
659
- 5. **Validate**: Use \`verify\` to run the test from scratch
660
-
661
- ## Tips
662
-
663
- - Be specific with element descriptions: "blue Sign In button in the header" > "button"
664
- - Use \`check\` after actions to verify they succeeded
665
- - Start simple - get one step working before adding more
666
- `;
643
+ }
667
644
 
668
- fs.writeFileSync(defaultAgentPath, defaultAgentContent);
669
- console.log(chalk.green(` Created default agent: ${defaultAgentPath}`));
670
- }
645
+ if (copiedCount > 0) {
646
+ console.log(chalk.green(` Copied ${copiedCount} agent(s) to ${agentsDestDir}`));
647
+ } else {
648
+ console.log(chalk.gray(" Agents already exist, skipping..."));
671
649
  }
672
650
  }
673
651
 
@@ -705,7 +683,7 @@ You are an expert at writing automated tests using the TestDriver library. Your
705
683
  console.log(" 1. Run your tests:");
706
684
  console.log(chalk.gray(" npx vitest run\n"));
707
685
  console.log(" 2. Use AI agents to write tests:");
708
- console.log(chalk.gray(" Open VSCode/Cursor and use @test-writer agent\n"));
686
+ console.log(chalk.gray(" Open VSCode/Cursor and use @testdriver agent\n"));
709
687
  console.log(" 3. MCP server configured:");
710
688
  console.log(chalk.gray(" TestDriver tools available via MCP in .vscode/mcp.json\n"));
711
689
  console.log(
@@ -47,7 +47,7 @@ class SetupCommand extends Command {
47
47
 
48
48
  this.installSkills(sourceSkills, path.join(CLAUDE_HOME, "skills"));
49
49
  this.installAgents(sourceAgents, path.join(CLAUDE_HOME, "agents"));
50
- this.installMcp();
50
+ this.installClaudeMcp();
51
51
  this.installCursorMcp();
52
52
  await this.promptForApiKey();
53
53
 
@@ -125,7 +125,7 @@ class SetupCommand extends Command {
125
125
  /**
126
126
  * Add testdriver MCP server to ~/.claude.json
127
127
  */
128
- installMcp() {
128
+ installClaudeMcp() {
129
129
  let config = {};
130
130
 
131
131
  if (fs.existsSync(CLAUDE_MCP_FILE)) {
@@ -182,13 +182,13 @@ class SetupCommand extends Command {
182
182
  }
183
183
  }
184
184
 
185
- if (!config.mcpServers) {
186
- config.mcpServers = {};
185
+ if (!config.servers) {
186
+ config.servers = {};
187
187
  }
188
188
 
189
- const alreadyConfigured = config.mcpServers["testdriver-cloud"];
189
+ const alreadyConfigured = config.servers["testdriver-cloud"];
190
190
 
191
- Object.assign(config.mcpServers, CURSOR_MCP_SERVER_CONFIG);
191
+ Object.assign(config.servers, CURSOR_MCP_SERVER_CONFIG);
192
192
  fs.writeFileSync(CURSOR_MCP_FILE, JSON.stringify(config, null, 2) + "\n");
193
193
 
194
194
  if (alreadyConfigured) {
@@ -90,8 +90,8 @@ Note: This type uses \`Record<K, string | undefined>\` rather than \`Partial<Rec
90
90
  for compatibility with Zod schema generation. Both are functionally equivalent for validation.`);c.object({method:c.literal("ui/open-link"),params:c.object({url:c.string().describe("URL to open in the host's browser")})});var fI=c.object({isError:c.boolean().optional().describe("True if the host failed to open the URL (e.g., due to security policy).")}).passthrough(),pI=c.object({isError:c.boolean().optional().describe("True if the host rejected or failed to deliver the message.")}).passthrough();c.object({method:c.literal("ui/notifications/sandbox-proxy-ready"),params:c.object({})});var fo=c.object({connectDomains:c.array(c.string()).optional().describe("Origins for network requests (fetch/XHR/WebSocket)."),resourceDomains:c.array(c.string()).optional().describe("Origins for static resources (scripts, images, styles, fonts)."),frameDomains:c.array(c.string()).optional().describe("Origins for nested iframes (frame-src directive)."),baseUriDomains:c.array(c.string()).optional().describe("Allowed base URIs for the document (base-uri directive).")}),po=c.object({camera:c.object({}).optional().describe("Request camera access (Permission Policy `camera` feature)."),microphone:c.object({}).optional().describe("Request microphone access (Permission Policy `microphone` feature)."),geolocation:c.object({}).optional().describe("Request geolocation access (Permission Policy `geolocation` feature)."),clipboardWrite:c.object({}).optional().describe("Request clipboard write access (Permission Policy `clipboard-write` feature).")});c.object({method:c.literal("ui/notifications/size-changed"),params:c.object({width:c.number().optional().describe("New width in pixels."),height:c.number().optional().describe("New height in pixels.")})});var vI=c.object({method:c.literal("ui/notifications/tool-input"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Complete tool call arguments as key-value pairs.")).optional().describe("Complete tool call arguments as key-value pairs.")})}),hI=c.object({method:c.literal("ui/notifications/tool-input-partial"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Partial tool call arguments (incomplete, may change).")).optional().describe("Partial tool call arguments (incomplete, may change).")})}),gI=c.object({method:c.literal("ui/notifications/tool-cancelled"),params:c.object({reason:c.string().optional().describe('Optional reason for the cancellation (e.g., "user action", "timeout").')})}),$I=c.object({fonts:c.string().optional()}),_I=c.object({variables:mI.optional().describe("CSS variables for theming the app."),css:$I.optional().describe("CSS blocks that apps can inject.")}),bI=c.object({method:c.literal("ui/resource-teardown"),params:c.object({})});c.record(c.string(),c.unknown());var ts=c.object({text:c.object({}).optional().describe("Host supports text content blocks."),image:c.object({}).optional().describe("Host supports image content blocks."),audio:c.object({}).optional().describe("Host supports audio content blocks."),resource:c.object({}).optional().describe("Host supports resource content blocks."),resourceLink:c.object({}).optional().describe("Host supports resource link content blocks."),structuredContent:c.object({}).optional().describe("Host supports structured content.")}),yI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),openLinks:c.object({}).optional().describe("Host supports opening external URLs."),serverTools:c.object({listChanged:c.boolean().optional().describe("Host supports tools/list_changed notifications.")}).optional().describe("Host can proxy tool calls to the MCP server."),serverResources:c.object({listChanged:c.boolean().optional().describe("Host supports resources/list_changed notifications.")}).optional().describe("Host can proxy resource reads to the MCP server."),logging:c.object({}).optional().describe("Host accepts log messages."),sandbox:c.object({permissions:po.optional().describe("Permissions granted by the host (camera, microphone, geolocation)."),csp:fo.optional().describe("CSP domains approved by the host.")}).optional().describe("Sandbox configuration applied by the host."),updateModelContext:ts.optional().describe("Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns."),message:ts.optional().describe("Host supports receiving content messages (ui/message) from the view.")}),kI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),tools:c.object({listChanged:c.boolean().optional().describe("App supports tools/list_changed notifications.")}).optional().describe("App exposes MCP-style tools that the host can call."),availableDisplayModes:c.array(Ut).optional().describe("Display modes the app supports.")});c.object({method:c.literal("ui/notifications/initialized"),params:c.object({}).optional()});c.object({csp:fo.optional().describe("Content Security Policy configuration."),permissions:po.optional().describe("Sandbox permissions requested by the UI."),domain:c.string().optional().describe("Dedicated origin for view sandbox."),prefersBorder:c.boolean().optional().describe("Visual boundary preference - true if UI prefers a visible border.")});c.object({method:c.literal("ui/request-display-mode"),params:c.object({mode:Ut.describe("The display mode being requested.")})});var II=c.object({mode:Ut.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),wI=c.union([c.literal("model"),c.literal("app")]).describe("Tool visibility scope - who can access the tool.");c.object({resourceUri:c.string().optional(),visibility:c.array(wI).optional().describe(`Who can access this tool. Default: ["model", "app"]
91
91
  - "model": Tool visible to and callable by the agent
92
92
  - "app": Tool callable by the app from this server only`)});c.object({mimeTypes:c.array(c.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});c.object({method:c.literal("ui/message"),params:c.object({role:c.literal("user").describe('Message role, currently only "user" is supported.'),content:c.array(Ct).describe("Message content blocks (text, image, etc.).")})});c.object({method:c.literal("ui/notifications/sandbox-resource-ready"),params:c.object({html:c.string().describe("HTML content to load into the inner iframe."),sandbox:c.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:fo.optional().describe("CSP configuration from resource metadata."),permissions:po.optional().describe("Sandbox permissions from resource metadata.")})});var SI=c.object({method:c.literal("ui/notifications/tool-result"),params:Li.describe("Standard MCP tool execution result.")}),Ef=c.object({toolInfo:c.object({id:Pt.optional().describe("JSON-RPC id of the tools/call request."),tool:hr.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:cI.optional().describe("Current color theme preference."),styles:_I.optional().describe("Style configuration for theming the app."),displayMode:Ut.optional().describe("How the UI is currently displayed."),availableDisplayModes:c.array(Ut).optional().describe("Display modes the host supports."),containerDimensions:c.union([c.object({height:c.number().describe("Fixed container height in pixels.")}),c.object({maxHeight:c.union([c.number(),c.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(c.union([c.object({width:c.number().describe("Fixed container width in pixels.")}),c.object({maxWidth:c.union([c.number(),c.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
93
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),xI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Ef.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ai.describe("App identification (name and version)."),appCapabilities:kI.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var zI=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ai.describe("Host application identification and version."),hostCapabilities:yI.describe("Features and capabilities provided by the host."),hostContext:Ef.describe("Rich context about the host environment.")}).passthrough();function ZI(e){let t=document.documentElement;t.setAttribute("data-theme",e),t.style.colorScheme=e}function UI(e,t=document.documentElement){for(let[n,r]of Object.entries(e))r!==void 0&&t.style.setProperty(n,r)}function OI(e){if(document.getElementById("__mcp-host-fonts"))return;let t=document.createElement("style");t.id="__mcp-host-fonts",t.textContent=e,document.head.appendChild(t)}class NI extends U_{constructor(n,r={},i={autoResize:!0}){super(i);se(this,"_appInfo");se(this,"_capabilities");se(this,"options");se(this,"_hostCapabilities");se(this,"_hostInfo");se(this,"_hostContext");se(this,"sendOpenLink",this.openLink);this._appInfo=n,this._capabilities=r,this.options=i,this.setRequestHandler(Ci,a=>(console.log("Received ping:",a.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(n){this.setNotificationHandler(vI,r=>n(r.params))}set ontoolinputpartial(n){this.setNotificationHandler(hI,r=>n(r.params))}set ontoolresult(n){this.setNotificationHandler(SI,r=>n(r.params))}set ontoolcancelled(n){this.setNotificationHandler(gI,r=>n(r.params))}set onhostcontextchanged(n){this.setNotificationHandler(xI,r=>{this._hostContext={...this._hostContext,...r.params},n(r.params)})}set onteardown(n){this.setRequestHandler(bI,(r,i)=>n(r.params,i))}set oncalltool(n){this.setRequestHandler(Ws,(r,i)=>n(r.params,i))}set onlisttools(n){this.setRequestHandler(qs,(r,i)=>n(r.params,i))}assertCapabilityForMethod(n){}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){return await this.request({method:"tools/call",params:n},Li,r)}sendMessage(n,r){return this.request({method:"ui/message",params:n},pI,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this.request({method:"ui/update-model-context",params:n},tr,r)}openLink(n,r){return this.request({method:"ui/open-link",params:n},fI,r)}requestDisplayMode(n,r){return this.request({method:"ui/request-display-mode",params:n},II,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,i=0,a=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let s=document.documentElement,u=s.style.width,l=s.style.height;s.style.width="fit-content",s.style.height="fit-content";let m=s.getBoundingClientRect();s.style.width=u,s.style.height=l;let d=window.innerWidth-s.clientWidth,p=Math.ceil(m.width+d),g=Math.ceil(m.height);(p!==r||g!==i)&&(r=p,i=g,this.sendSizeChanged({width:p,height:g}))}))};a();let o=new ResizeObserver(a);return o.observe(document.documentElement),o.observe(document.body),()=>o.disconnect()}async connect(n=new N_(window.parent,window.parent),r){var i;await super.connect(n);try{let a=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:T_}},zI,r);if(a===void 0)throw Error(`Server sent invalid initialize result: ${a}`);this._hostCapabilities=a.hostCapabilities,this._hostInfo=a.hostInfo,this._hostContext=a.hostContext,await this.notification({method:"ui/notifications/initialized"}),(i=this.options)!=null&&i.autoResize&&this.setupSizeChangedNotifications()}catch(a){throw this.close(),a}}}const je=document.querySelector(".main"),Ee=document.getElementById("screenshot-container"),$e=document.getElementById("screenshot"),_t=document.getElementById("overlays"),Me=document.getElementById("action-status"),qe=document.getElementById("session-info"),vo=document.getElementById("loading-overlay"),TI=vo.querySelector(".loading-text"),rt=document.createElement("div");rt.id="target-info";rt.className="target-info hidden";let _i=0,qn=0,We=null;function PI(e){return e.structuredContent??{}}function ho(e="Waiting for screenshot..."){TI.textContent=e,vo.classList.remove("hidden")}function yt(){vo.classList.add("hidden")}function Df(e){var n,r,i;e.theme&&ZI(e.theme),(n=e.styles)!=null&&n.variables&&UI(e.styles.variables),(i=(r=e.styles)==null?void 0:r.css)!=null&&i.fonts&&OI(e.styles.css.fonts),e.safeAreaInsets&&(je.style.paddingTop=`${e.safeAreaInsets.top}px`,je.style.paddingRight=`${e.safeAreaInsets.right}px`,je.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,je.style.paddingLeft=`${e.safeAreaInsets.left}px`);const t=e.containerDimensions;t&&("height"in t?(document.documentElement.style.height="100vh",je.style.height="100%"):"maxHeight"in t&&t.maxHeight&&(document.documentElement.style.maxHeight=`${t.maxHeight}px`,je.style.maxHeight="100%"),"width"in t?(document.documentElement.style.width="100vw",je.style.width="100%"):"maxWidth"in t&&t.maxWidth&&(document.documentElement.style.maxWidth=`${t.maxWidth}px`,je.style.maxWidth="100%"))}function is(e,t,n){return t===0?e:e/t*n}function jI(){Ee.style.transform="none",Ee.classList.remove("zoomed")}function Rf(e){requestAnimationFrame(()=>{var i,a;_t.innerHTML="";const t=$e.clientWidth,n=$e.clientHeight;if(console.info("addOverlays:",{action:e.action,hasElement:!!e.element,hasClickPosition:!!e.clickPosition,displayedWidth:t,displayedHeight:n,naturalWidth:_i,naturalHeight:qn}),t===0||n===0){console.warn("addOverlays: Dimensions not ready, retrying..."),setTimeout(()=>Rf(e),50);return}if((e.action==="find"||e.action==="find_and_click"||e.action==="findall")&&e.element){const o=document.createElement("div");o.className="element-target",o.style.left=`${t/2}px`,o.style.top=`${n/2}px`;const s=document.createElement("div");s.className="crosshair-h",o.appendChild(s);const u=document.createElement("div");u.className="crosshair-v",o.appendChild(u);const l=document.createElement("div");l.className="element-label",l.textContent=((i=e.element)==null?void 0:i.description)||"Element",(a=e.element)!=null&&a.confidence&&(l.textContent+=` (${Math.round(e.element.confidence*100)}%)`),o.appendChild(l),_t.appendChild(o),console.info("addOverlays: Added element target at center")}if(e.clickPosition){const o=e.clickPosition.centerX??e.clickPosition.x,s=e.clickPosition.centerY??e.clickPosition.y;if(o!==void 0&&s!==void 0&&_i>0){const u=document.createElement("div");u.className="click-marker";const l=is(o,_i,t),m=is(s,qn,n);u.style.left=`${l}px`,u.style.top=`${m}px`;const d=document.createElement("div");d.className="click-ripple",u.appendChild(d),_t.appendChild(u),console.info("addOverlays: Added click marker at",{clickX:o,clickY:s,scaledX:l,scaledY:m})}}if(e.scrollDirection){const o=document.createElement("div");o.className=`scroll-indicator scroll-${e.scrollDirection}`,o.textContent=e.scrollDirection==="up"?"↑":e.scrollDirection==="down"?"↓":e.scrollDirection==="left"?"←":"→",_t.appendChild(o)}jI(),delete Ee.dataset.focalX,delete Ee.dataset.focalY})}function EI(e){_t.innerHTML="";const t=e.action||"unknown",n=e.success?"✓":"✗",r=e.success?"success":"error";let i=`${n} ${t}`;if(e.duration&&(i+=` (${e.duration}ms)`),e.assertion&&(i+=`: "${e.assertion}"`),e.text&&e.action==="type"&&(i+=`: "${e.text}"`),e.error&&(i+=` - ${e.error}`),Me.textContent=i,Me.className=r,e.debuggerUrl&&(We=e.debuggerUrl),e.session){const a=e.session.expiresIn?Math.round(e.session.expiresIn/1e3):0;if(qe.innerHTML="",We){const o=document.createElement("a");o.href=We,o.target="_blank",o.rel="noopener noreferrer",o.textContent=`${a}s remaining`,o.className="debugger-link",o.title=`Open debugger: ${We}`,qe.appendChild(o)}else qe.textContent=`${a}s remaining`;qe.className=a<30?"warning":""}else if(We){qe.innerHTML="";const a=document.createElement("a");a.href=We,a.target="_blank",a.rel="noopener noreferrer",a.textContent="Open Debugger",a.className="debugger-link",a.title=We,qe.appendChild(a)}else e.action==="session_start"&&(qe.textContent="Session started");if(e.element&&(e.action==="find"||e.action==="find_and_click")){const a=e.element;let o=`<strong>Target:</strong> "${a.description||"Element"}"`;if(a.centerX!==void 0&&a.centerY!==void 0&&(o+=` <span class="target-coords">(${Math.round(a.centerX)}, ${Math.round(a.centerY)})</span>`),a.confidence!==void 0){const s=Math.round(a.confidence*100);o+=` <span class="target-confidence ${s>=70?"high":s>=40?"medium":"low"}">${s}%</span>`}a.ref&&(o+=` <span class="target-ref">ref: ${a.ref}</span>`),rt.innerHTML=o,rt.classList.remove("hidden")}else rt.classList.add("hidden");e.imageUrl?(ho("Loading image..."),$e.onerror=()=>{console.error("Image failed to load"),$e.alt="Image failed to load",Ee.style.display="none",yt()},$e.onload=()=>{console.info("Image loaded:",$e.naturalWidth,"x",$e.naturalHeight),_i=$e.naturalWidth,qn=$e.naturalHeight,Ee.style.display="block",Rf(e),yt()},$e.src=e.imageUrl,$e.style.display="block"):($e.style.display="none",Ee.style.display="none",yt())}const Oe=new NI({name:"TestDriver Screenshot",version:"1.0.0"});async function DI(e){try{console.info("Fetching screenshot from resource:",e);const n=(await Oe.request({method:"resources/read",params:{uri:e}},Js)).contents[0];if(!n||!("blob"in n))return console.error("Resource did not contain blob data"),null;const r=`data:${n.mimeType||"image/png"};base64,${n.blob}`;return console.info("Screenshot fetched successfully, blob length:",n.blob.length),r}catch(t){return console.error("Failed to fetch screenshot resource:",t),null}}Oe.onteardown=async()=>(console.info("TestDriver app being torn down"),{});Oe.ontoolinput=e=>{console.info("Received tool input:",e),Me.textContent="Running action...",Me.className="loading",Ee.style.display="none",ho("Running action...")};Oe.ontoolresult=async e=>{console.info("Received tool result:",e),console.info("structuredContent:",e.structuredContent);const t=PI(e);console.info("Extracted data keys:",Object.keys(t)),console.info("Has imageUrl:",!!t.imageUrl),console.info("Has screenshotResourceUri:",!!t.screenshotResourceUri),console.info("Has croppedImageResourceUri:",!!t.croppedImageResourceUri);const n=t.screenshotResourceUri||t.croppedImageResourceUri;if(n&&!t.imageUrl){ho("Fetching image...");const r=await DI(n);r&&(t.imageUrl=r)}EI(t)};Oe.ontoolcancelled=e=>{console.info("Tool cancelled:",e.reason),Me.textContent=`Cancelled: ${e.reason}`,Me.className="error",yt()};Oe.onerror=e=>{console.error("App error:",e),Me.textContent=`Error: ${e}`,Me.className="error",yt()};Oe.onhostcontextchanged=Df;Oe.connect().then(()=>{const e=Oe.getHostContext();e&&Df(e)});const $i=document.querySelector(".screenshot-wrapper");$i&&$i.parentNode&&$i.parentNode.insertBefore(rt,$i.nextSibling);</script>
94
- <style rel="stylesheet" crossorigin>*{box-sizing:border-box;margin:0;padding:0}html,body{font-family:var(--font-sans, system-ui, -apple-system, sans-serif);font-size:var(--font-text-sm-size, 14px);line-height:var(--font-text-sm-line-height, 1.5);background:var(--color-background-primary, #fff);color:var(--color-text-primary, #1a1a1a);overflow:hidden;margin:0;padding:0}body{height:100%;display:flex;flex-direction:column}.main{width:100%;max-width:100%;display:flex;flex-direction:column;overflow:hidden}.screenshot-wrapper{position:relative;width:100%;flex:1;min-height:0;background:var(--color-background-secondary, #f5f5f5);border-radius:var(--border-radius-md, 8px);overflow:hidden;display:flex;flex-direction:column}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px;flex:1;min-height:100px;background:var(--color-background-secondary, #f5f5f5)}.loading-overlay.hidden{display:none}.loading-spinner{width:32px;height:32px;border:3px solid var(--color-border-primary, #e5e5e5);border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:var(--font-text-sm-size, 14px);color:var(--color-text-secondary, #666)}.screenshot-container{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center;justify-content:center}#screenshot{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}#overlays{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.element-target{position:absolute;width:24px;height:24px;margin-left:-12px;margin-top:-12px;background:#3b82f64d;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d;animation:target-pulse 1.5s ease-in-out infinite}@keyframes target-pulse{0%,to{transform:scale(1);border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d}50%{transform:scale(1.1);border-color:#60a5fa;box-shadow:0 0 0 2px #fff,0 0 12px #3b82f680}}.crosshair-h,.crosshair-v{position:absolute;background:#3b82f6}.crosshair-h{width:40px;height:2px;left:50%;top:50%;margin-left:-20px;margin-top:-1px}.crosshair-v{width:2px;height:40px;left:50%;top:50%;margin-left:-1px;margin-top:-20px}.element-label{position:absolute;top:100%;left:50%;transform:translate(-50%);margin-top:24px;background:#3b82f6;color:#fff;font-size:11px;padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px #0003}.click-marker{position:absolute;width:16px;height:16px;margin-left:-8px;margin-top:-8px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 8px #0000004d}.click-ripple{position:absolute;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px;border:2px solid #ef4444;border-radius:50%;animation:ripple 1s ease-out infinite}@keyframes ripple{0%{transform:scale(.5);opacity:1}to{transform:scale(2);opacity:0}}.scroll-indicator{position:absolute;font-size:48px;color:#3b82f6;text-shadow:0 2px 8px rgba(0,0,0,.3);animation:scroll-bounce .5s ease-in-out}.scroll-up{top:20px;left:50%;transform:translate(-50%)}.scroll-down{bottom:20px;left:50%;transform:translate(-50%)}.scroll-left{left:20px;top:50%;transform:translateY(-50%)}.scroll-right{right:20px;top:50%;transform:translateY(-50%)}@keyframes scroll-bounce{0%,to{opacity:1}50%{opacity:.5}}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-top:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-xs-size, 12px)}#action-status{font-weight:var(--font-weight-medium, 500)}#action-status.success{color:#10b981}#action-status.error{color:#ef4444}#action-status.loading,#session-info{color:var(--color-text-secondary, #666)}#session-info.warning{color:#f59e0b}.debugger-link{color:#3b82f6;text-decoration:none;transition:color .15s ease}.debugger-link:hover{color:#2563eb;text-decoration:underline}.target-info{padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-bottom:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-sm-size, 14px);color:var(--color-text-primary, #1a1a1a)}.target-info.hidden{display:none}.target-info strong{color:#3b82f6;font-weight:var(--font-weight-medium, 500)}.target-coords{color:var(--color-text-secondary, #666);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 12px);margin-left:4px}.target-confidence{display:inline-block;padding:1px 6px;border-radius:10px;font-size:var(--font-text-xs-size, 11px);font-weight:var(--font-weight-medium, 500);margin-left:6px}.target-confidence.high{background:#dcfce7;color:#166534}.target-confidence.medium{background:#fef3c7;color:#92400e}.target-confidence.low{background:#fee2e2;color:#991b1b}.target-ref{color:var(--color-text-tertiary, #999);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 11px);margin-left:8px}@media(prefers-color-scheme:dark){html:not([data-theme=light]) .element-target{border-color:#60a5fa;background:#60a5fa33;box-shadow:0 0 0 2px #1f2937,0 2px 8px #00000080}html:not([data-theme=light]) .crosshair-h,html:not([data-theme=light]) .crosshair-v{background:#60a5fa}html:not([data-theme=light]) .element-label{background:#2563eb}html:not([data-theme=light]) .click-marker{background:#f87171;border-color:#1f2937}html:not([data-theme=light]) .click-ripple{border-color:#f87171}html:not([data-theme=light]) .scroll-indicator,html:not([data-theme=light]) .debugger-link{color:#60a5fa}html:not([data-theme=light]) .debugger-link:hover{color:#93c5fd}html:not([data-theme=light]) .target-info strong{color:#60a5fa}html:not([data-theme=light]) .target-confidence.high{background:#166534;color:#dcfce7}html:not([data-theme=light]) .target-confidence.medium{background:#92400e;color:#fef3c7}html:not([data-theme=light]) .target-confidence.low{background:#991b1b;color:#fee2e2}}</style>
93
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),xI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Ef.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ai.describe("App identification (name and version)."),appCapabilities:kI.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var zI=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ai.describe("Host application identification and version."),hostCapabilities:yI.describe("Features and capabilities provided by the host."),hostContext:Ef.describe("Rich context about the host environment.")}).passthrough();function ZI(e){let t=document.documentElement;t.setAttribute("data-theme",e),t.style.colorScheme=e}function UI(e,t=document.documentElement){for(let[n,r]of Object.entries(e))r!==void 0&&t.style.setProperty(n,r)}function OI(e){if(document.getElementById("__mcp-host-fonts"))return;let t=document.createElement("style");t.id="__mcp-host-fonts",t.textContent=e,document.head.appendChild(t)}class NI extends U_{constructor(n,r={},i={autoResize:!0}){super(i);se(this,"_appInfo");se(this,"_capabilities");se(this,"options");se(this,"_hostCapabilities");se(this,"_hostInfo");se(this,"_hostContext");se(this,"sendOpenLink",this.openLink);this._appInfo=n,this._capabilities=r,this.options=i,this.setRequestHandler(Ci,a=>(console.log("Received ping:",a.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(n){this.setNotificationHandler(vI,r=>n(r.params))}set ontoolinputpartial(n){this.setNotificationHandler(hI,r=>n(r.params))}set ontoolresult(n){this.setNotificationHandler(SI,r=>n(r.params))}set ontoolcancelled(n){this.setNotificationHandler(gI,r=>n(r.params))}set onhostcontextchanged(n){this.setNotificationHandler(xI,r=>{this._hostContext={...this._hostContext,...r.params},n(r.params)})}set onteardown(n){this.setRequestHandler(bI,(r,i)=>n(r.params,i))}set oncalltool(n){this.setRequestHandler(Ws,(r,i)=>n(r.params,i))}set onlisttools(n){this.setRequestHandler(qs,(r,i)=>n(r.params,i))}assertCapabilityForMethod(n){}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){return await this.request({method:"tools/call",params:n},Li,r)}sendMessage(n,r){return this.request({method:"ui/message",params:n},pI,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this.request({method:"ui/update-model-context",params:n},tr,r)}openLink(n,r){return this.request({method:"ui/open-link",params:n},fI,r)}requestDisplayMode(n,r){return this.request({method:"ui/request-display-mode",params:n},II,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,i=0,a=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let s=document.documentElement,u=s.style.width,l=s.style.height;s.style.width="fit-content",s.style.height="fit-content";let m=s.getBoundingClientRect();s.style.width=u,s.style.height=l;let d=window.innerWidth-s.clientWidth,p=Math.ceil(m.width+d),g=Math.ceil(m.height);(p!==r||g!==i)&&(r=p,i=g,this.sendSizeChanged({width:p,height:g}))}))};a();let o=new ResizeObserver(a);return o.observe(document.documentElement),o.observe(document.body),()=>o.disconnect()}async connect(n=new N_(window.parent,window.parent),r){var i;await super.connect(n);try{let a=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:T_}},zI,r);if(a===void 0)throw Error(`Server sent invalid initialize result: ${a}`);this._hostCapabilities=a.hostCapabilities,this._hostInfo=a.hostInfo,this._hostContext=a.hostContext,await this.notification({method:"ui/notifications/initialized"}),(i=this.options)!=null&&i.autoResize&&this.setupSizeChangedNotifications()}catch(a){throw this.close(),a}}}const je=document.querySelector(".main"),Ee=document.getElementById("screenshot-container"),$e=document.getElementById("screenshot"),_t=document.getElementById("overlays"),Me=document.getElementById("action-status"),qe=document.getElementById("session-info"),vo=document.getElementById("loading-overlay"),TI=vo.querySelector(".loading-text"),rt=document.createElement("div");rt.id="target-info";rt.className="target-info hidden";let _i=0,qn=0,We=null;function PI(e){return e.structuredContent??{}}function ho(e="Waiting for screenshot..."){TI.textContent=e,vo.classList.remove("hidden")}function yt(){vo.classList.add("hidden")}function Df(e){var n,r,i;e.theme&&ZI(e.theme),(n=e.styles)!=null&&n.variables&&UI(e.styles.variables),(i=(r=e.styles)==null?void 0:r.css)!=null&&i.fonts&&OI(e.styles.css.fonts),e.safeAreaInsets&&(je.style.paddingTop=`${e.safeAreaInsets.top}px`,je.style.paddingRight=`${e.safeAreaInsets.right}px`,je.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,je.style.paddingLeft=`${e.safeAreaInsets.left}px`);const t=e.containerDimensions;t&&("height"in t?(document.documentElement.style.height="100vh",je.style.height="100%"):"maxHeight"in t&&t.maxHeight&&(document.documentElement.style.maxHeight=`${t.maxHeight}px`,je.style.maxHeight="100%"),"width"in t?(document.documentElement.style.width="100vw",je.style.width="100%"):"maxWidth"in t&&t.maxWidth&&(document.documentElement.style.maxWidth=`${t.maxWidth}px`,je.style.maxWidth="100%"))}function is(e,t,n){return t===0?e:e/t*n}function jI(){Ee.style.transform="none",Ee.classList.remove("zoomed")}function Rf(e){requestAnimationFrame(()=>{var i,a;_t.innerHTML="";const t=$e.clientWidth,n=$e.clientHeight;if(console.info("addOverlays:",{action:e.action,hasElement:!!e.element,hasClickPosition:!!e.clickPosition,displayedWidth:t,displayedHeight:n,naturalWidth:_i,naturalHeight:qn}),t===0||n===0){console.warn("addOverlays: Dimensions not ready, retrying..."),setTimeout(()=>Rf(e),50);return}if((e.action==="find"||e.action==="find_and_click"||e.action==="findall")&&e.element){const o=document.createElement("div");o.className="element-target",o.style.left=`${t/2}px`,o.style.top=`${n/2}px`;const s=document.createElement("div");s.className="crosshair-h",o.appendChild(s);const u=document.createElement("div");u.className="crosshair-v",o.appendChild(u);const l=document.createElement("div");l.className="element-label",l.textContent=((i=e.element)==null?void 0:i.description)||"Element",(a=e.element)!=null&&a.confidence&&(l.textContent+=` (${Math.round(e.element.confidence*100)}%)`),o.appendChild(l),_t.appendChild(o),console.info("addOverlays: Added element target at center")}if(e.clickPosition){const o=e.clickPosition.centerX??e.clickPosition.x,s=e.clickPosition.centerY??e.clickPosition.y;if(o!==void 0&&s!==void 0&&_i>0){const u=document.createElement("div");u.className="click-marker";const l=is(o,_i,t),m=is(s,qn,n);u.style.left=`${l}px`,u.style.top=`${m}px`;const d=document.createElement("div");d.className="click-ripple",u.appendChild(d),_t.appendChild(u),console.info("addOverlays: Added click marker at",{clickX:o,clickY:s,scaledX:l,scaledY:m})}}if(e.scrollDirection){const o=document.createElement("div");o.className=`scroll-indicator scroll-${e.scrollDirection}`,o.textContent=e.scrollDirection==="up"?"↑":e.scrollDirection==="down"?"↓":e.scrollDirection==="left"?"←":"→",_t.appendChild(o)}jI(),delete Ee.dataset.focalX,delete Ee.dataset.focalY})}function EI(e){_t.innerHTML="";const t=e.action||"unknown",n=e.success?"✓":"✗",r=e.success?"success":"error";let i=`${n} ${t}`;if(e.duration&&(i+=` (${e.duration}ms)`),e.assertion&&(i+=`: "${e.assertion}"`),e.text&&e.action==="type"&&(i+=`: "${e.text}"`),e.error&&(i+=` - ${e.error}`),Me.textContent=i,Me.className=r,e.debuggerUrl&&(We=e.debuggerUrl),e.session){const a=e.session.expiresIn?Math.round(e.session.expiresIn/1e3):0;if(qe.innerHTML="",We){const o=document.createElement("a");o.href=We,o.target="_blank",o.rel="noopener noreferrer",o.textContent=`${a}s remaining`,o.className="debugger-link",o.title=`Open debugger: ${We}`,qe.appendChild(o)}else qe.textContent=`${a}s remaining`;qe.className=a<30?"warning":""}else if(We){qe.innerHTML="";const a=document.createElement("a");a.href=We,a.target="_blank",a.rel="noopener noreferrer",a.textContent="Open Debugger",a.className="debugger-link",a.title=We,qe.appendChild(a)}else e.action==="session_start"&&(qe.textContent="Session started");if(e.element&&(e.action==="find"||e.action==="find_and_click")){const a=e.element;let o=`<strong>Target:</strong> "${a.description||"Element"}"`;if(a.centerX!==void 0&&a.centerY!==void 0&&(o+=` <span class="target-coords">(${Math.round(a.centerX)}, ${Math.round(a.centerY)})</span>`),a.confidence!==void 0){const s=Math.round(a.confidence*100);o+=` <span class="target-confidence ${s>=70?"high":s>=40?"medium":"low"}">${s}%</span>`}rt.innerHTML=o,rt.classList.remove("hidden")}else rt.classList.add("hidden");e.imageUrl?(ho("Loading image..."),$e.onerror=()=>{console.error("Image failed to load"),$e.alt="Image failed to load",Ee.style.display="none",yt()},$e.onload=()=>{console.info("Image loaded:",$e.naturalWidth,"x",$e.naturalHeight),_i=$e.naturalWidth,qn=$e.naturalHeight,Ee.style.display="block",Rf(e),yt()},$e.src=e.imageUrl,$e.style.display="block"):($e.style.display="none",Ee.style.display="none",yt())}const Oe=new NI({name:"TestDriver Screenshot",version:"1.0.0"});async function DI(e){try{console.info("Fetching screenshot from resource:",e);const n=(await Oe.request({method:"resources/read",params:{uri:e}},Js)).contents[0];if(!n||!("blob"in n))return console.error("Resource did not contain blob data"),null;const r=`data:${n.mimeType||"image/png"};base64,${n.blob}`;return console.info("Screenshot fetched successfully, blob length:",n.blob.length),r}catch(t){return console.error("Failed to fetch screenshot resource:",t),null}}Oe.onteardown=async()=>(console.info("TestDriver app being torn down"),{});Oe.ontoolinput=e=>{console.info("Received tool input:",e);const t=e.arguments,n=[];t&&(t.description&&n.push(`"${t.description}"`),t.text&&n.push(`"${t.text}"`),t.url&&n.push(`${t.url}`),t.direction&&n.push(`${t.direction}`),t.assertion&&n.push(`"${t.assertion}"`),t.task&&n.push(`"${t.task}"`),t.keys&&n.push(`[${t.keys.join("+")}]`),t.type&&n.push(`${t.type}`));const r=n.length>0?n.join(" "):"action";Me.textContent=`Running ${r}...`,Me.className="loading",Ee.style.display="none",ho(`Running ${r}...`)};Oe.ontoolresult=async e=>{console.info("Received tool result:",e),console.info("structuredContent:",e.structuredContent);const t=PI(e);console.info("Extracted data keys:",Object.keys(t)),console.info("Has imageUrl:",!!t.imageUrl),console.info("Has screenshotResourceUri:",!!t.screenshotResourceUri),console.info("Has croppedImageResourceUri:",!!t.croppedImageResourceUri);const n=t.screenshotResourceUri||t.croppedImageResourceUri;if(n&&!t.imageUrl){ho("Fetching image...");const r=await DI(n);r&&(t.imageUrl=r)}EI(t)};Oe.ontoolcancelled=e=>{console.info("Tool cancelled:",e.reason),Me.textContent=`Cancelled: ${e.reason}`,Me.className="error",yt()};Oe.onerror=e=>{console.error("App error:",e),Me.textContent=`Error: ${e}`,Me.className="error",yt()};Oe.onhostcontextchanged=Df;Oe.connect().then(()=>{const e=Oe.getHostContext();e&&Df(e)});const $i=document.querySelector(".screenshot-wrapper");$i&&$i.parentNode&&$i.parentNode.insertBefore(rt,$i.nextSibling);</script>
94
+ <style rel="stylesheet" crossorigin>*{box-sizing:border-box;margin:0;padding:0}html,body{font-family:var(--font-sans, system-ui, -apple-system, sans-serif);font-size:var(--font-text-sm-size, 14px);line-height:var(--font-text-sm-line-height, 1.5);background:var(--color-background-primary, #fff);color:var(--color-text-primary, #1a1a1a);overflow:hidden;margin:0;padding:0}body{height:100%;display:flex;flex-direction:column}.main{width:100%;max-width:100%;display:flex;flex-direction:column;overflow:hidden}.screenshot-wrapper{position:relative;width:100%;flex:1;min-height:0;background:var(--color-background-secondary, #f5f5f5);border-radius:var(--border-radius-md, 8px);overflow:hidden;display:flex;flex-direction:column}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px;flex:1;min-height:100px;background:var(--color-background-secondary, #f5f5f5)}.loading-overlay.hidden{display:none}.loading-spinner{width:32px;height:32px;border:3px solid var(--color-border-primary, #e5e5e5);border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:var(--font-text-sm-size, 14px);color:var(--color-text-secondary, #666)}.screenshot-container{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center;justify-content:center}#screenshot{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}#overlays{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.element-target{position:absolute;width:24px;height:24px;margin-left:-12px;margin-top:-12px;background:#3b82f64d;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d;animation:target-pulse 1.5s ease-in-out infinite}@keyframes target-pulse{0%,to{transform:scale(1);border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d}50%{transform:scale(1.1);border-color:#60a5fa;box-shadow:0 0 0 2px #fff,0 0 12px #3b82f680}}.crosshair-h,.crosshair-v{position:absolute;background:#3b82f6}.crosshair-h{width:40px;height:2px;left:50%;top:50%;margin-left:-20px;margin-top:-1px}.crosshair-v{width:2px;height:40px;left:50%;top:50%;margin-left:-1px;margin-top:-20px}.element-label{position:absolute;top:100%;left:50%;transform:translate(-50%);margin-top:24px;background:#3b82f6;color:#fff;font-size:11px;padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px #0003}.click-marker{position:absolute;width:16px;height:16px;margin-left:-8px;margin-top:-8px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 8px #0000004d}.click-ripple{position:absolute;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px;border:2px solid #ef4444;border-radius:50%;animation:ripple 1s ease-out infinite}@keyframes ripple{0%{transform:scale(.5);opacity:1}to{transform:scale(2);opacity:0}}.scroll-indicator{position:absolute;font-size:48px;color:#3b82f6;text-shadow:0 2px 8px rgba(0,0,0,.3);animation:scroll-bounce .5s ease-in-out}.scroll-up{top:20px;left:50%;transform:translate(-50%)}.scroll-down{bottom:20px;left:50%;transform:translate(-50%)}.scroll-left{left:20px;top:50%;transform:translateY(-50%)}.scroll-right{right:20px;top:50%;transform:translateY(-50%)}@keyframes scroll-bounce{0%,to{opacity:1}50%{opacity:.5}}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-top:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-xs-size, 12px)}#action-status{font-weight:var(--font-weight-medium, 500)}#action-status.success{color:#10b981}#action-status.error{color:#ef4444}#action-status.loading,#session-info{color:var(--color-text-secondary, #666)}#session-info.warning{color:#f59e0b}.debugger-link{color:#3b82f6;text-decoration:none;transition:color .15s ease}.debugger-link:hover{color:#2563eb;text-decoration:underline}.target-info{padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-bottom:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-sm-size, 14px);color:var(--color-text-primary, #1a1a1a)}.target-info.hidden{display:none}.target-info strong{color:#3b82f6;font-weight:var(--font-weight-medium, 500)}.target-coords{color:var(--color-text-secondary, #666);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 12px);margin-left:4px}.target-confidence{display:inline-block;padding:1px 6px;border-radius:10px;font-size:var(--font-text-xs-size, 11px);font-weight:var(--font-weight-medium, 500);margin-left:6px}.target-confidence.high{background:#dcfce7;color:#166534}.target-confidence.medium{background:#fef3c7;color:#92400e}.target-confidence.low{background:#fee2e2;color:#991b1b}@media(prefers-color-scheme:dark){html:not([data-theme=light]) .element-target{border-color:#60a5fa;background:#60a5fa33;box-shadow:0 0 0 2px #1f2937,0 2px 8px #00000080}html:not([data-theme=light]) .crosshair-h,html:not([data-theme=light]) .crosshair-v{background:#60a5fa}html:not([data-theme=light]) .element-label{background:#2563eb}html:not([data-theme=light]) .click-marker{background:#f87171;border-color:#1f2937}html:not([data-theme=light]) .click-ripple{border-color:#f87171}html:not([data-theme=light]) .scroll-indicator,html:not([data-theme=light]) .debugger-link{color:#60a5fa}html:not([data-theme=light]) .debugger-link:hover{color:#93c5fd}html:not([data-theme=light]) .target-info strong{color:#60a5fa}html:not([data-theme=light]) .target-confidence.high{background:#166534;color:#dcfce7}html:not([data-theme=light]) .target-confidence.medium{background:#92400e;color:#fef3c7}html:not([data-theme=light]) .target-confidence.low{background:#991b1b;color:#fee2e2}}</style>
95
95
  </head>
96
96
  <body>
97
97
  <main class="main">
@@ -150,7 +150,7 @@ export declare const SessionStartInputSchema: z.ZodObject<{
150
150
  os: z.ZodDefault<z.ZodEnum<["linux", "windows"]>>;
151
151
  /** Keep sandbox alive duration in ms (default: 5 minutes) */
152
152
  keepAlive: z.ZodDefault<z.ZodNumber>;
153
- /** Path to test file being built */
153
+ /** Path to test file - when provided, you MUST append generated code to this file after each action */
154
154
  testFile: z.ZodOptional<z.ZodString>;
155
155
  /** Reconnect to last sandbox */
156
156
  reconnect: z.ZodDefault<z.ZodBoolean>;
@@ -121,8 +121,8 @@ export const SessionStartInputSchema = z.object({
121
121
  os: z.enum(["linux", "windows"]).default("linux").describe("Sandbox OS"),
122
122
  /** Keep sandbox alive duration in ms (default: 5 minutes) */
123
123
  keepAlive: z.number().default(300000).describe("Keep sandbox alive for this many ms"),
124
- /** Path to test file being built */
125
- testFile: z.string().optional().describe("Path to test file being built"),
124
+ /** Path to test file - when provided, you MUST append generated code to this file after each action */
125
+ testFile: z.string().optional().describe("Path to test file. When provided, append generated code from each action to this file immediately."),
126
126
  /** Reconnect to last sandbox */
127
127
  reconnect: z.boolean().default(false).describe("Reconnect to last sandbox"),
128
128
  /** API endpoint URL */
@@ -226,14 +226,22 @@ function requireActiveSession() {
226
226
  * Images: imageUrl (data URL) goes to structuredContent for UI to display
227
227
  * The croppedImage from find() is small (~10KB) so it's acceptable as data URL
228
228
  *
229
- * If generatedCode is provided, it's appended to the text response so the agent
230
- * can add it to their test file.
229
+ * If generatedCode is provided, it's appended to the text response with instructions
230
+ * for the agent to write it to the test file.
231
231
  */
232
232
  function createToolResult(success, textContent, structuredData, generatedCode) {
233
- // Build text content - append generated code if provided
233
+ // Build text content - append generated code if provided with directive instructions
234
234
  let fullText = textContent;
235
235
  if (generatedCode && success) {
236
- fullText += `\n\nAdd to test file:\n${generatedCode}`;
236
+ // Get the test file from the current session
237
+ const session = sessionManager.getCurrentSession();
238
+ const testFile = session?.testFile;
239
+ if (testFile) {
240
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to ${testFile}:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
241
+ }
242
+ else {
243
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to the test file:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
244
+ }
237
245
  }
238
246
  const content = [{ type: "text", text: fullText }];
239
247
  logger.debug("createToolResult", {
@@ -244,9 +252,17 @@ function createToolResult(success, textContent, structuredData, generatedCode) {
244
252
  hasGeneratedCode: !!generatedCode
245
253
  });
246
254
  // structuredContent goes to UI (includes imageUrl for display)
255
+ // Always include success flag so UI can display correct status indicator
256
+ // Include generatedCode and testFile in structured data so agents can programmatically handle it
257
+ const session = sessionManager.getCurrentSession();
247
258
  return {
248
259
  content,
249
- structuredContent: structuredData,
260
+ structuredContent: {
261
+ ...structuredData,
262
+ success,
263
+ generatedCode: generatedCode && success ? generatedCode : undefined,
264
+ testFile: session?.testFile || undefined,
265
+ },
250
266
  };
251
267
  }
252
268
  // Create MCP server
@@ -322,6 +338,10 @@ registerAppTool(server, "session_start", {
322
338
  title: "Session Start",
323
339
  description: `Start a new TestDriver session and provision a sandbox with browser or app.
324
340
 
341
+ ⚠️ IMPORTANT - Test File Parameter:
342
+ When 'testFile' is provided, you MUST append the generated code to that file after EVERY successful action.
343
+ Each tool response includes "ACTION REQUIRED: Append this code..." - you must write that code to the test file immediately.
344
+
325
345
  Provision types:
326
346
  - chrome: Launch Chrome browser (default). Use 'url' for starting page.
327
347
  - chromeExtension: Launch Chrome with an extension. Use 'extensionPath' or 'extensionId'.
@@ -1369,50 +1389,6 @@ Only use 'screenshot' when you explicitly want to show something to the human us
1369
1389
  return createToolResult(false, `Screenshot failed: ${error}`, { error: String(error) });
1370
1390
  }
1371
1391
  });
1372
- // Verify
1373
- server.registerTool("verify", {
1374
- description: "Run the test file from scratch to verify it works",
1375
- inputSchema: z.object({
1376
- testFile: z.string().describe("Path to test file to run"),
1377
- }),
1378
- }, async (params) => {
1379
- const startTime = Date.now();
1380
- logger.info("verify: Starting", { testFile: params.testFile });
1381
- const session = sessionManager.getCurrentSession();
1382
- if (!fs.existsSync(params.testFile)) {
1383
- logger.warn("verify: Test file not found", { testFile: params.testFile });
1384
- return createToolResult(false, `Test file not found: ${params.testFile}`, { error: "Test file not found" });
1385
- }
1386
- const { execSync } = await import("child_process");
1387
- try {
1388
- logger.info("verify: Running vitest", { testFile: params.testFile });
1389
- const output = execSync(`npx vitest run "${params.testFile}" --reporter=verbose`, {
1390
- encoding: "utf-8",
1391
- timeout: 300000,
1392
- cwd: process.cwd(),
1393
- env: { ...process.env },
1394
- });
1395
- const duration = Date.now() - startTime;
1396
- logger.info("verify: Test passed", { testFile: params.testFile, duration });
1397
- return createToolResult(true, `✓ Test passed!\n\n${output}`, {
1398
- action: "verify",
1399
- success: true,
1400
- session: getSessionData(session),
1401
- duration,
1402
- });
1403
- }
1404
- catch (error) {
1405
- const duration = Date.now() - startTime;
1406
- logger.error("verify: Test failed", { testFile: params.testFile, error: error.message, duration });
1407
- return createToolResult(false, `✗ Test failed!\n\n${error.stdout || error.message}`, {
1408
- action: "verify",
1409
- success: false,
1410
- error: error.stdout || error.message,
1411
- session: getSessionData(session),
1412
- duration,
1413
- });
1414
- }
1415
- });
1416
1392
  // Start the server
1417
1393
  async function main() {
1418
1394
  logger.info("Starting TestDriver MCP Server", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.73",
3
+ "version": "7.2.74",
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",