@deriv-com/fe-mcp-servers 0.0.11 → 0.0.13

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.
@@ -18492,7 +18492,17 @@ var StdioServerTransport = class {
18492
18492
  };
18493
18493
 
18494
18494
  // maestro-ai/src/mcp.js
18495
- import { execSync } from "child_process";
18495
+ import { execSync, spawn } from "child_process";
18496
+ import {
18497
+ writeFileSync,
18498
+ existsSync,
18499
+ mkdirSync,
18500
+ readFileSync,
18501
+ openSync,
18502
+ closeSync,
18503
+ fsyncSync
18504
+ } from "fs";
18505
+ import { join, dirname } from "path";
18496
18506
  var FLOW_TYPES = {
18497
18507
  auth: {
18498
18508
  description: "Login, signup, or password reset flows",
@@ -18520,6 +18530,156 @@ var FLOW_TYPES = {
18520
18530
  requiresOnboarding: false
18521
18531
  }
18522
18532
  };
18533
+ function isMaestroInstalled() {
18534
+ try {
18535
+ execSync("which maestro", { encoding: "utf-8", stdio: "pipe" });
18536
+ return true;
18537
+ } catch {
18538
+ return false;
18539
+ }
18540
+ }
18541
+ function installMaestro() {
18542
+ const installCmd = 'curl -fsSL "https://get.maestro.mobile.dev" | bash';
18543
+ try {
18544
+ try {
18545
+ execSync(installCmd, {
18546
+ encoding: "utf-8",
18547
+ stdio: "pipe",
18548
+ shell: "/bin/bash",
18549
+ timeout: 12e4
18550
+ // 2 minute timeout for install
18551
+ });
18552
+ } catch (curlError) {
18553
+ const curlOutput = curlError.stderr || curlError.stdout || curlError.message;
18554
+ throw new Error(`Installation failed: ${curlOutput}`);
18555
+ }
18556
+ if (isMaestroInstalled()) {
18557
+ return {
18558
+ success: true,
18559
+ message: "\u2705 Maestro CLI installed successfully!",
18560
+ method: "curl"
18561
+ };
18562
+ }
18563
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
18564
+ const maestroBin = join(homeDir, ".maestro", "bin");
18565
+ process.env.PATH = `${maestroBin}:${process.env.PATH}`;
18566
+ try {
18567
+ execSync(`${join(maestroBin, "maestro")} --version`, {
18568
+ encoding: "utf-8",
18569
+ stdio: "pipe"
18570
+ });
18571
+ return {
18572
+ success: true,
18573
+ message: `\u2705 Maestro CLI installed successfully! Added ${maestroBin} to PATH.`,
18574
+ method: "curl"
18575
+ };
18576
+ } catch {
18577
+ return {
18578
+ success: false,
18579
+ message: `\u26A0\uFE0F Maestro installed but not in PATH. Add ${maestroBin} to your PATH and restart your terminal.`,
18580
+ method: "curl"
18581
+ };
18582
+ }
18583
+ } catch (error2) {
18584
+ return {
18585
+ success: false,
18586
+ message: `\u274C Failed to install Maestro: ${error2.message}
18587
+
18588
+ Manual install:
18589
+ ${installCmd}`,
18590
+ method: "curl"
18591
+ };
18592
+ }
18593
+ }
18594
+ function ensureMaestroAvailable() {
18595
+ if (isMaestroInstalled()) {
18596
+ return {
18597
+ available: true,
18598
+ message: "Maestro CLI is available",
18599
+ installed: false
18600
+ };
18601
+ }
18602
+ const installResult = installMaestro();
18603
+ return {
18604
+ available: installResult.success,
18605
+ message: installResult.message,
18606
+ installed: installResult.success
18607
+ };
18608
+ }
18609
+ function getMaestroVersion() {
18610
+ try {
18611
+ const version2 = execSync("maestro --version", {
18612
+ encoding: "utf-8",
18613
+ stdio: "pipe"
18614
+ }).trim();
18615
+ return version2;
18616
+ } catch {
18617
+ return null;
18618
+ }
18619
+ }
18620
+ function ensureMaestroInstalled(options = {}) {
18621
+ const { autoInstall = true, forceReinstall = false } = options;
18622
+ const result = {
18623
+ installed: false,
18624
+ version: null,
18625
+ wasInstalled: false,
18626
+ message: "",
18627
+ details: {
18628
+ installMethod: null,
18629
+ path: null
18630
+ }
18631
+ };
18632
+ const alreadyInstalled = isMaestroInstalled();
18633
+ const currentVersion = alreadyInstalled ? getMaestroVersion() : null;
18634
+ if (alreadyInstalled && !forceReinstall) {
18635
+ result.installed = true;
18636
+ result.version = currentVersion;
18637
+ result.wasInstalled = false;
18638
+ result.message = `\u2705 Maestro CLI is already installed (${currentVersion || "version unknown"})`;
18639
+ try {
18640
+ const path = execSync("which maestro", {
18641
+ encoding: "utf-8",
18642
+ stdio: "pipe"
18643
+ }).trim();
18644
+ result.details.path = path;
18645
+ } catch {
18646
+ }
18647
+ return result;
18648
+ }
18649
+ if (!autoInstall) {
18650
+ result.installed = false;
18651
+ result.version = null;
18652
+ result.wasInstalled = false;
18653
+ result.message = `\u274C Maestro CLI is not installed. Set autoInstall: true to install automatically.`;
18654
+ result.details.installCommand = 'curl -fsSL "https://get.maestro.mobile.dev" | bash';
18655
+ return result;
18656
+ }
18657
+ const installResult = installMaestro();
18658
+ if (installResult.success) {
18659
+ const newVersion = getMaestroVersion();
18660
+ result.installed = true;
18661
+ result.version = newVersion;
18662
+ result.wasInstalled = true;
18663
+ result.message = installResult.message;
18664
+ result.details.installMethod = installResult.method;
18665
+ try {
18666
+ const path = execSync("which maestro", {
18667
+ encoding: "utf-8",
18668
+ stdio: "pipe"
18669
+ }).trim();
18670
+ result.details.path = path;
18671
+ } catch {
18672
+ }
18673
+ } else {
18674
+ result.installed = false;
18675
+ result.version = null;
18676
+ result.wasInstalled = false;
18677
+ result.message = installResult.message;
18678
+ result.details.installMethod = installResult.method;
18679
+ result.details.installCommand = 'curl -fsSL "https://get.maestro.mobile.dev" | bash';
18680
+ }
18681
+ return result;
18682
+ }
18523
18683
  function getMaestroCheatSheet() {
18524
18684
  const commands = [
18525
18685
  {
@@ -19289,138 +19449,1382 @@ ${analysis.shouldCreateTest ? `1. Review the changed UI files above
19289
19449
  }
19290
19450
  };
19291
19451
  }
19292
-
19293
- // maestro-ai/src/mcp-server.js
19294
- var transport = new StdioServerTransport();
19295
- var server = new Server(
19296
- {
19297
- name: "Maestro AI MCP Server",
19298
- version: "0.0.1"
19299
- },
19300
- {
19301
- capabilities: {
19302
- tools: {}
19452
+ function writeTestFile(options = {}) {
19453
+ const {
19454
+ yaml,
19455
+ fileName,
19456
+ directory = "maestro",
19457
+ basePath = process.cwd(),
19458
+ execute = true,
19459
+ deviceId = null,
19460
+ env = {},
19461
+ feature = "",
19462
+ action = "",
19463
+ flowType = null,
19464
+ changedElements = [],
19465
+ existingTests = []
19466
+ } = options;
19467
+ const generation = generateMaestroTest({
19468
+ feature,
19469
+ action,
19470
+ flowType,
19471
+ changedElements,
19472
+ existingTests
19473
+ });
19474
+ if (!yaml || !fileName) {
19475
+ return {
19476
+ success: false,
19477
+ filePath: null,
19478
+ message: "\u274C Missing required parameters: yaml and fileName are required"
19479
+ };
19480
+ }
19481
+ const normalizedFileName = fileName.endsWith(".yaml") ? fileName : `${fileName}.yaml`;
19482
+ const targetDir = join(basePath, directory);
19483
+ const filePath = join(targetDir, normalizedFileName);
19484
+ try {
19485
+ if (!existsSync(targetDir)) {
19486
+ mkdirSync(targetDir, { recursive: true });
19487
+ }
19488
+ writeFileSync(filePath, yaml, "utf-8");
19489
+ const fd = openSync(filePath, "r");
19490
+ fsyncSync(fd);
19491
+ closeSync(fd);
19492
+ const result = {
19493
+ success: true,
19494
+ filePath,
19495
+ message: `\u2705 Test file written successfully to: ${filePath}`,
19496
+ generation
19497
+ // Include test generation guidelines and instructions
19498
+ };
19499
+ if (execute) {
19500
+ const sleep = (ms) => {
19501
+ const end = Date.now() + ms;
19502
+ while (Date.now() < end) {
19503
+ }
19504
+ };
19505
+ sleep(500);
19506
+ const execution = executeTest({
19507
+ flowFile: filePath,
19508
+ deviceId,
19509
+ env
19510
+ });
19511
+ result.execution = execution;
19512
+ result.message += execution.success ? "\n\u2705 Test executed successfully!" : "\n\u274C Test execution failed.";
19303
19513
  }
19514
+ return result;
19515
+ } catch (error2) {
19516
+ return {
19517
+ success: false,
19518
+ filePath: null,
19519
+ message: `\u274C Failed to write test file: ${error2.message}`
19520
+ };
19304
19521
  }
19305
- );
19306
- server.setRequestHandler(ListToolsRequestSchema, async () => {
19307
- return {
19308
- tools: [
19309
- {
19310
- name: "maestro_generate_test",
19311
- description: `Generates comprehensive Maestro test instructions for web applications.
19312
-
19313
- ## Purpose
19314
- Creates test planning instructions with guidelines, checklists, and best practices.
19522
+ }
19523
+ function executeTest(options = {}) {
19524
+ const { flowFile, deviceId = null, env = {}, timeout = 12e4 } = options;
19525
+ if (!flowFile) {
19526
+ return {
19527
+ success: false,
19528
+ output: "\u274C Missing required parameter: flowFile",
19529
+ exitCode: 1,
19530
+ duration: 0
19531
+ };
19532
+ }
19533
+ const maestroCheck = ensureMaestroAvailable();
19534
+ if (!maestroCheck.available) {
19535
+ return {
19536
+ success: false,
19537
+ output: maestroCheck.message,
19538
+ exitCode: 1,
19539
+ duration: 0,
19540
+ maestroInstalled: false
19541
+ };
19542
+ }
19543
+ const args = ["test"];
19544
+ if (deviceId) {
19545
+ args.push("--device", deviceId);
19546
+ }
19547
+ Object.entries(env).forEach(([key, value]) => {
19548
+ args.push("-e", `${key}=${value}`);
19549
+ });
19550
+ args.push(flowFile);
19551
+ const command = `maestro ${args.join(" ")}`;
19552
+ const startTime = Date.now();
19553
+ try {
19554
+ const output = execSync(command, {
19555
+ encoding: "utf-8",
19556
+ timeout,
19557
+ maxBuffer: 10 * 1024 * 1024,
19558
+ env: { ...process.env, ...env }
19559
+ });
19560
+ const duration3 = Date.now() - startTime;
19561
+ return {
19562
+ success: true,
19563
+ output: `${maestroCheck.installed ? "\u{1F4E6} " + maestroCheck.message + "\n\n" : ""}\u2705 Test passed!
19315
19564
 
19316
- ## CRITICAL Rules
19317
- 1. Only test CHANGED functionality (from git diff)
19318
- 2. Never test existing/unchanged UI elements
19319
- 3. Use extendedWaitUntil (not fixed timeouts)
19320
- 4. Text selectors first, id fallback
19321
- 5. Reuse existing flows via runFlow
19565
+ Command: ${command}
19322
19566
 
19323
- ## Flow Types (Optional)
19324
- If flowType is provided, includes a starter template:
19325
- - auth: Login, signup flows
19326
- - sidebar: Sidebar panel interactions
19327
- - form: Form fill and submit
19328
- - modal: Modal/dialog interactions
19329
- - navigation: Page-to-page navigation
19330
- - extended: Build on existing flow
19567
+ ${output}`,
19568
+ exitCode: 0,
19569
+ duration: duration3,
19570
+ command,
19571
+ maestroInstalled: maestroCheck.installed
19572
+ };
19573
+ } catch (error2) {
19574
+ const duration3 = Date.now() - startTime;
19575
+ const output = error2.stdout || error2.stderr || error2.message;
19576
+ return {
19577
+ success: false,
19578
+ output: `${maestroCheck.installed ? "\u{1F4E6} " + maestroCheck.message + "\n\n" : ""}\u274C Test failed!
19331
19579
 
19332
- If flowType is omitted, provides guidance to use other tools (cheat_sheet, pattern, ui_inspection).
19580
+ Command: ${command}
19333
19581
 
19334
- ## Output
19335
- Returns guidelines checklist, suggested filename, and optionally a template.`,
19336
- inputSchema: {
19337
- type: "object",
19338
- properties: {
19339
- feature: {
19340
- type: "string",
19341
- description: 'The feature being tested (e.g., "User Settings", "Login")'
19342
- },
19343
- action: {
19344
- type: "string",
19345
- description: 'The action being tested (e.g., "Reset defaults", "Submit form")'
19346
- },
19347
- flowType: {
19348
- type: "string",
19349
- enum: [
19350
- "auth",
19351
- "sidebar",
19352
- "form",
19353
- "modal",
19354
- "navigation",
19355
- "extended"
19356
- ],
19357
- description: "Type of flow template (optional - omit for template-free generation)"
19358
- },
19359
- changedElements: {
19360
- type: "array",
19361
- items: { type: "string" },
19362
- description: "List of UI elements that were changed (from git diff)"
19363
- },
19364
- existingTests: {
19365
- type: "array",
19366
- items: { type: "string" },
19367
- description: "List of existing related test files to potentially extend"
19368
- }
19369
- },
19370
- required: ["feature", "action"]
19371
- }
19372
- },
19373
- {
19374
- name: "maestro_cheat_sheet",
19375
- description: `Returns Maestro commands quick reference.
19582
+ ${output}`,
19583
+ exitCode: error2.status || 1,
19584
+ duration: duration3,
19585
+ command,
19586
+ maestroInstalled: maestroCheck.installed
19587
+ };
19588
+ }
19589
+ }
19590
+ function executeTestsSequentially(options = {}) {
19591
+ const {
19592
+ flowFiles = [],
19593
+ deviceId = null,
19594
+ env = {},
19595
+ stopOnFailure = false
19596
+ } = options;
19597
+ if (!flowFiles || flowFiles.length === 0) {
19598
+ return {
19599
+ success: false,
19600
+ results: [],
19601
+ summary: {
19602
+ total: 0,
19603
+ passed: 0,
19604
+ failed: 0,
19605
+ skipped: 0,
19606
+ totalDuration: 0
19607
+ }
19608
+ };
19609
+ }
19610
+ const results = [];
19611
+ let stopped = false;
19612
+ for (const flowFile of flowFiles) {
19613
+ if (stopped) {
19614
+ results.push({
19615
+ flowFile,
19616
+ success: false,
19617
+ output: "\u23ED\uFE0F Skipped due to previous failure",
19618
+ exitCode: -1,
19619
+ duration: 0,
19620
+ skipped: true
19621
+ });
19622
+ continue;
19623
+ }
19624
+ const result = executeTest({ flowFile, deviceId, env });
19625
+ results.push({
19626
+ flowFile,
19627
+ ...result,
19628
+ skipped: false
19629
+ });
19630
+ if (!result.success && stopOnFailure) {
19631
+ stopped = true;
19632
+ }
19633
+ }
19634
+ const passed = results.filter((r) => r.success).length;
19635
+ const failed = results.filter((r) => !r.success && !r.skipped).length;
19636
+ const skipped = results.filter((r) => r.skipped).length;
19637
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
19638
+ return {
19639
+ success: failed === 0,
19640
+ results,
19641
+ summary: {
19642
+ total: flowFiles.length,
19643
+ passed,
19644
+ failed,
19645
+ skipped,
19646
+ totalDuration,
19647
+ report: `
19648
+ ## \u{1F4CA} Test Execution Summary
19376
19649
 
19377
- ## Includes
19378
- - All core commands (launchApp, tapOn, inputText, etc.)
19379
- - Selector strategy (text \u2192 id \u2192 index \u2192 repeat)
19380
- - Waiting strategy (extendedWaitUntil)
19381
- - File naming conventions
19382
- - Environment variable setup
19650
+ | Metric | Value |
19651
+ |--------|-------|
19652
+ | Total Tests | ${flowFiles.length} |
19653
+ | \u2705 Passed | ${passed} |
19654
+ | \u274C Failed | ${failed} |
19655
+ | \u23ED\uFE0F Skipped | ${skipped} |
19656
+ | \u23F1\uFE0F Duration | ${(totalDuration / 1e3).toFixed(2)}s |
19383
19657
 
19384
- Use this before writing any Maestro YAML to ensure correct syntax.`,
19385
- inputSchema: {
19386
- type: "object",
19387
- properties: {},
19388
- required: []
19658
+ ### Results by Flow:
19659
+ ${results.map(
19660
+ (r) => `- ${r.success ? "\u2705" : r.skipped ? "\u23ED\uFE0F" : "\u274C"} \`${r.flowFile}\` (${(r.duration / 1e3).toFixed(2)}s)`
19661
+ ).join("\n")}
19662
+ `
19663
+ }
19664
+ };
19665
+ }
19666
+ function discoverTestFiles(options = {}) {
19667
+ const { directory = "maestro", basePath = process.cwd() } = options;
19668
+ const targetDir = join(basePath, directory);
19669
+ if (!existsSync(targetDir)) {
19670
+ return {
19671
+ files: [],
19672
+ count: 0,
19673
+ message: `\u274C Directory not found: ${targetDir}`
19674
+ };
19675
+ }
19676
+ try {
19677
+ const result = execSync(`find "${targetDir}" -name "*.yaml" -type f`, {
19678
+ encoding: "utf-8"
19679
+ });
19680
+ const files = result.trim().split("\n").filter(Boolean);
19681
+ return {
19682
+ files,
19683
+ count: files.length,
19684
+ message: `Found ${files.length} test file(s) in ${targetDir}`
19685
+ };
19686
+ } catch (error2) {
19687
+ return {
19688
+ files: [],
19689
+ count: 0,
19690
+ message: `\u274C Error discovering files: ${error2.message}`
19691
+ };
19692
+ }
19693
+ }
19694
+ function runAllTests(options = {}) {
19695
+ const {
19696
+ directory = "maestro",
19697
+ basePath = process.cwd(),
19698
+ deviceId = null,
19699
+ env = {},
19700
+ stopOnFailure = false,
19701
+ files = null
19702
+ } = options;
19703
+ let discovery;
19704
+ let filesToRun;
19705
+ if (files && Array.isArray(files) && files.length > 0) {
19706
+ filesToRun = files.map((f) => {
19707
+ if (f.startsWith("/")) {
19708
+ return f;
19709
+ }
19710
+ return join(basePath, f);
19711
+ });
19712
+ discovery = {
19713
+ files: filesToRun,
19714
+ count: filesToRun.length,
19715
+ message: `Using ${filesToRun.length} specified test file(s)`,
19716
+ mode: "specified"
19717
+ };
19718
+ } else {
19719
+ discovery = discoverTestFiles({ directory, basePath });
19720
+ discovery.mode = "discovered";
19721
+ filesToRun = discovery.files;
19722
+ }
19723
+ if (!filesToRun || filesToRun.length === 0) {
19724
+ return {
19725
+ discovery,
19726
+ execution: {
19727
+ success: false,
19728
+ results: [],
19729
+ summary: {
19730
+ total: 0,
19731
+ passed: 0,
19732
+ failed: 0,
19733
+ skipped: 0,
19734
+ totalDuration: 0,
19735
+ report: "No test files found to execute."
19389
19736
  }
19390
- },
19391
- {
19392
- name: "maestro_flow_template",
19393
- description: `Returns a specific Maestro flow template by type.
19394
-
19395
- ## Available Types
19396
- - auth: Login/signup with onboarding dismissal
19397
- - sidebar: Sidebar panel interactions
19398
- - form: Form fill and submit
19399
- - modal: Modal/dialog interactions
19400
- - navigation: Page-to-page navigation
19401
- - extended: Build on existing flow with runFlow
19402
-
19403
- ## Customization Options
19404
- - name: Flow name
19405
- - baseUrl: Override default localhost:8443
19406
- - waitForElement: Initial element to wait for
19407
- - baseFlow: For extended type, the base flow to extend`,
19408
- inputSchema: {
19409
- type: "object",
19410
- properties: {
19411
- flowType: {
19412
- type: "string",
19413
- enum: [
19414
- "auth",
19415
- "sidebar",
19416
- "form",
19417
- "modal",
19418
- "navigation",
19419
- "extended"
19420
- ],
19421
- description: "Type of flow template to return"
19422
- },
19423
- name: {
19737
+ }
19738
+ };
19739
+ }
19740
+ const execution = executeTestsSequentially({
19741
+ flowFiles: filesToRun,
19742
+ deviceId,
19743
+ env,
19744
+ stopOnFailure
19745
+ });
19746
+ return {
19747
+ discovery,
19748
+ execution
19749
+ };
19750
+ }
19751
+ function extractTaskIdFromUrl(url2) {
19752
+ if (!url2 || typeof url2 !== "string") {
19753
+ return {
19754
+ success: false,
19755
+ taskId: null,
19756
+ message: "No URL provided"
19757
+ };
19758
+ }
19759
+ const trimmedUrl = url2.trim();
19760
+ const directPattern = /app\.clickup\.com\/t\/([a-zA-Z0-9]+)/;
19761
+ const directMatch = trimmedUrl.match(directPattern);
19762
+ if (directMatch) {
19763
+ return {
19764
+ success: true,
19765
+ taskId: directMatch[1],
19766
+ message: `Extracted task ID: ${directMatch[1]}`
19767
+ };
19768
+ }
19769
+ const queryPattern = /[?&]p=([a-zA-Z0-9]+)/;
19770
+ const queryMatch = trimmedUrl.match(queryPattern);
19771
+ if (queryMatch) {
19772
+ return {
19773
+ success: true,
19774
+ taskId: queryMatch[1],
19775
+ message: `Extracted task ID from query: ${queryMatch[1]}`
19776
+ };
19777
+ }
19778
+ const listViewPattern = /\/li\/\d+\/([a-zA-Z0-9]+)/;
19779
+ const listViewMatch = trimmedUrl.match(listViewPattern);
19780
+ if (listViewMatch) {
19781
+ return {
19782
+ success: true,
19783
+ taskId: listViewMatch[1],
19784
+ message: `Extracted task ID from list view: ${listViewMatch[1]}`
19785
+ };
19786
+ }
19787
+ const taskIdPattern = /^[a-zA-Z0-9]{6,12}$/;
19788
+ if (taskIdPattern.test(trimmedUrl)) {
19789
+ return {
19790
+ success: true,
19791
+ taskId: trimmedUrl,
19792
+ message: `Using provided value as task ID: ${trimmedUrl}`
19793
+ };
19794
+ }
19795
+ return {
19796
+ success: false,
19797
+ taskId: null,
19798
+ message: `Could not extract task ID from URL: ${trimmedUrl}`
19799
+ };
19800
+ }
19801
+ async function fetchClickUpTask(options = {}) {
19802
+ const { taskUrl = null } = options;
19803
+ const apiToken = process.env.CLICKUP_API_TOKEN;
19804
+ if (!taskUrl) {
19805
+ return {
19806
+ success: false,
19807
+ skipped: false,
19808
+ needsInput: true,
19809
+ prompt: "Do you have a ClickUp task URL for this feature? Paste the URL to generate tests based on acceptance criteria, or type 'skip' to continue with git-based analysis only.",
19810
+ message: "No ClickUp task URL provided. Provide a URL or skip to continue."
19811
+ };
19812
+ }
19813
+ const skipCommands = ["skip", "no", "none", "n", ""];
19814
+ if (skipCommands.includes(taskUrl.toLowerCase().trim())) {
19815
+ return {
19816
+ success: true,
19817
+ skipped: true,
19818
+ needsInput: false,
19819
+ message: "Skipping ClickUp integration. Continuing with git-based analysis.",
19820
+ task: null
19821
+ };
19822
+ }
19823
+ const extraction = extractTaskIdFromUrl(taskUrl);
19824
+ if (!extraction.success) {
19825
+ return {
19826
+ success: false,
19827
+ skipped: false,
19828
+ needsInput: false,
19829
+ message: extraction.message,
19830
+ error: "Invalid ClickUp URL format"
19831
+ };
19832
+ }
19833
+ const taskId = extraction.taskId;
19834
+ if (!apiToken) {
19835
+ return {
19836
+ success: false,
19837
+ skipped: false,
19838
+ needsInput: false,
19839
+ message: "\u274C CLICKUP_API_TOKEN environment variable is not set. Please configure it in your MCP settings.",
19840
+ error: "Missing API token",
19841
+ taskId
19842
+ };
19843
+ }
19844
+ try {
19845
+ const response = await fetch(
19846
+ `https://api.clickup.com/api/v2/task/${taskId}`,
19847
+ {
19848
+ method: "GET",
19849
+ headers: {
19850
+ Authorization: apiToken,
19851
+ "Content-Type": "application/json"
19852
+ }
19853
+ }
19854
+ );
19855
+ if (!response.ok) {
19856
+ const errorText = await response.text();
19857
+ return {
19858
+ success: false,
19859
+ skipped: false,
19860
+ needsInput: false,
19861
+ message: `\u274C ClickUp API error (${response.status}): ${errorText}`,
19862
+ error: `API returned ${response.status}`,
19863
+ taskId
19864
+ };
19865
+ }
19866
+ const task = await response.json();
19867
+ return {
19868
+ success: true,
19869
+ skipped: false,
19870
+ needsInput: false,
19871
+ message: `\u2705 Successfully fetched task: ${task.name}`,
19872
+ task,
19873
+ taskId,
19874
+ taskName: task.name,
19875
+ taskStatus: task.status?.status,
19876
+ taskUrl: task.url
19877
+ };
19878
+ } catch (error2) {
19879
+ return {
19880
+ success: false,
19881
+ skipped: false,
19882
+ needsInput: false,
19883
+ message: `\u274C Failed to fetch ClickUp task: ${error2.message}`,
19884
+ error: error2.message,
19885
+ taskId
19886
+ };
19887
+ }
19888
+ }
19889
+ function parseAcceptanceCriteria(options = {}) {
19890
+ const { task, acSource = null } = options;
19891
+ if (!task) {
19892
+ return {
19893
+ success: false,
19894
+ acItems: [],
19895
+ source: null,
19896
+ message: "No task provided for parsing acceptance criteria"
19897
+ };
19898
+ }
19899
+ const sourceConfig = acSource || process.env.CLICKUP_AC_SOURCE || "checklist:Acceptance Criteria";
19900
+ const acItems = [];
19901
+ const sources = sourceConfig.split(",").map((s) => s.trim());
19902
+ const usedSources = [];
19903
+ for (const source of sources) {
19904
+ const [sourceType, sourceName] = source.includes(":") ? source.split(":", 2) : [source, null];
19905
+ switch (sourceType.toLowerCase()) {
19906
+ case "checklist": {
19907
+ const checklists = task.checklists || [];
19908
+ for (const checklist of checklists) {
19909
+ if (!sourceName || checklist.name.toLowerCase().includes(sourceName.toLowerCase())) {
19910
+ const items = checklist.items || [];
19911
+ for (const item of items) {
19912
+ acItems.push({
19913
+ id: item.id,
19914
+ text: item.name,
19915
+ completed: item.resolved || false,
19916
+ source: `checklist:${checklist.name}`,
19917
+ type: "checklist_item",
19918
+ order: item.orderindex
19919
+ });
19920
+ }
19921
+ usedSources.push(`checklist:${checklist.name}`);
19922
+ }
19923
+ }
19924
+ break;
19925
+ }
19926
+ case "custom_field": {
19927
+ const customFields = task.custom_fields || [];
19928
+ for (const field of customFields) {
19929
+ if (!sourceName || field.name.toLowerCase().includes(sourceName.toLowerCase())) {
19930
+ if (field.value) {
19931
+ if (Array.isArray(field.value)) {
19932
+ field.value.forEach((v, idx) => {
19933
+ acItems.push({
19934
+ id: `${field.id}_${idx}`,
19935
+ text: typeof v === "object" ? v.name || v.value : v,
19936
+ completed: false,
19937
+ source: `custom_field:${field.name}`,
19938
+ type: "custom_field_item",
19939
+ order: idx
19940
+ });
19941
+ });
19942
+ } else if (typeof field.value === "string") {
19943
+ const lines = field.value.split("\n").filter((l) => l.trim());
19944
+ lines.forEach((line, idx) => {
19945
+ const checkboxMatch = line.match(/^[\s]*[-*\[\]xX✓✗]\s*(.*)/);
19946
+ acItems.push({
19947
+ id: `${field.id}_${idx}`,
19948
+ text: checkboxMatch ? checkboxMatch[1].trim() : line.trim(),
19949
+ completed: /^[\s]*[\[x\]✓]/.test(line),
19950
+ source: `custom_field:${field.name}`,
19951
+ type: "custom_field_item",
19952
+ order: idx
19953
+ });
19954
+ });
19955
+ }
19956
+ usedSources.push(`custom_field:${field.name}`);
19957
+ }
19958
+ }
19959
+ }
19960
+ break;
19961
+ }
19962
+ case "description": {
19963
+ let description = task.description || task.text_content || task.content || "";
19964
+ const rawDescription = description;
19965
+ if (description) {
19966
+ description = description.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
19967
+ description = description.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
19968
+ const acSectionPattern = /(?:acceptance\s*criteria|ac|requirements?)[\s:]*\n([\s\S]*?)(?:\n\n|\n#|$)/i;
19969
+ const sectionMatch = description.match(acSectionPattern);
19970
+ const textToParse = sectionMatch ? sectionMatch[1] : description;
19971
+ const lines = textToParse.split("\n").filter((l) => l.trim());
19972
+ lines.forEach((line, idx) => {
19973
+ const itemMatch = line.match(
19974
+ /^[\s]*(?:[-–—*•◦▪▸►‣⁃]|\d+[.)\]:]|\[[\sx✓✗☐☑]?\])\s*(.*)/
19975
+ );
19976
+ if (itemMatch && itemMatch[1] && itemMatch[1].trim().length > 0) {
19977
+ acItems.push({
19978
+ id: `desc_${idx}`,
19979
+ text: itemMatch[1].trim(),
19980
+ completed: /\[[x✓☑]\]/i.test(line),
19981
+ source: "description",
19982
+ type: "description_item",
19983
+ order: idx
19984
+ });
19985
+ }
19986
+ });
19987
+ if (!acItems.some((item) => item.source === "description")) {
19988
+ const headerMatch = description.match(
19989
+ /acceptance\s*criteria\s*:?\s*\n?([\s\S]*)/i
19990
+ );
19991
+ if (headerMatch) {
19992
+ const afterHeader = headerMatch[1].trim();
19993
+ const criteriaLines = afterHeader.split("\n").filter((l) => l.trim());
19994
+ criteriaLines.forEach((line, idx) => {
19995
+ const trimmedLine = line.trim();
19996
+ if (trimmedLine.startsWith("-") || trimmedLine.startsWith("*") || trimmedLine.startsWith("\u2022")) {
19997
+ acItems.push({
19998
+ id: `desc_${idx}`,
19999
+ text: trimmedLine.replace(/^[-*•]\s*/, "").trim(),
20000
+ completed: false,
20001
+ source: "description",
20002
+ type: "description_item",
20003
+ order: idx
20004
+ });
20005
+ } else if (trimmedLine.length > 10 && idx < 10) {
20006
+ acItems.push({
20007
+ id: `desc_fallback_${idx}`,
20008
+ text: trimmedLine,
20009
+ completed: false,
20010
+ source: "description",
20011
+ type: "description_item_fallback",
20012
+ order: idx
20013
+ });
20014
+ }
20015
+ });
20016
+ }
20017
+ }
20018
+ if (acItems.some((item) => item.source === "description")) {
20019
+ usedSources.push("description");
20020
+ }
20021
+ }
20022
+ if (!acItems.some((item) => item.source === "description")) {
20023
+ if (!task._debug) task._debug = {};
20024
+ task._debug.rawDescription = rawDescription?.substring(0, 500);
20025
+ task._debug.processedDescription = description?.substring(0, 500);
20026
+ task._debug.descriptionLength = description?.length || 0;
20027
+ }
20028
+ break;
20029
+ }
20030
+ default:
20031
+ break;
20032
+ }
20033
+ }
20034
+ const result = {
20035
+ success: acItems.length > 0,
20036
+ acItems,
20037
+ sources: usedSources,
20038
+ totalCount: acItems.length,
20039
+ completedCount: acItems.filter((item) => item.completed).length,
20040
+ message: acItems.length > 0 ? `\u2705 Found ${acItems.length} acceptance criteria items from: ${usedSources.join(", ")}` : `\u26A0\uFE0F No acceptance criteria found in sources: ${sourceConfig}`
20041
+ };
20042
+ if (acItems.length === 0 && task._debug) {
20043
+ result.debug = {
20044
+ rawDescriptionPreview: task._debug.rawDescription,
20045
+ processedDescriptionPreview: task._debug.processedDescription,
20046
+ descriptionLength: task._debug.descriptionLength,
20047
+ hint: "Check if the description contains bullet points (-, *, \u2022) or is formatted differently"
20048
+ };
20049
+ }
20050
+ if (acItems.length === 0) {
20051
+ result.taskInfo = {
20052
+ hasDescription: !!(task.description || task.text_content || task.content),
20053
+ hasChecklists: !!(task.checklists && task.checklists.length > 0),
20054
+ checklistNames: task.checklists?.map((c) => c.name) || [],
20055
+ hasCustomFields: !!(task.custom_fields && task.custom_fields.length > 0),
20056
+ customFieldNames: task.custom_fields?.map((f) => f.name) || []
20057
+ };
20058
+ }
20059
+ return result;
20060
+ }
20061
+ function validateAcceptanceCriteria(options = {}) {
20062
+ const { acItems = [] } = options;
20063
+ if (!acItems || acItems.length === 0) {
20064
+ return {
20065
+ success: false,
20066
+ validatedItems: [],
20067
+ testableCount: 0,
20068
+ message: "No acceptance criteria items provided for validation"
20069
+ };
20070
+ }
20071
+ const validatedItems = acItems.map((item) => {
20072
+ const validation = {
20073
+ ...item,
20074
+ isTestable: true,
20075
+ testabilityScore: 0,
20076
+ testabilityReasons: [],
20077
+ suggestedFlowType: null,
20078
+ uiKeywords: []
20079
+ };
20080
+ const text = item.text.toLowerCase();
20081
+ const uiKeywords = {
20082
+ button: ["click", "button", "tap", "press"],
20083
+ form: ["input", "enter", "fill", "type", "form", "field"],
20084
+ navigation: ["navigate", "go to", "open", "redirect", "page", "screen"],
20085
+ visibility: ["see", "display", "show", "visible", "appear"],
20086
+ modal: ["modal", "dialog", "popup", "overlay"],
20087
+ validation: ["error", "valid", "invalid", "required", "message"]
20088
+ };
20089
+ for (const [category, keywords] of Object.entries(uiKeywords)) {
20090
+ for (const keyword of keywords) {
20091
+ if (text.includes(keyword)) {
20092
+ validation.uiKeywords.push(keyword);
20093
+ validation.testabilityScore += 10;
20094
+ validation.testabilityReasons.push(
20095
+ `Contains UI keyword: "${keyword}"`
20096
+ );
20097
+ if (!validation.suggestedFlowType) {
20098
+ if (category === "form") validation.suggestedFlowType = "form";
20099
+ else if (category === "navigation")
20100
+ validation.suggestedFlowType = "navigation";
20101
+ else if (category === "modal")
20102
+ validation.suggestedFlowType = "modal";
20103
+ else if (category === "button" || category === "visibility")
20104
+ validation.suggestedFlowType = "navigation";
20105
+ }
20106
+ }
20107
+ }
20108
+ }
20109
+ const authKeywords = [
20110
+ "login",
20111
+ "logout",
20112
+ "sign in",
20113
+ "sign out",
20114
+ "password",
20115
+ "authenticate"
20116
+ ];
20117
+ if (authKeywords.some((k) => text.includes(k))) {
20118
+ validation.suggestedFlowType = "auth";
20119
+ validation.testabilityScore += 15;
20120
+ validation.testabilityReasons.push("Contains auth-related keyword");
20121
+ }
20122
+ const sidebarKeywords = ["sidebar", "menu", "panel", "drawer"];
20123
+ if (sidebarKeywords.some((k) => text.includes(k))) {
20124
+ validation.suggestedFlowType = "sidebar";
20125
+ validation.testabilityScore += 10;
20126
+ validation.testabilityReasons.push("Contains sidebar-related keyword");
20127
+ }
20128
+ const nonTestablePatterns = [
20129
+ "backend",
20130
+ "api",
20131
+ "database",
20132
+ "performance",
20133
+ "security",
20134
+ "code review",
20135
+ "documentation"
20136
+ ];
20137
+ if (nonTestablePatterns.some((p) => text.includes(p))) {
20138
+ validation.testabilityScore -= 20;
20139
+ validation.testabilityReasons.push("Contains non-UI pattern");
20140
+ }
20141
+ validation.isTestable = validation.testabilityScore > 0;
20142
+ return validation;
20143
+ });
20144
+ const testableItems = validatedItems.filter((item) => item.isTestable);
20145
+ return {
20146
+ success: testableItems.length > 0,
20147
+ validatedItems,
20148
+ testableCount: testableItems.length,
20149
+ nonTestableCount: validatedItems.length - testableItems.length,
20150
+ message: testableItems.length > 0 ? `\u2705 ${testableItems.length}/${validatedItems.length} items are testable via Maestro` : "\u26A0\uFE0F No testable acceptance criteria found",
20151
+ suggestedFlowTypes: [
20152
+ ...new Set(
20153
+ testableItems.map((item) => item.suggestedFlowType).filter(Boolean)
20154
+ )
20155
+ ]
20156
+ };
20157
+ }
20158
+ function extractUITextFromFiles(repoPath, files) {
20159
+ const uiTexts = [];
20160
+ for (const file of files) {
20161
+ try {
20162
+ const filePath = join(repoPath, file);
20163
+ if (!existsSync(filePath)) continue;
20164
+ const content = readFileSync(filePath, "utf-8");
20165
+ const jsxTextPattern = />([^<>{}\n]{2,100})</g;
20166
+ let match;
20167
+ while ((match = jsxTextPattern.exec(content)) !== null) {
20168
+ const text = match[1].trim();
20169
+ if (text && !text.startsWith("{") && !text.includes("className")) {
20170
+ uiTexts.push(text);
20171
+ }
20172
+ }
20173
+ const stringLiteralPattern = /['"`]([^'"`\n]{2,50})['"`]/g;
20174
+ while ((match = stringLiteralPattern.exec(content)) !== null) {
20175
+ const text = match[1].trim();
20176
+ if (text && !text.includes("./") && !text.includes("../") && !text.startsWith("http") && !text.includes("className") && !text.match(/^[a-z_]+$/) && // snake_case identifiers
20177
+ !text.match(/^[a-zA-Z]+\.[a-zA-Z]+/)) {
20178
+ uiTexts.push(text);
20179
+ }
20180
+ }
20181
+ const attrPattern = /(?:label|title|placeholder|value|text)=["'`]([^"'`\n]{2,100})["'`]/gi;
20182
+ while ((match = attrPattern.exec(content)) !== null) {
20183
+ uiTexts.push(match[1].trim());
20184
+ }
20185
+ } catch {
20186
+ }
20187
+ }
20188
+ return [...new Set(uiTexts)];
20189
+ }
20190
+ function mapACToUIElements(options = {}) {
20191
+ const { acItems = [], repoPath = process.cwd(), changedFiles = [] } = options;
20192
+ if (!acItems || acItems.length === 0) {
20193
+ const analysis = analyzeChangesForTest({ repoPath });
20194
+ return {
20195
+ success: true,
20196
+ mode: "git_only",
20197
+ mappings: [],
20198
+ gitAnalysis: analysis.analysis,
20199
+ changedUIFiles: analysis.analysis.changedUIFiles,
20200
+ interactiveElements: analysis.analysis.interactiveElements,
20201
+ suggestedFlowType: analysis.analysis.suggestedFlowType,
20202
+ message: "No AC items provided. Using git-based analysis for test generation.",
20203
+ recommendations: analysis.recommendations
20204
+ };
20205
+ }
20206
+ const gitAnalysis = analyzeChangesForTest({ repoPath });
20207
+ const filesToCheck = changedFiles.length > 0 ? changedFiles : gitAnalysis.analysis.changedUIFiles;
20208
+ const actualUITexts = extractUITextFromFiles(repoPath, filesToCheck);
20209
+ const sourceCode = getSourceCodeContent(repoPath, filesToCheck);
20210
+ const validationErrors = [];
20211
+ const validationWarnings = [];
20212
+ const validationInfo = [];
20213
+ const mappings = acItems.map((item) => {
20214
+ const mapping = {
20215
+ ...item,
20216
+ matchedFiles: [],
20217
+ matchedElements: [],
20218
+ matchedUITexts: [],
20219
+ relatedUIElements: [],
20220
+ confidence: 0,
20221
+ validated: false,
20222
+ validationStatus: "pending",
20223
+ validationMessage: "",
20224
+ intent: extractIntent(item.text)
20225
+ };
20226
+ const acText = item.text;
20227
+ const intent = mapping.intent;
20228
+ const intentMatch = validateIntent(
20229
+ intent,
20230
+ sourceCode,
20231
+ actualUITexts,
20232
+ gitAnalysis
20233
+ );
20234
+ if (intentMatch.found) {
20235
+ mapping.validated = true;
20236
+ mapping.confidence = intentMatch.confidence;
20237
+ mapping.validationStatus = "passed";
20238
+ mapping.validationMessage = intentMatch.message;
20239
+ mapping.matchedUITexts.push(...intentMatch.matches);
20240
+ validationInfo.push({
20241
+ acItem: acText,
20242
+ intent: intent.action,
20243
+ message: `\u2705 Intent validated: ${intentMatch.message}`
20244
+ });
20245
+ } else {
20246
+ const keywordMatch = findRelatedUIElements(
20247
+ acText,
20248
+ actualUITexts,
20249
+ sourceCode
20250
+ );
20251
+ const specificValueCheck = validateSpecificValues(
20252
+ acText,
20253
+ keywordMatch.elements
20254
+ );
20255
+ if (specificValueCheck.hasMismatch) {
20256
+ mapping.validated = false;
20257
+ mapping.validationStatus = "failed";
20258
+ mapping.confidence = 0;
20259
+ mapping.validationMessage = specificValueCheck.message;
20260
+ mapping.relatedUIElements = keywordMatch.elements;
20261
+ validationErrors.push({
20262
+ acItem: acText,
20263
+ expected: specificValueCheck.expected,
20264
+ found: specificValueCheck.found,
20265
+ message: `\u274C ${specificValueCheck.message}`
20266
+ });
20267
+ } else if (keywordMatch.found) {
20268
+ mapping.relatedUIElements = keywordMatch.elements;
20269
+ mapping.confidence = keywordMatch.confidence;
20270
+ if (keywordMatch.confidence >= 70) {
20271
+ mapping.validated = true;
20272
+ mapping.validationStatus = "passed";
20273
+ mapping.validationMessage = `Found related UI elements: ${keywordMatch.elements.slice(0, 3).join(", ")}`;
20274
+ validationInfo.push({
20275
+ acItem: acText,
20276
+ message: `\u2705 Found related UI elements for "${acText}"`,
20277
+ elements: keywordMatch.elements
20278
+ });
20279
+ } else {
20280
+ mapping.validationStatus = "soft_match";
20281
+ mapping.validationMessage = `Possible match with confidence ${keywordMatch.confidence}%`;
20282
+ validationWarnings.push({
20283
+ acItem: acText,
20284
+ message: `\u26A0\uFE0F Weak match for "${acText}" - confidence ${keywordMatch.confidence}%`,
20285
+ elements: keywordMatch.elements
20286
+ });
20287
+ }
20288
+ }
20289
+ }
20290
+ for (const file of filesToCheck) {
20291
+ const fileName = file.toLowerCase();
20292
+ const textWords = acText.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
20293
+ const matchingWords = textWords.filter((word) => fileName.includes(word));
20294
+ if (matchingWords.length > 0) {
20295
+ mapping.matchedFiles.push({
20296
+ file,
20297
+ matchingWords,
20298
+ confidence: Math.min(matchingWords.length * 25, 100)
20299
+ });
20300
+ if (!mapping.validated) {
20301
+ mapping.confidence = Math.max(
20302
+ mapping.confidence,
20303
+ matchingWords.length * 25
20304
+ );
20305
+ }
20306
+ }
20307
+ }
20308
+ for (const element of gitAnalysis.analysis.interactiveElements) {
20309
+ const elementLower = element.toLowerCase();
20310
+ const acLower = acText.toLowerCase();
20311
+ if (acLower.includes(elementLower) || item.uiKeywords?.some((k) => k.toLowerCase() === elementLower)) {
20312
+ mapping.matchedElements.push(element);
20313
+ if (!mapping.validated) {
20314
+ mapping.confidence = Math.max(mapping.confidence, 60);
20315
+ }
20316
+ }
20317
+ }
20318
+ if (mapping.validationStatus === "pending") {
20319
+ if (mapping.confidence >= 50 || mapping.matchedFiles.length > 0 || mapping.matchedElements.length > 0) {
20320
+ mapping.validationStatus = "soft_match";
20321
+ mapping.validationMessage = "Found related code changes but could not verify exact implementation";
20322
+ } else {
20323
+ mapping.validationStatus = "unmatched";
20324
+ mapping.validationMessage = "No matching UI elements or code changes found";
20325
+ validationWarnings.push({
20326
+ acItem: acText,
20327
+ message: `\u26A0\uFE0F Could not find implementation for "${acText}"`,
20328
+ suggestion: "This AC may not be implemented yet, or may be in unchanged files"
20329
+ });
20330
+ }
20331
+ }
20332
+ mapping.confidence = Math.min(mapping.confidence, 100);
20333
+ return mapping;
20334
+ });
20335
+ const passedMappings = mappings.filter(
20336
+ (m) => m.validationStatus === "passed"
20337
+ );
20338
+ const failedMappings = mappings.filter(
20339
+ (m) => m.validationStatus === "failed"
20340
+ );
20341
+ const softMatchMappings = mappings.filter(
20342
+ (m) => m.validationStatus === "soft_match"
20343
+ );
20344
+ const unmatchedMappings = mappings.filter(
20345
+ (m) => m.validationStatus === "unmatched"
20346
+ );
20347
+ const validatedCount = passedMappings.length + softMatchMappings.length;
20348
+ const validationRate = mappings.length > 0 ? passedMappings.length / mappings.length : 0;
20349
+ const success = failedMappings.length === 0 && validationRate >= 0.7;
20350
+ return {
20351
+ success,
20352
+ mode: "ac_with_git",
20353
+ mappings,
20354
+ passedCount: passedMappings.length,
20355
+ softMatchCount: softMatchMappings.length,
20356
+ unmatchedCount: unmatchedMappings.length,
20357
+ validationRate: Math.round(validationRate * 100),
20358
+ validationErrors,
20359
+ validationWarnings,
20360
+ validationInfo,
20361
+ actualUITexts: actualUITexts.slice(0, 50),
20362
+ diagnostics: {
20363
+ filesScanned: filesToCheck.length,
20364
+ uiTextsExtracted: actualUITexts.length,
20365
+ acItemsProcessed: acItems.length,
20366
+ validationApproach: "intent-based"
20367
+ },
20368
+ gitAnalysis: gitAnalysis.analysis,
20369
+ changedUIFiles: gitAnalysis.analysis.changedUIFiles,
20370
+ interactiveElements: gitAnalysis.analysis.interactiveElements,
20371
+ suggestedFlowType: acItems[0]?.suggestedFlowType || gitAnalysis.analysis.suggestedFlowType,
20372
+ message: generateValidationMessage(
20373
+ success,
20374
+ passedMappings,
20375
+ softMatchMappings,
20376
+ failedMappings,
20377
+ unmatchedMappings
20378
+ ),
20379
+ recommendations: generateRecommendations(
20380
+ mappings,
20381
+ actualUITexts,
20382
+ gitAnalysis
20383
+ )
20384
+ };
20385
+ }
20386
+ function extractIntent(acText) {
20387
+ const text = acText.toLowerCase();
20388
+ const intentPatterns = [
20389
+ {
20390
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(click|tap|press|select)/i,
20391
+ action: "click"
20392
+ },
20393
+ {
20394
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(see|view|display|show)/i,
20395
+ action: "display"
20396
+ },
20397
+ {
20398
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(enter|input|type|fill)/i,
20399
+ action: "input"
20400
+ },
20401
+ {
20402
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(navigate|go to|redirect)/i,
20403
+ action: "navigate"
20404
+ },
20405
+ {
20406
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(validate|check|verify)/i,
20407
+ action: "validate"
20408
+ },
20409
+ {
20410
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(submit|save|send)/i,
20411
+ action: "submit"
20412
+ },
20413
+ {
20414
+ pattern: /(?:should|can|must)\s+(?:be able to\s+)?(open|close|toggle)/i,
20415
+ action: "toggle"
20416
+ }
20417
+ ];
20418
+ for (const { pattern, action } of intentPatterns) {
20419
+ if (pattern.test(text)) {
20420
+ const match = text.match(pattern);
20421
+ const afterAction = text.substring(
20422
+ text.indexOf(match[0]) + match[0].length
20423
+ );
20424
+ const target = afterAction.split(/\s+/).filter((w) => w.length > 2).slice(0, 5).join(" ");
20425
+ return { action, target, text: acText };
20426
+ }
20427
+ }
20428
+ const keywords = text.split(/\s+/).filter((w) => w.length > 3);
20429
+ return { action: "unknown", target: keywords.join(" "), text: acText };
20430
+ }
20431
+ function validateIntent(intent, sourceCode, uiTexts, gitAnalysis) {
20432
+ const { action, target } = intent;
20433
+ const matches = [];
20434
+ let confidence = 0;
20435
+ const actionPatterns = {
20436
+ click: ["onClick", "onPress", "onTap", "button", "Button", "clickable"],
20437
+ display: ["visible", "show", "display", "render", "return"],
20438
+ input: ["input", "Input", "onChange", "value", "setValue"],
20439
+ navigate: ["navigate", "redirect", "push", "route", "Router"],
20440
+ validate: ["validate", "error", "required", "check", "verify"],
20441
+ submit: ["onSubmit", "submit", "handleSubmit", "post", "send"],
20442
+ toggle: ["toggle", "open", "close", "setState", "setOpen"]
20443
+ };
20444
+ const patterns = actionPatterns[action] || [];
20445
+ const foundPatterns = patterns.filter((p) => sourceCode.includes(p));
20446
+ if (foundPatterns.length > 0) {
20447
+ confidence += 30;
20448
+ matches.push({ type: "code_pattern", patterns: foundPatterns });
20449
+ }
20450
+ const targetWords = target.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
20451
+ const foundTexts = uiTexts.filter(
20452
+ (text) => targetWords.some((word) => text.toLowerCase().includes(word))
20453
+ );
20454
+ if (foundTexts.length > 0) {
20455
+ confidence += 40;
20456
+ matches.push({ type: "ui_text", texts: foundTexts.slice(0, 5) });
20457
+ }
20458
+ const hasInteractiveElements = gitAnalysis.analysis.interactiveElements.some(
20459
+ (el) => patterns.some((p) => el.includes(p))
20460
+ );
20461
+ if (hasInteractiveElements) {
20462
+ confidence += 30;
20463
+ matches.push({ type: "interactive_element" });
20464
+ }
20465
+ return {
20466
+ found: confidence >= 50,
20467
+ confidence: Math.min(confidence, 100),
20468
+ matches,
20469
+ message: confidence >= 50 ? `${action} action detected with ${confidence}% confidence` : `Could not validate ${action} action`
20470
+ };
20471
+ }
20472
+ function validateSpecificValues(acText, foundUIElements) {
20473
+ const quotedPattern = /["']([^"']+)["']/g;
20474
+ const quotedValues = [];
20475
+ let match;
20476
+ while ((match = quotedPattern.exec(acText)) !== null) {
20477
+ quotedValues.push(match[1]);
20478
+ }
20479
+ if (quotedValues.length === 0) {
20480
+ return { hasMismatch: false };
20481
+ }
20482
+ for (const quotedValue of quotedValues) {
20483
+ const quotedLower = quotedValue.toLowerCase();
20484
+ const exactMatch = foundUIElements.some(
20485
+ (el) => el.toLowerCase() === quotedLower || el.toLowerCase().includes(quotedLower)
20486
+ );
20487
+ if (!exactMatch) {
20488
+ const relatedElements = foundUIElements.filter((el) => {
20489
+ const quotedWords = quotedLower.split(/\s+/).filter((w) => w.length > 2);
20490
+ const elLower = el.toLowerCase();
20491
+ return quotedWords.some((word) => elLower.includes(word));
20492
+ });
20493
+ if (relatedElements.length > 0) {
20494
+ return {
20495
+ hasMismatch: true,
20496
+ expected: quotedValue,
20497
+ found: relatedElements,
20498
+ message: `AC requires "${quotedValue}" but only found: ${relatedElements.slice(0, 5).join(", ")}. The specific value "${quotedValue}" does NOT exist in the UI.`
20499
+ };
20500
+ } else {
20501
+ return {
20502
+ hasMismatch: true,
20503
+ expected: quotedValue,
20504
+ found: [],
20505
+ message: `AC requires "${quotedValue}" but this value was NOT found in any UI element.`
20506
+ };
20507
+ }
20508
+ }
20509
+ }
20510
+ return { hasMismatch: false };
20511
+ }
20512
+ function findRelatedUIElements(acText, uiTexts, sourceCode) {
20513
+ const elements = [];
20514
+ let confidence = 0;
20515
+ const stopWords = /* @__PURE__ */ new Set([
20516
+ "the",
20517
+ "a",
20518
+ "an",
20519
+ "is",
20520
+ "should",
20521
+ "can",
20522
+ "must",
20523
+ "be",
20524
+ "to",
20525
+ "of",
20526
+ "and",
20527
+ "or"
20528
+ ]);
20529
+ const cleanText = acText.replace(/["'`]/g, "");
20530
+ const keywords = cleanText.toLowerCase().split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
20531
+ for (const uiText of uiTexts) {
20532
+ const uiTextLower = uiText.toLowerCase();
20533
+ const matchingKeywords = keywords.filter((kw) => uiTextLower.includes(kw));
20534
+ if (matchingKeywords.length > 0) {
20535
+ elements.push(uiText);
20536
+ confidence += matchingKeywords.length * 15;
20537
+ }
20538
+ }
20539
+ const codeMatches = keywords.filter(
20540
+ (kw) => sourceCode.toLowerCase().includes(kw)
20541
+ );
20542
+ if (codeMatches.length > 0) {
20543
+ confidence += codeMatches.length * 10;
20544
+ }
20545
+ return {
20546
+ found: elements.length > 0,
20547
+ elements,
20548
+ confidence: Math.min(confidence, 100)
20549
+ };
20550
+ }
20551
+ function getSourceCodeContent(repoPath, files) {
20552
+ let content = "";
20553
+ for (const file of files) {
20554
+ try {
20555
+ const filePath = join(repoPath, file);
20556
+ if (existsSync(filePath)) {
20557
+ content += readFileSync(filePath, "utf-8") + "\n";
20558
+ }
20559
+ } catch {
20560
+ }
20561
+ }
20562
+ return content;
20563
+ }
20564
+ function generateValidationMessage(success, passed, softMatch, failed, unmatched) {
20565
+ const total = passed.length + softMatch.length + failed.length + unmatched.length;
20566
+ if (failed.length > 0) {
20567
+ return `\u274C VALIDATION FAILED: ${failed.length} AC item(s) have SPECIFIC VALUE MISMATCHES. Workflow stopped.`;
20568
+ }
20569
+ const validationRate = Math.round(passed.length / total * 100);
20570
+ if (success) {
20571
+ return `\u2705 Validated ${passed.length} AC items with high confidence, ${softMatch.length} with partial evidence (${validationRate}% validation rate)`;
20572
+ } else {
20573
+ return `\u274C VALIDATION FAILED: Only ${validationRate}% validation rate (${passed.length}/${total} items). Workflow stopped - review unmatched items before proceeding.`;
20574
+ }
20575
+ }
20576
+ function generateRecommendations(mappings, uiTexts, gitAnalysis) {
20577
+ const passed = mappings.filter((m) => m.validationStatus === "passed");
20578
+ const failed = mappings.filter((m) => m.validationStatus === "failed");
20579
+ const softMatch = mappings.filter((m) => m.validationStatus === "soft_match");
20580
+ const unmatched = mappings.filter((m) => m.validationStatus === "unmatched");
20581
+ const total = mappings.length;
20582
+ const validatedCount = passed.length + softMatch.length;
20583
+ const validationRate = total > 0 ? validatedCount / total : 0;
20584
+ const success = failed.length === 0 && validationRate >= 0.7;
20585
+ return `
20586
+ ## \u{1F3AF} AC Validation Results (Intent-Based + Strict Value Checking)
20587
+
20588
+ ### \u274C FAILED - Specific Value Mismatches (${failed.length}):
20589
+ ${failed.length > 0 ? failed.map(
20590
+ (m) => `- **${m.text}**
20591
+ \u274C ${m.validationMessage}
20592
+ Expected: "${m.intent.target}"
20593
+ Found: ${m.relatedUIElements.slice(0, 5).join(", ")}`
20594
+ ).join("\n") : "_None_"}
20595
+
20596
+ ### \u2705 Validated (${passed.length}):
20597
+ ${passed.length > 0 ? passed.map(
20598
+ (m) => `- **${m.text}** (${m.confidence}% confidence)
20599
+ ${m.validationMessage}`
20600
+ ).join("\n") : "_None_"}
20601
+
20602
+ ### \u26A0\uFE0F Partial Matches (${softMatch.length}):
20603
+ ${softMatch.length > 0 ? softMatch.map(
20604
+ (m) => `- **${m.text}** (${m.confidence}% confidence)
20605
+ ${m.validationMessage}`
20606
+ ).join("\n") : "_None_"}
20607
+
20608
+ ### \u2753 Unmatched (${unmatched.length}):
20609
+ ${unmatched.length > 0 ? unmatched.map((m) => `- **${m.text}**
20610
+ ${m.validationMessage}`).join("\n") : "_None_"}
20611
+
20612
+ ### \u{1F4CB} UI Elements Found (sample):
20613
+ ${uiTexts.slice(0, 15).map((t) => `- "${t}"`).join("\n")}
20614
+
20615
+ ### \u{1F4A1} Recommendation:
20616
+ ${failed.length > 0 ? `\u274C **WORKFLOW STOPPED** - ${failed.length} AC item(s) have SPECIFIC VALUE MISMATCHES
20617
+
20618
+ **Critical Issues:**
20619
+ ${failed.map((m) => `- "${m.text}" expects values that DON'T EXIST in the UI`).join("\n")}
20620
+
20621
+ **Action Required:**
20622
+ 1. Check if the AC has the correct expected values (typos, outdated requirements?)
20623
+ 2. Verify if the implementation is missing these specific values
20624
+ 3. Update EITHER the AC OR the implementation to match
20625
+ 4. Re-run validation after fixes
20626
+
20627
+ **Do not proceed with test generation until ALL validations pass.**` : !success ? `\u274C **WORKFLOW STOPPED** - Validation rate too low (${Math.round(validationRate * 100)}%)
20628
+
20629
+ **Action Required:**
20630
+ 1. Review the unmatched AC items above
20631
+ 2. Verify if they are implemented in unchanged files
20632
+ 3. Update the AC to match actual implementation, OR
20633
+ 4. Complete the implementation to match AC requirements
20634
+
20635
+ **Do not proceed with test generation until validation passes (\u226570%).**` : passed.length === mappings.length ? "\u2705 All AC items validated - proceed with test generation" : "\u2705 Sufficient validation - safe to proceed with test generation"}
20636
+ `;
20637
+ }
20638
+
20639
+ // maestro-ai/src/mcp-server.js
20640
+ var transport = new StdioServerTransport();
20641
+ var server = new Server(
20642
+ {
20643
+ name: "Maestro AI MCP Server",
20644
+ version: "0.0.1"
20645
+ },
20646
+ {
20647
+ capabilities: {
20648
+ tools: {}
20649
+ }
20650
+ }
20651
+ );
20652
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
20653
+ return {
20654
+ tools: [
20655
+ {
20656
+ name: "maestro_ensure_installed",
20657
+ description: `\u26A0\uFE0F PREREQUISITE: Call this FIRST before using maestro_write_test or maestro_run_all_tests!
20658
+
20659
+ Checks if Maestro CLI is installed and automatically installs it if missing.
20660
+
20661
+ ## \u{1F6A8} CRITICAL: Workflow Order
20662
+ When a user asks to "generate and run tests" or "run Maestro tests", follow this exact sequence:
20663
+ 1. FIRST: maestro_ensure_installed() \u2190 YOU ARE HERE
20664
+ 2. NEXT: clickup_fetch_task() \u2190 Call this immediately after (asks user for ClickUp URL)
20665
+ 3. THEN: Based on user response, continue with ClickUp or git-based flow
20666
+
20667
+ After this tool succeeds, your NEXT action must be to call clickup_fetch_task() without parameters.
20668
+
20669
+ ## Purpose
20670
+ Ensures Maestro CLI is available before running any Maestro commands. This tool:
20671
+ - Checks if Maestro CLI is installed on the system
20672
+ - Automatically installs it if missing (via Homebrew or curl)
20673
+ - Returns version and path information
20674
+
20675
+ ## When to Use
20676
+ - ALWAYS before maestro_write_test (if execute=true)
20677
+ - ALWAYS before maestro_run_all_tests
20678
+ - When setting up a new development environment
20679
+ - To troubleshoot Maestro installation issues
20680
+
20681
+ ## Installation Method
20682
+ Uses curl installer: \`curl -fsSL "https://get.maestro.mobile.dev" | bash\`
20683
+
20684
+ ## Output
20685
+ - installed: Whether Maestro is now installed
20686
+ - version: Installed version (if available)
20687
+ - wasInstalled: Whether this call performed the installation
20688
+ - message: Status message
20689
+ - details: Additional info (path, install method)`,
20690
+ inputSchema: {
20691
+ type: "object",
20692
+ properties: {
20693
+ autoInstall: {
20694
+ type: "boolean",
20695
+ description: "Whether to automatically install Maestro if not found (default: true)"
20696
+ },
20697
+ forceReinstall: {
20698
+ type: "boolean",
20699
+ description: "Whether to force reinstall even if already installed (default: false)"
20700
+ }
20701
+ },
20702
+ required: []
20703
+ }
20704
+ },
20705
+ {
20706
+ name: "maestro_generate_test",
20707
+ description: `Generates comprehensive Maestro test instructions for web applications.
20708
+
20709
+ ## \u26D4 PREREQUISITE: Validation Must Pass First!
20710
+
20711
+ If you called map_AC_to_UI and it returned success: false:
20712
+ - **DO NOT call this tool!**
20713
+ - Report validation errors to user first
20714
+ - Get user confirmation before proceeding
20715
+
20716
+ ## Purpose
20717
+ Creates test planning instructions with guidelines, checklists, and best practices.
20718
+
20719
+ ## CRITICAL Rules
20720
+ 1. **Validation must pass** - Don't generate tests for mismatched AC
20721
+ 2. Only test CHANGED functionality (from git diff)
20722
+ 3. Never test existing/unchanged UI elements
20723
+ 4. Use extendedWaitUntil (not fixed timeouts)
20724
+ 5. Text selectors first, id fallback
20725
+ 6. Reuse existing flows via runFlow
20726
+
20727
+ ## Flow Types (Optional)
20728
+ If flowType is provided, includes a starter template:
20729
+ - auth: Login, signup flows
20730
+ - sidebar: Sidebar panel interactions
20731
+ - form: Form fill and submit
20732
+ - modal: Modal/dialog interactions
20733
+ - navigation: Page-to-page navigation
20734
+ - extended: Build on existing flow
20735
+
20736
+ If flowType is omitted, provides guidance to use other tools (cheat_sheet, pattern, ui_inspection).
20737
+
20738
+ ## Output
20739
+ Returns guidelines checklist, suggested filename, and optionally a template.`,
20740
+ inputSchema: {
20741
+ type: "object",
20742
+ properties: {
20743
+ feature: {
20744
+ type: "string",
20745
+ description: 'The feature being tested (e.g., "User Settings", "Login")'
20746
+ },
20747
+ action: {
20748
+ type: "string",
20749
+ description: 'The action being tested (e.g., "Reset defaults", "Submit form")'
20750
+ },
20751
+ flowType: {
20752
+ type: "string",
20753
+ enum: [
20754
+ "auth",
20755
+ "sidebar",
20756
+ "form",
20757
+ "modal",
20758
+ "navigation",
20759
+ "extended"
20760
+ ],
20761
+ description: "Type of flow template (optional - omit for template-free generation)"
20762
+ },
20763
+ changedElements: {
20764
+ type: "array",
20765
+ items: { type: "string" },
20766
+ description: "List of UI elements that were changed (from git diff)"
20767
+ },
20768
+ existingTests: {
20769
+ type: "array",
20770
+ items: { type: "string" },
20771
+ description: "List of existing related test files to potentially extend"
20772
+ }
20773
+ },
20774
+ required: ["feature", "action"]
20775
+ }
20776
+ },
20777
+ {
20778
+ name: "maestro_cheat_sheet",
20779
+ description: `Returns Maestro commands quick reference.
20780
+
20781
+ ## Includes
20782
+ - All core commands (launchApp, tapOn, inputText, etc.)
20783
+ - Selector strategy (text \u2192 id \u2192 index \u2192 repeat)
20784
+ - Waiting strategy (extendedWaitUntil)
20785
+ - File naming conventions
20786
+ - Environment variable setup
20787
+
20788
+ Use this before writing any Maestro YAML to ensure correct syntax.`,
20789
+ inputSchema: {
20790
+ type: "object",
20791
+ properties: {},
20792
+ required: []
20793
+ }
20794
+ },
20795
+ {
20796
+ name: "maestro_flow_template",
20797
+ description: `Returns a specific Maestro flow template by type.
20798
+
20799
+ ## Available Types
20800
+ - auth: Login/signup with onboarding dismissal
20801
+ - sidebar: Sidebar panel interactions
20802
+ - form: Form fill and submit
20803
+ - modal: Modal/dialog interactions
20804
+ - navigation: Page-to-page navigation
20805
+ - extended: Build on existing flow with runFlow
20806
+
20807
+ ## Customization Options
20808
+ - name: Flow name
20809
+ - baseUrl: Override default localhost:8443
20810
+ - waitForElement: Initial element to wait for
20811
+ - baseFlow: For extended type, the base flow to extend`,
20812
+ inputSchema: {
20813
+ type: "object",
20814
+ properties: {
20815
+ flowType: {
20816
+ type: "string",
20817
+ enum: [
20818
+ "auth",
20819
+ "sidebar",
20820
+ "form",
20821
+ "modal",
20822
+ "navigation",
20823
+ "extended"
20824
+ ],
20825
+ description: "Type of flow template to return"
20826
+ },
20827
+ name: {
19424
20828
  type: "string",
19425
20829
  description: "Custom name for the flow"
19426
20830
  },
@@ -19499,6 +20903,13 @@ Each pattern includes "when to use" guidance.`,
19499
20903
  name: "maestro_analyze_changes",
19500
20904
  description: `Analyzes actual git changes in a repository and provides test recommendations.
19501
20905
 
20906
+ ## Workflow Position: Step 3
20907
+ Call this AFTER the clickup_fetch_task() prompt has been resolved:
20908
+ 1. maestro_ensure_installed() \u2190 Step 1
20909
+ 2. clickup_fetch_task() \u2190 Step 2 (ask user for ClickUp URL)
20910
+ 3. maestro_analyze_changes() \u2190 YOU ARE HERE - Step 3
20911
+ 4. map_AC_to_UI() \u2192 maestro_generate_test() \u2192 maestro_write_test()
20912
+
19502
20913
  ## Purpose
19503
20914
  Automatically runs git commands to:
19504
20915
  - Detect changed files (unstaged, staged, or committed)
@@ -19542,6 +20953,366 @@ Automatically runs git commands to:
19542
20953
  },
19543
20954
  required: ["repoPath"]
19544
20955
  }
20956
+ },
20957
+ {
20958
+ name: "maestro_write_test",
20959
+ description: `Writes a Maestro test YAML file to disk and automatically executes it.
20960
+
20961
+ ## \u{1F6A8} PREREQUISITES - Check BOTH before using this tool!
20962
+
20963
+ 1. **maestro_ensure_installed()** - Must be called first
20964
+ 2. **map_AC_to_UI validation** - If called with ClickUp AC, validation must pass (success: true)
20965
+
20966
+ ## \u26D4 DO NOT USE IF:
20967
+ - map_AC_to_UI returned success: false (validation failed)
20968
+ - User hasn't confirmed how to handle validation mismatches
20969
+
20970
+ ## Purpose
20971
+ Saves generated test YAML to a file and AUTOMATICALLY RUNS IT. This is the primary tool for "generate and run" workflows.
20972
+
20973
+ ## IMPORTANT
20974
+ - \u26A0\uFE0F ALWAYS call maestro_ensure_installed() first to ensure Maestro CLI is available!
20975
+ - \u26A0\uFE0F If using ClickUp AC, validation MUST pass before generating tests!
20976
+ - This tool ALREADY executes the test by default - do NOT call maestro_run_all_tests after this!
20977
+ - Use this for running newly generated tests
20978
+ - The execution result is included in the response
20979
+ - This tool internally calls maestro_generate_test to provide guidelines and instructions
20980
+
20981
+ ## Parameters
20982
+ - yaml: The YAML content to write (required)
20983
+ - fileName: Name of the file, e.g., "login_test.yaml" (required)
20984
+ - directory: Target directory (default: "maestro")
20985
+ - basePath: Project base path (default: current directory)
20986
+ - execute: Whether to auto-execute after writing (default: true)
20987
+ - deviceId: Device ID for execution (optional)
20988
+ - env: Environment variables for execution (optional)
20989
+ - feature: Feature being tested (for test generation guidelines)
20990
+ - action: Action being tested (for test generation guidelines)
20991
+ - flowType: Type of flow template (auth, sidebar, form, modal, navigation, extended)
20992
+ - changedElements: List of UI elements that were changed
20993
+ - existingTests: List of existing related test files
20994
+
20995
+ ## Output
20996
+ - success: boolean
20997
+ - filePath: Path where file was written
20998
+ - message: Status message
20999
+ - generation: Test generation guidelines and instructions (from maestro_generate_test)
21000
+ - execution: Test execution results (if execute=true)`,
21001
+ inputSchema: {
21002
+ type: "object",
21003
+ properties: {
21004
+ yaml: {
21005
+ type: "string",
21006
+ description: "The YAML content to write"
21007
+ },
21008
+ fileName: {
21009
+ type: "string",
21010
+ description: 'Name of the file (e.g., "login_test.yaml")'
21011
+ },
21012
+ directory: {
21013
+ type: "string",
21014
+ description: 'Target directory (default: "maestro")'
21015
+ },
21016
+ basePath: {
21017
+ type: "string",
21018
+ description: "Project base path (default: current directory)"
21019
+ },
21020
+ execute: {
21021
+ type: "boolean",
21022
+ description: "Whether to auto-execute the test after writing (default: true)"
21023
+ },
21024
+ deviceId: {
21025
+ type: "string",
21026
+ description: "Device ID to run on (optional)"
21027
+ },
21028
+ env: {
21029
+ type: "object",
21030
+ description: "Environment variables to pass",
21031
+ additionalProperties: { type: "string" }
21032
+ },
21033
+ feature: {
21034
+ type: "string",
21035
+ description: 'Feature being tested for generation guidelines (e.g., "User Settings")'
21036
+ },
21037
+ action: {
21038
+ type: "string",
21039
+ description: 'Action being tested for generation guidelines (e.g., "Reset defaults")'
21040
+ },
21041
+ flowType: {
21042
+ type: "string",
21043
+ enum: [
21044
+ "auth",
21045
+ "sidebar",
21046
+ "form",
21047
+ "modal",
21048
+ "navigation",
21049
+ "extended"
21050
+ ],
21051
+ description: "Type of flow template for generation guidelines"
21052
+ },
21053
+ changedElements: {
21054
+ type: "array",
21055
+ items: { type: "string" },
21056
+ description: "List of UI elements that were changed (from git diff)"
21057
+ },
21058
+ existingTests: {
21059
+ type: "array",
21060
+ items: { type: "string" },
21061
+ description: "List of existing related test files to potentially extend"
21062
+ }
21063
+ },
21064
+ required: ["yaml", "fileName"]
21065
+ }
21066
+ },
21067
+ {
21068
+ name: "maestro_discover_tests",
21069
+ description: `Discovers all Maestro test files in a directory.
21070
+
21071
+ ## Purpose
21072
+ Lists all .yaml test files in the maestro/ directory.
21073
+
21074
+ ## Parameters
21075
+ - directory: Directory to search (default: "maestro")
21076
+ - basePath: Project base path
21077
+
21078
+ ## Output
21079
+ - files: Array of file paths
21080
+ - count: Number of files found`,
21081
+ inputSchema: {
21082
+ type: "object",
21083
+ properties: {
21084
+ directory: {
21085
+ type: "string",
21086
+ description: 'Directory to search (default: "maestro")'
21087
+ },
21088
+ basePath: {
21089
+ type: "string",
21090
+ description: "Project base path"
21091
+ }
21092
+ },
21093
+ required: []
21094
+ }
21095
+ },
21096
+ {
21097
+ name: "maestro_run_all_tests",
21098
+ description: `Runs the ENTIRE Maestro test suite.
21099
+
21100
+ ## \u{1F6A8} PREREQUISITE: Call maestro_ensure_installed() FIRST before using this tool!
21101
+
21102
+ ## \u26A0\uFE0F ONLY use when user EXPLICITLY asks to "run all tests" or "run the test suite"
21103
+
21104
+ ## DO NOT USE for:
21105
+ - "generate and run test cases" \u2192 use maestro_write_test instead (it auto-executes)
21106
+ - Running a newly generated test \u2192 maestro_write_test already does this
21107
+ - Any workflow that involves generating new tests
21108
+
21109
+ ## Purpose
21110
+ Discovers and executes ALL existing .yaml test files in the maestro/ directory.
21111
+
21112
+ ## Parameters
21113
+ - files: Specific test file paths (optional)
21114
+ - directory: Directory containing tests (default: "maestro")
21115
+ - basePath: Project base path
21116
+ - deviceId: Target device ID
21117
+ - env: Environment variables
21118
+ - stopOnFailure: Stop on first failure (default: false)
21119
+
21120
+ ## Output
21121
+ - discovery: List of test files found
21122
+ - execution: Results with pass/fail status for each test
21123
+ - summary: Aggregated statistics`,
21124
+ inputSchema: {
21125
+ type: "object",
21126
+ properties: {
21127
+ files: {
21128
+ type: "array",
21129
+ items: { type: "string" },
21130
+ description: "Specific test file paths to run (if provided, only these files are executed instead of discovering all)"
21131
+ },
21132
+ directory: {
21133
+ type: "string",
21134
+ description: 'Directory containing tests (default: "maestro") - only used if files is not provided'
21135
+ },
21136
+ basePath: {
21137
+ type: "string",
21138
+ description: "Project base path"
21139
+ },
21140
+ deviceId: {
21141
+ type: "string",
21142
+ description: "Device ID to run on"
21143
+ },
21144
+ env: {
21145
+ type: "object",
21146
+ description: "Environment variables",
21147
+ additionalProperties: { type: "string" }
21148
+ },
21149
+ stopOnFailure: {
21150
+ type: "boolean",
21151
+ description: "Stop on first failure (default: false)"
21152
+ }
21153
+ },
21154
+ required: []
21155
+ }
21156
+ },
21157
+ {
21158
+ name: "clickup_fetch_task",
21159
+ description: `Fetches a ClickUp task by URL to extract acceptance criteria for test generation.
21160
+
21161
+ ## \u{1F6A8} ASK FIRST Pattern
21162
+ When called WITHOUT a taskUrl, this tool returns a prompt asking the user:
21163
+ > "Do you have a ClickUp task URL for this feature? Paste the URL to generate tests based on acceptance criteria, or type 'skip' to continue with git-based analysis only."
21164
+
21165
+ ## Workflow Position: Step 2
21166
+ This is the MANDATORY second step after maestro_ensure_installed():
21167
+ 1. maestro_ensure_installed() \u2190 Step 1 (completed)
21168
+ 2. clickup_fetch_task() \u2190 YOU ARE HERE - Step 2
21169
+ 3. Wait for user response, then:
21170
+ - If URL provided \u2192 validate_acceptance_criteria \u2192 maestro_analyze_changes \u2192 map_AC_to_UI
21171
+ - If skipped \u2192 maestro_analyze_changes \u2192 map_AC_to_UI (git-only mode)
21172
+
21173
+ ## How to Use
21174
+ 1. Call WITHOUT parameters first: clickup_fetch_task()
21175
+ 2. Show the returned prompt to the user
21176
+ 3. Based on user's response:
21177
+ - URL provided: Call again with clickup_fetch_task(taskUrl: "<user's url>")
21178
+ - User says "skip"/"no": Call with clickup_fetch_task(taskUrl: "skip")
21179
+
21180
+ ## URL Parsing
21181
+ Extracts task ID from various ClickUp URL formats:
21182
+ - https://app.clickup.com/t/abc123
21183
+ - https://app.clickup.com/t/86abc123
21184
+ - https://app.clickup.com/{workspace}/v/...?p=abc123
21185
+
21186
+ ## Environment Variables
21187
+ Requires CLICKUP_API_TOKEN to be set in MCP configuration.
21188
+
21189
+ ## Output
21190
+ - If no URL: Returns prompt for user input (needsInput: true)
21191
+ - If URL provided: Returns full task object with checklists, custom fields, description
21192
+ - If "skip": Returns { skipped: true } to signal continue without ClickUp`,
21193
+ inputSchema: {
21194
+ type: "object",
21195
+ properties: {
21196
+ taskUrl: {
21197
+ type: "string",
21198
+ description: 'ClickUp task URL (optional - if omitted, returns a prompt asking for URL). Use "skip" to bypass ClickUp integration.'
21199
+ }
21200
+ },
21201
+ required: []
21202
+ }
21203
+ },
21204
+ {
21205
+ name: "validate_acceptance_criteria",
21206
+ description: `Parses and validates acceptance criteria from a ClickUp task for testability.
21207
+
21208
+ ## Purpose
21209
+ Extracts AC items from multiple sources and validates them for Maestro test generation.
21210
+
21211
+ ## AC Sources (configured via CLICKUP_AC_SOURCE env var)
21212
+ - checklist:Checklist Name - Parse from a specific checklist
21213
+ - custom_field:Field Name - Parse from a custom field
21214
+ - description - Parse from task description
21215
+ - Multiple sources: "checklist:Acceptance Criteria,custom_field:AC,description"
21216
+
21217
+ ## Validation
21218
+ Each AC item is analyzed for:
21219
+ - UI-related keywords (click, button, form, input, navigate, etc.)
21220
+ - Testability score (0-100)
21221
+ - Suggested flow type (auth, form, sidebar, modal, navigation)
21222
+ - Whether it's testable via Maestro
21223
+
21224
+ ## Output
21225
+ - validatedItems: Array of AC items with testability analysis
21226
+ - testableCount: Number of items that can be tested
21227
+ - suggestedFlowTypes: Recommended flow types based on AC content`,
21228
+ inputSchema: {
21229
+ type: "object",
21230
+ properties: {
21231
+ task: {
21232
+ type: "object",
21233
+ description: "ClickUp task object (from clickup_fetch_task)"
21234
+ },
21235
+ acSource: {
21236
+ type: "string",
21237
+ description: 'AC source specification (optional, uses CLICKUP_AC_SOURCE env var if not provided). Format: "checklist:Name" or "custom_field:Name" or "description"'
21238
+ }
21239
+ },
21240
+ required: ["task"]
21241
+ }
21242
+ },
21243
+ {
21244
+ name: "map_AC_to_UI",
21245
+ description: `Maps acceptance criteria items to UI elements based on git changes.
21246
+
21247
+ ## \u{1F6D1} BLOCKING VALIDATION - STOP ON FAILURE!
21248
+
21249
+ This tool performs **strict validation** of AC against actual UI implementation.
21250
+ ## Purpose
21251
+ Cross-references AC items with git diff and source code to:
21252
+ - Extract actual UI text from
21253
+ I source files
21254
+ - Compare AC text against real UI strings
21255
+ - **FAIL if values don't match** (e.g., AC: "20 days" vs UI: "30 days")
21256
+ - Report validation errors that must be resolved
21257
+
21258
+ ## \u26D4 CRITICAL: When success=false, STOP THE WORKFLOW!
21259
+
21260
+ ## Modes
21261
+ - ac with_git: When AC items are provided, performs HARD validation
21262
+ - git_only: When no AC items, uses pure git-based analysis (soft mode)
21263
+
21264
+ ## Output
21265
+ - success: **false** if any AC doesn't match UI, **true** if all validated
21266
+ - validationErrors: Array of mismatches (AC expected vs UI actual)
21267
+ - validationWarnings: Items that couldn't be fully verified
21268
+ - actualUITexts: Sample of UI text strings found in source code
21269
+
21270
+ If this tool returns success: false:
21271
+ 1. **STOP IMMEDIATELY** - Do NOT call any more tools
21272
+ 3. **END your response** - Do not continue, do not ask questions
21273
+
21274
+ ## Output Format When Validation Fails:
21275
+ ## \u2705 CORRECT BEHAVIOR:
21276
+ When validation fails, respond with:
21277
+ "\u274C Validation failed. The acceptance criteria don't match the UI:
21278
+ - [list mismatches]
21279
+
21280
+ **Recommendation:** Update the ClickUp task acceptance criteria to match the actual UI implementation, OR fix the UI to match the AC requirements.
21281
+
21282
+ **Actual UI values available:** [list what exists]
21283
+
21284
+ *Workflow stopped. Re-run after resolving the mismatches.*"
21285
+
21286
+ ## \u274C FORBIDDEN ACTIONS AFTER FAILURE:
21287
+ - Calling maestro_generate_test
21288
+ - Calling maestro_write_test
21289
+ - Saying "I'll generate a test based on what actually exists"
21290
+ - Asking follow-up questions
21291
+ - Continuing the workflow in any way
21292
+
21293
+ ## Output
21294
+ - success: **false** = STOP WORKFLOW, output recommendation, END RESPONSE
21295
+ - success: **true** = OK to proceed with test generation`,
21296
+ inputSchema: {
21297
+ type: "object",
21298
+ properties: {
21299
+ acItems: {
21300
+ type: "array",
21301
+ items: { type: "object" },
21302
+ description: "Validated AC items from validate_acceptance_criteria (optional - if omitted, uses git-only analysis)"
21303
+ },
21304
+ repoPath: {
21305
+ type: "string",
21306
+ description: "Path to the git repository (default: current directory)"
21307
+ },
21308
+ changedFiles: {
21309
+ type: "array",
21310
+ items: { type: "string" },
21311
+ description: "List of changed files to match against (optional - auto-detected from git if omitted)"
21312
+ }
21313
+ },
21314
+ required: []
21315
+ }
19545
21316
  }
19546
21317
  ]
19547
21318
  };
@@ -19550,6 +21321,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
19550
21321
  const { name, arguments: args } = request.params;
19551
21322
  let result;
19552
21323
  switch (name) {
21324
+ case "maestro_ensure_installed": {
21325
+ const { autoInstall, forceReinstall } = args || {};
21326
+ result = ensureMaestroInstalled({
21327
+ autoInstall: autoInstall !== false,
21328
+ // defaults to true
21329
+ forceReinstall: forceReinstall || false
21330
+ });
21331
+ break;
21332
+ }
19553
21333
  case "maestro_generate_test": {
19554
21334
  const { feature, action, flowType, changedElements, existingTests } = args || {};
19555
21335
  result = generateMaestroTest({
@@ -19600,6 +21380,93 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
19600
21380
  });
19601
21381
  break;
19602
21382
  }
21383
+ case "maestro_write_test": {
21384
+ const {
21385
+ yaml,
21386
+ fileName,
21387
+ directory,
21388
+ basePath,
21389
+ execute,
21390
+ deviceId,
21391
+ env,
21392
+ feature,
21393
+ action,
21394
+ flowType,
21395
+ changedElements,
21396
+ existingTests
21397
+ } = args || {};
21398
+ result = writeTestFile({
21399
+ yaml,
21400
+ fileName,
21401
+ directory: directory || "maestro",
21402
+ basePath: basePath || process.cwd(),
21403
+ execute: execute !== false,
21404
+ // defaults to true
21405
+ deviceId: deviceId || null,
21406
+ env: env || {},
21407
+ feature: feature || "",
21408
+ action: action || "",
21409
+ flowType: flowType || null,
21410
+ changedElements: changedElements || [],
21411
+ existingTests: existingTests || []
21412
+ });
21413
+ break;
21414
+ }
21415
+ case "maestro_discover_tests": {
21416
+ const { directory, basePath } = args || {};
21417
+ result = discoverTestFiles({
21418
+ directory: directory || "maestro",
21419
+ basePath: basePath || process.cwd()
21420
+ });
21421
+ break;
21422
+ }
21423
+ case "maestro_run_all_tests": {
21424
+ const { files, directory, basePath, deviceId, env, stopOnFailure } = args || {};
21425
+ result = runAllTests({
21426
+ files: files || null,
21427
+ directory: directory || "maestro",
21428
+ basePath: basePath || process.cwd(),
21429
+ deviceId: deviceId || null,
21430
+ env: env || {},
21431
+ stopOnFailure: stopOnFailure || false
21432
+ });
21433
+ break;
21434
+ }
21435
+ case "clickup_fetch_task": {
21436
+ const { taskUrl } = args || {};
21437
+ result = await fetchClickUpTask({
21438
+ taskUrl: taskUrl || null
21439
+ });
21440
+ break;
21441
+ }
21442
+ case "validate_acceptance_criteria": {
21443
+ const { task, acSource } = args || {};
21444
+ const parsed = parseAcceptanceCriteria({
21445
+ task,
21446
+ acSource: acSource || null
21447
+ });
21448
+ if (parsed.success) {
21449
+ const validated = validateAcceptanceCriteria({
21450
+ acItems: parsed.acItems
21451
+ });
21452
+ result = {
21453
+ ...parsed,
21454
+ ...validated
21455
+ };
21456
+ } else {
21457
+ result = parsed;
21458
+ }
21459
+ break;
21460
+ }
21461
+ case "map_AC_to_UI": {
21462
+ const { acItems, repoPath, changedFiles } = args || {};
21463
+ result = mapACToUIElements({
21464
+ acItems: acItems || [],
21465
+ repoPath: repoPath || process.cwd(),
21466
+ changedFiles: changedFiles || []
21467
+ });
21468
+ break;
21469
+ }
19603
21470
  default:
19604
21471
  throw new Error(`Unknown tool: ${name}`);
19605
21472
  }