testdriverai 7.3.1 → 7.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +4 -0
  3. package/.github/workflows/acceptance-linux-scheduled.yaml +45 -0
  4. package/.github/workflows/acceptance-windows-scheduled.yaml +54 -0
  5. package/.github/workflows/acceptance.yaml +106 -0
  6. package/.github/workflows/publish.yaml +75 -0
  7. package/.github/workflows/test-init.yml +157 -0
  8. package/.github/workflows/testdriver.yml +170 -0
  9. package/.github/workflows/windows-self-hosted.yaml +82 -0
  10. package/.prettierignore +4 -0
  11. package/.prettierrc +1 -0
  12. package/CHANGELOG.md +158 -0
  13. package/SKILLs.md +17 -0
  14. package/agent/index.js +32 -3
  15. package/ai/.claude-plugin/plugin.json +9 -0
  16. package/docs/GITHUB_COMMENTS.md +330 -0
  17. package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
  18. package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
  19. package/docs/TEST-GITHUB-COMMENTS.md +129 -0
  20. package/docs/_scripts/generate-skills.js +149 -0
  21. package/docs/_scripts/link-replacer.js +164 -0
  22. package/docs/_scripts/upload-docs-to-openai.js +284 -0
  23. package/docs/claude-mcp-plugin.mdx +160 -0
  24. package/docs/docs.json +394 -0
  25. package/docs/github-integration-setup.md +266 -0
  26. package/docs/guide/best-practices-polling.mdx +154 -0
  27. package/docs/images/content/account/newprojectsettings.png +0 -0
  28. package/docs/images/content/account/projectpage.png +0 -0
  29. package/docs/images/content/account/projectreplays.png +0 -0
  30. package/docs/images/content/account/team-manage.png +0 -0
  31. package/docs/images/content/account/teampage.png +0 -0
  32. package/docs/images/content/extension/cursor.svg +1 -0
  33. package/docs/images/content/extension/vscode.svg +57 -0
  34. package/docs/images/content/extension/windsurf.svg +3 -0
  35. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  36. package/docs/images/content/side-by-side.png +0 -0
  37. package/docs/images/content/vscode/ide-full.png +0 -0
  38. package/docs/images/content/vscode/running.png +0 -0
  39. package/docs/images/content/vscode/vscode-2-assert.png +0 -0
  40. package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
  41. package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
  42. package/docs/images/content/vscode/vscode-file-creation.png +0 -0
  43. package/docs/images/content/vscode/vscode-install.png +0 -0
  44. package/docs/images/content/vscode/vscode-overview.png +0 -0
  45. package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
  46. package/docs/images/content/vscode/vscode-stopchat.png +0 -0
  47. package/docs/images/content/vscode/vscode-stoptest.png +0 -0
  48. package/docs/images/content/vscode/vscode-tdservice.png +0 -0
  49. package/docs/images/content/vscode/vscode-test-output.png +0 -0
  50. package/docs/images/content/vscode/vscode-testhistory.png +0 -0
  51. package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
  52. package/docs/images/content/vscode/vscode-testpane.png +0 -0
  53. package/docs/images/template/dark.png +0 -0
  54. package/docs/images/template/icon.png +0 -0
  55. package/docs/images/template/light.png +0 -0
  56. package/docs/snippets/calendar-link.mdx +4 -0
  57. package/docs/snippets/gitignore-warning.mdx +7 -0
  58. package/docs/snippets/lifecycle-warning.mdx +6 -0
  59. package/docs/snippets/test-prereqs.mdx +12 -0
  60. package/docs/snippets/tests/assert-replay.mdx +7 -0
  61. package/docs/snippets/tests/assert-yaml.mdx +8 -0
  62. package/docs/snippets/tests/exec-js-replay.mdx +7 -0
  63. package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
  64. package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
  65. package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
  66. package/docs/snippets/tests/hover-image-replay.mdx +7 -0
  67. package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
  68. package/docs/snippets/tests/hover-text-replay.mdx +7 -0
  69. package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
  70. package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
  71. package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
  72. package/docs/snippets/tests/match-image-replay.mdx +7 -0
  73. package/docs/snippets/tests/match-image-yaml.mdx +17 -0
  74. package/docs/snippets/tests/press-keys-replay.mdx +7 -0
  75. package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
  76. package/docs/snippets/tests/remember-replay.mdx +7 -0
  77. package/docs/snippets/tests/remember-yaml.mdx +28 -0
  78. package/docs/snippets/tests/scroll-replay.mdx +7 -0
  79. package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
  80. package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
  81. package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
  82. package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
  83. package/docs/snippets/tests/scroll-yaml.mdx +30 -0
  84. package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
  85. package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
  86. package/docs/snippets/tests/type-replay.mdx +7 -0
  87. package/docs/snippets/tests/type-yaml.mdx +28 -0
  88. package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
  89. package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
  90. package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
  91. package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
  92. package/docs/snippets/tests/wait-replay.mdx +7 -0
  93. package/docs/snippets/tests/wait-yaml.mdx +13 -0
  94. package/docs/styles.css +65 -0
  95. package/docs/v6/account/dashboard.mdx +16 -0
  96. package/docs/v6/account/enterprise.mdx +110 -0
  97. package/docs/v6/account/pricing.mdx +33 -0
  98. package/docs/v6/account/projects.mdx +33 -0
  99. package/docs/v6/account/team.mdx +35 -0
  100. package/docs/v6/action/ami.mdx +109 -0
  101. package/docs/v6/action/performance.mdx +105 -0
  102. package/docs/v6/action/secrets.mdx +93 -0
  103. package/docs/v6/apps/chrome-extensions.mdx +48 -0
  104. package/docs/v6/apps/desktop-apps.mdx +93 -0
  105. package/docs/v6/apps/mobile-apps.mdx +26 -0
  106. package/docs/v6/apps/static-websites.mdx +54 -0
  107. package/docs/v6/apps/tauri-apps.mdx +361 -0
  108. package/docs/v6/bugs/jira.mdx +232 -0
  109. package/docs/v6/cli/overview.mdx +66 -0
  110. package/docs/v6/commands/assert.mdx +45 -0
  111. package/docs/v6/commands/exec.mdx +282 -0
  112. package/docs/v6/commands/focus-application.mdx +44 -0
  113. package/docs/v6/commands/hover-image.mdx +69 -0
  114. package/docs/v6/commands/hover-text.mdx +47 -0
  115. package/docs/v6/commands/if.mdx +53 -0
  116. package/docs/v6/commands/match-image.mdx +67 -0
  117. package/docs/v6/commands/press-keys.mdx +87 -0
  118. package/docs/v6/commands/remember.mdx +49 -0
  119. package/docs/v6/commands/run.mdx +44 -0
  120. package/docs/v6/commands/scroll-until-image.mdx +66 -0
  121. package/docs/v6/commands/scroll-until-text.mdx +60 -0
  122. package/docs/v6/commands/scroll.mdx +69 -0
  123. package/docs/v6/commands/type.mdx +45 -0
  124. package/docs/v6/commands/wait-for-image.mdx +54 -0
  125. package/docs/v6/commands/wait-for-text.mdx +48 -0
  126. package/docs/v6/commands/wait.mdx +45 -0
  127. package/docs/v6/exporting/junit.mdx +218 -0
  128. package/docs/v6/exporting/playwright.mdx +197 -0
  129. package/docs/v6/features/auto-healing.mdx +144 -0
  130. package/docs/v6/features/generation.mdx +116 -0
  131. package/docs/v6/features/parallel-testing.mdx +151 -0
  132. package/docs/v6/features/reusable-snippets.mdx +131 -0
  133. package/docs/v6/features/selectorless.mdx +80 -0
  134. package/docs/v6/features/visual-assertions.mdx +139 -0
  135. package/docs/v6/getting-started/ci.mdx +146 -0
  136. package/docs/v6/getting-started/cli.mdx +91 -0
  137. package/docs/v6/getting-started/editing.mdx +100 -0
  138. package/docs/v6/getting-started/playwright.mdx +342 -0
  139. package/docs/v6/getting-started/running.mdx +48 -0
  140. package/docs/v6/getting-started/self-hosting.mdx +408 -0
  141. package/docs/v6/getting-started/vscode.mdx +88 -0
  142. package/docs/v6/guide/assertions.mdx +189 -0
  143. package/docs/v6/guide/authentication.mdx +136 -0
  144. package/docs/v6/guide/code.mdx +65 -0
  145. package/docs/v6/guide/dashcam.mdx +118 -0
  146. package/docs/v6/guide/environment-variables.mdx +26 -0
  147. package/docs/v6/guide/lifecycle.mdx +242 -0
  148. package/docs/v6/guide/locating.mdx +141 -0
  149. package/docs/v6/guide/protips.mdx +43 -0
  150. package/docs/v6/guide/variables.mdx +143 -0
  151. package/docs/v6/guide/waiting.mdx +130 -0
  152. package/docs/v6/importing/csv.mdx +196 -0
  153. package/docs/v6/importing/gherkin.mdx +143 -0
  154. package/docs/v6/importing/jira.mdx +164 -0
  155. package/docs/v6/importing/testrail.mdx +162 -0
  156. package/docs/v6/integrations/electron.mdx +146 -0
  157. package/docs/v6/integrations/netlify.mdx +100 -0
  158. package/docs/v6/integrations/vercel.mdx +125 -0
  159. package/docs/v6/interactive/explore.mdx +99 -0
  160. package/docs/v6/interactive/run.mdx +52 -0
  161. package/docs/v6/interactive/save.mdx +63 -0
  162. package/docs/v6/overview/comparison.mdx +101 -0
  163. package/docs/v6/overview/faq.mdx +162 -0
  164. package/docs/v6/overview/performance.mdx +52 -0
  165. package/docs/v6/overview/quickstart.mdx +137 -0
  166. package/docs/v6/overview/what-is-testdriver.mdx +85 -0
  167. package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
  168. package/docs/v6/scenarios/cookie-banner.mdx +32 -0
  169. package/docs/v6/scenarios/file-upload.mdx +33 -0
  170. package/docs/v6/scenarios/form-filling.mdx +32 -0
  171. package/docs/v6/scenarios/log-in.mdx +75 -0
  172. package/docs/v6/scenarios/pdf-generation.mdx +25 -0
  173. package/docs/v6/scenarios/spell-check.mdx +22 -0
  174. package/docs/v6/security/action.mdx +84 -0
  175. package/docs/v6/security/agent.mdx +73 -0
  176. package/docs/v6/security/platform.mdx +77 -0
  177. package/docs/v6/tutorials/advanced-test.mdx +81 -0
  178. package/docs/v6/tutorials/basic-test.mdx +45 -0
  179. package/docs/v7/_drafts/agents.mdx +852 -0
  180. package/docs/v7/_drafts/architecture.mdx +399 -0
  181. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  182. package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -0
  183. package/docs/v7/_drafts/best-practices.mdx +486 -0
  184. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  185. package/docs/v7/_drafts/caching-selectors.mdx +424 -0
  186. package/docs/v7/_drafts/caching.mdx +366 -0
  187. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  188. package/docs/v7/_drafts/commands/assert.mdx +45 -0
  189. package/docs/v7/_drafts/commands/exec.mdx +282 -0
  190. package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
  191. package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
  192. package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
  193. package/docs/v7/_drafts/commands/if.mdx +53 -0
  194. package/docs/v7/_drafts/commands/match-image.mdx +67 -0
  195. package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
  196. package/docs/v7/_drafts/commands/remember.mdx +49 -0
  197. package/docs/v7/_drafts/commands/run.mdx +44 -0
  198. package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
  199. package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
  200. package/docs/v7/_drafts/commands/scroll.mdx +69 -0
  201. package/docs/v7/_drafts/commands/type.mdx +45 -0
  202. package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
  203. package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
  204. package/docs/v7/_drafts/commands/wait.mdx +45 -0
  205. package/docs/v7/_drafts/configuration.mdx +378 -0
  206. package/docs/v7/_drafts/contributing.mdx +174 -0
  207. package/docs/v7/_drafts/core.mdx +458 -0
  208. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  209. package/docs/v7/_drafts/debugging.mdx +349 -0
  210. package/docs/v7/_drafts/error-handling.mdx +501 -0
  211. package/docs/v7/_drafts/faq.mdx +393 -0
  212. package/docs/v7/_drafts/hooks.mdx +360 -0
  213. package/docs/v7/_drafts/init-command.mdx +95 -0
  214. package/docs/v7/_drafts/installation.mdx +420 -0
  215. package/docs/v7/_drafts/migration.mdx +562 -0
  216. package/docs/v7/_drafts/observable.mdx +604 -0
  217. package/docs/v7/_drafts/playwright.mdx +342 -0
  218. package/docs/v7/_drafts/plugin-migration.mdx +220 -0
  219. package/docs/v7/_drafts/powerful.mdx +419 -0
  220. package/docs/v7/_drafts/presets.mdx +210 -0
  221. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  222. package/docs/v7/_drafts/prompt-cache.mdx +200 -0
  223. package/docs/v7/_drafts/provision.mdx +390 -0
  224. package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
  225. package/docs/v7/_drafts/readme.mdx +135 -0
  226. package/docs/v7/_drafts/reports.mdx +414 -0
  227. package/docs/v7/_drafts/scalable.mdx +754 -0
  228. package/docs/v7/_drafts/screenshot.mdx +155 -0
  229. package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
  230. package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
  231. package/docs/v7/_drafts/sdk-migration.mdx +474 -0
  232. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  233. package/docs/v7/_drafts/self-hosting.mdx +369 -0
  234. package/docs/v7/_drafts/test-recording.mdx +382 -0
  235. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  236. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  237. package/docs/v7/_drafts/vitest.mdx +535 -0
  238. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  239. package/{ai/skills/testdriver:ai/SKILL.md → docs/v7/ai.mdx} +4 -3
  240. package/{ai/skills/testdriver:assert/SKILL.md → docs/v7/assert.mdx} +4 -3
  241. package/{ai/skills/testdriver:aws-setup/SKILL.md → docs/v7/aws-setup.mdx} +4 -3
  242. package/{ai/skills/testdriver:caching/SKILL.md → docs/v7/caching.mdx} +7 -3
  243. package/{ai/skills/testdriver:captcha/SKILL.md → docs/v7/captcha.mdx} +4 -3
  244. package/{ai/skills/testdriver:ci-cd/SKILL.md → docs/v7/ci-cd.mdx} +4 -3
  245. package/{ai/skills/testdriver:click/SKILL.md → docs/v7/click.mdx} +4 -3
  246. package/{ai/skills/testdriver:client/SKILL.md → docs/v7/client.mdx} +8 -3
  247. package/{ai/skills/testdriver:cloud/SKILL.md → docs/v7/cloud.mdx} +4 -3
  248. package/{ai/skills/testdriver:customizing-devices/SKILL.md → docs/v7/customizing-devices.mdx} +3 -3
  249. package/{ai/skills/testdriver:dashcam/SKILL.md → docs/v7/dashcam.mdx} +4 -3
  250. package/docs/v7/debugging-with-screenshots.mdx +402 -0
  251. package/{ai/skills/testdriver:device-config/SKILL.md → docs/v7/device-config.mdx} +3 -3
  252. package/{ai/skills/testdriver:double-click/SKILL.md → docs/v7/double-click.mdx} +3 -3
  253. package/{ai/skills/testdriver:elements/SKILL.md → docs/v7/elements.mdx} +4 -3
  254. package/{ai/skills/testdriver:enterprise/SKILL.md → docs/v7/enterprise.mdx} +5 -3
  255. package/docs/v7/examples.mdx +5 -0
  256. package/{ai/skills/testdriver:exec/SKILL.md → docs/v7/exec.mdx} +4 -3
  257. package/{ai/skills/testdriver:find/SKILL.md → docs/v7/find.mdx} +4 -3
  258. package/{ai/skills/testdriver:focus-application/SKILL.md → docs/v7/focus-application.mdx} +4 -3
  259. package/{ai/skills/testdriver:generating-tests/SKILL.md → docs/v7/generating-tests.mdx} +3 -3
  260. package/{ai/skills/testdriver:hover/SKILL.md → docs/v7/hover.mdx} +4 -3
  261. package/{ai/skills/testdriver:locating-elements/SKILL.md → docs/v7/locating-elements.mdx} +3 -3
  262. package/{ai/skills/testdriver:making-assertions/SKILL.md → docs/v7/making-assertions.mdx} +3 -3
  263. package/{ai/skills/testdriver:mouse-down/SKILL.md → docs/v7/mouse-down.mdx} +3 -3
  264. package/{ai/skills/testdriver:mouse-up/SKILL.md → docs/v7/mouse-up.mdx} +3 -3
  265. package/docs/v7/ocr.mdx +236 -0
  266. package/{ai/skills/testdriver:performing-actions/SKILL.md → docs/v7/performing-actions.mdx} +3 -3
  267. package/{ai/skills/testdriver:press-keys/SKILL.md → docs/v7/press-keys.mdx} +4 -3
  268. package/{ai/skills/testdriver:quickstart/SKILL.md → docs/v7/quickstart.mdx} +6 -20
  269. package/{ai/skills/testdriver:reusable-code/SKILL.md → docs/v7/reusable-code.mdx} +3 -3
  270. package/{ai/skills/testdriver:right-click/SKILL.md → docs/v7/right-click.mdx} +3 -3
  271. package/{ai/skills/testdriver:running-tests/SKILL.md → docs/v7/running-tests.mdx} +7 -3
  272. package/{ai/skills/testdriver:screenshot/SKILL.md → docs/v7/screenshot.mdx} +88 -6
  273. package/{ai/skills/testdriver:scroll/SKILL.md → docs/v7/scroll.mdx} +40 -3
  274. package/{ai/skills/testdriver:secrets/SKILL.md → docs/v7/secrets.mdx} +3 -3
  275. package/{ai/skills/testdriver:self-hosted/SKILL.md → docs/v7/self-hosted.mdx} +4 -3
  276. package/{ai/skills/testdriver:type/SKILL.md → docs/v7/type.mdx} +4 -3
  277. package/{ai/skills/testdriver:variables/SKILL.md → docs/v7/variables.mdx} +3 -3
  278. package/{ai/skills/testdriver:waiting-for-elements/SKILL.md → docs/v7/waiting-for-elements.mdx} +3 -3
  279. package/{ai/skills/testdriver:what-is-testdriver/SKILL.md → docs/v7/what-is-testdriver.mdx} +3 -3
  280. package/eslint.config.js +67 -0
  281. package/examples/ai.test.mjs +30 -0
  282. package/examples/assert.test.mjs +47 -0
  283. package/examples/captcha-api.test.mjs +50 -0
  284. package/examples/chrome-extension.test.mjs +94 -0
  285. package/examples/drag-and-drop.test.mjs +58 -0
  286. package/examples/element-not-found.test.mjs +26 -0
  287. package/examples/exec-output.test.mjs +59 -0
  288. package/examples/exec-pwsh.test.mjs +57 -0
  289. package/examples/focus-window.test.mjs +36 -0
  290. package/examples/formatted-logging.test.mjs +26 -0
  291. package/examples/hover-image.test.mjs +52 -0
  292. package/examples/hover-text-with-description.test.mjs +56 -0
  293. package/examples/hover-text.test.mjs +27 -0
  294. package/examples/installer.test.mjs +49 -0
  295. package/examples/launch-vscode-linux.test.mjs +54 -0
  296. package/examples/match-image.test.mjs +54 -0
  297. package/examples/no-provision.test.mjs +23 -0
  298. package/examples/press-keys.test.mjs +50 -0
  299. package/examples/prompt.test.mjs +33 -0
  300. package/examples/scroll-keyboard.test.mjs +37 -0
  301. package/examples/scroll-until-image.test.mjs +39 -0
  302. package/examples/scroll-until-text.test.mjs +67 -0
  303. package/examples/scroll.test.mjs +41 -0
  304. package/examples/type.test.mjs +45 -0
  305. package/examples/windows-installer.test.mjs +53 -0
  306. package/jsconfig.json +26 -0
  307. package/lib/vitest/hooks.mjs +3 -0
  308. package/manual/test-init-command.js +223 -0
  309. package/mcp-server/README.md +312 -0
  310. package/mcp-server/mcp-app.html +28 -0
  311. package/mcp-server/mcp-config.example.json +19 -0
  312. package/mcp-server/package-lock.json +4018 -0
  313. package/mcp-server/package.json +29 -0
  314. package/mcp-server/src/codegen.ts +189 -0
  315. package/mcp-server/src/mcp-app.css +360 -0
  316. package/mcp-server/src/mcp-app.ts +547 -0
  317. package/mcp-server/src/provision-types.ts +209 -0
  318. package/mcp-server/src/server.ts +2313 -0
  319. package/mcp-server/src/session.ts +194 -0
  320. package/mcp-server/tsconfig.json +16 -0
  321. package/mcp-server/vite.config.ts +23 -0
  322. package/package.json +2 -17
  323. package/scripts/generate-skills.js +94 -0
  324. package/sdk.js +3 -0
  325. package/setup/aws/cloudformation.yaml +470 -0
  326. package/setup/aws/spawn-runner.sh +190 -0
  327. package/test/api-resilience.test.mjs +0 -0
  328. package/test/captcha-solver.test.mjs +152 -0
  329. package/test/chrome-remote-debugging.test.mjs +66 -0
  330. package/test/duckduckgo/experiment.test.mjs +28 -0
  331. package/test/duckduckgo/setup.test.mjs +29 -0
  332. package/test/manual/debug-locate-response.js +82 -0
  333. package/test/manual/reconnect-provision.test.mjs +49 -0
  334. package/test/manual/test-console-logs.test.mjs +42 -0
  335. package/test/manual/test-find-api.js +73 -0
  336. package/test/manual/test-init.sh +54 -0
  337. package/test/manual/test-prompt-cache.js +97 -0
  338. package/test/manual/test-provision-auth.mjs +22 -0
  339. package/test/manual/test-sandbox-render.js +29 -0
  340. package/test/manual/test-sdk-methods.js +15 -0
  341. package/test/manual/test-sdk-refactor.js +53 -0
  342. package/test/manual/test-stack-trace.mjs +57 -0
  343. package/test/manual/verify-element-api.js +89 -0
  344. package/test/manual/verify-types.js +0 -0
  345. package/test/manual-unawaited-promise.test.mjs +31 -0
  346. package/test-ide-preview.mjs +17 -0
  347. package/tests/airbnb-booking.test.mjs +39 -0
  348. package/tests/airbnb-search.test.mjs +43 -0
  349. package/tests/example.test.js +33 -0
  350. package/tests/login.js +28 -0
  351. package/vitest.config.mjs +24 -0
  352. package/vscode-extension/.vscodeignore +12 -0
  353. package/vscode-extension/README.md +94 -0
  354. package/vscode-extension/media/icon.png +0 -0
  355. package/vscode-extension/package-lock.json +4126 -0
  356. package/vscode-extension/package.json +86 -0
  357. package/vscode-extension/src/extension.ts +829 -0
  358. package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
  359. package/vscode-extension/tsconfig.json +16 -0
  360. package/ai/skills/testdriver:examples/SKILL.md +0 -7
  361. package/ai/skills/testdriver:mcp-workflow/SKILL.md +0 -410
  362. package/ai/skills/testdriver:testdriver/SKILL.md +0 -523
@@ -0,0 +1,2313 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TestDriver MCP Server
4
+ * Enables AI agents to iteratively build tests with visual feedback
5
+ */
6
+
7
+ // Configure logger to use stderr to avoid corrupting MCP JSON-RPC stream on stdout
8
+ process.env.TD_STDIO = "stderr";
9
+ // Enable debug mode to preserve croppedImage in SDK responses (needed for MCP App visuals)
10
+ process.env.TD_DEBUG = "true";
11
+
12
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
13
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import type { Variables } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";
16
+ import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
17
+ import * as Sentry from "@sentry/node";
18
+ import * as fs from "fs";
19
+ import * as os from "os";
20
+ import * as path from "path";
21
+ import { fileURLToPath, pathToFileURL } from "url";
22
+ import { z } from "zod";
23
+
24
+ import { generateActionCode } from "./codegen.js";
25
+ import { getProvisionOptions, SessionStartInputSchema, type SessionStartInput } from "./provision-types.js";
26
+ import { sessionManager, type SessionState } from "./session.js";
27
+
28
+ // =============================================================================
29
+ // Sentry
30
+ // =============================================================================
31
+
32
+ // Read version from main package.json (../../package.json from mcp-server/dist/)
33
+ const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
34
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
35
+ const version = packageJson.version || "1.0.0";
36
+
37
+ const isSentryEnabled = () => {
38
+ if (process.env.TD_TELEMETRY === "false") {
39
+ return false;
40
+ }
41
+ return true;
42
+ };
43
+
44
+ if (isSentryEnabled()) {
45
+ console.error("Analytics enabled. Set TD_TELEMETRY=false to disable.");
46
+ Sentry.init({
47
+ dsn:
48
+ process.env.SENTRY_DSN ||
49
+ "https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
50
+ environment: "mcp",
51
+ release: `testdriverai-mcp@${version}`,
52
+ sampleRate: 1.0,
53
+ tracesSampleRate: 1.0,
54
+ sendDefaultPii: true,
55
+ integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
56
+ initialScope: {
57
+ tags: {
58
+ platform: os.platform(),
59
+ arch: os.arch(),
60
+ nodeVersion: process.version,
61
+ },
62
+ },
63
+ beforeSend(event, hint) {
64
+ const error = hint.originalException;
65
+ // Don't send user-initiated exits
66
+ if (error && typeof error === "object" && "message" in error) {
67
+ const msg = (error as { message: string }).message;
68
+ if (msg.includes("User cancelled")) {
69
+ return null;
70
+ }
71
+ }
72
+ return event;
73
+ },
74
+ });
75
+ }
76
+
77
+ function captureException(error: Error, context: { tags?: Record<string, string>; extra?: Record<string, unknown> } = {}) {
78
+ if (!isSentryEnabled()) return;
79
+
80
+ Sentry.withScope((scope) => {
81
+ if (context.tags) {
82
+ Object.entries(context.tags).forEach(([key, value]) => {
83
+ scope.setTag(key, value);
84
+ });
85
+ }
86
+ if (context.extra) {
87
+ Object.entries(context.extra).forEach(([key, value]) => {
88
+ scope.setExtra(key, value);
89
+ });
90
+ }
91
+ Sentry.captureException(error);
92
+ });
93
+ }
94
+
95
+ function setSessionContext(sessionId: string, sandboxId?: string) {
96
+ if (!isSentryEnabled()) return;
97
+
98
+ Sentry.setTag("session", sessionId);
99
+ if (sandboxId) {
100
+ Sentry.setTag("sandbox", sandboxId);
101
+ }
102
+ Sentry.setContext("session", {
103
+ sessionId,
104
+ sandboxId,
105
+ });
106
+ }
107
+
108
+ async function flushSentry(timeout = 2000) {
109
+ if (!isSentryEnabled()) return;
110
+ await Sentry.flush(timeout);
111
+ }
112
+
113
+ // =============================================================================
114
+ // Logging
115
+ // =============================================================================
116
+
117
+ const LOG_LEVELS = {
118
+ DEBUG: 0,
119
+ INFO: 1,
120
+ WARN: 2,
121
+ ERROR: 3,
122
+ } as const;
123
+
124
+ type LogLevel = keyof typeof LOG_LEVELS;
125
+
126
+ // Set via TD_LOG_LEVEL env var (default: INFO)
127
+ const currentLogLevel = LOG_LEVELS[(process.env.TD_LOG_LEVEL?.toUpperCase() as LogLevel) || "INFO"] ?? LOG_LEVELS.INFO;
128
+
129
+ function log(level: LogLevel, message: string, data?: Record<string, unknown>) {
130
+ if (LOG_LEVELS[level] < currentLogLevel) return;
131
+
132
+ const timestamp = new Date().toISOString();
133
+ const prefix = `[${timestamp}] [${level}]`;
134
+ const dataStr = data ? ` ${JSON.stringify(data)}` : "";
135
+ console.error(`${prefix} ${message}${dataStr}`);
136
+ }
137
+
138
+ const logger = {
139
+ debug: (msg: string, data?: Record<string, unknown>) => log("DEBUG", msg, data),
140
+ info: (msg: string, data?: Record<string, unknown>) => log("INFO", msg, data),
141
+ warn: (msg: string, data?: Record<string, unknown>) => log("WARN", msg, data),
142
+ error: (msg: string, data?: Record<string, unknown>) => log("ERROR", msg, data),
143
+ };
144
+
145
+ // Get directory for UI files - works both from source and compiled
146
+ const __filename = fileURLToPath(import.meta.url);
147
+ const __dirname = path.dirname(__filename);
148
+ const DIST_DIR = __filename.endsWith(".ts")
149
+ ? path.join(__dirname, "..", "dist")
150
+ : __dirname;
151
+
152
+ // Resource URI for the screenshot result UI
153
+ const RESOURCE_URI = "ui://testdriver/mcp-app.html";
154
+
155
+ // Resource URI base for serving screenshot blobs (with dynamic IDs)
156
+ const SCREENSHOT_RESOURCE_BASE = "screenshot://testdriver/screenshot";
157
+ const CROPPED_IMAGE_RESOURCE_BASE = "screenshot://testdriver/cropped";
158
+
159
+ // SDK instance (will be initialized on session start)
160
+ let sdk: any = null;
161
+
162
+ // Last screenshot base64 for check comparisons
163
+ let lastScreenshotBase64: string | null = null;
164
+
165
+ // =============================================================================
166
+ // Image Store - Stores images with unique IDs for reload persistence
167
+ // =============================================================================
168
+
169
+ interface StoredImage {
170
+ data: string; // base64 image data
171
+ type: "screenshot" | "cropped";
172
+ timestamp: number;
173
+ }
174
+
175
+ // Map of image ID -> image data
176
+ const imageStore = new Map<string, StoredImage>();
177
+
178
+ // Counter for generating unique image IDs
179
+ let imageIdCounter = 0;
180
+
181
+ // Maximum number of images to store (to prevent memory leaks)
182
+ const MAX_STORED_IMAGES = 100;
183
+
184
+ /**
185
+ * Store an image and return its unique resource URI
186
+ */
187
+ function storeImage(data: string, type: "screenshot" | "cropped"): string {
188
+ const id = `${type}-${++imageIdCounter}`;
189
+
190
+ // Clean up old images if we exceed the limit
191
+ if (imageStore.size >= MAX_STORED_IMAGES) {
192
+ // Remove oldest images (first entries in the map)
193
+ const entriesToRemove = Math.floor(MAX_STORED_IMAGES / 4);
194
+ const keys = Array.from(imageStore.keys()).slice(0, entriesToRemove);
195
+ for (const key of keys) {
196
+ imageStore.delete(key);
197
+ }
198
+ logger.debug("storeImage: Cleaned up old images", { removed: entriesToRemove, remaining: imageStore.size });
199
+ }
200
+
201
+ imageStore.set(id, {
202
+ data,
203
+ type,
204
+ timestamp: Date.now(),
205
+ });
206
+
207
+ logger.debug("storeImage: Stored image", { id, type, dataLength: data.length });
208
+
209
+ const base = type === "screenshot" ? SCREENSHOT_RESOURCE_BASE : CROPPED_IMAGE_RESOURCE_BASE;
210
+ return `${base}/${id}`;
211
+ }
212
+
213
+ /**
214
+ * Get an image by its ID
215
+ */
216
+ function getStoredImage(id: string): StoredImage | undefined {
217
+ return imageStore.get(id);
218
+ }
219
+
220
+ /**
221
+ * Get session info for structured content
222
+ */
223
+ function getSessionData(session: SessionState | null) {
224
+ if (!session) return { id: null, expiresIn: 0 };
225
+ return {
226
+ id: session.sessionId,
227
+ expiresIn: sessionManager.getTimeRemaining(session.sessionId),
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Check if session is ready for use - returns error result if not
233
+ * This helper provides clear, actionable error messages for the AI
234
+ *
235
+ * Auto-extends the session on each successful check to prevent expiry during active use
236
+ */
237
+ function requireActiveSession(): { valid: true } | { valid: false; error: CallToolResult } {
238
+ const session = sessionManager.getCurrentSession();
239
+
240
+ // No session ever created
241
+ if (!sdk || !session) {
242
+ return {
243
+ valid: false,
244
+ error: createToolResult(
245
+ false,
246
+ "ERROR: No active session. You must call session_start first to create a sandbox before using any other tools.",
247
+ {
248
+ error: "NO_SESSION",
249
+ action: "session_start",
250
+ message: "No sandbox session exists. Call session_start to create one."
251
+ }
252
+ )
253
+ };
254
+ }
255
+
256
+ // Session exists but has expired
257
+ if (!sessionManager.isSessionValid(session.sessionId)) {
258
+ // Clear the SDK reference since the sandbox is no longer available
259
+ sdk = null;
260
+ return {
261
+ valid: false,
262
+ error: createToolResult(
263
+ false,
264
+ "ERROR: Session has expired or timed out. The sandbox is no longer available. You must call session_start again to create a new sandbox session before continuing.",
265
+ {
266
+ error: "SESSION_EXPIRED",
267
+ action: "session_start",
268
+ message: "The previous sandbox session has expired. Call session_start to create a new one.",
269
+ expiredSessionId: session.sessionId
270
+ }
271
+ )
272
+ };
273
+ }
274
+
275
+ // Auto-extend session on each command to prevent expiry during active use
276
+ // This resets the expiry timer back to the original keepAlive duration
277
+ sessionManager.refreshSession(session.sessionId);
278
+
279
+ return { valid: true };
280
+ }
281
+
282
+ /**
283
+ * Create tool result with structured content for MCP App
284
+ * Images: imageUrl (data URL) goes to structuredContent for UI to display
285
+ * The croppedImage from find() is small (~10KB) so it's acceptable as data URL
286
+ *
287
+ * If generatedCode is provided, it's appended to the text response with instructions
288
+ * for the agent to write it to the test file.
289
+ */
290
+ function createToolResult(
291
+ success: boolean,
292
+ textContent: string,
293
+ structuredData: Record<string, unknown>,
294
+ generatedCode?: string
295
+ ): CallToolResult {
296
+ // Build text content - append generated code if provided with directive instructions
297
+ let fullText = textContent;
298
+ if (generatedCode && success) {
299
+ // Get the test file from the current session
300
+ const session = sessionManager.getCurrentSession();
301
+ const testFile = session?.testFile;
302
+
303
+ if (testFile) {
304
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to ${testFile}:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
305
+ } else {
306
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to the test file:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
307
+ }
308
+ }
309
+
310
+ const content: CallToolResult["content"] = [{ type: "text", text: fullText }];
311
+
312
+ logger.debug("createToolResult", {
313
+ success,
314
+ action: structuredData.action,
315
+ hasImage: !!structuredData.imageUrl,
316
+ duration: structuredData.duration,
317
+ hasGeneratedCode: !!generatedCode
318
+ });
319
+
320
+ // structuredContent goes to UI (includes imageUrl for display)
321
+ // Always include success flag so UI can display correct status indicator
322
+ // Include generatedCode and testFile in structured data so agents can programmatically handle it
323
+ const session = sessionManager.getCurrentSession();
324
+ return {
325
+ content,
326
+ structuredContent: {
327
+ ...structuredData,
328
+ success,
329
+ generatedCode: generatedCode && success ? generatedCode : undefined,
330
+ testFile: session?.testFile || undefined,
331
+ },
332
+ };
333
+ }
334
+
335
+ // Create MCP server wrapped with Sentry for automatic tracing
336
+ const server = isSentryEnabled()
337
+ ? Sentry.wrapMcpServerWithSentry(
338
+ new McpServer({
339
+ name: "testdriver",
340
+ version: version,
341
+ })
342
+ )
343
+ : new McpServer({
344
+ name: "testdriver",
345
+ version: version,
346
+ });
347
+
348
+ // Element reference storage (for click/hover after find)
349
+ // Stores actual Element instances - no raw coordinates as input
350
+ const elementRefs = new Map<string, { element: any; description: string; coords: { x: number; y: number; centerX: number; centerY: number } }>();
351
+
352
+ // =============================================================================
353
+ // Register UI Resource
354
+ // =============================================================================
355
+
356
+ registerAppResource(
357
+ server,
358
+ RESOURCE_URI,
359
+ RESOURCE_URI,
360
+ { mimeType: RESOURCE_MIME_TYPE, description: "TestDriver Screenshot Viewer UI" },
361
+ async (): Promise<ReadResourceResult> => {
362
+ const htmlPath = path.join(DIST_DIR, "mcp-app.html");
363
+
364
+ if (!fs.existsSync(htmlPath)) {
365
+ throw new Error(`UI file not found: ${htmlPath}`);
366
+ }
367
+
368
+ const html = fs.readFileSync(htmlPath, "utf-8");
369
+ return {
370
+ contents: [{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html }],
371
+ };
372
+ }
373
+ );
374
+
375
+ // Register screenshot resource template for serving binary blobs by ID
376
+ server.registerResource(
377
+ "Screenshot",
378
+ new ResourceTemplate(`${SCREENSHOT_RESOURCE_BASE}/{imageId}`, { list: undefined }),
379
+ {
380
+ description: "Screenshot from TestDriver session served as base64 blob",
381
+ mimeType: "image/png",
382
+ },
383
+ async (uri: URL, variables: Variables): Promise<ReadResourceResult> => {
384
+ const imageId = variables.imageId as string;
385
+ const image = getStoredImage(imageId);
386
+
387
+ if (!image) {
388
+ throw new Error(`Screenshot not found: ${imageId}. It may have been cleaned up.`);
389
+ }
390
+
391
+ logger.debug("screenshot resource: Serving screenshot blob", {
392
+ imageId,
393
+ blobLength: image.data.length
394
+ });
395
+
396
+ return {
397
+ contents: [{
398
+ uri: uri.href,
399
+ mimeType: "image/png",
400
+ blob: image.data,
401
+ }],
402
+ };
403
+ }
404
+ );
405
+
406
+ // Register cropped image resource template for serving find operation results by ID
407
+ server.registerResource(
408
+ "CroppedImage",
409
+ new ResourceTemplate(`${CROPPED_IMAGE_RESOURCE_BASE}/{imageId}`, { list: undefined }),
410
+ {
411
+ description: "Cropped image from find operations served as base64 blob",
412
+ mimeType: "image/png",
413
+ },
414
+ async (uri: URL, variables: Variables): Promise<ReadResourceResult> => {
415
+ const imageId = variables.imageId as string;
416
+ const image = getStoredImage(imageId);
417
+
418
+ if (!image) {
419
+ throw new Error(`Cropped image not found: ${imageId}. It may have been cleaned up.`);
420
+ }
421
+
422
+ logger.debug("cropped image resource: Serving cropped image blob", {
423
+ imageId,
424
+ blobLength: image.data.length
425
+ });
426
+
427
+ return {
428
+ contents: [{
429
+ uri: uri.href,
430
+ mimeType: "image/png",
431
+ blob: image.data,
432
+ }],
433
+ };
434
+ }
435
+ );
436
+
437
+ // =============================================================================
438
+ // Tools
439
+ // =============================================================================
440
+
441
+ // Session Start
442
+ registerAppTool(
443
+ server,
444
+ "session_start",
445
+ {
446
+ title: "Session Start",
447
+ description: `Start a new TestDriver session and provision a sandbox with browser or app.
448
+
449
+ ⚠️ IMPORTANT - Test File Parameter:
450
+ When 'testFile' is provided, you MUST append the generated code to that file after EVERY successful action.
451
+ Each tool response includes "ACTION REQUIRED: Append this code..." - you must write that code to the test file immediately.
452
+
453
+ Provision types:
454
+ - chrome: Launch Chrome browser (default). Use 'url' for starting page.
455
+ - chromeExtension: Launch Chrome with an extension. Use 'extensionPath' or 'extensionId'.
456
+ - vscode: Launch VS Code. Use 'workspace' and optional 'extensions'.
457
+ - installer: Download and install an app. Use 'installerUrl' (required).
458
+ - electron: Launch an Electron app. Use 'appPath' (required).
459
+
460
+ Self-hosted mode:
461
+ - Provide 'ip' parameter to connect directly to a self-hosted Windows instance
462
+ - Set 'os' to 'windows' when connecting to Windows instances
463
+ - The IP can be from an AWS EC2 instance spawned via CloudFormation
464
+ - See https://docs.testdriver.ai/v7/aws-setup for AWS setup guide
465
+
466
+ Debug mode (connect to existing sandbox):
467
+ - Provide 'sandboxId' to connect to an existing sandbox (e.g., from a failed test with debugOnFailure: true)
468
+ - Skips provisioning - connects to sandbox in its current state
469
+ - Use this to interactively debug failed tests without re-running from scratch`,
470
+ inputSchema: SessionStartInputSchema as any,
471
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
472
+ },
473
+ async (params: SessionStartInput): Promise<CallToolResult> => {
474
+ const startTime = Date.now();
475
+ logger.info("session_start: Starting", {
476
+ type: params.type,
477
+ url: params.url,
478
+ os: params.os,
479
+ reconnect: params.reconnect,
480
+ sandboxId: params.sandboxId,
481
+ });
482
+
483
+ try {
484
+ // Validate required fields for specific provision types (unless connecting to existing sandbox)
485
+ if (!params.sandboxId) {
486
+ if (params.type === "installer" && !params.installerUrl) {
487
+ return createToolResult(false, "installer type requires 'installerUrl' parameter", { error: "Missing required parameter: installerUrl" });
488
+ }
489
+ if (params.type === "electron" && !params.appPath) {
490
+ return createToolResult(false, "electron type requires 'appPath' parameter", { error: "Missing required parameter: appPath" });
491
+ }
492
+ }
493
+
494
+ // Create new session
495
+ const newSession = sessionManager.createSession({
496
+ os: params.os,
497
+ keepAlive: params.keepAlive,
498
+ testFile: params.testFile,
499
+ });
500
+ logger.debug("session_start: Session created", { sessionId: newSession.sessionId });
501
+
502
+ // Determine API root
503
+ const apiRoot = params.apiRoot || process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
504
+ logger.debug("session_start: Using API root", { apiRoot });
505
+
506
+ // Initialize SDK
507
+ logger.debug("session_start: Initializing SDK");
508
+ const TestDriverSDK = (await import("../../sdk.js")).default;
509
+
510
+ // Determine preview mode from environment variable
511
+ // TD_PREVIEW can be "ide", "browser", or "none"
512
+ // Default to "none" for MCP server (headless) unless explicitly set
513
+ const previewMode = process.env.TD_PREVIEW || "none";
514
+ logger.debug("session_start: Preview mode", { preview: previewMode });
515
+
516
+ // Get IP from params or environment (for self-hosted instances)
517
+ const instanceIp = params.ip || process.env.TD_IP;
518
+
519
+ // Get API key - check multiple sources for GitHub Copilot coding agent compatibility
520
+ // 1. TD_API_KEY (standard environment variable)
521
+ // 2. COPILOT_MCP_TD_API_KEY (fallback for GitHub Copilot coding agent)
522
+ const apiKey = process.env.TD_API_KEY || process.env.COPILOT_MCP_TD_API_KEY || "";
523
+
524
+ if (!apiKey) {
525
+ logger.error("session_start: No API key found", {
526
+ hasTD_API_KEY: !!process.env.TD_API_KEY,
527
+ hasCOPILOT_MCP_TD_API_KEY: !!process.env.COPILOT_MCP_TD_API_KEY,
528
+ availableEnvVars: Object.keys(process.env).filter(k => k.includes('TD') || k.includes('COPILOT_MCP'))
529
+ });
530
+ return createToolResult(false, "No API key found. Please set TD_API_KEY or COPILOT_MCP_TD_API_KEY environment variable.", {
531
+ error: "Missing API key",
532
+ hint: "For GitHub Copilot coding agent, create a Copilot environment secret named COPILOT_MCP_TD_API_KEY"
533
+ });
534
+ }
535
+
536
+ logger.debug("session_start: API key found", {
537
+ source: process.env.TD_API_KEY ? "TD_API_KEY" : "COPILOT_MCP_TD_API_KEY",
538
+ keyPrefix: apiKey.substring(0, 7) + "..."
539
+ });
540
+
541
+ sdk = new TestDriverSDK(apiKey, {
542
+ os: params.os,
543
+ logging: false,
544
+ apiRoot,
545
+ preview: previewMode as "browser" | "ide" | "none",
546
+ ip: instanceIp,
547
+ });
548
+
549
+ // Handle sandboxId mode - connect to existing sandbox (debug-on-failure mode)
550
+ if (params.sandboxId) {
551
+ logger.info("session_start: Connecting to existing sandbox (debug mode)", { sandboxId: params.sandboxId });
552
+ await sdk.connect({
553
+ sandboxId: params.sandboxId,
554
+ keepAlive: params.keepAlive,
555
+ });
556
+
557
+ // Get sandbox ID
558
+ const instance = sdk.getInstance();
559
+ logger.info("session_start: Connected to existing sandbox", { instanceId: instance?.instanceId });
560
+ sessionManager.activateSession(newSession.sessionId, instance?.instanceId || params.sandboxId);
561
+
562
+ // Set Sentry context for error tracking
563
+ setSessionContext(newSession.sessionId, instance?.instanceId);
564
+
565
+ // Capture screenshot of current state
566
+ logger.debug("session_start: Capturing screenshot of existing sandbox");
567
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
568
+
569
+ let screenshotResourceUri: string | undefined;
570
+ if (screenshotBase64) {
571
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
572
+ lastScreenshotBase64 = screenshotBase64;
573
+ }
574
+
575
+ const duration = Date.now() - startTime;
576
+ logger.info("session_start: Connected to existing sandbox", { duration, sessionId: newSession.sessionId, sandboxId: params.sandboxId });
577
+
578
+ return createToolResult(
579
+ true,
580
+ `Connected to existing sandbox (debug mode)
581
+ Session: ${newSession.sessionId}
582
+ Sandbox: ${params.sandboxId}
583
+ Expires in: ${Math.round(params.keepAlive / 1000)}s
584
+
585
+ You are now connected to the sandbox in its current state. Use find, click, type, etc. to interact.`,
586
+ {
587
+ action: "session_start",
588
+ sessionId: newSession.sessionId,
589
+ sandboxId: params.sandboxId,
590
+ debugMode: true,
591
+ screenshotResourceUri,
592
+ duration
593
+ },
594
+ "// Connected to existing sandbox - no provision code needed"
595
+ );
596
+ }
597
+
598
+ // Connect to sandbox
599
+ if (instanceIp) {
600
+ logger.info("session_start: Connecting to self-hosted instance...", { ip: instanceIp });
601
+ } else {
602
+ logger.info("session_start: Connecting to cloud sandbox...");
603
+ }
604
+ await sdk.connect({
605
+ reconnect: params.reconnect,
606
+ keepAlive: params.keepAlive,
607
+ ip: instanceIp,
608
+ });
609
+
610
+ // Get sandbox ID
611
+ const instance = sdk.getInstance();
612
+ logger.info("session_start: Connected to sandbox", { instanceId: instance?.instanceId });
613
+ sessionManager.activateSession(newSession.sessionId, instance?.instanceId || "unknown");
614
+
615
+ // Set Sentry context for error tracking
616
+ setSessionContext(newSession.sessionId, instance?.instanceId);
617
+
618
+ // Get provision-specific options
619
+ const provisionOptions = getProvisionOptions(params);
620
+ let provisionCmd = "";
621
+
622
+ // Provision based on type
623
+ switch (params.type) {
624
+ case "chrome": {
625
+ const chromeOpts = provisionOptions as { url: string; maximized?: boolean; guest?: boolean };
626
+ logger.info("session_start: Provisioning Chrome", { url: chromeOpts.url });
627
+ await sdk.provision.chrome(chromeOpts);
628
+ provisionCmd = "provision.chrome";
629
+ logger.debug("session_start: Chrome provisioned");
630
+ break;
631
+ }
632
+
633
+ case "chromeExtension": {
634
+ const extOpts = provisionOptions as { extensionPath?: string; extensionId?: string; maximized?: boolean };
635
+ logger.info("session_start: Provisioning Chrome Extension", { extensionPath: extOpts.extensionPath, extensionId: extOpts.extensionId });
636
+ await sdk.provision.chromeExtension(extOpts);
637
+ provisionCmd = "provision.chromeExtension";
638
+ logger.debug("session_start: Chrome Extension provisioned");
639
+ break;
640
+ }
641
+
642
+ case "vscode": {
643
+ const vscodeOpts = provisionOptions as { workspace?: string; extensions?: string[] };
644
+ logger.info("session_start: Provisioning VS Code", { workspace: vscodeOpts.workspace });
645
+ await sdk.provision.vscode(vscodeOpts);
646
+ provisionCmd = "provision.vscode";
647
+ logger.debug("session_start: VS Code provisioned");
648
+ break;
649
+ }
650
+
651
+ case "installer": {
652
+ const installerOpts = provisionOptions as { url: string; filename?: string; appName?: string; launch?: boolean };
653
+ logger.info("session_start: Provisioning installer", { url: installerOpts.url });
654
+ await sdk.provision.installer(installerOpts);
655
+ provisionCmd = "provision.installer";
656
+ logger.debug("session_start: Installer provisioned");
657
+ break;
658
+ }
659
+
660
+ case "electron": {
661
+ const electronOpts = provisionOptions as { appPath: string; args?: string[] };
662
+ logger.info("session_start: Provisioning Electron", { appPath: electronOpts.appPath });
663
+ await sdk.provision.electron(electronOpts);
664
+ provisionCmd = "provision.electron";
665
+ logger.debug("session_start: Electron app provisioned");
666
+ break;
667
+ }
668
+ }
669
+
670
+ // Capture initial screenshot after provisioning
671
+ logger.debug("session_start: Capturing initial screenshot");
672
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
673
+
674
+ let screenshotResourceUri: string | undefined;
675
+ if (screenshotBase64) {
676
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
677
+ lastScreenshotBase64 = screenshotBase64;
678
+ }
679
+
680
+ const duration = Date.now() - startTime;
681
+ logger.info("session_start: Completed", { duration, sessionId: newSession.sessionId, selfHosted: !!instanceIp });
682
+
683
+ // Generate the code for this provision action
684
+ const generatedCode = generateActionCode(provisionCmd, provisionOptions);
685
+
686
+ // Build debugger URL for the session
687
+ const debuggerUrl = instance?.debuggerUrl || (instanceIp ? `http://${instanceIp}:9222` : null);
688
+
689
+ const connectionType = instanceIp ? `Self-hosted (${instanceIp})` : "Cloud";
690
+ return createToolResult(
691
+ true,
692
+ `Session started: ${newSession.sessionId}\nConnection: ${connectionType}\nType: ${params.type}\nSandbox: ${instance?.instanceId}\nExpires in: ${Math.round(params.keepAlive / 1000)}s
693
+
694
+ IMPORTANT - If creating a new test project, use these EXACT dependencies in package.json:
695
+ {
696
+ "type": "module",
697
+ "devDependencies": {
698
+ "testdriverai": "beta",
699
+ "vitest": "^4.0.0"
700
+ },
701
+ "scripts": {
702
+ "test": "vitest"
703
+ }
704
+ }`,
705
+ {
706
+ action: "session_start",
707
+ sessionId: newSession.sessionId,
708
+ provisionType: params.type,
709
+ selfHosted: !!instanceIp,
710
+ instanceIp: instanceIp || undefined,
711
+ debuggerUrl,
712
+ screenshotResourceUri,
713
+ duration
714
+ },
715
+ generatedCode
716
+ );
717
+ } catch (error) {
718
+ logger.error("session_start: Failed", { error: String(error) });
719
+ captureException(error as Error, { tags: { tool: "session_start" }, extra: { params } });
720
+ throw error;
721
+ }
722
+ }
723
+ );
724
+
725
+ // Session Status
726
+ server.registerTool(
727
+ "session_status",
728
+ {
729
+ description: "Check the current session status and time remaining",
730
+ inputSchema: z.object({}),
731
+ },
732
+ async (): Promise<CallToolResult> => {
733
+ const startTime = Date.now();
734
+ logger.info("session_status: Checking");
735
+ const session = sessionManager.getCurrentSession();
736
+
737
+ if (!session) {
738
+ logger.warn("session_status: No active session");
739
+ return createToolResult(false, "No active session", { error: "No active session. Call session_start first." });
740
+ }
741
+
742
+ const summary = sessionManager.getSessionSummary(session.sessionId);
743
+ const duration = Date.now() - startTime;
744
+ logger.info("session_status: Completed", {
745
+ sessionId: session.sessionId,
746
+ status: session.status,
747
+ timeRemaining: summary?.timeRemaining,
748
+ duration
749
+ });
750
+
751
+ return createToolResult(
752
+ true,
753
+ `Session: ${session.sessionId}\nStatus: ${session.status}\nTime remaining: ${Math.round((summary?.timeRemaining || 0) / 1000)}s`,
754
+ { action: "session_status", ...summary, sessionId: session.sessionId, status: session.status, duration }
755
+ );
756
+ }
757
+ );
758
+
759
+ // Session Extend
760
+ server.registerTool(
761
+ "session_extend",
762
+ {
763
+ description: "Extend the session keepAlive time",
764
+ inputSchema: z.object({
765
+ additionalMs: z.number().default(60000).describe("Additional time in ms"),
766
+ }),
767
+ },
768
+ async (params) => {
769
+ logger.info("session_extend: Extending", { additionalMs: params.additionalMs });
770
+ const session = sessionManager.getCurrentSession();
771
+
772
+ if (!session) {
773
+ logger.warn("session_extend: No active session");
774
+ return { content: [{ type: "text" as const, text: "No active session" }] };
775
+ }
776
+
777
+ sessionManager.extendSession(session.sessionId, params.additionalMs);
778
+ const newExpiry = sessionManager.getTimeRemaining(session.sessionId);
779
+ logger.info("session_extend: Extended", { sessionId: session.sessionId, newExpiry });
780
+
781
+ return {
782
+ content: [
783
+ {
784
+ type: "text" as const,
785
+ text: `Session extended by ${params.additionalMs / 1000}s. New expiry: ${Math.round(newExpiry / 1000)}s`,
786
+ },
787
+ ],
788
+ };
789
+ }
790
+ );
791
+
792
+ // Find Element
793
+ registerAppTool(
794
+ server,
795
+ "find",
796
+ {
797
+ title: "Find Element",
798
+ description: "Find an element on screen by natural language description",
799
+ inputSchema: z.object({
800
+ description: z.string().describe("Natural language description of the element"),
801
+ timeout: z.number().optional().describe("Timeout in ms for polling"),
802
+ }) as any,
803
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
804
+ },
805
+ async (params: { description: string; timeout?: number }): Promise<CallToolResult> => {
806
+ const startTime = Date.now();
807
+ logger.info("find: Starting", { description: params.description, timeout: params.timeout });
808
+
809
+ const sessionCheck = requireActiveSession();
810
+ if (!sessionCheck.valid) {
811
+ logger.warn("find: No active session");
812
+ return sessionCheck.error;
813
+ }
814
+
815
+ try {
816
+ logger.debug("find: Calling SDK find");
817
+ const element = await sdk.find(params.description, params.timeout ? { timeout: params.timeout } : undefined);
818
+ const found = element.found();
819
+ const coords = element.getCoordinates();
820
+
821
+ // Store element ref for later use (stores actual Element instance)
822
+ const elementRef = `el-${Date.now()}`;
823
+ if (found && coords) {
824
+ elementRefs.set(elementRef, {
825
+ element: element, // Store the actual Element instance
826
+ description: params.description,
827
+ coords: {
828
+ x: coords.x,
829
+ y: coords.y,
830
+ centerX: coords.centerX,
831
+ centerY: coords.centerY,
832
+ },
833
+ });
834
+ logger.info("find: Element found", {
835
+ description: params.description,
836
+ coords: { x: coords.centerX, y: coords.centerY },
837
+ confidence: element.confidence,
838
+ elementRef
839
+ });
840
+ } else {
841
+ logger.warn("find: Element not found", { description: params.description });
842
+ }
843
+
844
+ // Return raw SDK response directly
845
+ const rawResponse = element._response || {};
846
+ const duration = Date.now() - startTime;
847
+
848
+ // Store cropped image for resource serving (instead of inline data URL)
849
+ let croppedImageResourceUri: string | undefined;
850
+ const croppedImage = rawResponse.croppedImage;
851
+ if (croppedImage) {
852
+ const imageData = croppedImage.startsWith('data:')
853
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
854
+ : croppedImage;
855
+ croppedImageResourceUri = storeImage(imageData, "cropped");
856
+ // Remove croppedImage from response to avoid context bloat
857
+ delete rawResponse.croppedImage;
858
+ }
859
+
860
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
861
+ delete rawResponse.extractedText;
862
+ delete rawResponse.pixelDiffImage;
863
+
864
+ // Generate code for this find action
865
+ const generatedCode = found ? generateActionCode("find", { description: params.description }) : undefined;
866
+
867
+ // Build element info for display (cropped image is always centered on element)
868
+ const elementInfo = found ? {
869
+ description: params.description,
870
+ centerX: coords?.centerX,
871
+ centerY: coords?.centerY,
872
+ confidence: element.confidence,
873
+ ref: elementRef,
874
+ } : undefined;
875
+
876
+ return createToolResult(
877
+ found,
878
+ found
879
+ ? `Found: "${params.description}" at (${rawResponse.coordinates?.x}, ${rawResponse.coordinates?.y})\nRef: ${elementRef}`
880
+ : `Element not found: "${params.description}"`,
881
+ {
882
+ ...rawResponse,
883
+ action: "find",
884
+ element: elementInfo,
885
+ ref: elementRef,
886
+ croppedImageResourceUri,
887
+ duration,
888
+ },
889
+ generatedCode
890
+ );
891
+ } catch (error) {
892
+ logger.error("find: Failed", { error: String(error), description: params.description });
893
+ captureException(error as Error, { tags: { tool: "find" }, extra: { description: params.description } });
894
+ throw error;
895
+ }
896
+ }
897
+ );
898
+
899
+ // Find All Elements
900
+ registerAppTool(
901
+ server,
902
+ "findall",
903
+ {
904
+ title: "Find All Elements",
905
+ description: "Find all elements on screen matching a natural language description. Returns an array of element references.",
906
+ inputSchema: z.object({
907
+ description: z.string().describe("Natural language description of the elements to find"),
908
+ timeout: z.number().optional().describe("Timeout in ms for polling"),
909
+ }) as any,
910
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
911
+ },
912
+ async (params: { description: string; timeout?: number }): Promise<CallToolResult> => {
913
+ const startTime = Date.now();
914
+ logger.info("findall: Starting", { description: params.description, timeout: params.timeout });
915
+
916
+ const sessionCheck = requireActiveSession();
917
+ if (!sessionCheck.valid) {
918
+ logger.warn("findall: No active session");
919
+ return sessionCheck.error;
920
+ }
921
+
922
+ try {
923
+ logger.debug("findall: Calling SDK findAll");
924
+ const elements = await sdk.findAll(params.description, params.timeout ? { timeout: params.timeout } : undefined);
925
+ const count = elements.length;
926
+
927
+ // Store element refs for later use
928
+ const refs: string[] = [];
929
+ const elementInfos: Array<{ ref: string; x: number; y: number; centerX: number; centerY: number; confidence: number }> = [];
930
+
931
+ for (let i = 0; i < elements.length; i++) {
932
+ const element = elements[i];
933
+ const coords = element.getCoordinates();
934
+ const elementRef = `el-${Date.now()}-${i}`;
935
+
936
+ if (coords) {
937
+ elementRefs.set(elementRef, {
938
+ element: element,
939
+ description: `${params.description} [${i}]`,
940
+ coords: {
941
+ x: coords.x,
942
+ y: coords.y,
943
+ centerX: coords.centerX,
944
+ centerY: coords.centerY,
945
+ },
946
+ });
947
+ refs.push(elementRef);
948
+ elementInfos.push({
949
+ ref: elementRef,
950
+ x: coords.x,
951
+ y: coords.y,
952
+ centerX: coords.centerX,
953
+ centerY: coords.centerY,
954
+ confidence: element.confidence,
955
+ });
956
+ }
957
+ }
958
+
959
+ logger.info("findall: Elements found", {
960
+ description: params.description,
961
+ count,
962
+ refs
963
+ });
964
+
965
+ // Get the first element's response for the image (shows all highlights)
966
+ const rawResponse = elements[0]?._response || {};
967
+ const duration = Date.now() - startTime;
968
+
969
+ // Store cropped image for resource serving (instead of inline data URL)
970
+ let croppedImageResourceUri: string | undefined;
971
+ const croppedImage = rawResponse.croppedImage;
972
+ if (croppedImage) {
973
+ const imageData = croppedImage.startsWith('data:')
974
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
975
+ : croppedImage;
976
+ croppedImageResourceUri = storeImage(imageData, "cropped");
977
+ // Remove croppedImage from response to avoid context bloat
978
+ delete rawResponse.croppedImage;
979
+ }
980
+
981
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
982
+ delete rawResponse.extractedText;
983
+ delete rawResponse.pixelDiffImage;
984
+
985
+ // Generate code for this findall action
986
+ const generatedCode = count > 0 ? generateActionCode("findall", { description: params.description }) : undefined;
987
+
988
+ // Build refs list for text output
989
+ const refsList = refs.map((ref, i) => ` [${i}] ${ref}`).join('\n');
990
+
991
+ return createToolResult(
992
+ count > 0,
993
+ count > 0
994
+ ? `Found ${count} elements matching "${params.description}":\n${refsList}`
995
+ : `No elements found matching: "${params.description}"`,
996
+ {
997
+ ...rawResponse,
998
+ count,
999
+ refs,
1000
+ elements: elementInfos,
1001
+ croppedImageResourceUri,
1002
+ duration,
1003
+ },
1004
+ generatedCode
1005
+ );
1006
+ } catch (error) {
1007
+ logger.error("findall: Failed", { error: String(error), description: params.description });
1008
+ captureException(error as Error, { tags: { tool: "findall" }, extra: { description: params.description } });
1009
+ throw error;
1010
+ }
1011
+ }
1012
+ );
1013
+
1014
+ // Click
1015
+ registerAppTool(
1016
+ server,
1017
+ "click",
1018
+ {
1019
+ title: "Click Element",
1020
+ description: "Click on a previously found element. Use 'find' first to locate the element.",
1021
+ inputSchema: z.object({
1022
+ elementRef: z.string().describe("Reference to previously found element (required). Get this from a 'find' call."),
1023
+ action: z.enum(["click", "double-click", "right-click"]).default("click"),
1024
+ }) as any,
1025
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1026
+ },
1027
+ async (params: { elementRef: string; action: "click" | "double-click" | "right-click" }): Promise<CallToolResult> => {
1028
+ const startTime = Date.now();
1029
+ logger.info("click: Starting", { elementRef: params.elementRef, action: params.action });
1030
+
1031
+ const sessionCheck = requireActiveSession();
1032
+ if (!sessionCheck.valid) {
1033
+ logger.warn("click: No active session");
1034
+ return sessionCheck.error;
1035
+ }
1036
+
1037
+ // Look up the element reference
1038
+ const ref = elementRefs.get(params.elementRef);
1039
+ if (!ref) {
1040
+ logger.warn("click: Element reference not found", { elementRef: params.elementRef });
1041
+ return createToolResult(false, `Element reference "${params.elementRef}" not found. Use 'find' first to locate the element.`, { error: "Element reference not found" });
1042
+ }
1043
+
1044
+ const { element, description, coords } = ref;
1045
+
1046
+ try {
1047
+ logger.debug("click: Executing click on element", { description, action: params.action });
1048
+
1049
+ // Use the Element's click method instead of raw coordinates
1050
+ if (params.action === "click") {
1051
+ await element.click();
1052
+ } else if (params.action === "double-click") {
1053
+ await element.doubleClick();
1054
+ } else if (params.action === "right-click") {
1055
+ await element.rightClick();
1056
+ }
1057
+
1058
+ // Capture screenshot after click to show result
1059
+ logger.debug("click: Capturing screenshot after click");
1060
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1061
+
1062
+ let screenshotResourceUri: string | undefined;
1063
+ if (screenshotBase64) {
1064
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1065
+ lastScreenshotBase64 = screenshotBase64;
1066
+ }
1067
+
1068
+ const rawResponse = element._response || {};
1069
+ // Remove large data from response to reduce context bloat
1070
+ delete rawResponse.croppedImage;
1071
+ delete rawResponse.extractedText;
1072
+ delete rawResponse.pixelDiffImage;
1073
+
1074
+ const duration = Date.now() - startTime;
1075
+ logger.info("click: Completed", { description, duration });
1076
+
1077
+ // Generate code for this click action
1078
+ const generatedCode = generateActionCode("click", { action: params.action });
1079
+
1080
+ return createToolResult(
1081
+ true,
1082
+ `Clicked on "${description}"`,
1083
+ {
1084
+ ...rawResponse,
1085
+ action: "click",
1086
+ clickAction: params.action,
1087
+ clickPosition: coords,
1088
+ screenshotResourceUri,
1089
+ duration
1090
+ },
1091
+ generatedCode
1092
+ );
1093
+ } catch (error) {
1094
+ logger.error("click: Failed", { error: String(error), description });
1095
+ captureException(error as Error, { tags: { tool: "click" }, extra: { elementRef: params.elementRef, action: params.action } });
1096
+ throw error;
1097
+ }
1098
+ }
1099
+ );
1100
+
1101
+ // Hover
1102
+ registerAppTool(
1103
+ server,
1104
+ "hover",
1105
+ {
1106
+ title: "Hover Element",
1107
+ description: "Hover over a previously found element. Use 'find' first to locate the element.",
1108
+ inputSchema: z.object({
1109
+ elementRef: z.string().describe("Reference to previously found element (required). Get this from a 'find' call."),
1110
+ }) as any,
1111
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1112
+ },
1113
+ async (params: { elementRef: string }): Promise<CallToolResult> => {
1114
+ const startTime = Date.now();
1115
+ logger.info("hover: Starting", { elementRef: params.elementRef });
1116
+
1117
+ const sessionCheck = requireActiveSession();
1118
+ if (!sessionCheck.valid) {
1119
+ logger.warn("hover: No active session");
1120
+ return sessionCheck.error;
1121
+ }
1122
+
1123
+ // Look up the element reference
1124
+ const ref = elementRefs.get(params.elementRef);
1125
+ if (!ref) {
1126
+ logger.warn("hover: Element reference not found", { elementRef: params.elementRef });
1127
+ return createToolResult(false, `Element reference "${params.elementRef}" not found. Use 'find' first to locate the element.`, { error: "Element reference not found" });
1128
+ }
1129
+
1130
+ const { element, description, coords } = ref;
1131
+
1132
+ try {
1133
+ logger.debug("hover: Executing hover on element", { description });
1134
+ await element.hover();
1135
+
1136
+ // Capture screenshot after hover to show result
1137
+ logger.debug("hover: Capturing screenshot after hover");
1138
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1139
+
1140
+ let screenshotResourceUri: string | undefined;
1141
+ if (screenshotBase64) {
1142
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1143
+ lastScreenshotBase64 = screenshotBase64;
1144
+ }
1145
+
1146
+ const rawResponse = element._response || {};
1147
+ // Remove large data from response to reduce context bloat
1148
+ delete rawResponse.croppedImage;
1149
+ delete rawResponse.extractedText;
1150
+ delete rawResponse.pixelDiffImage;
1151
+
1152
+ const duration = Date.now() - startTime;
1153
+ logger.info("hover: Completed", { description, duration });
1154
+
1155
+ // Generate code for this hover action
1156
+ const generatedCode = generateActionCode("hover", {});
1157
+
1158
+ return createToolResult(
1159
+ true,
1160
+ `Hovered over "${description}"`,
1161
+ {
1162
+ ...rawResponse,
1163
+ action: "hover",
1164
+ screenshotResourceUri,
1165
+ duration
1166
+ },
1167
+ generatedCode
1168
+ );
1169
+ } catch (error) {
1170
+ logger.error("hover: Failed", { error: String(error), description });
1171
+ captureException(error as Error, { tags: { tool: "hover" }, extra: { elementRef: params.elementRef } });
1172
+ throw error;
1173
+ }
1174
+ }
1175
+ );
1176
+
1177
+ // Wait
1178
+ server.registerTool(
1179
+ "wait",
1180
+ {
1181
+ description: "Wait for a specified amount of time",
1182
+ inputSchema: z.object({
1183
+ timeout: z.number().default(3000).describe("Time to wait in milliseconds (default: 3000)"),
1184
+ }),
1185
+ },
1186
+ async (params): Promise<CallToolResult> => {
1187
+ const startTime = Date.now();
1188
+ logger.info("wait: Starting", { timeout: params.timeout });
1189
+
1190
+ const sessionCheck = requireActiveSession();
1191
+ if (!sessionCheck.valid) {
1192
+ logger.warn("wait: No active session");
1193
+ return sessionCheck.error;
1194
+ }
1195
+
1196
+ try {
1197
+ logger.debug("wait: Waiting", { timeout: params.timeout });
1198
+ await sdk.wait(params.timeout);
1199
+
1200
+ const duration = Date.now() - startTime;
1201
+ logger.info("wait: Completed", { timeout: params.timeout, duration });
1202
+
1203
+ // Generate code for this wait action
1204
+ const generatedCode = generateActionCode("wait", { timeout: params.timeout });
1205
+
1206
+ return createToolResult(
1207
+ true,
1208
+ `Waited for ${params.timeout}ms`,
1209
+ { action: "wait", timeout: params.timeout, duration },
1210
+ generatedCode
1211
+ );
1212
+ } catch (error) {
1213
+ logger.error("wait: Failed", { error: String(error) });
1214
+ captureException(error as Error, { tags: { tool: "wait" }, extra: { timeout: params.timeout } });
1215
+ throw error;
1216
+ }
1217
+ }
1218
+ );
1219
+
1220
+ // Focus Application
1221
+ server.registerTool(
1222
+ "focus_application",
1223
+ {
1224
+ description: "Bring an application window to the foreground",
1225
+ inputSchema: z.object({
1226
+ name: z.string().describe("Name of the application to focus (e.g., 'Google Chrome', 'Visual Studio Code')"),
1227
+ }),
1228
+ },
1229
+ async (params): Promise<CallToolResult> => {
1230
+ const startTime = Date.now();
1231
+ logger.info("focus_application: Starting", { name: params.name });
1232
+
1233
+ const sessionCheck = requireActiveSession();
1234
+ if (!sessionCheck.valid) {
1235
+ logger.warn("focus_application: No active session");
1236
+ return sessionCheck.error;
1237
+ }
1238
+
1239
+ try {
1240
+ logger.debug("focus_application: Focusing", { name: params.name });
1241
+ await sdk.focusApplication(params.name);
1242
+
1243
+ const duration = Date.now() - startTime;
1244
+ logger.info("focus_application: Completed", { name: params.name, duration });
1245
+
1246
+ // Generate code for this focus action
1247
+ const generatedCode = generateActionCode("focus_application", { name: params.name });
1248
+
1249
+ return createToolResult(
1250
+ true,
1251
+ `Focused application: "${params.name}"`,
1252
+ { action: "focus", name: params.name, duration },
1253
+ generatedCode
1254
+ );
1255
+ } catch (error) {
1256
+ logger.error("focus_application: Failed", { error: String(error), name: params.name });
1257
+ captureException(error as Error, { tags: { tool: "focus_application" }, extra: { name: params.name } });
1258
+ throw error;
1259
+ }
1260
+ }
1261
+ );
1262
+
1263
+ // Find and Click
1264
+ registerAppTool(
1265
+ server,
1266
+ "find_and_click",
1267
+ {
1268
+ title: "Find and Click",
1269
+ description: "Find an element and click it in one action",
1270
+ inputSchema: z.object({
1271
+ description: z.string().describe("Natural language description of element"),
1272
+ action: z.enum(["click", "double-click", "right-click"]).default("click"),
1273
+ }) as any,
1274
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1275
+ },
1276
+ async (params: { description: string; action: "click" | "double-click" | "right-click" }): Promise<CallToolResult> => {
1277
+ const startTime = Date.now();
1278
+ logger.info("find_and_click: Starting", { description: params.description, action: params.action });
1279
+
1280
+ const sessionCheck = requireActiveSession();
1281
+ if (!sessionCheck.valid) {
1282
+ logger.warn("find_and_click: No active session");
1283
+ return sessionCheck.error;
1284
+ }
1285
+
1286
+ try {
1287
+ logger.debug("find_and_click: Finding element");
1288
+ const element = await sdk.find(params.description);
1289
+ const found = element.found();
1290
+
1291
+ if (!found) {
1292
+ logger.warn("find_and_click: Element not found", { description: params.description });
1293
+
1294
+ // Capture screenshot to show current state even when element not found
1295
+ const rawResponse = element._response || {};
1296
+ const duration = Date.now() - startTime;
1297
+
1298
+ // Store cropped image (screenshot) for resource serving
1299
+ let croppedImageResourceUri: string | undefined;
1300
+ const croppedImage = rawResponse.croppedImage;
1301
+ if (croppedImage) {
1302
+ const imageData = croppedImage.startsWith('data:')
1303
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
1304
+ : croppedImage;
1305
+ croppedImageResourceUri = storeImage(imageData, "screenshot");
1306
+ delete rawResponse.croppedImage;
1307
+ }
1308
+
1309
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
1310
+ delete rawResponse.extractedText;
1311
+ delete rawResponse.pixelDiffImage;
1312
+
1313
+ return createToolResult(
1314
+ false,
1315
+ `Element not found: "${params.description}"`,
1316
+ {
1317
+ ...rawResponse,
1318
+ action: "find_and_click",
1319
+ error: "Element not found",
1320
+ croppedImageResourceUri,
1321
+ duration
1322
+ }
1323
+ );
1324
+ }
1325
+
1326
+ const coords = element.getCoordinates();
1327
+
1328
+ // Store element ref for later use (in case user wants to interact again)
1329
+ const elementRef = `el-${Date.now()}`;
1330
+ if (coords) {
1331
+ elementRefs.set(elementRef, {
1332
+ element: element,
1333
+ description: params.description,
1334
+ coords: {
1335
+ x: coords.x,
1336
+ y: coords.y,
1337
+ centerX: coords.centerX,
1338
+ centerY: coords.centerY,
1339
+ },
1340
+ });
1341
+ }
1342
+
1343
+ logger.debug("find_and_click: Element found, clicking", { action: params.action, elementRef });
1344
+ if (params.action === "click") {
1345
+ await element.click();
1346
+ } else if (params.action === "double-click") {
1347
+ await element.doubleClick();
1348
+ } else if (params.action === "right-click") {
1349
+ await element.rightClick();
1350
+ }
1351
+
1352
+ // Return raw SDK response directly
1353
+ const rawResponse = element._response || {};
1354
+ const duration = Date.now() - startTime;
1355
+
1356
+ // Store cropped image for resource serving (instead of inline data URL)
1357
+ let croppedImageResourceUri: string | undefined;
1358
+ const croppedImage = rawResponse.croppedImage;
1359
+ if (croppedImage) {
1360
+ const imageData = croppedImage.startsWith('data:')
1361
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
1362
+ : croppedImage;
1363
+ croppedImageResourceUri = storeImage(imageData, "cropped");
1364
+ // Remove croppedImage from response to avoid context bloat
1365
+ delete rawResponse.croppedImage;
1366
+ }
1367
+
1368
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
1369
+ delete rawResponse.extractedText;
1370
+ delete rawResponse.pixelDiffImage;
1371
+
1372
+ // Generate code for this find_and_click action
1373
+ const generatedCode = generateActionCode("find_and_click", { description: params.description, action: params.action });
1374
+
1375
+ // Build element info for display (match find action format)
1376
+ const elementInfo = coords ? {
1377
+ description: params.description,
1378
+ centerX: coords.centerX,
1379
+ centerY: coords.centerY,
1380
+ confidence: element.confidence,
1381
+ ref: elementRef,
1382
+ } : undefined;
1383
+
1384
+ return createToolResult(
1385
+ true,
1386
+ `Found and clicked: "${params.description}" at (${rawResponse.coordinates?.x}, ${rawResponse.coordinates?.y})\nRef: ${elementRef}`,
1387
+ {
1388
+ ...rawResponse,
1389
+ action: "find_and_click",
1390
+ element: elementInfo,
1391
+ ref: elementRef,
1392
+ clickAction: params.action,
1393
+ clickPosition: coords ? { x: coords.centerX, y: coords.centerY } : undefined,
1394
+ croppedImageResourceUri,
1395
+ duration,
1396
+ },
1397
+ generatedCode
1398
+ );
1399
+ } catch (error) {
1400
+ logger.error("find_and_click: Failed", { error: String(error), description: params.description });
1401
+ captureException(error as Error, { tags: { tool: "find_and_click" }, extra: { description: params.description, action: params.action } });
1402
+ throw error;
1403
+ }
1404
+ }
1405
+ );
1406
+
1407
+ // Type
1408
+ server.registerTool(
1409
+ "type",
1410
+ {
1411
+ description: "Type text into the currently focused field",
1412
+ inputSchema: z.object({
1413
+ text: z.string().describe("Text to type"),
1414
+ secret: z.boolean().default(false).describe("Whether this is sensitive data"),
1415
+ delay: z.number().optional().describe("Delay between keystrokes in ms"),
1416
+ }),
1417
+ },
1418
+ async (params): Promise<CallToolResult> => {
1419
+ const startTime = Date.now();
1420
+ logger.info("type: Starting", { textLength: params.text.length, secret: params.secret });
1421
+
1422
+ const sessionCheck = requireActiveSession();
1423
+ if (!sessionCheck.valid) {
1424
+ logger.warn("type: No active session");
1425
+ return sessionCheck.error;
1426
+ }
1427
+
1428
+ try {
1429
+ logger.debug("type: Typing text");
1430
+ await sdk.type(params.text, { secret: params.secret, delay: params.delay });
1431
+
1432
+ const duration = Date.now() - startTime;
1433
+ logger.info("type: Completed", { duration });
1434
+
1435
+ // Generate code for this type action
1436
+ const generatedCode = generateActionCode("type", { text: params.text, secret: params.secret });
1437
+
1438
+ return createToolResult(
1439
+ true,
1440
+ `Typed: ${params.secret ? "[secret text]" : `"${params.text}"`}`,
1441
+ { action: "type", text: params.secret ? "[SECRET]" : params.text, duration },
1442
+ generatedCode
1443
+ );
1444
+ } catch (error) {
1445
+ logger.error("type: Failed", { error: String(error) });
1446
+ captureException(error as Error, { tags: { tool: "type" }, extra: { textLength: params.text.length, secret: params.secret } });
1447
+ throw error;
1448
+ }
1449
+ }
1450
+ );
1451
+
1452
+ // Press Keys
1453
+ server.registerTool(
1454
+ "press_keys",
1455
+ {
1456
+ description: "Press keyboard keys or shortcuts",
1457
+ inputSchema: z.object({
1458
+ keys: z.array(z.string()).describe("Array of keys to press (e.g., ['ctrl', 'a'])"),
1459
+ }),
1460
+ },
1461
+ async (params): Promise<CallToolResult> => {
1462
+ const startTime = Date.now();
1463
+ logger.info("press_keys: Starting", { keys: params.keys });
1464
+
1465
+ const sessionCheck = requireActiveSession();
1466
+ if (!sessionCheck.valid) {
1467
+ logger.warn("press_keys: No active session");
1468
+ return sessionCheck.error;
1469
+ }
1470
+
1471
+ try {
1472
+ logger.debug("press_keys: Pressing keys");
1473
+ await sdk.pressKeys(params.keys);
1474
+
1475
+ const duration = Date.now() - startTime;
1476
+ logger.info("press_keys: Completed", { keys: params.keys, duration });
1477
+
1478
+ // Generate code for this press_keys action
1479
+ const generatedCode = generateActionCode("press_keys", { keys: params.keys });
1480
+
1481
+ return createToolResult(
1482
+ true,
1483
+ `Pressed keys: ${params.keys.join(" + ")}`,
1484
+ { action: "press_keys", keys: params.keys, duration },
1485
+ generatedCode
1486
+ );
1487
+ } catch (error) {
1488
+ logger.error("press_keys: Failed", { error: String(error), keys: params.keys });
1489
+ captureException(error as Error, { tags: { tool: "press_keys" }, extra: { keys: params.keys } });
1490
+ throw error;
1491
+ }
1492
+ }
1493
+ );
1494
+
1495
+ // Scroll
1496
+ server.registerTool(
1497
+ "scroll",
1498
+ {
1499
+ description: "Scroll the page or element",
1500
+ inputSchema: z.object({
1501
+ direction: z.enum(["up", "down", "left", "right"]).default("down"),
1502
+ amount: z.number().optional().describe("Amount to scroll in pixels"),
1503
+ }),
1504
+ },
1505
+ async (params): Promise<CallToolResult> => {
1506
+ const startTime = Date.now();
1507
+ logger.info("scroll: Starting", { direction: params.direction, amount: params.amount });
1508
+
1509
+ const sessionCheck = requireActiveSession();
1510
+ if (!sessionCheck.valid) {
1511
+ logger.warn("scroll: No active session");
1512
+ return sessionCheck.error;
1513
+ }
1514
+
1515
+ try {
1516
+ logger.debug("scroll: Scrolling");
1517
+ await sdk.scroll(params.direction, params.amount ? { amount: params.amount } : undefined);
1518
+
1519
+ const duration = Date.now() - startTime;
1520
+ logger.info("scroll: Completed", { direction: params.direction, duration });
1521
+
1522
+ // Generate code for this scroll action
1523
+ const generatedCode = generateActionCode("scroll", { direction: params.direction, amount: params.amount });
1524
+
1525
+ return createToolResult(
1526
+ true,
1527
+ `Scrolled ${params.direction}${params.amount ? ` by ${params.amount}px` : ""}`,
1528
+ { action: "scroll", scrollDirection: params.direction, direction: params.direction, amount: params.amount, duration },
1529
+ generatedCode
1530
+ );
1531
+ } catch (error) {
1532
+ logger.error("scroll: Failed", { error: String(error), direction: params.direction });
1533
+ captureException(error as Error, { tags: { tool: "scroll" }, extra: { direction: params.direction, amount: params.amount } });
1534
+ throw error;
1535
+ }
1536
+ }
1537
+ );
1538
+
1539
+ // Assert - generates code for test files
1540
+ server.registerTool(
1541
+ "assert",
1542
+ {
1543
+ description: `Make an AI-powered assertion about the current screen state. GENERATES CODE for the test file.
1544
+
1545
+ Use this when you want a verification step recorded in the generated test. This will add code like:
1546
+ const assertResult = await testdriver.assert("your assertion");
1547
+ expect(assertResult).toBeTruthy();
1548
+
1549
+ Unlike 'check' which is for your understanding during development, 'assert' creates verification code that runs in CI/CD.`,
1550
+ inputSchema: z.object({
1551
+ assertion: z.string().describe("Natural language assertion to verify"),
1552
+ }),
1553
+ },
1554
+ async (params): Promise<CallToolResult> => {
1555
+ const startTime = Date.now();
1556
+ logger.info("assert: Starting", { assertion: params.assertion });
1557
+
1558
+ const sessionCheck = requireActiveSession();
1559
+ if (!sessionCheck.valid) {
1560
+ logger.warn("assert: No active session");
1561
+ return sessionCheck.error;
1562
+ }
1563
+
1564
+ try {
1565
+ logger.debug("assert: Running assertion");
1566
+ const result = await sdk.assert(params.assertion);
1567
+
1568
+ const duration = Date.now() - startTime;
1569
+ logger.info("assert: Completed", { assertion: params.assertion, passed: result, duration });
1570
+
1571
+ // Generate code for this assert action
1572
+ const generatedCode = generateActionCode("assert", { assertion: params.assertion });
1573
+
1574
+ return createToolResult(
1575
+ result,
1576
+ result ? `✓ Assertion passed: "${params.assertion}"` : `✗ Assertion failed: "${params.assertion}"`,
1577
+ { action: "assert", assertion: params.assertion, passed: result, success: result, duration },
1578
+ generatedCode
1579
+ );
1580
+ } catch (error) {
1581
+ logger.error("assert: Failed", { error: String(error), assertion: params.assertion });
1582
+ captureException(error as Error, { tags: { tool: "assert" }, extra: { assertion: params.assertion } });
1583
+ throw error;
1584
+ }
1585
+ }
1586
+ );
1587
+
1588
+ // Check - AI uses this to understand the screen state (DOES NOT generate code)
1589
+ registerAppTool(
1590
+ server,
1591
+ "check",
1592
+ {
1593
+ title: "Check Screen State",
1594
+ description: `👁️ THIS IS HOW YOU SEE THE SCREEN. Use this tool whenever you need to understand what's currently displayed.
1595
+
1596
+ This tool captures a screenshot and returns AI analysis to YOU. Use it to:
1597
+ - See what's on the screen right now
1598
+ - Verify if your last action worked
1599
+ - Understand the current application state
1600
+ - Check if elements are visible or if navigation completed
1601
+
1602
+ Examples:
1603
+ - "What is currently on the screen?"
1604
+ - "Did the button click work?"
1605
+ - "Is the login form visible?"
1606
+ - "Did the page navigate to the dashboard?"
1607
+
1608
+ ⚠️ Do NOT use 'screenshot' to see the screen - that only shows the user, not you.
1609
+
1610
+ Note: This tool does NOT generate test code. Use 'assert' when you want to add a verification step to the test file.
1611
+
1612
+ You can optionally provide a reference image URI to compare against a previous state.`,
1613
+ inputSchema: z.object({
1614
+ task: z.string().describe("The task or condition to verify (e.g., 'Did the login succeed?', 'Is the modal visible?')"),
1615
+ referenceImageUri: z.string().optional().describe("Optional screenshot resource URI (e.g., 'screenshot://testdriver/screenshot/screenshot-1') to compare against instead of the automatically captured 'before' screenshot. Use a screenshotResourceUri from a previous action."),
1616
+ }) as any,
1617
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1618
+ },
1619
+ async (params: { task: string; referenceImageUri?: string }): Promise<CallToolResult> => {
1620
+ const startTime = Date.now();
1621
+ logger.info("check: Starting", { task: params.task, hasReferenceImageUri: !!params.referenceImageUri });
1622
+
1623
+ const sessionCheck = requireActiveSession();
1624
+ if (!sessionCheck.valid) {
1625
+ logger.warn("check: No active session");
1626
+ return sessionCheck.error;
1627
+ }
1628
+
1629
+ try {
1630
+ // Capture current screenshot
1631
+ logger.debug("check: Capturing current screenshot");
1632
+ const currentScreenshot = await sdk.agent.system.captureScreenBase64(1, false, true);
1633
+
1634
+ // Use provided reference image URI, last screenshot as "before" state, or current if no previous screenshot
1635
+ let beforeScreenshot: string;
1636
+ if (params.referenceImageUri) {
1637
+ // Extract image ID from URI (e.g., "screenshot://testdriver/screenshot/screenshot-1" -> "screenshot-1")
1638
+ const uriParts = params.referenceImageUri.split('/');
1639
+ const imageId = uriParts[uriParts.length - 1];
1640
+
1641
+ logger.info("check: Looking up reference image", {
1642
+ referenceImageUri: params.referenceImageUri,
1643
+ extractedImageId: imageId,
1644
+ imageStoreSize: imageStore.size,
1645
+ availableKeys: Array.from(imageStore.keys())
1646
+ });
1647
+
1648
+ const storedImage = getStoredImage(imageId);
1649
+
1650
+ if (storedImage) {
1651
+ logger.info("check: Found reference image", {
1652
+ imageId,
1653
+ dataLength: storedImage.data?.length,
1654
+ type: storedImage.type,
1655
+ hasData: !!storedImage.data
1656
+ });
1657
+ beforeScreenshot = storedImage.data;
1658
+ } else {
1659
+ logger.warn("check: Reference image NOT found in store, falling back to last screenshot", {
1660
+ referenceImageUri: params.referenceImageUri,
1661
+ imageId,
1662
+ imageStoreSize: imageStore.size,
1663
+ availableKeys: Array.from(imageStore.keys())
1664
+ });
1665
+ beforeScreenshot = lastScreenshotBase64 || currentScreenshot;
1666
+ }
1667
+ } else {
1668
+ beforeScreenshot = lastScreenshotBase64 || currentScreenshot;
1669
+ }
1670
+
1671
+ // Update last screenshot for next check
1672
+ lastScreenshotBase64 = currentScreenshot;
1673
+
1674
+ // Get system state
1675
+ const mousePosition = await sdk.agent.system.getMousePosition();
1676
+ const activeWindow = await sdk.agent.system.activeWin();
1677
+
1678
+ // Call the check endpoint
1679
+ logger.info("check: Calling check API endpoint", {
1680
+ hasLastScreenshot: beforeScreenshot !== currentScreenshot,
1681
+ usingReferenceImageUri: !!params.referenceImageUri,
1682
+ beforeScreenshotLength: beforeScreenshot?.length || 0,
1683
+ currentScreenshotLength: currentScreenshot?.length || 0,
1684
+ beforeScreenshotPreview: beforeScreenshot?.substring(0, 50),
1685
+ currentScreenshotPreview: currentScreenshot?.substring(0, 50)
1686
+ });
1687
+ const response = await sdk.agent.sdk.req("check", {
1688
+ tasks: [params.task],
1689
+ images: [beforeScreenshot, currentScreenshot],
1690
+ mousePosition,
1691
+ activeWindow,
1692
+ });
1693
+
1694
+ const aiResponse = response.data;
1695
+
1696
+ // Store screenshot for resource serving
1697
+ let screenshotResourceUri: string | undefined;
1698
+ if (currentScreenshot) {
1699
+ screenshotResourceUri = storeImage(currentScreenshot, "screenshot");
1700
+ }
1701
+
1702
+ // Determine if the check passed based on the AI response
1703
+ // The AI typically returns markdown with its analysis
1704
+ // We consider it "complete" if the response doesn't contain code blocks (indicating more work needed)
1705
+ const hasCodeBlocks = aiResponse && (
1706
+ aiResponse.includes("```yml") ||
1707
+ aiResponse.includes("```yaml") ||
1708
+ aiResponse.includes("- command:")
1709
+ );
1710
+ const isComplete = !hasCodeBlocks;
1711
+
1712
+ const duration = Date.now() - startTime;
1713
+ logger.info("check: Completed", { task: params.task, complete: isComplete, duration });
1714
+
1715
+ // Note: check doesn't generate code - it's for AI understanding, not test recording
1716
+ return createToolResult(
1717
+ isComplete,
1718
+ isComplete
1719
+ ? `✓ Task appears complete: "${params.task}"\n\nAI Analysis:\n${aiResponse}`
1720
+ : `⚠ Task may not be complete: "${params.task}"\n\nAI Analysis:\n${aiResponse}`,
1721
+ {
1722
+ action: "check",
1723
+ task: params.task,
1724
+ complete: isComplete,
1725
+ success: isComplete,
1726
+ aiResponse,
1727
+ screenshotResourceUri,
1728
+ duration
1729
+ }
1730
+ );
1731
+ } catch (error) {
1732
+ logger.error("check: Failed", { error: String(error), task: params.task });
1733
+ captureException(error as Error, { tags: { tool: "check" }, extra: { task: params.task } });
1734
+ throw error;
1735
+ }
1736
+ }
1737
+ );
1738
+
1739
+ // Exec
1740
+ server.registerTool(
1741
+ "exec",
1742
+ {
1743
+ description: "Execute code in the sandbox (JavaScript, shell, or PowerShell)",
1744
+ inputSchema: z.object({
1745
+ language: z.enum(["js", "sh", "pwsh"]).default("js"),
1746
+ code: z.string().describe("Code to execute"),
1747
+ timeout: z.number().default(30000).describe("Timeout in ms"),
1748
+ }),
1749
+ },
1750
+ async (params): Promise<CallToolResult> => {
1751
+ const startTime = Date.now();
1752
+ logger.info("exec: Starting", { language: params.language, codeLength: params.code.length, timeout: params.timeout });
1753
+
1754
+ const sessionCheck = requireActiveSession();
1755
+ if (!sessionCheck.valid) {
1756
+ logger.warn("exec: No active session");
1757
+ return sessionCheck.error;
1758
+ }
1759
+
1760
+ try {
1761
+ logger.debug("exec: Executing code", { language: params.language });
1762
+ const output = await sdk.exec(params.language, params.code, params.timeout);
1763
+
1764
+ const duration = Date.now() - startTime;
1765
+ logger.info("exec: Completed", { language: params.language, outputLength: output?.length || 0, duration });
1766
+
1767
+ // Generate code for this exec action
1768
+ const generatedCode = generateActionCode("exec", { language: params.language, code: params.code, timeout: params.timeout });
1769
+
1770
+ return createToolResult(
1771
+ true,
1772
+ `Executed ${params.language} code:\n${output || "(no output)"}`,
1773
+ { action: "exec", language: params.language, output, duration },
1774
+ generatedCode
1775
+ );
1776
+ } catch (error) {
1777
+ logger.error("exec: Failed", { error: String(error), language: params.language });
1778
+ captureException(error as Error, { tags: { tool: "exec" }, extra: { language: params.language, codeLength: params.code.length } });
1779
+ throw error;
1780
+ }
1781
+ }
1782
+ );
1783
+
1784
+ // Parse auto-screenshot filename format: <seq>-<action>-<phase>-L<line>-<description>.png
1785
+ // Example: 001-click-before-L42-submit-button.png
1786
+ // Example: 003-click-error-L42-submit-button.png (error phase when action fails)
1787
+ interface ParsedScreenshotInfo {
1788
+ sequence?: number;
1789
+ action?: string;
1790
+ phase?: "before" | "after" | "error";
1791
+ lineNumber?: number;
1792
+ description?: string;
1793
+ }
1794
+
1795
+ function parseScreenshotFilename(filename: string): ParsedScreenshotInfo {
1796
+ // Match pattern: 001-click-before-L42-submit-button.png or 001-click-error-L42-submit-button.png
1797
+ const match = filename.match(/^(\d+)-([a-z]+)-(before|after|error)-L(\d+)-(.+)\.png$/i);
1798
+ if (match) {
1799
+ return {
1800
+ sequence: parseInt(match[1], 10),
1801
+ action: match[2].toLowerCase(),
1802
+ phase: match[3].toLowerCase() as "before" | "after" | "error",
1803
+ lineNumber: parseInt(match[4], 10),
1804
+ description: match[5],
1805
+ };
1806
+ }
1807
+ return {};
1808
+ }
1809
+
1810
+ // List Local Screenshots - lists screenshots saved to .testdriver directory
1811
+ server.registerTool(
1812
+ "list_local_screenshots",
1813
+ {
1814
+ description: `List and filter screenshots saved in the .testdriver directory.
1815
+
1816
+ Screenshots from auto-screenshot feature use the format: <seq>-<action>-<phase>-L<line>-<description>.png
1817
+ Example: 001-click-before-L42-submit-button.png
1818
+
1819
+ This tool supports powerful filtering to find specific screenshots:
1820
+ - By test file (directory)
1821
+ - By line number or range
1822
+ - By action type (click, find, type, assert, etc.)
1823
+ - By phase (before/after/error - error screenshots are captured when actions fail)
1824
+ - By regex pattern on filename
1825
+ - By sequence number range
1826
+
1827
+ Returns a list of screenshot paths that can be viewed with the 'view_local_screenshot' tool.`,
1828
+ inputSchema: z.object({
1829
+ directory: z.string().optional().describe("Test file or subdirectory to search (e.g., 'login.test', 'mcp-screenshots'). If not provided, searches all."),
1830
+ line: z.number().optional().describe("Filter by exact line number from test file (e.g., 42 matches L42)"),
1831
+ lineRange: z.object({
1832
+ start: z.number().describe("Start line number (inclusive)"),
1833
+ end: z.number().describe("End line number (inclusive)"),
1834
+ }).optional().describe("Filter by line number range (e.g., { start: 10, end: 20 })"),
1835
+ action: z.string().optional().describe("Filter by action type: click, find, type, assert, provision, scroll, hover, etc."),
1836
+ phase: z.enum(["before", "after", "error"]).optional().describe("Filter by phase: 'before' (pre-action), 'after' (post-action), or 'error' (when action fails)"),
1837
+ pattern: z.string().optional().describe("Regex pattern to match against filename (e.g., 'submit|login' or 'button.*click')"),
1838
+ sequence: z.number().optional().describe("Filter by exact sequence number"),
1839
+ sequenceRange: z.object({
1840
+ start: z.number().describe("Start sequence (inclusive)"),
1841
+ end: z.number().describe("End sequence (inclusive)"),
1842
+ }).optional().describe("Filter by sequence range (e.g., { start: 1, end: 10 })"),
1843
+ limit: z.number().optional().describe("Maximum number of results to return (default: 50)"),
1844
+ sortBy: z.enum(["modified", "sequence", "line"]).optional().describe("Sort by: 'modified' (newest first), 'sequence' (execution order), or 'line' (line number). Default: 'modified'"),
1845
+ }),
1846
+ },
1847
+ async (params): Promise<CallToolResult> => {
1848
+ const startTime = Date.now();
1849
+ logger.info("list_local_screenshots: Starting", { ...params });
1850
+
1851
+ try {
1852
+ // Find .testdriver directory - check current working directory and common locations
1853
+ const possiblePaths = [
1854
+ path.join(process.cwd(), ".testdriver"),
1855
+ path.join(os.homedir(), ".testdriver"),
1856
+ ];
1857
+
1858
+ let testdriverDir: string | null = null;
1859
+ for (const p of possiblePaths) {
1860
+ if (fs.existsSync(p)) {
1861
+ testdriverDir = p;
1862
+ break;
1863
+ }
1864
+ }
1865
+
1866
+ if (!testdriverDir) {
1867
+ logger.warn("list_local_screenshots: .testdriver directory not found");
1868
+ return createToolResult(false, "No .testdriver directory found. Screenshots are saved here during test runs.", { error: "Directory not found" });
1869
+ }
1870
+
1871
+ interface ScreenshotInfo {
1872
+ path: string;
1873
+ name: string;
1874
+ modified: Date;
1875
+ size: number;
1876
+ parsed: ParsedScreenshotInfo;
1877
+ }
1878
+
1879
+ const screenshots: ScreenshotInfo[] = [];
1880
+
1881
+ // Compile regex pattern if provided
1882
+ let regexPattern: RegExp | null = null;
1883
+ if (params.pattern) {
1884
+ try {
1885
+ regexPattern = new RegExp(params.pattern, "i");
1886
+ } catch {
1887
+ return createToolResult(false, `Invalid regex pattern: ${params.pattern}`, { error: "Invalid regex" });
1888
+ }
1889
+ }
1890
+
1891
+ // Function to recursively find PNG files
1892
+ const findPngFiles = (dir: string) => {
1893
+ if (!fs.existsSync(dir)) return;
1894
+
1895
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1896
+ for (const entry of entries) {
1897
+ const fullPath = path.join(dir, entry.name);
1898
+ if (entry.isDirectory()) {
1899
+ // If a specific directory was requested, only search that one
1900
+ if (!params.directory || entry.name === params.directory || dir !== testdriverDir) {
1901
+ findPngFiles(fullPath);
1902
+ }
1903
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".png")) {
1904
+ const parsed = parseScreenshotFilename(entry.name);
1905
+
1906
+ // Apply filters
1907
+ if (params.line !== undefined && parsed.lineNumber !== params.line) continue;
1908
+ if (params.lineRange && (
1909
+ parsed.lineNumber === undefined ||
1910
+ parsed.lineNumber < params.lineRange.start ||
1911
+ parsed.lineNumber > params.lineRange.end
1912
+ )) continue;
1913
+ if (params.action && parsed.action !== params.action.toLowerCase()) continue;
1914
+ if (params.phase && parsed.phase !== params.phase) continue;
1915
+ if (params.sequence !== undefined && parsed.sequence !== params.sequence) continue;
1916
+ if (params.sequenceRange && (
1917
+ parsed.sequence === undefined ||
1918
+ parsed.sequence < params.sequenceRange.start ||
1919
+ parsed.sequence > params.sequenceRange.end
1920
+ )) continue;
1921
+ if (regexPattern && !regexPattern.test(entry.name)) continue;
1922
+
1923
+ const stats = fs.statSync(fullPath);
1924
+ screenshots.push({
1925
+ path: fullPath,
1926
+ name: entry.name,
1927
+ modified: stats.mtime,
1928
+ size: stats.size,
1929
+ parsed,
1930
+ });
1931
+ }
1932
+ }
1933
+ };
1934
+
1935
+ findPngFiles(testdriverDir);
1936
+
1937
+ // Sort based on sortBy parameter
1938
+ const sortBy = params.sortBy || "modified";
1939
+ if (sortBy === "modified") {
1940
+ screenshots.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1941
+ } else if (sortBy === "sequence") {
1942
+ screenshots.sort((a, b) => (a.parsed.sequence ?? Infinity) - (b.parsed.sequence ?? Infinity));
1943
+ } else if (sortBy === "line") {
1944
+ screenshots.sort((a, b) => (a.parsed.lineNumber ?? Infinity) - (b.parsed.lineNumber ?? Infinity));
1945
+ }
1946
+
1947
+ const duration = Date.now() - startTime;
1948
+ logger.info("list_local_screenshots: Completed", { count: screenshots.length, duration });
1949
+
1950
+ if (screenshots.length === 0) {
1951
+ const filters = [];
1952
+ if (params.directory) filters.push(`directory=${params.directory}`);
1953
+ if (params.line) filters.push(`line=${params.line}`);
1954
+ if (params.lineRange) filters.push(`lineRange=${params.lineRange.start}-${params.lineRange.end}`);
1955
+ if (params.action) filters.push(`action=${params.action}`);
1956
+ if (params.phase) filters.push(`phase=${params.phase}`);
1957
+ if (params.pattern) filters.push(`pattern=${params.pattern}`);
1958
+ if (params.sequence) filters.push(`sequence=${params.sequence}`);
1959
+ if (params.sequenceRange) filters.push(`sequenceRange=${params.sequenceRange.start}-${params.sequenceRange.end}`);
1960
+
1961
+ const filterMsg = filters.length > 0 ? ` with filters: ${filters.join(", ")}` : "";
1962
+ return createToolResult(true, `No screenshots found in .testdriver directory${filterMsg}.`, {
1963
+ action: "list_local_screenshots",
1964
+ count: 0,
1965
+ directory: testdriverDir,
1966
+ filters: params,
1967
+ duration
1968
+ });
1969
+ }
1970
+
1971
+ const limit = params.limit || 50;
1972
+ const limitedScreenshots = screenshots.slice(0, limit);
1973
+
1974
+ // Format the list for display with parsed info
1975
+ const screenshotList = limitedScreenshots.map((s, i) => {
1976
+ const relativePath = path.relative(testdriverDir!, s.path);
1977
+ const sizeKB = Math.round(s.size / 1024);
1978
+ const timeAgo = formatTimeAgo(s.modified);
1979
+
1980
+ // Add parsed info if available
1981
+ const parts = [`${i + 1}. ${relativePath}`];
1982
+ const meta = [];
1983
+ if (s.parsed.lineNumber) meta.push(`L${s.parsed.lineNumber}`);
1984
+ if (s.parsed.action) meta.push(s.parsed.action);
1985
+ if (s.parsed.phase) meta.push(s.parsed.phase);
1986
+ meta.push(`${sizeKB}KB`);
1987
+ meta.push(timeAgo);
1988
+ parts.push(`(${meta.join(", ")})`);
1989
+
1990
+ return parts.join(" ");
1991
+ }).join("\n");
1992
+
1993
+ const message = screenshots.length > limit
1994
+ ? `Found ${screenshots.length} screenshots (showing ${limit} results, sorted by ${sortBy}):\n\n${screenshotList}`
1995
+ : `Found ${screenshots.length} screenshot(s) (sorted by ${sortBy}):\n\n${screenshotList}`;
1996
+
1997
+ return createToolResult(true, message, {
1998
+ action: "list_local_screenshots",
1999
+ count: screenshots.length,
2000
+ returned: limitedScreenshots.length,
2001
+ directory: testdriverDir,
2002
+ filters: params,
2003
+ sortBy,
2004
+ screenshots: limitedScreenshots.map(s => ({
2005
+ path: s.path,
2006
+ relativePath: path.relative(testdriverDir!, s.path),
2007
+ name: s.name,
2008
+ modified: s.modified.toISOString(),
2009
+ sizeBytes: s.size,
2010
+ sequence: s.parsed.sequence,
2011
+ action: s.parsed.action,
2012
+ phase: s.parsed.phase,
2013
+ lineNumber: s.parsed.lineNumber,
2014
+ description: s.parsed.description,
2015
+ })),
2016
+ duration
2017
+ });
2018
+ } catch (error) {
2019
+ logger.error("list_local_screenshots: Failed", { error: String(error) });
2020
+ captureException(error as Error, { tags: { tool: "list_local_screenshots" } });
2021
+ throw error;
2022
+ }
2023
+ }
2024
+ );
2025
+
2026
+ // Helper to format time ago
2027
+ function formatTimeAgo(date: Date): string {
2028
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
2029
+ if (seconds < 60) return `${seconds}s ago`;
2030
+ const minutes = Math.floor(seconds / 60);
2031
+ if (minutes < 60) return `${minutes}m ago`;
2032
+ const hours = Math.floor(minutes / 60);
2033
+ if (hours < 24) return `${hours}h ago`;
2034
+ const days = Math.floor(hours / 24);
2035
+ return `${days}d ago`;
2036
+ }
2037
+
2038
+ // View Local Screenshot - view a screenshot from .testdriver directory
2039
+ // Returns the image so AI clients that support images can see it
2040
+ // Also displays to the user via MCP App
2041
+ registerAppTool(
2042
+ server,
2043
+ "view_local_screenshot",
2044
+ {
2045
+ title: "View Local Screenshot",
2046
+ description: `View a screenshot from the .testdriver directory.
2047
+
2048
+ Use 'list_local_screenshots' first to see available screenshots, then use this tool to view one.
2049
+
2050
+ This tool returns the image content so AI clients that support images can see it directly.
2051
+ The image is also displayed to the user via the MCP App UI.
2052
+
2053
+ Useful for:
2054
+ - Reviewing screenshots from previous test runs
2055
+ - Debugging test failures by examining saved screenshots
2056
+ - Comparing current screen state to saved screenshots`,
2057
+ inputSchema: z.object({
2058
+ path: z.string().describe("Full path to the screenshot file (from list_local_screenshots)"),
2059
+ }) as any,
2060
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
2061
+ },
2062
+ async (params: { path: string }): Promise<CallToolResult> => {
2063
+ const startTime = Date.now();
2064
+ logger.info("view_local_screenshot: Starting", { path: params.path });
2065
+
2066
+ try {
2067
+ // Validate the path exists and is a PNG
2068
+ if (!fs.existsSync(params.path)) {
2069
+ logger.warn("view_local_screenshot: File not found", { path: params.path });
2070
+ return createToolResult(false, `Screenshot not found: ${params.path}`, { error: "File not found" });
2071
+ }
2072
+
2073
+ if (!params.path.toLowerCase().endsWith(".png")) {
2074
+ logger.warn("view_local_screenshot: Not a PNG file", { path: params.path });
2075
+ return createToolResult(false, "Only PNG files are supported", { error: "Invalid file type" });
2076
+ }
2077
+
2078
+ // Security check - only allow files from .testdriver directory
2079
+ const normalizedPath = path.resolve(params.path);
2080
+ if (!normalizedPath.includes(".testdriver")) {
2081
+ logger.warn("view_local_screenshot: Path not in .testdriver", { path: normalizedPath });
2082
+ return createToolResult(false, "Can only view screenshots from .testdriver directory", { error: "Security: path not allowed" });
2083
+ }
2084
+
2085
+ // Read the file
2086
+ const imageBuffer = fs.readFileSync(params.path);
2087
+ const imageBase64 = imageBuffer.toString("base64");
2088
+
2089
+ // Store image for MCP App UI display
2090
+ const screenshotResourceUri = storeImage(imageBase64, "screenshot");
2091
+
2092
+ const stats = fs.statSync(params.path);
2093
+ const sizeKB = Math.round(stats.size / 1024);
2094
+ const fileName = path.basename(params.path);
2095
+
2096
+ const duration = Date.now() - startTime;
2097
+ logger.info("view_local_screenshot: Completed", { path: params.path, sizeKB, duration });
2098
+
2099
+ // Return the image content for AI clients that support images
2100
+ // The content array includes both text and image for maximum compatibility
2101
+ const content: CallToolResult["content"] = [
2102
+ { type: "text", text: `Screenshot: ${fileName} (${sizeKB}KB)` },
2103
+ {
2104
+ type: "image",
2105
+ data: imageBase64,
2106
+ mimeType: "image/png"
2107
+ },
2108
+ ];
2109
+
2110
+ return {
2111
+ content,
2112
+ structuredContent: {
2113
+ action: "view_local_screenshot",
2114
+ success: true,
2115
+ path: params.path,
2116
+ fileName,
2117
+ sizeBytes: stats.size,
2118
+ modified: stats.mtime.toISOString(),
2119
+ screenshotResourceUri,
2120
+ duration
2121
+ },
2122
+ };
2123
+ } catch (error) {
2124
+ logger.error("view_local_screenshot: Failed", { error: String(error), path: params.path });
2125
+ captureException(error as Error, { tags: { tool: "view_local_screenshot" }, extra: { path: params.path } });
2126
+ throw error;
2127
+ }
2128
+ }
2129
+ );
2130
+
2131
+ // Screenshot - captures full screen to show user the current state
2132
+ // NOTE: This is for SHOWING the user the screen, not for AI understanding.
2133
+ // Use 'check' tool for AI to understand screen state.
2134
+ registerAppTool(
2135
+ server,
2136
+ "screenshot",
2137
+ {
2138
+ title: "Screenshot",
2139
+ description: `Display a screenshot to the user. This tool does NOT return the image to you (the AI).
2140
+
2141
+ ⚠️ IMPORTANT: Do NOT use this tool to understand the screen state. The screenshot is ONLY displayed to the human user - you will NOT receive the image or any analysis.
2142
+
2143
+ If you need to:
2144
+ - See what's on screen → use 'check' instead
2145
+ - Verify an action worked → use 'check' instead
2146
+ - Understand the current state → use 'check' instead
2147
+
2148
+ Only use 'screenshot' when you explicitly want to show something to the human user without needing to see it yourself.`,
2149
+ inputSchema: z.object({}),
2150
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
2151
+ },
2152
+ async (): Promise<CallToolResult> => {
2153
+ const startTime = Date.now();
2154
+ logger.info("screenshot: Starting");
2155
+
2156
+ const sessionCheck = requireActiveSession();
2157
+ if (!sessionCheck.valid) {
2158
+ logger.warn("screenshot: No active session");
2159
+ return sessionCheck.error;
2160
+ }
2161
+
2162
+ try {
2163
+ // Capture full screen screenshot
2164
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
2165
+
2166
+ let screenshotResourceUri: string | undefined;
2167
+ if (screenshotBase64) {
2168
+ // Store raw base64 for the resource blob with unique ID
2169
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
2170
+ }
2171
+
2172
+ const duration = Date.now() - startTime;
2173
+ logger.info("screenshot: Completed", { duration, hasImage: !!screenshotBase64 });
2174
+
2175
+ // Only send the resource URI - the MCP app will fetch the image via resources/read
2176
+ // This keeps the base64 image data OUT of AI context
2177
+ return createToolResult(
2178
+ true,
2179
+ "Screenshot captured and displayed to user",
2180
+ {
2181
+ action: "screenshot",
2182
+ screenshotResourceUri,
2183
+ duration
2184
+ }
2185
+ );
2186
+ } catch (error) {
2187
+ logger.error("screenshot: Failed", { error: String(error) });
2188
+ return createToolResult(false, `Screenshot failed: ${error}`, { error: String(error) });
2189
+ }
2190
+ }
2191
+ );
2192
+
2193
+ // Init - Initialize a new TestDriver project
2194
+ server.registerTool(
2195
+ "init",
2196
+ {
2197
+ description: `Initialize a new TestDriver project with Vitest SDK examples.
2198
+
2199
+ This creates:
2200
+ - package.json with proper dependencies
2201
+ - Example test files (tests/example.test.js, tests/login.js)
2202
+ - vitest.config.js
2203
+ - .gitignore
2204
+ - GitHub Actions workflow (.github/workflows/testdriver.yml)
2205
+ - VSCode MCP config (.vscode/mcp.json)
2206
+ - VSCode extensions recommendations (.vscode/extensions.json)
2207
+ - TestDriver skills (.github/skills/)
2208
+ - TestDriver agents (.github/agents/)
2209
+ - .env file with API key (if provided)
2210
+
2211
+ API Key: The apiKey parameter is optional. If not provided, you'll need to manually add TD_API_KEY to the .env file after initialization. The project structure will still be created successfully.`,
2212
+ inputSchema: z.object({
2213
+ directory: z.string().optional().describe("Target directory (defaults to current working directory)"),
2214
+ apiKey: z.string().optional().describe("TestDriver API key (will be saved to .env)"),
2215
+ skipInstall: z.boolean().default(false).describe("Skip npm install step"),
2216
+ }),
2217
+ },
2218
+ async (params): Promise<CallToolResult> => {
2219
+ const startTime = Date.now();
2220
+ const targetDir = params.directory ? path.resolve(params.directory) : process.cwd();
2221
+
2222
+ logger.info("init: Starting", { targetDir, hasApiKey: !!params.apiKey, skipInstall: params.skipInstall });
2223
+
2224
+ try {
2225
+ // Import the shared init logic (dynamic import for ESM/CJS compatibility)
2226
+ const initProjectPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "lib", "init-project.js");
2227
+ const { initProject } = await import(pathToFileURL(initProjectPath).href);
2228
+
2229
+ // Run the shared init logic
2230
+ const result = await initProject({
2231
+ targetDir,
2232
+ apiKey: params.apiKey,
2233
+ skipInstall: params.skipInstall,
2234
+ });
2235
+
2236
+ const duration = Date.now() - startTime;
2237
+ logger.info("init: Completed", { targetDir, duration, success: result.success });
2238
+
2239
+ const nextSteps = `
2240
+
2241
+ 📚 Next steps:
2242
+
2243
+ 1. Run your tests:
2244
+ vitest run
2245
+
2246
+ 2. Use AI agents to write tests:
2247
+ Open VSCode/Cursor and use @testdriver agent
2248
+
2249
+ 3. MCP server configured:
2250
+ TestDriver tools available via MCP in .vscode/mcp.json
2251
+
2252
+ 4. For CI/CD, add TD_API_KEY to your GitHub repository secrets:
2253
+ Settings → Secrets → Actions → New repository secret
2254
+
2255
+ Learn more at https://docs.testdriver.ai/v7/getting-started/
2256
+ `;
2257
+
2258
+ const allMessages = [...result.results, ...result.errors.map((e: string) => `⚠️ ${e}`)];
2259
+
2260
+ return createToolResult(
2261
+ result.success,
2262
+ result.success
2263
+ ? `✅ TestDriver project initialized successfully!\n\n${allMessages.join("\n")}${nextSteps}`
2264
+ : `⚠️ TestDriver project initialization completed with errors:\n\n${allMessages.join("\n")}`,
2265
+ {
2266
+ action: "init",
2267
+ targetDir,
2268
+ filesCreated: result.results.length,
2269
+ hasApiKey: !!params.apiKey,
2270
+ errors: result.errors,
2271
+ duration
2272
+ }
2273
+ );
2274
+ } catch (error) {
2275
+ logger.error("init: Failed", { error: String(error), targetDir });
2276
+ captureException(error as Error, { tags: { tool: "init" }, extra: { targetDir } });
2277
+ throw error;
2278
+ }
2279
+ }
2280
+ );
2281
+
2282
+
2283
+ // Start the server
2284
+ async function main() {
2285
+ logger.info("Starting TestDriver MCP Server", {
2286
+ version,
2287
+ logLevel: process.env.TD_LOG_LEVEL || "INFO",
2288
+ distDir: DIST_DIR,
2289
+ sentryEnabled: isSentryEnabled(),
2290
+ });
2291
+
2292
+ const transport = new StdioServerTransport();
2293
+ await server.connect(transport);
2294
+
2295
+ logger.info("TestDriver MCP Server running on stdio");
2296
+
2297
+ // Handle graceful shutdown
2298
+ const shutdown = async () => {
2299
+ logger.info("Shutting down MCP Server");
2300
+ await flushSentry();
2301
+ process.exit(0);
2302
+ };
2303
+
2304
+ process.on("SIGINT", shutdown);
2305
+ process.on("SIGTERM", shutdown);
2306
+ }
2307
+
2308
+ main().catch(async (error) => {
2309
+ logger.error("Server failed to start", { error: String(error) });
2310
+ captureException(error as Error, { tags: { phase: "startup" } });
2311
+ await flushSentry();
2312
+ process.exit(1);
2313
+ });