mcp-maestro-mobile-ai 1.3.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,826 +1,1394 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Maestro MCP Server
5
- * AI-Assisted Mobile Automation using Model Context Protocol
6
- *
7
- * This server exposes tools for running Maestro mobile tests
8
- * that can be called by AI clients (Cursor, Claude Desktop, VS Code Copilot)
9
- */
10
-
11
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import {
14
- CallToolRequestSchema,
15
- ListToolsRequestSchema,
16
- ListResourcesRequestSchema,
17
- ReadResourceRequestSchema,
18
- } from "@modelcontextprotocol/sdk/types.js";
19
- import { config } from "dotenv";
20
- import { fileURLToPath } from "url";
21
- import { dirname, join } from "path";
22
-
23
- // Import tools
24
- import { readPromptFile, listPromptFiles } from "./tools/promptTools.js";
25
- import { validateMaestroYaml } from "./tools/validateTools.js";
26
- import { runTest, runTestSuite, generateTestReport, listTestReports, runTestSuiteWithReport } from "./tools/runTools.js";
27
- import {
28
- getAppConfig,
29
- getTestResults,
30
- takeScreenshot,
31
- checkDevice,
32
- checkApp,
33
- cleanupResults,
34
- listDevices,
35
- selectDevice,
36
- clearDevice,
37
- } from "./tools/utilityTools.js";
38
- import {
39
- registerAppElements,
40
- registerAppScreen,
41
- saveFlow,
42
- getFlows,
43
- removeFlow,
44
- getAIContext,
45
- getAppContext,
46
- clearContext,
47
- listContexts,
48
- getYamlInstructions,
49
- validateYamlBeforeRun,
50
- getTestPattern,
51
- getScreenAnalysis,
52
- } from "./tools/contextTools.js";
53
- import { logger } from "./utils/logger.js";
54
- import { validatePrerequisites } from "./utils/prerequisites.js";
55
-
56
- // Load environment variables
57
- const __filename = fileURLToPath(import.meta.url);
58
- const __dirname = dirname(__filename);
59
- config({ path: join(__dirname, "../../.env") });
60
-
61
- // Create MCP Server
62
- const server = new Server(
63
- {
64
- name: "mcp-maestro-mobile-ai",
65
- version: "1.2.0",
66
- },
67
- {
68
- capabilities: {
69
- tools: {},
70
- resources: {},
71
- },
72
- }
73
- );
74
-
75
- // ============================================
76
- // TOOL DEFINITIONS
77
- // ============================================
78
-
79
- const TOOLS = [
80
- // === Prompt File Tools ===
81
- {
82
- name: "read_prompt_file",
83
- description:
84
- "Read test prompts from a .txt or .md file. Each line in the file is treated as a separate test case prompt.",
85
- inputSchema: {
86
- type: "object",
87
- properties: {
88
- file: {
89
- type: "string",
90
- description:
91
- 'Path to the prompt file (e.g., "prompts/login-tests.txt")',
92
- },
93
- },
94
- required: ["file"],
95
- },
96
- },
97
- {
98
- name: "list_prompt_files",
99
- description: "List all available prompt files in the prompts directory.",
100
- inputSchema: {
101
- type: "object",
102
- properties: {
103
- directory: {
104
- type: "string",
105
- description:
106
- 'Directory to search for prompt files (default: "prompts")',
107
- },
108
- },
109
- },
110
- },
111
-
112
- // === Device Management Tools ===
113
- {
114
- name: "list_devices",
115
- description:
116
- "List all connected Android devices and emulators. Shows device ID, type (emulator/physical), and model name. Use this to see available devices before selecting one.",
117
- inputSchema: {
118
- type: "object",
119
- properties: {},
120
- },
121
- },
122
- {
123
- name: "select_device",
124
- description:
125
- "Select a specific device to run tests on. All subsequent tests will run on this device until changed. Use list_devices first to see available device IDs.",
126
- inputSchema: {
127
- type: "object",
128
- properties: {
129
- deviceId: {
130
- type: "string",
131
- description:
132
- 'Device ID to select (e.g., "emulator-5554" or "RF8M12345XY" for physical device)',
133
- },
134
- },
135
- required: ["deviceId"],
136
- },
137
- },
138
- {
139
- name: "clear_device",
140
- description:
141
- "Clear the device selection. Tests will run on the first available device (default behavior).",
142
- inputSchema: {
143
- type: "object",
144
- properties: {},
145
- },
146
- },
147
- {
148
- name: "check_device",
149
- description:
150
- "Check if an Android emulator or device is connected. Shows connection status and which device is selected.",
151
- inputSchema: {
152
- type: "object",
153
- properties: {},
154
- },
155
- },
156
- {
157
- name: "check_app",
158
- description:
159
- "Check if the target app is installed on the connected device. Verifies the app is ready for testing.",
160
- inputSchema: {
161
- type: "object",
162
- properties: {
163
- appId: {
164
- type: "string",
165
- description:
166
- "Optional: App package ID to check. Uses configured APP_ID if not provided.",
167
- },
168
- },
169
- },
170
- },
171
-
172
- // === Configuration Tools ===
173
- {
174
- name: "get_app_config",
175
- description:
176
- "Get the current app configuration including appId, platform, selected device, and other settings.",
177
- inputSchema: {
178
- type: "object",
179
- properties: {},
180
- },
181
- },
182
-
183
- // === Validation Tools ===
184
- {
185
- name: "validate_maestro_yaml",
186
- description:
187
- "Validate Maestro YAML for syntax AND structure errors. Checks for: missing appId, missing clearState/launchApp, inputText without tapOn (which causes text to go to wrong fields). Always validate before running!",
188
- inputSchema: {
189
- type: "object",
190
- properties: {
191
- yaml: {
192
- type: "string",
193
- description: "The Maestro YAML content to validate",
194
- },
195
- },
196
- required: ["yaml"],
197
- },
198
- },
199
-
200
- // === Test Execution Tools ===
201
- {
202
- name: "run_test",
203
- description: `Run a single Maestro test. IMPORTANT: The YAML MUST follow these rules or it will be REJECTED:
204
-
205
- 1. STRUCTURE: Must start with appId, then clearState, then launchApp
206
- 2. TEXT INPUT: ALWAYS use tapOn BEFORE inputText (or text goes to wrong field!)
207
- CORRECT: - tapOn: "Username" then - inputText: "value"
208
- WRONG: - inputText: "value" (missing tapOn!)
209
- 3. Use visible text labels for elements when testIDs are unknown
210
-
211
- Example valid YAML:
212
- appId: com.example.app
213
- ---
214
- - clearState
215
- - launchApp
216
- - tapOn: "Username"
217
- - inputText: "user@example.com"
218
- - tapOn: "Password"
219
- - inputText: "password123"
220
- - tapOn: "Sign In"
221
- - assertVisible: "Welcome"`,
222
- inputSchema: {
223
- type: "object",
224
- properties: {
225
- yaml: {
226
- type: "string",
227
- description: "The Maestro YAML flow content. MUST use tapOn before inputText for each field!",
228
- },
229
- name: {
230
- type: "string",
231
- description: "Name for this test (used for reporting and screenshots)",
232
- },
233
- retries: {
234
- type: "number",
235
- description:
236
- "Optional: Number of retries if test fails (default: from config or 0)",
237
- },
238
- },
239
- required: ["yaml", "name"],
240
- },
241
- },
242
- {
243
- name: "run_test_suite",
244
- description:
245
- "Run multiple Maestro tests in sequence. Each YAML must follow the rules: appId at top, clearState, launchApp, and ALWAYS tapOn before inputText!",
246
- inputSchema: {
247
- type: "object",
248
- properties: {
249
- tests: {
250
- type: "array",
251
- items: {
252
- type: "object",
253
- properties: {
254
- yaml: {
255
- type: "string",
256
- description: "The Maestro YAML. MUST use tapOn before inputText!",
257
- },
258
- name: {
259
- type: "string",
260
- description: "Name for this test",
261
- },
262
- },
263
- required: ["yaml", "name"],
264
- },
265
- description: "Array of tests to run",
266
- },
267
- retries: {
268
- type: "number",
269
- description:
270
- "Optional: Number of retries for failed tests (default: from config or 0)",
271
- },
272
- },
273
- required: ["tests"],
274
- },
275
- },
276
-
277
- // === Results & Reporting Tools ===
278
- {
279
- name: "run_tests_with_report",
280
- description:
281
- "Run multiple tests and automatically generate HTML + JSON report. Use this when running tests from a prompt file. Returns report path that can be opened in browser.",
282
- inputSchema: {
283
- type: "object",
284
- properties: {
285
- tests: {
286
- type: "array",
287
- items: {
288
- type: "object",
289
- properties: {
290
- yaml: {
291
- type: "string",
292
- description: "The Maestro YAML. MUST use tapOn before inputText!",
293
- },
294
- name: {
295
- type: "string",
296
- description: "Name for this test",
297
- },
298
- },
299
- required: ["yaml", "name"],
300
- },
301
- description: "Array of tests to run",
302
- },
303
- promptFile: {
304
- type: "string",
305
- description: "Name of the prompt file (for report metadata)",
306
- },
307
- appId: {
308
- type: "string",
309
- description: "App ID (for report metadata)",
310
- },
311
- retries: {
312
- type: "number",
313
- description: "Number of retries for failed tests",
314
- },
315
- },
316
- required: ["tests"],
317
- },
318
- },
319
- {
320
- name: "generate_report",
321
- description:
322
- "Generate HTML and JSON report from test results. Call this after running tests to create a visual summary report.",
323
- inputSchema: {
324
- type: "object",
325
- properties: {
326
- results: {
327
- type: "array",
328
- items: {
329
- type: "object",
330
- properties: {
331
- name: { type: "string", description: "Test name" },
332
- success: { type: "boolean", description: "Whether the test passed" },
333
- duration: { type: "number", description: "Test duration in milliseconds" },
334
- error: { type: "string", description: "Error message if test failed" },
335
- },
336
- required: ["name", "success"],
337
- },
338
- description: "Array of test results with name, success, duration, error fields",
339
- },
340
- promptFile: {
341
- type: "string",
342
- description: "Name of the prompt file",
343
- },
344
- appId: {
345
- type: "string",
346
- description: "App ID",
347
- },
348
- },
349
- required: ["results"],
350
- },
351
- },
352
- {
353
- name: "list_reports",
354
- description:
355
- "List all generated test reports. Returns paths to HTML and JSON report files.",
356
- inputSchema: {
357
- type: "object",
358
- properties: {},
359
- },
360
- },
361
- {
362
- name: "get_test_results",
363
- description: "Get the results from the last test run or a specific run by ID.",
364
- inputSchema: {
365
- type: "object",
366
- properties: {
367
- runId: {
368
- type: "string",
369
- description: "Optional: Specific run ID to get results for",
370
- },
371
- },
372
- },
373
- },
374
- {
375
- name: "take_screenshot",
376
- description:
377
- "Take a screenshot of the current device screen. Useful for debugging or verification.",
378
- inputSchema: {
379
- type: "object",
380
- properties: {
381
- name: {
382
- type: "string",
383
- description: "Name for the screenshot file",
384
- },
385
- },
386
- required: ["name"],
387
- },
388
- },
389
- {
390
- name: "cleanup_results",
391
- description:
392
- "Clean up old test results and screenshots to free up disk space. Keeps the most recent results.",
393
- inputSchema: {
394
- type: "object",
395
- properties: {
396
- keepLast: {
397
- type: "number",
398
- description: "Number of recent results to keep (default: 50)",
399
- },
400
- deleteScreenshots: {
401
- type: "boolean",
402
- description: "Whether to delete old screenshots (default: true)",
403
- },
404
- },
405
- },
406
- },
407
-
408
- // === App Context/Training Tools ===
409
- {
410
- name: "register_elements",
411
- description:
412
- "Register UI elements for an app to help AI generate better YAML. Provide testIDs, accessibilityLabels, and text values for app elements. This teaches the AI about your app's UI structure.",
413
- inputSchema: {
414
- type: "object",
415
- properties: {
416
- appId: {
417
- type: "string",
418
- description: "App package ID (e.g., 'com.myapp')",
419
- },
420
- elements: {
421
- type: "object",
422
- description:
423
- "Object containing element definitions. Each key is the element name, value contains: testId, accessibilityLabel, text, type, description",
424
- },
425
- },
426
- required: ["appId", "elements"],
427
- },
428
- },
429
- {
430
- name: "register_screen",
431
- description:
432
- "Register a screen structure for an app. Define what elements and actions are available on each screen.",
433
- inputSchema: {
434
- type: "object",
435
- properties: {
436
- appId: {
437
- type: "string",
438
- description: "App package ID",
439
- },
440
- screenName: {
441
- type: "string",
442
- description: "Name of the screen (e.g., 'LoginScreen', 'Dashboard')",
443
- },
444
- screenData: {
445
- type: "object",
446
- description:
447
- "Screen data including: description, elements (array of element names), actions (array of possible actions)",
448
- },
449
- },
450
- required: ["appId", "screenName", "screenData"],
451
- },
452
- },
453
- {
454
- name: "save_successful_flow",
455
- description:
456
- "Save a successful test flow as a pattern for future reference. Call this after a test passes to help AI learn from successful patterns.",
457
- inputSchema: {
458
- type: "object",
459
- properties: {
460
- appId: {
461
- type: "string",
462
- description: "App package ID",
463
- },
464
- flowName: {
465
- type: "string",
466
- description: "Name for this flow pattern",
467
- },
468
- yamlContent: {
469
- type: "string",
470
- description: "The successful Maestro YAML content",
471
- },
472
- description: {
473
- type: "string",
474
- description: "Optional: Description of what this flow does",
475
- },
476
- },
477
- required: ["appId", "flowName", "yamlContent"],
478
- },
479
- },
480
- {
481
- name: "get_saved_flows",
482
- description:
483
- "Get all saved successful flows for an app. Use these as patterns when generating new tests.",
484
- inputSchema: {
485
- type: "object",
486
- properties: {
487
- appId: {
488
- type: "string",
489
- description: "App package ID",
490
- },
491
- },
492
- required: ["appId"],
493
- },
494
- },
495
- {
496
- name: "delete_flow",
497
- description: "Delete a saved flow pattern.",
498
- inputSchema: {
499
- type: "object",
500
- properties: {
501
- appId: {
502
- type: "string",
503
- description: "App package ID",
504
- },
505
- flowName: {
506
- type: "string",
507
- description: "Name of the flow to delete",
508
- },
509
- },
510
- required: ["appId", "flowName"],
511
- },
512
- },
513
- {
514
- name: "get_ai_context",
515
- description:
516
- "Get the formatted AI context for an app. This returns all registered elements, screens, and example flows in a format optimized for AI consumption. ALWAYS call this before generating Maestro YAML to get app-specific information.",
517
- inputSchema: {
518
- type: "object",
519
- properties: {
520
- appId: {
521
- type: "string",
522
- description: "App package ID",
523
- },
524
- },
525
- required: ["appId"],
526
- },
527
- },
528
- {
529
- name: "get_full_context",
530
- description:
531
- "Get the complete raw app context including all elements, screens, and flows.",
532
- inputSchema: {
533
- type: "object",
534
- properties: {
535
- appId: {
536
- type: "string",
537
- description: "App package ID",
538
- },
539
- },
540
- required: ["appId"],
541
- },
542
- },
543
- {
544
- name: "clear_app_context",
545
- description: "Clear all saved context for an app (elements, screens, flows).",
546
- inputSchema: {
547
- type: "object",
548
- properties: {
549
- appId: {
550
- type: "string",
551
- description: "App package ID",
552
- },
553
- },
554
- required: ["appId"],
555
- },
556
- },
557
- {
558
- name: "list_app_contexts",
559
- description: "List all apps that have saved context data.",
560
- inputSchema: {
561
- type: "object",
562
- properties: {},
563
- },
564
- },
565
-
566
- // === YAML Generation Tools (CRITICAL) ===
567
- {
568
- name: "get_yaml_instructions",
569
- description:
570
- "CRITICAL: Call this BEFORE generating any Maestro YAML. Returns the exact rules and patterns for generating valid YAML that works consistently. Includes app-specific context if available.",
571
- inputSchema: {
572
- type: "object",
573
- properties: {
574
- appId: {
575
- type: "string",
576
- description: "App package ID to get app-specific context",
577
- },
578
- },
579
- },
580
- },
581
- {
582
- name: "validate_yaml_structure",
583
- description:
584
- "Validate YAML structure before running a test. Checks for common issues like missing 'tapOn' before 'inputText' which causes text to go to wrong fields.",
585
- inputSchema: {
586
- type: "object",
587
- properties: {
588
- yamlContent: {
589
- type: "string",
590
- description: "The Maestro YAML content to validate",
591
- },
592
- },
593
- required: ["yamlContent"],
594
- },
595
- },
596
- {
597
- name: "get_test_pattern",
598
- description:
599
- "Get a standard test pattern template. Available: login, form, search, navigation, list, settings, logout. Use these as starting points.",
600
- inputSchema: {
601
- type: "object",
602
- properties: {
603
- patternName: {
604
- type: "string",
605
- description: "Pattern name: login, form, search, navigation, list, settings, or logout",
606
- enum: ["login", "form", "search", "navigation", "list", "settings", "logout"],
607
- },
608
- },
609
- required: ["patternName"],
610
- },
611
- },
612
- {
613
- name: "get_screen_analysis_help",
614
- description:
615
- "Get instructions on how to gather UI element information from the user. Call this when you don't know the exact element names/labels on a screen. Returns questions to ask the user.",
616
- inputSchema: {
617
- type: "object",
618
- properties: {},
619
- },
620
- },
621
- ];
622
-
623
- // ============================================
624
- // HANDLERS
625
- // ============================================
626
-
627
- // List available tools
628
- server.setRequestHandler(ListToolsRequestSchema, async () => {
629
- logger.info("Listing available tools");
630
- return { tools: TOOLS };
631
- });
632
-
633
- // Handle tool calls
634
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
635
- const { name, arguments: args } = request.params;
636
- logger.info(`Tool called: ${name}`, { args });
637
-
638
- try {
639
- switch (name) {
640
- // Prompt tools
641
- case "read_prompt_file":
642
- return await readPromptFile(args.file);
643
-
644
- case "list_prompt_files":
645
- return await listPromptFiles(args.directory);
646
-
647
- // Device management tools
648
- case "list_devices":
649
- return await listDevices();
650
-
651
- case "select_device":
652
- return await selectDevice(args.deviceId);
653
-
654
- case "clear_device":
655
- return await clearDevice();
656
-
657
- case "check_device":
658
- return await checkDevice();
659
-
660
- case "check_app":
661
- return await checkApp(args.appId);
662
-
663
- // Config tools
664
- case "get_app_config":
665
- return await getAppConfig();
666
-
667
- // Validation tools
668
- case "validate_maestro_yaml":
669
- return await validateMaestroYaml(args.yaml);
670
-
671
- // Execution tools
672
- case "run_test":
673
- return await runTest(args.yaml, args.name, { retries: args.retries });
674
-
675
- case "run_test_suite":
676
- return await runTestSuite(args.tests, { retries: args.retries });
677
-
678
- // Results & reporting tools
679
- case "run_tests_with_report":
680
- return await runTestSuiteWithReport(args.tests, {
681
- promptFile: args.promptFile,
682
- appId: args.appId,
683
- retries: args.retries,
684
- });
685
-
686
- case "generate_report":
687
- return await generateTestReport(args.results, {
688
- promptFile: args.promptFile,
689
- appId: args.appId,
690
- });
691
-
692
- case "list_reports":
693
- return await listTestReports();
694
-
695
- case "get_test_results":
696
- return await getTestResults(args.runId);
697
-
698
- case "take_screenshot":
699
- return await takeScreenshot(args.name);
700
-
701
- case "cleanup_results":
702
- return await cleanupResults({
703
- keepLast: args.keepLast,
704
- deleteScreenshots: args.deleteScreenshots,
705
- });
706
-
707
- // App context/training tools
708
- case "register_elements":
709
- return await registerAppElements(args.appId, args.elements);
710
-
711
- case "register_screen":
712
- return await registerAppScreen(args.appId, args.screenName, args.screenData);
713
-
714
- case "save_successful_flow":
715
- return await saveFlow(args.appId, args.flowName, args.yamlContent, args.description);
716
-
717
- case "get_saved_flows":
718
- return await getFlows(args.appId);
719
-
720
- case "delete_flow":
721
- return await removeFlow(args.appId, args.flowName);
722
-
723
- case "get_ai_context":
724
- return await getAIContext(args.appId);
725
-
726
- case "get_full_context":
727
- return await getAppContext(args.appId);
728
-
729
- case "clear_app_context":
730
- return await clearContext(args.appId);
731
-
732
- case "list_app_contexts":
733
- return await listContexts();
734
-
735
- // YAML generation tools
736
- case "get_yaml_instructions":
737
- return await getYamlInstructions(args.appId);
738
-
739
- case "validate_yaml_structure":
740
- return await validateYamlBeforeRun(args.yamlContent);
741
-
742
- case "get_test_pattern":
743
- return await getTestPattern(args.patternName);
744
-
745
- case "get_screen_analysis_help":
746
- return await getScreenAnalysis();
747
-
748
- default:
749
- throw new Error(`Unknown tool: ${name}`);
750
- }
751
- } catch (error) {
752
- logger.error(`Tool error: ${name}`, { error: error.message });
753
- return {
754
- content: [
755
- {
756
- type: "text",
757
- text: JSON.stringify({
758
- success: false,
759
- error: error.message,
760
- }),
761
- },
762
- ],
763
- };
764
- }
765
- });
766
-
767
- // List available resources (prompt files)
768
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
769
- const result = await listPromptFiles("prompts");
770
- const files = JSON.parse(result.content[0].text).files || [];
771
-
772
- return {
773
- resources: files.map((file) => ({
774
- uri: `prompts://${file}`,
775
- name: file,
776
- mimeType: "text/plain",
777
- description: `Prompt file: ${file}`,
778
- })),
779
- };
780
- });
781
-
782
- // Read a resource
783
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
784
- const uri = request.params.uri;
785
- const file = uri.replace("prompts://", "");
786
- const result = await readPromptFile(`prompts/${file}`);
787
-
788
- return {
789
- contents: [
790
- {
791
- uri,
792
- mimeType: "text/plain",
793
- text: result.content[0].text,
794
- },
795
- ],
796
- };
797
- });
798
-
799
- // ============================================
800
- // START SERVER
801
- // ============================================
802
-
803
- async function main() {
804
- logger.info("Starting MCP Maestro Mobile AI v1.2.0...");
805
- logger.info("");
806
-
807
- // Validate prerequisites before starting
808
- // This will exit with code 2 if critical prerequisites are missing
809
- await validatePrerequisites({
810
- exitOnError: true,
811
- checkDevice: false, // Don't require device at startup
812
- });
813
-
814
- logger.info("");
815
- logger.info("Prerequisites validated. Starting server...");
816
-
817
- const transport = new StdioServerTransport();
818
- await server.connect(transport);
819
-
820
- logger.info("MCP Maestro Mobile AI server running on stdio");
821
- }
822
-
823
- main().catch((error) => {
824
- logger.error("Failed to start server", { error: error.message });
825
- process.exit(1);
826
- });
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Maestro MCP Server
5
+ * AI-Assisted Mobile Automation using Model Context Protocol
6
+ *
7
+ * This server exposes tools for running Maestro mobile tests
8
+ * that can be called by AI clients (Cursor, Claude Desktop, VS Code Copilot)
9
+ *
10
+ * Security: All tool inputs are validated using Zod schemas before execution.
11
+ * See schemas/toolSchemas.js for validation rules.
12
+ */
13
+
14
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import {
17
+ CallToolRequestSchema,
18
+ ListToolsRequestSchema,
19
+ ListResourcesRequestSchema,
20
+ ReadResourceRequestSchema,
21
+ } from "@modelcontextprotocol/sdk/types.js";
22
+ import { config } from "dotenv";
23
+ import { fileURLToPath } from "url";
24
+ import { dirname, join } from "path";
25
+
26
+ // Import tools
27
+ import { readPromptFile, listPromptFiles } from "./tools/promptTools.js";
28
+ import { validateMaestroYaml } from "./tools/validateTools.js";
29
+ import {
30
+ runTest,
31
+ runTestSuite,
32
+ generateTestReport,
33
+ listTestReports,
34
+ runTestSuiteWithReport,
35
+ // Cache management
36
+ runTestWithCache,
37
+ saveTestToCache,
38
+ listCachedTests,
39
+ clearTestCache,
40
+ deleteCachedTest,
41
+ getTestCacheStats,
42
+ lookupCachedTest,
43
+ } from "./tools/runTools.js";
44
+ import {
45
+ getAppConfig,
46
+ getTestResults,
47
+ takeScreenshot,
48
+ checkDevice,
49
+ checkApp,
50
+ cleanupResults,
51
+ listDevices,
52
+ selectDevice,
53
+ clearDevice,
54
+ } from "./tools/utilityTools.js";
55
+ import {
56
+ registerAppElements,
57
+ registerAppScreen,
58
+ saveFlow,
59
+ getFlows,
60
+ removeFlow,
61
+ getAIContext,
62
+ getAppContext,
63
+ clearContext,
64
+ listContexts,
65
+ getYamlInstructions,
66
+ validateYamlBeforeRun,
67
+ getTestPattern,
68
+ getScreenAnalysis,
69
+ // Known Issues Tools
70
+ getKnownIssues,
71
+ getPlatformRules,
72
+ getGenerationRules,
73
+ // Prompt Analysis & Generation
74
+ validateAndGenerate,
75
+ analyzeTestPrompt,
76
+ } from "./tools/contextTools.js";
77
+ import { logger } from "./utils/logger.js";
78
+ import { validatePrerequisites } from "./utils/prerequisites.js";
79
+
80
+ // Import security and validation
81
+ import {
82
+ SecurityError,
83
+ isSafeModeEnabled,
84
+ getSecurityConfig,
85
+ logSecurityEvent,
86
+ } from "./utils/security.js";
87
+ import { validateToolInput } from "./schemas/toolSchemas.js";
88
+
89
+ // Load environment variables
90
+ const __filename = fileURLToPath(import.meta.url);
91
+ const __dirname = dirname(__filename);
92
+ config({ path: join(__dirname, "../../.env") });
93
+
94
+ // Server version - updated for YAML caching features
95
+ const SERVER_VERSION = "1.6.0";
96
+
97
+ // Create MCP Server
98
+ const server = new Server(
99
+ {
100
+ name: "mcp-maestro-mobile-ai",
101
+ version: SERVER_VERSION,
102
+ },
103
+ {
104
+ capabilities: {
105
+ tools: {},
106
+ resources: {},
107
+ },
108
+ }
109
+ );
110
+
111
+ // ============================================
112
+ // TOOL DEFINITIONS
113
+ // ============================================
114
+
115
+ const TOOLS = [
116
+ // === Prompt File Tools ===
117
+ {
118
+ name: "read_prompt_file",
119
+ description:
120
+ "Read test prompts from a .txt or .md file. Each line in the file is treated as a separate test case prompt.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ file: {
125
+ type: "string",
126
+ description:
127
+ 'Path to the prompt file (e.g., "prompts/login-tests.txt")',
128
+ },
129
+ },
130
+ required: ["file"],
131
+ },
132
+ },
133
+ {
134
+ name: "list_prompt_files",
135
+ description: "List all available prompt files in the prompts directory.",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ directory: {
140
+ type: "string",
141
+ description:
142
+ 'Directory to search for prompt files (default: "prompts")',
143
+ },
144
+ },
145
+ },
146
+ },
147
+
148
+ // === Device Management Tools ===
149
+ {
150
+ name: "list_devices",
151
+ description:
152
+ "List all connected Android devices and emulators. Shows device ID, type (emulator/physical), and model name. Use this to see available devices before selecting one.",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {},
156
+ },
157
+ },
158
+ {
159
+ name: "select_device",
160
+ description:
161
+ "Select a specific device to run tests on. All subsequent tests will run on this device until changed. Use list_devices first to see available device IDs.",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {
165
+ deviceId: {
166
+ type: "string",
167
+ description:
168
+ 'Device ID to select (e.g., "emulator-5554" or "RF8M12345XY" for physical device)',
169
+ },
170
+ },
171
+ required: ["deviceId"],
172
+ },
173
+ },
174
+ {
175
+ name: "clear_device",
176
+ description:
177
+ "Clear the device selection. Tests will run on the first available device (default behavior).",
178
+ inputSchema: {
179
+ type: "object",
180
+ properties: {},
181
+ },
182
+ },
183
+ {
184
+ name: "check_device",
185
+ description:
186
+ "Check if an Android emulator or device is connected. Shows connection status and which device is selected.",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: {},
190
+ },
191
+ },
192
+ {
193
+ name: "check_app",
194
+ description:
195
+ "Check if the target app is installed on the connected device. Verifies the app is ready for testing.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ appId: {
200
+ type: "string",
201
+ description:
202
+ "Optional: App package ID to check. Uses configured APP_ID if not provided.",
203
+ },
204
+ },
205
+ },
206
+ },
207
+
208
+ // === Configuration Tools ===
209
+ {
210
+ name: "get_app_config",
211
+ description:
212
+ "Get the current app configuration including appId, platform, selected device, and other settings.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {},
216
+ },
217
+ },
218
+
219
+ // === Validation Tools ===
220
+ {
221
+ name: "validate_maestro_yaml",
222
+ description:
223
+ "Validate Maestro YAML for syntax AND structure errors. Checks for: missing appId, missing clearState/launchApp, inputText without tapOn (which causes text to go to wrong fields). Always validate before running!",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ yaml: {
228
+ type: "string",
229
+ description: "The Maestro YAML content to validate",
230
+ },
231
+ },
232
+ required: ["yaml"],
233
+ },
234
+ },
235
+
236
+ // === Test Execution Tools ===
237
+ {
238
+ name: "run_test",
239
+ description: `Run a single Maestro test. IMPORTANT: The YAML MUST follow these rules or it will be REJECTED:
240
+
241
+ 1. STRUCTURE: Must start with appId, then clearState, then launchApp
242
+ 2. TEXT INPUT: ALWAYS use tapOn BEFORE inputText (or text goes to wrong field!)
243
+ CORRECT: - tapOn: "Username" then - inputText: "value"
244
+ WRONG: - inputText: "value" (missing tapOn!)
245
+ 3. Use visible text labels for elements when testIDs are unknown
246
+
247
+ Example valid YAML:
248
+ appId: com.example.app
249
+ ---
250
+ - clearState
251
+ - launchApp
252
+ - tapOn: "Username"
253
+ - inputText: "user@example.com"
254
+ - tapOn: "Password"
255
+ - inputText: "password123"
256
+ - tapOn: "Sign In"
257
+ - assertVisible: "Welcome"`,
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ yaml: {
262
+ type: "string",
263
+ description:
264
+ "The Maestro YAML flow content. MUST use tapOn before inputText for each field!",
265
+ },
266
+ name: {
267
+ type: "string",
268
+ description:
269
+ "Name for this test (used for reporting and screenshots)",
270
+ },
271
+ retries: {
272
+ type: "number",
273
+ description:
274
+ "Optional: Number of retries if test fails (default: from config or 0)",
275
+ },
276
+ },
277
+ required: ["yaml", "name"],
278
+ },
279
+ },
280
+ {
281
+ name: "run_test_suite",
282
+ description:
283
+ "Run multiple Maestro tests in sequence. Each YAML must follow the rules: appId at top, clearState, launchApp, and ALWAYS tapOn before inputText!",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {
287
+ tests: {
288
+ type: "array",
289
+ items: {
290
+ type: "object",
291
+ properties: {
292
+ yaml: {
293
+ type: "string",
294
+ description:
295
+ "The Maestro YAML. MUST use tapOn before inputText!",
296
+ },
297
+ name: {
298
+ type: "string",
299
+ description: "Name for this test",
300
+ },
301
+ },
302
+ required: ["yaml", "name"],
303
+ },
304
+ description: "Array of tests to run",
305
+ },
306
+ retries: {
307
+ type: "number",
308
+ description:
309
+ "Optional: Number of retries for failed tests (default: from config or 0)",
310
+ },
311
+ },
312
+ required: ["tests"],
313
+ },
314
+ },
315
+
316
+ // === Results & Reporting Tools ===
317
+ {
318
+ name: "run_tests_with_report",
319
+ description:
320
+ "Run multiple tests and automatically generate HTML + JSON report. Use this when running tests from a prompt file. Returns report path that can be opened in browser.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: {
324
+ tests: {
325
+ type: "array",
326
+ items: {
327
+ type: "object",
328
+ properties: {
329
+ yaml: {
330
+ type: "string",
331
+ description:
332
+ "The Maestro YAML. MUST use tapOn before inputText!",
333
+ },
334
+ name: {
335
+ type: "string",
336
+ description: "Name for this test",
337
+ },
338
+ },
339
+ required: ["yaml", "name"],
340
+ },
341
+ description: "Array of tests to run",
342
+ },
343
+ promptFile: {
344
+ type: "string",
345
+ description: "Name of the prompt file (for report metadata)",
346
+ },
347
+ appId: {
348
+ type: "string",
349
+ description: "App ID (for report metadata)",
350
+ },
351
+ retries: {
352
+ type: "number",
353
+ description: "Number of retries for failed tests",
354
+ },
355
+ },
356
+ required: ["tests"],
357
+ },
358
+ },
359
+ {
360
+ name: "generate_report",
361
+ description:
362
+ "Generate HTML and JSON report from test results. Call this after running tests to create a visual summary report.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ results: {
367
+ type: "array",
368
+ items: {
369
+ type: "object",
370
+ properties: {
371
+ name: { type: "string", description: "Test name" },
372
+ success: {
373
+ type: "boolean",
374
+ description: "Whether the test passed",
375
+ },
376
+ duration: {
377
+ type: "number",
378
+ description: "Test duration in milliseconds",
379
+ },
380
+ error: {
381
+ type: "string",
382
+ description: "Error message if test failed",
383
+ },
384
+ },
385
+ required: ["name", "success"],
386
+ },
387
+ description:
388
+ "Array of test results with name, success, duration, error fields",
389
+ },
390
+ promptFile: {
391
+ type: "string",
392
+ description: "Name of the prompt file",
393
+ },
394
+ appId: {
395
+ type: "string",
396
+ description: "App ID",
397
+ },
398
+ },
399
+ required: ["results"],
400
+ },
401
+ },
402
+ {
403
+ name: "list_reports",
404
+ description:
405
+ "List all generated test reports. Returns paths to HTML and JSON report files.",
406
+ inputSchema: {
407
+ type: "object",
408
+ properties: {},
409
+ },
410
+ },
411
+ {
412
+ name: "get_test_results",
413
+ description:
414
+ "Get the results from the last test run or a specific run by ID.",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ runId: {
419
+ type: "string",
420
+ description: "Optional: Specific run ID to get results for",
421
+ },
422
+ },
423
+ },
424
+ },
425
+ {
426
+ name: "take_screenshot",
427
+ description:
428
+ "Take a screenshot of the current device screen. Useful for debugging or verification.",
429
+ inputSchema: {
430
+ type: "object",
431
+ properties: {
432
+ name: {
433
+ type: "string",
434
+ description: "Name for the screenshot file",
435
+ },
436
+ },
437
+ required: ["name"],
438
+ },
439
+ },
440
+ {
441
+ name: "cleanup_results",
442
+ description:
443
+ "Clean up old test results and screenshots to free up disk space. Keeps the most recent results.",
444
+ inputSchema: {
445
+ type: "object",
446
+ properties: {
447
+ keepLast: {
448
+ type: "number",
449
+ description: "Number of recent results to keep (default: 50)",
450
+ },
451
+ deleteScreenshots: {
452
+ type: "boolean",
453
+ description: "Whether to delete old screenshots (default: true)",
454
+ },
455
+ },
456
+ },
457
+ },
458
+
459
+ // === App Context/Training Tools ===
460
+ {
461
+ name: "register_elements",
462
+ description:
463
+ "Register UI elements for an app to help AI generate better YAML. Provide testIDs, accessibilityLabels, and text values for app elements. This teaches the AI about your app's UI structure.",
464
+ inputSchema: {
465
+ type: "object",
466
+ properties: {
467
+ appId: {
468
+ type: "string",
469
+ description: "App package ID (e.g., 'com.myapp')",
470
+ },
471
+ elements: {
472
+ type: "object",
473
+ description:
474
+ "Object containing element definitions. Each key is the element name, value contains: testId, accessibilityLabel, text, type, description",
475
+ },
476
+ },
477
+ required: ["appId", "elements"],
478
+ },
479
+ },
480
+ {
481
+ name: "register_screen",
482
+ description:
483
+ "Register a screen structure for an app. Define what elements and actions are available on each screen.",
484
+ inputSchema: {
485
+ type: "object",
486
+ properties: {
487
+ appId: {
488
+ type: "string",
489
+ description: "App package ID",
490
+ },
491
+ screenName: {
492
+ type: "string",
493
+ description: "Name of the screen (e.g., 'LoginScreen', 'Dashboard')",
494
+ },
495
+ screenData: {
496
+ type: "object",
497
+ description:
498
+ "Screen data including: description, elements (array of element names), actions (array of possible actions)",
499
+ },
500
+ },
501
+ required: ["appId", "screenName", "screenData"],
502
+ },
503
+ },
504
+ {
505
+ name: "save_successful_flow",
506
+ description:
507
+ "Save a successful test flow as a pattern for future reference. Call this after a test passes to help AI learn from successful patterns.",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: {
511
+ appId: {
512
+ type: "string",
513
+ description: "App package ID",
514
+ },
515
+ flowName: {
516
+ type: "string",
517
+ description: "Name for this flow pattern",
518
+ },
519
+ yamlContent: {
520
+ type: "string",
521
+ description: "The successful Maestro YAML content",
522
+ },
523
+ description: {
524
+ type: "string",
525
+ description: "Optional: Description of what this flow does",
526
+ },
527
+ },
528
+ required: ["appId", "flowName", "yamlContent"],
529
+ },
530
+ },
531
+ {
532
+ name: "get_saved_flows",
533
+ description:
534
+ "Get all saved successful flows for an app. Use these as patterns when generating new tests.",
535
+ inputSchema: {
536
+ type: "object",
537
+ properties: {
538
+ appId: {
539
+ type: "string",
540
+ description: "App package ID",
541
+ },
542
+ },
543
+ required: ["appId"],
544
+ },
545
+ },
546
+ {
547
+ name: "delete_flow",
548
+ description: "Delete a saved flow pattern.",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ appId: {
553
+ type: "string",
554
+ description: "App package ID",
555
+ },
556
+ flowName: {
557
+ type: "string",
558
+ description: "Name of the flow to delete",
559
+ },
560
+ },
561
+ required: ["appId", "flowName"],
562
+ },
563
+ },
564
+ {
565
+ name: "get_ai_context",
566
+ description:
567
+ "Get the formatted AI context for an app. This returns all registered elements, screens, and example flows in a format optimized for AI consumption. ALWAYS call this before generating Maestro YAML to get app-specific information.",
568
+ inputSchema: {
569
+ type: "object",
570
+ properties: {
571
+ appId: {
572
+ type: "string",
573
+ description: "App package ID",
574
+ },
575
+ },
576
+ required: ["appId"],
577
+ },
578
+ },
579
+ {
580
+ name: "get_full_context",
581
+ description:
582
+ "Get the complete raw app context including all elements, screens, and flows.",
583
+ inputSchema: {
584
+ type: "object",
585
+ properties: {
586
+ appId: {
587
+ type: "string",
588
+ description: "App package ID",
589
+ },
590
+ },
591
+ required: ["appId"],
592
+ },
593
+ },
594
+ {
595
+ name: "clear_app_context",
596
+ description:
597
+ "Clear all saved context for an app (elements, screens, flows).",
598
+ inputSchema: {
599
+ type: "object",
600
+ properties: {
601
+ appId: {
602
+ type: "string",
603
+ description: "App package ID",
604
+ },
605
+ },
606
+ required: ["appId"],
607
+ },
608
+ },
609
+ {
610
+ name: "list_app_contexts",
611
+ description: "List all apps that have saved context data.",
612
+ inputSchema: {
613
+ type: "object",
614
+ properties: {},
615
+ },
616
+ },
617
+
618
+ // === YAML Generation Tools (CRITICAL) ===
619
+ {
620
+ name: "get_yaml_instructions",
621
+ description:
622
+ "CRITICAL: Call this BEFORE generating any Maestro YAML. Returns the exact rules and patterns for generating valid YAML that works consistently. Includes app-specific context if available.",
623
+ inputSchema: {
624
+ type: "object",
625
+ properties: {
626
+ appId: {
627
+ type: "string",
628
+ description: "App package ID to get app-specific context",
629
+ },
630
+ },
631
+ },
632
+ },
633
+ {
634
+ name: "validate_yaml_structure",
635
+ description:
636
+ "Validate YAML structure before running a test. Checks for common issues like missing 'tapOn' before 'inputText' which causes text to go to wrong fields.",
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ yamlContent: {
641
+ type: "string",
642
+ description: "The Maestro YAML content to validate",
643
+ },
644
+ },
645
+ required: ["yamlContent"],
646
+ },
647
+ },
648
+ {
649
+ name: "get_test_pattern",
650
+ description:
651
+ "Get a standard test pattern template. Available: login, form, search, navigation, list, settings, logout, dropdown, flutter_login_with_dropdown, flutter_multi_dropdown. Use these as starting points.",
652
+ inputSchema: {
653
+ type: "object",
654
+ properties: {
655
+ patternName: {
656
+ type: "string",
657
+ description:
658
+ "Pattern name: login, form, search, navigation, list, settings, logout, dropdown, flutter_login_with_dropdown, or flutter_multi_dropdown",
659
+ enum: [
660
+ "login",
661
+ "form",
662
+ "search",
663
+ "navigation",
664
+ "list",
665
+ "settings",
666
+ "logout",
667
+ "dropdown",
668
+ "flutter_login_with_dropdown",
669
+ "flutter_multi_dropdown",
670
+ ],
671
+ },
672
+ },
673
+ required: ["patternName"],
674
+ },
675
+ },
676
+ {
677
+ name: "get_screen_analysis_help",
678
+ description:
679
+ "Get instructions on how to gather UI element information from the user. Call this when you don't know the exact element names/labels on a screen. Returns questions to ask the user.",
680
+ inputSchema: {
681
+ type: "object",
682
+ properties: {},
683
+ },
684
+ },
685
+
686
+ // === Interaction Patterns & Generation Rules ===
687
+ {
688
+ name: "get_known_issues",
689
+ description:
690
+ "Get generic interaction patterns for reliable mobile test automation. These patterns are framework-agnostic and improve test reliability across all mobile apps. Optionally filter by category.",
691
+ inputSchema: {
692
+ type: "object",
693
+ properties: {
694
+ category: {
695
+ type: "string",
696
+ description:
697
+ "Filter by category: keyboard_handling, element_interaction, timing_synchronization, input_handling, dropdown_picker, navigation, scroll_visibility, fallback_strategies",
698
+ enum: [
699
+ "keyboard_handling",
700
+ "element_interaction",
701
+ "timing_synchronization",
702
+ "input_handling",
703
+ "dropdown_picker",
704
+ "navigation",
705
+ "scroll_visibility",
706
+ "fallback_strategies",
707
+ ],
708
+ },
709
+ },
710
+ },
711
+ },
712
+ {
713
+ name: "get_platform_rules",
714
+ description:
715
+ "Get interaction patterns for a specific category. Use this to understand how to handle specific interaction types (keyboard, dropdowns, timing, etc.).",
716
+ inputSchema: {
717
+ type: "object",
718
+ properties: {
719
+ category: {
720
+ type: "string",
721
+ description:
722
+ "Pattern category: keyboard_handling, element_interaction, timing_synchronization, input_handling, dropdown_picker, navigation, scroll_visibility, fallback_strategies",
723
+ enum: [
724
+ "keyboard_handling",
725
+ "element_interaction",
726
+ "timing_synchronization",
727
+ "input_handling",
728
+ "dropdown_picker",
729
+ "navigation",
730
+ "scroll_visibility",
731
+ "fallback_strategies",
732
+ ],
733
+ },
734
+ },
735
+ required: ["category"],
736
+ },
737
+ },
738
+ {
739
+ name: "get_generation_rules",
740
+ description:
741
+ "Get all YAML generation rules and best practices. These are framework-agnostic rules that improve test reliability across any mobile application.",
742
+ inputSchema: {
743
+ type: "object",
744
+ properties: {},
745
+ },
746
+ },
747
+
748
+ // === Prompt Analysis & YAML Generation ===
749
+ {
750
+ name: "validate_prompt",
751
+ description: `Analyze a user's test description using the Action-Element-Verification model and generate Maestro YAML.
752
+
753
+ This tool:
754
+ 1. Extracts ACTIONS (tap, input, scroll, select, verify...)
755
+ 2. Identifies ELEMENTS (button, field, dropdown, list, chart...)
756
+ 3. Finds VALUES (credentials, search terms, selections...)
757
+ 4. Detects VERIFICATIONS (visible, contains, success...)
758
+ 5. Assesses completeness and either:
759
+ - Generates clean YAML (if complete)
760
+ - Asks clarifying questions (if incomplete)
761
+ - Generates YAML with warnings (if forceGenerate=true)
762
+
763
+ Use this as the PRIMARY tool for converting natural language test descriptions to Maestro YAML.`,
764
+ inputSchema: {
765
+ type: "object",
766
+ properties: {
767
+ userPrompt: {
768
+ type: "string",
769
+ description:
770
+ "Natural language test description. Example: 'Test login with username abc and password 1234 on com.example.app'",
771
+ },
772
+ appId: {
773
+ type: "string",
774
+ description:
775
+ "Optional app package ID. Provide if not included in the prompt.",
776
+ },
777
+ forceGenerate: {
778
+ type: "boolean",
779
+ description:
780
+ "If true, generate YAML with assumptions even if information is incomplete. Default: false",
781
+ },
782
+ },
783
+ required: ["userPrompt"],
784
+ },
785
+ },
786
+ {
787
+ name: "analyze_prompt",
788
+ description:
789
+ "Analyze a test prompt WITHOUT generating YAML. Returns detailed breakdown of detected actions, elements, values, and verifications. Use this to understand what the system can extract from a prompt.",
790
+ inputSchema: {
791
+ type: "object",
792
+ properties: {
793
+ userPrompt: {
794
+ type: "string",
795
+ description: "The test description to analyze",
796
+ },
797
+ appId: {
798
+ type: "string",
799
+ description: "Optional app package ID for context",
800
+ },
801
+ },
802
+ required: ["userPrompt"],
803
+ },
804
+ },
805
+
806
+ // === YAML Cache Tools ===
807
+ {
808
+ name: "save_to_cache",
809
+ description:
810
+ "Save a successful test's YAML to cache for faster future execution. After a test passes, call this to store the YAML. When the same prompt is used again, the cached YAML will be reused automatically.",
811
+ inputSchema: {
812
+ type: "object",
813
+ properties: {
814
+ prompt: {
815
+ type: "string",
816
+ description: "The original test prompt/description",
817
+ },
818
+ yaml: {
819
+ type: "string",
820
+ description: "The YAML content to cache",
821
+ },
822
+ testName: {
823
+ type: "string",
824
+ description: "Name of the test",
825
+ },
826
+ appId: {
827
+ type: "string",
828
+ description: "Optional app package ID",
829
+ },
830
+ },
831
+ required: ["prompt", "yaml", "testName"],
832
+ },
833
+ },
834
+ {
835
+ name: "lookup_cache",
836
+ description:
837
+ "Check if a YAML is cached for a given prompt. Returns the cached YAML if found, or indicates no cache exists.",
838
+ inputSchema: {
839
+ type: "object",
840
+ properties: {
841
+ prompt: {
842
+ type: "string",
843
+ description: "The test prompt to look up",
844
+ },
845
+ appId: {
846
+ type: "string",
847
+ description: "Optional app package ID",
848
+ },
849
+ },
850
+ required: ["prompt"],
851
+ },
852
+ },
853
+ {
854
+ name: "list_cache",
855
+ description:
856
+ "List all cached test YAMLs. Shows test names, prompts, and usage statistics.",
857
+ inputSchema: {
858
+ type: "object",
859
+ properties: {},
860
+ },
861
+ },
862
+ {
863
+ name: "clear_cache",
864
+ description:
865
+ "Clear all cached test YAMLs. After clearing, all tests will generate fresh YAML.",
866
+ inputSchema: {
867
+ type: "object",
868
+ properties: {},
869
+ },
870
+ },
871
+ {
872
+ name: "delete_from_cache",
873
+ description:
874
+ "Delete a specific cached test by its hash. Use list_cache to find the hash.",
875
+ inputSchema: {
876
+ type: "object",
877
+ properties: {
878
+ hash: {
879
+ type: "string",
880
+ description: "The cache hash to delete (from list_cache output)",
881
+ },
882
+ },
883
+ required: ["hash"],
884
+ },
885
+ },
886
+ {
887
+ name: "get_cache_stats",
888
+ description:
889
+ "Get cache statistics including total cached tests, execution counts, and most used tests.",
890
+ inputSchema: {
891
+ type: "object",
892
+ properties: {},
893
+ },
894
+ },
895
+ {
896
+ name: "run_test_with_cache",
897
+ description:
898
+ "Run a test with automatic cache lookup. If a cached YAML exists for the prompt, it will be used. Otherwise, the provided YAML will be executed.",
899
+ inputSchema: {
900
+ type: "object",
901
+ properties: {
902
+ prompt: {
903
+ type: "string",
904
+ description: "The original test prompt - used for cache lookup",
905
+ },
906
+ yaml: {
907
+ type: "string",
908
+ description: "The YAML to use if not cached",
909
+ },
910
+ name: {
911
+ type: "string",
912
+ description: "Test name",
913
+ },
914
+ appId: {
915
+ type: "string",
916
+ description: "Optional app package ID",
917
+ },
918
+ retries: {
919
+ type: "number",
920
+ description: "Number of retries if test fails",
921
+ },
922
+ },
923
+ required: ["prompt", "yaml", "name"],
924
+ },
925
+ },
926
+ ];
927
+
928
+ // ============================================
929
+ // HANDLERS
930
+ // ============================================
931
+
932
+ // List available tools
933
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
934
+ logger.info("Listing available tools");
935
+ return { tools: TOOLS };
936
+ });
937
+
938
+ // ============================================
939
+ // VALIDATION MIDDLEWARE
940
+ // ============================================
941
+
942
+ /**
943
+ * Create a validation error response
944
+ */
945
+ function createValidationErrorResponse(toolName, validationResult) {
946
+ logSecurityEvent("TOOL_VALIDATION_FAILED", {
947
+ tool: toolName,
948
+ errors: validationResult.errors,
949
+ });
950
+
951
+ return {
952
+ content: [
953
+ {
954
+ type: "text",
955
+ text: JSON.stringify({
956
+ success: false,
957
+ error: "Input validation failed",
958
+ validationErrors: validationResult.errors,
959
+ message: validationResult.message,
960
+ hint: "Please check the input parameters and try again.",
961
+ }),
962
+ },
963
+ ],
964
+ };
965
+ }
966
+
967
+ /**
968
+ * Create a security error response
969
+ */
970
+ function createSecurityErrorResponse(error) {
971
+ return {
972
+ content: [
973
+ {
974
+ type: "text",
975
+ text: JSON.stringify({
976
+ success: false,
977
+ error: "Security violation",
978
+ code: error.code,
979
+ message: error.message,
980
+ details: error.details,
981
+ }),
982
+ },
983
+ ],
984
+ };
985
+ }
986
+
987
+ // Handle tool calls
988
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
989
+ const { name, arguments: args } = request.params;
990
+ const startTime = Date.now();
991
+
992
+ logger.info(`Tool called: ${name}`, { args });
993
+
994
+ // ============================================
995
+ // STEP 1: Validate input with Zod schema
996
+ // ============================================
997
+ const validationResult = validateToolInput(name, args);
998
+
999
+ if (!validationResult.success) {
1000
+ logger.warn(`Validation failed for tool: ${name}`, {
1001
+ errors: validationResult.errors,
1002
+ });
1003
+ return createValidationErrorResponse(name, validationResult);
1004
+ }
1005
+
1006
+ // Use validated/sanitized data
1007
+ const validatedArgs = validationResult.data;
1008
+
1009
+ // ============================================
1010
+ // STEP 2: Log security event for audit trail
1011
+ // ============================================
1012
+ logSecurityEvent("TOOL_EXECUTION_START", {
1013
+ tool: name,
1014
+ safeMode: isSafeModeEnabled(),
1015
+ hasArgs: Object.keys(validatedArgs || {}).length > 0,
1016
+ });
1017
+
1018
+ try {
1019
+ // ============================================
1020
+ // STEP 3: Execute tool with validated args
1021
+ // ============================================
1022
+ let result;
1023
+
1024
+ switch (name) {
1025
+ // Prompt tools
1026
+ case "read_prompt_file":
1027
+ result = await readPromptFile(validatedArgs.file);
1028
+ break;
1029
+
1030
+ case "list_prompt_files":
1031
+ result = await listPromptFiles(validatedArgs.directory);
1032
+ break;
1033
+
1034
+ // Device management tools
1035
+ case "list_devices":
1036
+ result = await listDevices();
1037
+ break;
1038
+
1039
+ case "select_device":
1040
+ result = await selectDevice(validatedArgs.deviceId);
1041
+ break;
1042
+
1043
+ case "clear_device":
1044
+ result = await clearDevice();
1045
+ break;
1046
+
1047
+ case "check_device":
1048
+ result = await checkDevice();
1049
+ break;
1050
+
1051
+ case "check_app":
1052
+ result = await checkApp(validatedArgs.appId);
1053
+ break;
1054
+
1055
+ // Config tools
1056
+ case "get_app_config":
1057
+ result = await getAppConfig();
1058
+ break;
1059
+
1060
+ // Validation tools
1061
+ case "validate_maestro_yaml":
1062
+ result = await validateMaestroYaml(validatedArgs.yaml);
1063
+ break;
1064
+
1065
+ // Execution tools
1066
+ case "run_test":
1067
+ result = await runTest(validatedArgs.yaml, validatedArgs.name, {
1068
+ retries: validatedArgs.retries,
1069
+ });
1070
+ break;
1071
+
1072
+ case "run_test_suite":
1073
+ result = await runTestSuite(validatedArgs.tests, {
1074
+ retries: validatedArgs.retries,
1075
+ });
1076
+ break;
1077
+
1078
+ // Results & reporting tools
1079
+ case "run_tests_with_report":
1080
+ result = await runTestSuiteWithReport(validatedArgs.tests, {
1081
+ promptFile: validatedArgs.promptFile,
1082
+ appId: validatedArgs.appId,
1083
+ retries: validatedArgs.retries,
1084
+ });
1085
+ break;
1086
+
1087
+ case "generate_report":
1088
+ result = await generateTestReport(validatedArgs.results, {
1089
+ promptFile: validatedArgs.promptFile,
1090
+ appId: validatedArgs.appId,
1091
+ });
1092
+ break;
1093
+
1094
+ case "list_reports":
1095
+ result = await listTestReports();
1096
+ break;
1097
+
1098
+ case "get_test_results":
1099
+ result = await getTestResults(validatedArgs.runId);
1100
+ break;
1101
+
1102
+ case "take_screenshot":
1103
+ result = await takeScreenshot(validatedArgs.name);
1104
+ break;
1105
+
1106
+ case "cleanup_results":
1107
+ result = await cleanupResults({
1108
+ keepLast: validatedArgs.keepLast,
1109
+ deleteScreenshots: validatedArgs.deleteScreenshots,
1110
+ });
1111
+ break;
1112
+
1113
+ // App context/training tools
1114
+ case "register_elements":
1115
+ result = await registerAppElements(
1116
+ validatedArgs.appId,
1117
+ validatedArgs.elements
1118
+ );
1119
+ break;
1120
+
1121
+ case "register_screen":
1122
+ result = await registerAppScreen(
1123
+ validatedArgs.appId,
1124
+ validatedArgs.screenName,
1125
+ validatedArgs.screenData
1126
+ );
1127
+ break;
1128
+
1129
+ case "save_successful_flow":
1130
+ result = await saveFlow(
1131
+ validatedArgs.appId,
1132
+ validatedArgs.flowName,
1133
+ validatedArgs.yamlContent,
1134
+ validatedArgs.description
1135
+ );
1136
+ break;
1137
+
1138
+ case "get_saved_flows":
1139
+ result = await getFlows(validatedArgs.appId);
1140
+ break;
1141
+
1142
+ case "delete_flow":
1143
+ result = await removeFlow(validatedArgs.appId, validatedArgs.flowName);
1144
+ break;
1145
+
1146
+ case "get_ai_context":
1147
+ result = await getAIContext(validatedArgs.appId);
1148
+ break;
1149
+
1150
+ case "get_full_context":
1151
+ result = await getAppContext(validatedArgs.appId);
1152
+ break;
1153
+
1154
+ case "clear_app_context":
1155
+ result = await clearContext(validatedArgs.appId);
1156
+ break;
1157
+
1158
+ case "list_app_contexts":
1159
+ result = await listContexts();
1160
+ break;
1161
+
1162
+ // YAML generation tools
1163
+ case "get_yaml_instructions":
1164
+ result = await getYamlInstructions(validatedArgs.appId);
1165
+ break;
1166
+
1167
+ case "validate_yaml_structure":
1168
+ result = await validateYamlBeforeRun(validatedArgs.yamlContent);
1169
+ break;
1170
+
1171
+ case "get_test_pattern":
1172
+ result = await getTestPattern(validatedArgs.patternName);
1173
+ break;
1174
+
1175
+ case "get_screen_analysis_help":
1176
+ result = await getScreenAnalysis();
1177
+ break;
1178
+
1179
+ // Known Issues & Platform Rules
1180
+ case "get_known_issues":
1181
+ result = await getKnownIssues(
1182
+ validatedArgs.platform,
1183
+ validatedArgs.category
1184
+ );
1185
+ break;
1186
+
1187
+ case "get_platform_rules":
1188
+ result = await getPlatformRules(validatedArgs.category);
1189
+ break;
1190
+
1191
+ case "get_generation_rules":
1192
+ result = await getGenerationRules();
1193
+ break;
1194
+
1195
+ // Prompt Analysis & YAML Generation
1196
+ case "validate_prompt":
1197
+ result = await validateAndGenerate(validatedArgs.userPrompt, {
1198
+ appId: validatedArgs.appId,
1199
+ forceGenerate: validatedArgs.forceGenerate,
1200
+ });
1201
+ break;
1202
+
1203
+ case "analyze_prompt":
1204
+ result = await analyzeTestPrompt(
1205
+ validatedArgs.userPrompt,
1206
+ validatedArgs.appId
1207
+ );
1208
+ break;
1209
+
1210
+ // YAML Cache tools
1211
+ case "save_to_cache":
1212
+ result = await saveTestToCache(
1213
+ validatedArgs.prompt,
1214
+ validatedArgs.yaml,
1215
+ validatedArgs.testName,
1216
+ validatedArgs.appId
1217
+ );
1218
+ break;
1219
+
1220
+ case "lookup_cache":
1221
+ result = await lookupCachedTest(
1222
+ validatedArgs.prompt,
1223
+ validatedArgs.appId
1224
+ );
1225
+ break;
1226
+
1227
+ case "list_cache":
1228
+ result = await listCachedTests();
1229
+ break;
1230
+
1231
+ case "clear_cache":
1232
+ result = await clearTestCache();
1233
+ break;
1234
+
1235
+ case "delete_from_cache":
1236
+ result = await deleteCachedTest(validatedArgs.hash);
1237
+ break;
1238
+
1239
+ case "get_cache_stats":
1240
+ result = await getTestCacheStats();
1241
+ break;
1242
+
1243
+ case "run_test_with_cache":
1244
+ result = await runTestWithCache(
1245
+ validatedArgs.prompt,
1246
+ validatedArgs.yaml,
1247
+ validatedArgs.name,
1248
+ {
1249
+ appId: validatedArgs.appId,
1250
+ retries: validatedArgs.retries,
1251
+ }
1252
+ );
1253
+ break;
1254
+
1255
+ default:
1256
+ throw new Error(`Unknown tool: ${name}`);
1257
+ }
1258
+
1259
+ // ============================================
1260
+ // STEP 4: Log successful execution
1261
+ // ============================================
1262
+ const duration = Date.now() - startTime;
1263
+ logSecurityEvent("TOOL_EXECUTION_SUCCESS", {
1264
+ tool: name,
1265
+ duration: `${duration}ms`,
1266
+ });
1267
+
1268
+ return result;
1269
+ } catch (error) {
1270
+ // ============================================
1271
+ // ERROR HANDLING
1272
+ // ============================================
1273
+ const duration = Date.now() - startTime;
1274
+
1275
+ // Handle SecurityError specifically
1276
+ if (error instanceof SecurityError) {
1277
+ logger.error(`Security error in tool: ${name}`, {
1278
+ code: error.code,
1279
+ message: error.message,
1280
+ });
1281
+
1282
+ logSecurityEvent("TOOL_SECURITY_ERROR", {
1283
+ tool: name,
1284
+ code: error.code,
1285
+ duration: `${duration}ms`,
1286
+ });
1287
+
1288
+ return createSecurityErrorResponse(error);
1289
+ }
1290
+
1291
+ // Handle general errors
1292
+ logger.error(`Tool error: ${name}`, { error: error.message });
1293
+
1294
+ logSecurityEvent("TOOL_EXECUTION_ERROR", {
1295
+ tool: name,
1296
+ error: error.message,
1297
+ duration: `${duration}ms`,
1298
+ });
1299
+
1300
+ return {
1301
+ content: [
1302
+ {
1303
+ type: "text",
1304
+ text: JSON.stringify({
1305
+ success: false,
1306
+ error: error.message,
1307
+ }),
1308
+ },
1309
+ ],
1310
+ };
1311
+ }
1312
+ });
1313
+
1314
+ // List available resources (prompt files)
1315
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
1316
+ const result = await listPromptFiles("prompts");
1317
+ const files = JSON.parse(result.content[0].text).files || [];
1318
+
1319
+ return {
1320
+ resources: files.map((file) => ({
1321
+ uri: `prompts://${file}`,
1322
+ name: file,
1323
+ mimeType: "text/plain",
1324
+ description: `Prompt file: ${file}`,
1325
+ })),
1326
+ };
1327
+ });
1328
+
1329
+ // Read a resource
1330
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1331
+ const uri = request.params.uri;
1332
+ const file = uri.replace("prompts://", "");
1333
+ const result = await readPromptFile(`prompts/${file}`);
1334
+
1335
+ return {
1336
+ contents: [
1337
+ {
1338
+ uri,
1339
+ mimeType: "text/plain",
1340
+ text: result.content[0].text,
1341
+ },
1342
+ ],
1343
+ };
1344
+ });
1345
+
1346
+ // ============================================
1347
+ // START SERVER
1348
+ // ============================================
1349
+
1350
+ async function main() {
1351
+ logger.info(`Starting MCP Maestro Mobile AI v${SERVER_VERSION}...`);
1352
+ logger.info("");
1353
+
1354
+ // Log security configuration
1355
+ const securityConfig = getSecurityConfig();
1356
+ logger.info("Security Configuration:", securityConfig);
1357
+ logger.info(
1358
+ ` Safe Mode: ${securityConfig.safeMode ? "ENABLED ✓" : "DISABLED ⚠"}`
1359
+ );
1360
+ logger.info(` Security Mode: ${securityConfig.mode}`);
1361
+ logger.info(
1362
+ ` Security Logging: ${
1363
+ securityConfig.logSecurityEvents ? "ENABLED" : "DISABLED"
1364
+ }`
1365
+ );
1366
+ logger.info("");
1367
+
1368
+ // Validate prerequisites before starting
1369
+ // This will exit with code 2 if critical prerequisites are missing
1370
+ await validatePrerequisites({
1371
+ exitOnError: true,
1372
+ checkDevice: false, // Don't require device at startup
1373
+ });
1374
+
1375
+ logger.info("");
1376
+ logger.info("Prerequisites validated. Starting server...");
1377
+
1378
+ const transport = new StdioServerTransport();
1379
+ await server.connect(transport);
1380
+
1381
+ logger.info(
1382
+ `MCP Maestro Mobile AI server v${SERVER_VERSION} running on stdio`
1383
+ );
1384
+
1385
+ logSecurityEvent("SERVER_STARTED", {
1386
+ version: SERVER_VERSION,
1387
+ safeMode: securityConfig.safeMode,
1388
+ });
1389
+ }
1390
+
1391
+ main().catch((error) => {
1392
+ logger.error("Failed to start server", { error: error.message });
1393
+ process.exit(1);
1394
+ });