mcp-maestro-mobile-ai 1.3.0 → 1.4.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,816 +1,1059 @@
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
- description: "Array of test results with name, success, duration, error fields",
329
- },
330
- promptFile: {
331
- type: "string",
332
- description: "Name of the prompt file",
333
- },
334
- appId: {
335
- type: "string",
336
- description: "App ID",
337
- },
338
- },
339
- required: ["results"],
340
- },
341
- },
342
- {
343
- name: "list_reports",
344
- description:
345
- "List all generated test reports. Returns paths to HTML and JSON report files.",
346
- inputSchema: {
347
- type: "object",
348
- properties: {},
349
- },
350
- },
351
- {
352
- name: "get_test_results",
353
- description: "Get the results from the last test run or a specific run by ID.",
354
- inputSchema: {
355
- type: "object",
356
- properties: {
357
- runId: {
358
- type: "string",
359
- description: "Optional: Specific run ID to get results for",
360
- },
361
- },
362
- },
363
- },
364
- {
365
- name: "take_screenshot",
366
- description:
367
- "Take a screenshot of the current device screen. Useful for debugging or verification.",
368
- inputSchema: {
369
- type: "object",
370
- properties: {
371
- name: {
372
- type: "string",
373
- description: "Name for the screenshot file",
374
- },
375
- },
376
- required: ["name"],
377
- },
378
- },
379
- {
380
- name: "cleanup_results",
381
- description:
382
- "Clean up old test results and screenshots to free up disk space. Keeps the most recent results.",
383
- inputSchema: {
384
- type: "object",
385
- properties: {
386
- keepLast: {
387
- type: "number",
388
- description: "Number of recent results to keep (default: 50)",
389
- },
390
- deleteScreenshots: {
391
- type: "boolean",
392
- description: "Whether to delete old screenshots (default: true)",
393
- },
394
- },
395
- },
396
- },
397
-
398
- // === App Context/Training Tools ===
399
- {
400
- name: "register_elements",
401
- description:
402
- "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.",
403
- inputSchema: {
404
- type: "object",
405
- properties: {
406
- appId: {
407
- type: "string",
408
- description: "App package ID (e.g., 'com.myapp')",
409
- },
410
- elements: {
411
- type: "object",
412
- description:
413
- "Object containing element definitions. Each key is the element name, value contains: testId, accessibilityLabel, text, type, description",
414
- },
415
- },
416
- required: ["appId", "elements"],
417
- },
418
- },
419
- {
420
- name: "register_screen",
421
- description:
422
- "Register a screen structure for an app. Define what elements and actions are available on each screen.",
423
- inputSchema: {
424
- type: "object",
425
- properties: {
426
- appId: {
427
- type: "string",
428
- description: "App package ID",
429
- },
430
- screenName: {
431
- type: "string",
432
- description: "Name of the screen (e.g., 'LoginScreen', 'Dashboard')",
433
- },
434
- screenData: {
435
- type: "object",
436
- description:
437
- "Screen data including: description, elements (array of element names), actions (array of possible actions)",
438
- },
439
- },
440
- required: ["appId", "screenName", "screenData"],
441
- },
442
- },
443
- {
444
- name: "save_successful_flow",
445
- description:
446
- "Save a successful test flow as a pattern for future reference. Call this after a test passes to help AI learn from successful patterns.",
447
- inputSchema: {
448
- type: "object",
449
- properties: {
450
- appId: {
451
- type: "string",
452
- description: "App package ID",
453
- },
454
- flowName: {
455
- type: "string",
456
- description: "Name for this flow pattern",
457
- },
458
- yamlContent: {
459
- type: "string",
460
- description: "The successful Maestro YAML content",
461
- },
462
- description: {
463
- type: "string",
464
- description: "Optional: Description of what this flow does",
465
- },
466
- },
467
- required: ["appId", "flowName", "yamlContent"],
468
- },
469
- },
470
- {
471
- name: "get_saved_flows",
472
- description:
473
- "Get all saved successful flows for an app. Use these as patterns when generating new tests.",
474
- inputSchema: {
475
- type: "object",
476
- properties: {
477
- appId: {
478
- type: "string",
479
- description: "App package ID",
480
- },
481
- },
482
- required: ["appId"],
483
- },
484
- },
485
- {
486
- name: "delete_flow",
487
- description: "Delete a saved flow pattern.",
488
- inputSchema: {
489
- type: "object",
490
- properties: {
491
- appId: {
492
- type: "string",
493
- description: "App package ID",
494
- },
495
- flowName: {
496
- type: "string",
497
- description: "Name of the flow to delete",
498
- },
499
- },
500
- required: ["appId", "flowName"],
501
- },
502
- },
503
- {
504
- name: "get_ai_context",
505
- description:
506
- "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.",
507
- inputSchema: {
508
- type: "object",
509
- properties: {
510
- appId: {
511
- type: "string",
512
- description: "App package ID",
513
- },
514
- },
515
- required: ["appId"],
516
- },
517
- },
518
- {
519
- name: "get_full_context",
520
- description:
521
- "Get the complete raw app context including all elements, screens, and flows.",
522
- inputSchema: {
523
- type: "object",
524
- properties: {
525
- appId: {
526
- type: "string",
527
- description: "App package ID",
528
- },
529
- },
530
- required: ["appId"],
531
- },
532
- },
533
- {
534
- name: "clear_app_context",
535
- description: "Clear all saved context for an app (elements, screens, flows).",
536
- inputSchema: {
537
- type: "object",
538
- properties: {
539
- appId: {
540
- type: "string",
541
- description: "App package ID",
542
- },
543
- },
544
- required: ["appId"],
545
- },
546
- },
547
- {
548
- name: "list_app_contexts",
549
- description: "List all apps that have saved context data.",
550
- inputSchema: {
551
- type: "object",
552
- properties: {},
553
- },
554
- },
555
-
556
- // === YAML Generation Tools (CRITICAL) ===
557
- {
558
- name: "get_yaml_instructions",
559
- description:
560
- "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.",
561
- inputSchema: {
562
- type: "object",
563
- properties: {
564
- appId: {
565
- type: "string",
566
- description: "App package ID to get app-specific context",
567
- },
568
- },
569
- },
570
- },
571
- {
572
- name: "validate_yaml_structure",
573
- description:
574
- "Validate YAML structure before running a test. Checks for common issues like missing 'tapOn' before 'inputText' which causes text to go to wrong fields.",
575
- inputSchema: {
576
- type: "object",
577
- properties: {
578
- yamlContent: {
579
- type: "string",
580
- description: "The Maestro YAML content to validate",
581
- },
582
- },
583
- required: ["yamlContent"],
584
- },
585
- },
586
- {
587
- name: "get_test_pattern",
588
- description:
589
- "Get a standard test pattern template. Available: login, form, search, navigation, list, settings, logout. Use these as starting points.",
590
- inputSchema: {
591
- type: "object",
592
- properties: {
593
- patternName: {
594
- type: "string",
595
- description: "Pattern name: login, form, search, navigation, list, settings, or logout",
596
- enum: ["login", "form", "search", "navigation", "list", "settings", "logout"],
597
- },
598
- },
599
- required: ["patternName"],
600
- },
601
- },
602
- {
603
- name: "get_screen_analysis_help",
604
- description:
605
- "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.",
606
- inputSchema: {
607
- type: "object",
608
- properties: {},
609
- },
610
- },
611
- ];
612
-
613
- // ============================================
614
- // HANDLERS
615
- // ============================================
616
-
617
- // List available tools
618
- server.setRequestHandler(ListToolsRequestSchema, async () => {
619
- logger.info("Listing available tools");
620
- return { tools: TOOLS };
621
- });
622
-
623
- // Handle tool calls
624
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
625
- const { name, arguments: args } = request.params;
626
- logger.info(`Tool called: ${name}`, { args });
627
-
628
- try {
629
- switch (name) {
630
- // Prompt tools
631
- case "read_prompt_file":
632
- return await readPromptFile(args.file);
633
-
634
- case "list_prompt_files":
635
- return await listPromptFiles(args.directory);
636
-
637
- // Device management tools
638
- case "list_devices":
639
- return await listDevices();
640
-
641
- case "select_device":
642
- return await selectDevice(args.deviceId);
643
-
644
- case "clear_device":
645
- return await clearDevice();
646
-
647
- case "check_device":
648
- return await checkDevice();
649
-
650
- case "check_app":
651
- return await checkApp(args.appId);
652
-
653
- // Config tools
654
- case "get_app_config":
655
- return await getAppConfig();
656
-
657
- // Validation tools
658
- case "validate_maestro_yaml":
659
- return await validateMaestroYaml(args.yaml);
660
-
661
- // Execution tools
662
- case "run_test":
663
- return await runTest(args.yaml, args.name, { retries: args.retries });
664
-
665
- case "run_test_suite":
666
- return await runTestSuite(args.tests, { retries: args.retries });
667
-
668
- // Results & reporting tools
669
- case "run_tests_with_report":
670
- return await runTestSuiteWithReport(args.tests, {
671
- promptFile: args.promptFile,
672
- appId: args.appId,
673
- retries: args.retries,
674
- });
675
-
676
- case "generate_report":
677
- return await generateTestReport(args.results, {
678
- promptFile: args.promptFile,
679
- appId: args.appId,
680
- });
681
-
682
- case "list_reports":
683
- return await listTestReports();
684
-
685
- case "get_test_results":
686
- return await getTestResults(args.runId);
687
-
688
- case "take_screenshot":
689
- return await takeScreenshot(args.name);
690
-
691
- case "cleanup_results":
692
- return await cleanupResults({
693
- keepLast: args.keepLast,
694
- deleteScreenshots: args.deleteScreenshots,
695
- });
696
-
697
- // App context/training tools
698
- case "register_elements":
699
- return await registerAppElements(args.appId, args.elements);
700
-
701
- case "register_screen":
702
- return await registerAppScreen(args.appId, args.screenName, args.screenData);
703
-
704
- case "save_successful_flow":
705
- return await saveFlow(args.appId, args.flowName, args.yamlContent, args.description);
706
-
707
- case "get_saved_flows":
708
- return await getFlows(args.appId);
709
-
710
- case "delete_flow":
711
- return await removeFlow(args.appId, args.flowName);
712
-
713
- case "get_ai_context":
714
- return await getAIContext(args.appId);
715
-
716
- case "get_full_context":
717
- return await getAppContext(args.appId);
718
-
719
- case "clear_app_context":
720
- return await clearContext(args.appId);
721
-
722
- case "list_app_contexts":
723
- return await listContexts();
724
-
725
- // YAML generation tools
726
- case "get_yaml_instructions":
727
- return await getYamlInstructions(args.appId);
728
-
729
- case "validate_yaml_structure":
730
- return await validateYamlBeforeRun(args.yamlContent);
731
-
732
- case "get_test_pattern":
733
- return await getTestPattern(args.patternName);
734
-
735
- case "get_screen_analysis_help":
736
- return await getScreenAnalysis();
737
-
738
- default:
739
- throw new Error(`Unknown tool: ${name}`);
740
- }
741
- } catch (error) {
742
- logger.error(`Tool error: ${name}`, { error: error.message });
743
- return {
744
- content: [
745
- {
746
- type: "text",
747
- text: JSON.stringify({
748
- success: false,
749
- error: error.message,
750
- }),
751
- },
752
- ],
753
- };
754
- }
755
- });
756
-
757
- // List available resources (prompt files)
758
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
759
- const result = await listPromptFiles("prompts");
760
- const files = JSON.parse(result.content[0].text).files || [];
761
-
762
- return {
763
- resources: files.map((file) => ({
764
- uri: `prompts://${file}`,
765
- name: file,
766
- mimeType: "text/plain",
767
- description: `Prompt file: ${file}`,
768
- })),
769
- };
770
- });
771
-
772
- // Read a resource
773
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
774
- const uri = request.params.uri;
775
- const file = uri.replace("prompts://", "");
776
- const result = await readPromptFile(`prompts/${file}`);
777
-
778
- return {
779
- contents: [
780
- {
781
- uri,
782
- mimeType: "text/plain",
783
- text: result.content[0].text,
784
- },
785
- ],
786
- };
787
- });
788
-
789
- // ============================================
790
- // START SERVER
791
- // ============================================
792
-
793
- async function main() {
794
- logger.info("Starting MCP Maestro Mobile AI v1.2.0...");
795
- logger.info("");
796
-
797
- // Validate prerequisites before starting
798
- // This will exit with code 2 if critical prerequisites are missing
799
- await validatePrerequisites({
800
- exitOnError: true,
801
- checkDevice: false, // Don't require device at startup
802
- });
803
-
804
- logger.info("");
805
- logger.info("Prerequisites validated. Starting server...");
806
-
807
- const transport = new StdioServerTransport();
808
- await server.connect(transport);
809
-
810
- logger.info("MCP Maestro Mobile AI server running on stdio");
811
- }
812
-
813
- main().catch((error) => {
814
- logger.error("Failed to start server", { error: error.message });
815
- process.exit(1);
816
- });
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
+ } from "./tools/runTools.js";
36
+ import {
37
+ getAppConfig,
38
+ getTestResults,
39
+ takeScreenshot,
40
+ checkDevice,
41
+ checkApp,
42
+ cleanupResults,
43
+ listDevices,
44
+ selectDevice,
45
+ clearDevice,
46
+ } from "./tools/utilityTools.js";
47
+ import {
48
+ registerAppElements,
49
+ registerAppScreen,
50
+ saveFlow,
51
+ getFlows,
52
+ removeFlow,
53
+ getAIContext,
54
+ getAppContext,
55
+ clearContext,
56
+ listContexts,
57
+ getYamlInstructions,
58
+ validateYamlBeforeRun,
59
+ getTestPattern,
60
+ getScreenAnalysis,
61
+ } from "./tools/contextTools.js";
62
+ import { logger } from "./utils/logger.js";
63
+ import { validatePrerequisites } from "./utils/prerequisites.js";
64
+
65
+ // Import security and validation
66
+ import {
67
+ SecurityError,
68
+ isSafeModeEnabled,
69
+ getSecurityConfig,
70
+ logSecurityEvent,
71
+ } from "./utils/security.js";
72
+ import { validateToolInput } from "./schemas/toolSchemas.js";
73
+
74
+ // Load environment variables
75
+ const __filename = fileURLToPath(import.meta.url);
76
+ const __dirname = dirname(__filename);
77
+ config({ path: join(__dirname, "../../.env") });
78
+
79
+ // Server version - updated for security features
80
+ const SERVER_VERSION = "1.4.0";
81
+
82
+ // Create MCP Server
83
+ const server = new Server(
84
+ {
85
+ name: "mcp-maestro-mobile-ai",
86
+ version: SERVER_VERSION,
87
+ },
88
+ {
89
+ capabilities: {
90
+ tools: {},
91
+ resources: {},
92
+ },
93
+ }
94
+ );
95
+
96
+ // ============================================
97
+ // TOOL DEFINITIONS
98
+ // ============================================
99
+
100
+ const TOOLS = [
101
+ // === Prompt File Tools ===
102
+ {
103
+ name: "read_prompt_file",
104
+ description:
105
+ "Read test prompts from a .txt or .md file. Each line in the file is treated as a separate test case prompt.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ file: {
110
+ type: "string",
111
+ description:
112
+ 'Path to the prompt file (e.g., "prompts/login-tests.txt")',
113
+ },
114
+ },
115
+ required: ["file"],
116
+ },
117
+ },
118
+ {
119
+ name: "list_prompt_files",
120
+ description: "List all available prompt files in the prompts directory.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ directory: {
125
+ type: "string",
126
+ description:
127
+ 'Directory to search for prompt files (default: "prompts")',
128
+ },
129
+ },
130
+ },
131
+ },
132
+
133
+ // === Device Management Tools ===
134
+ {
135
+ name: "list_devices",
136
+ description:
137
+ "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.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {},
141
+ },
142
+ },
143
+ {
144
+ name: "select_device",
145
+ description:
146
+ "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.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ deviceId: {
151
+ type: "string",
152
+ description:
153
+ 'Device ID to select (e.g., "emulator-5554" or "RF8M12345XY" for physical device)',
154
+ },
155
+ },
156
+ required: ["deviceId"],
157
+ },
158
+ },
159
+ {
160
+ name: "clear_device",
161
+ description:
162
+ "Clear the device selection. Tests will run on the first available device (default behavior).",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {},
166
+ },
167
+ },
168
+ {
169
+ name: "check_device",
170
+ description:
171
+ "Check if an Android emulator or device is connected. Shows connection status and which device is selected.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {},
175
+ },
176
+ },
177
+ {
178
+ name: "check_app",
179
+ description:
180
+ "Check if the target app is installed on the connected device. Verifies the app is ready for testing.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ appId: {
185
+ type: "string",
186
+ description:
187
+ "Optional: App package ID to check. Uses configured APP_ID if not provided.",
188
+ },
189
+ },
190
+ },
191
+ },
192
+
193
+ // === Configuration Tools ===
194
+ {
195
+ name: "get_app_config",
196
+ description:
197
+ "Get the current app configuration including appId, platform, selected device, and other settings.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {},
201
+ },
202
+ },
203
+
204
+ // === Validation Tools ===
205
+ {
206
+ name: "validate_maestro_yaml",
207
+ description:
208
+ "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!",
209
+ inputSchema: {
210
+ type: "object",
211
+ properties: {
212
+ yaml: {
213
+ type: "string",
214
+ description: "The Maestro YAML content to validate",
215
+ },
216
+ },
217
+ required: ["yaml"],
218
+ },
219
+ },
220
+
221
+ // === Test Execution Tools ===
222
+ {
223
+ name: "run_test",
224
+ description: `Run a single Maestro test. IMPORTANT: The YAML MUST follow these rules or it will be REJECTED:
225
+
226
+ 1. STRUCTURE: Must start with appId, then clearState, then launchApp
227
+ 2. TEXT INPUT: ALWAYS use tapOn BEFORE inputText (or text goes to wrong field!)
228
+ CORRECT: - tapOn: "Username" then - inputText: "value"
229
+ WRONG: - inputText: "value" (missing tapOn!)
230
+ 3. Use visible text labels for elements when testIDs are unknown
231
+
232
+ Example valid YAML:
233
+ appId: com.example.app
234
+ ---
235
+ - clearState
236
+ - launchApp
237
+ - tapOn: "Username"
238
+ - inputText: "user@example.com"
239
+ - tapOn: "Password"
240
+ - inputText: "password123"
241
+ - tapOn: "Sign In"
242
+ - assertVisible: "Welcome"`,
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {
246
+ yaml: {
247
+ type: "string",
248
+ description:
249
+ "The Maestro YAML flow content. MUST use tapOn before inputText for each field!",
250
+ },
251
+ name: {
252
+ type: "string",
253
+ description:
254
+ "Name for this test (used for reporting and screenshots)",
255
+ },
256
+ retries: {
257
+ type: "number",
258
+ description:
259
+ "Optional: Number of retries if test fails (default: from config or 0)",
260
+ },
261
+ },
262
+ required: ["yaml", "name"],
263
+ },
264
+ },
265
+ {
266
+ name: "run_test_suite",
267
+ description:
268
+ "Run multiple Maestro tests in sequence. Each YAML must follow the rules: appId at top, clearState, launchApp, and ALWAYS tapOn before inputText!",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ tests: {
273
+ type: "array",
274
+ items: {
275
+ type: "object",
276
+ properties: {
277
+ yaml: {
278
+ type: "string",
279
+ description:
280
+ "The Maestro YAML. MUST use tapOn before inputText!",
281
+ },
282
+ name: {
283
+ type: "string",
284
+ description: "Name for this test",
285
+ },
286
+ },
287
+ required: ["yaml", "name"],
288
+ },
289
+ description: "Array of tests to run",
290
+ },
291
+ retries: {
292
+ type: "number",
293
+ description:
294
+ "Optional: Number of retries for failed tests (default: from config or 0)",
295
+ },
296
+ },
297
+ required: ["tests"],
298
+ },
299
+ },
300
+
301
+ // === Results & Reporting Tools ===
302
+ {
303
+ name: "run_tests_with_report",
304
+ description:
305
+ "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.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ tests: {
310
+ type: "array",
311
+ items: {
312
+ type: "object",
313
+ properties: {
314
+ yaml: {
315
+ type: "string",
316
+ description:
317
+ "The Maestro YAML. MUST use tapOn before inputText!",
318
+ },
319
+ name: {
320
+ type: "string",
321
+ description: "Name for this test",
322
+ },
323
+ },
324
+ required: ["yaml", "name"],
325
+ },
326
+ description: "Array of tests to run",
327
+ },
328
+ promptFile: {
329
+ type: "string",
330
+ description: "Name of the prompt file (for report metadata)",
331
+ },
332
+ appId: {
333
+ type: "string",
334
+ description: "App ID (for report metadata)",
335
+ },
336
+ retries: {
337
+ type: "number",
338
+ description: "Number of retries for failed tests",
339
+ },
340
+ },
341
+ required: ["tests"],
342
+ },
343
+ },
344
+ {
345
+ name: "generate_report",
346
+ description:
347
+ "Generate HTML and JSON report from test results. Call this after running tests to create a visual summary report.",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {
351
+ results: {
352
+ type: "array",
353
+ items: {
354
+ type: "object",
355
+ properties: {
356
+ name: { type: "string", description: "Test name" },
357
+ success: {
358
+ type: "boolean",
359
+ description: "Whether the test passed",
360
+ },
361
+ duration: {
362
+ type: "number",
363
+ description: "Test duration in milliseconds",
364
+ },
365
+ error: {
366
+ type: "string",
367
+ description: "Error message if test failed",
368
+ },
369
+ },
370
+ required: ["name", "success"],
371
+ },
372
+ description:
373
+ "Array of test results with name, success, duration, error fields",
374
+ },
375
+ promptFile: {
376
+ type: "string",
377
+ description: "Name of the prompt file",
378
+ },
379
+ appId: {
380
+ type: "string",
381
+ description: "App ID",
382
+ },
383
+ },
384
+ required: ["results"],
385
+ },
386
+ },
387
+ {
388
+ name: "list_reports",
389
+ description:
390
+ "List all generated test reports. Returns paths to HTML and JSON report files.",
391
+ inputSchema: {
392
+ type: "object",
393
+ properties: {},
394
+ },
395
+ },
396
+ {
397
+ name: "get_test_results",
398
+ description:
399
+ "Get the results from the last test run or a specific run by ID.",
400
+ inputSchema: {
401
+ type: "object",
402
+ properties: {
403
+ runId: {
404
+ type: "string",
405
+ description: "Optional: Specific run ID to get results for",
406
+ },
407
+ },
408
+ },
409
+ },
410
+ {
411
+ name: "take_screenshot",
412
+ description:
413
+ "Take a screenshot of the current device screen. Useful for debugging or verification.",
414
+ inputSchema: {
415
+ type: "object",
416
+ properties: {
417
+ name: {
418
+ type: "string",
419
+ description: "Name for the screenshot file",
420
+ },
421
+ },
422
+ required: ["name"],
423
+ },
424
+ },
425
+ {
426
+ name: "cleanup_results",
427
+ description:
428
+ "Clean up old test results and screenshots to free up disk space. Keeps the most recent results.",
429
+ inputSchema: {
430
+ type: "object",
431
+ properties: {
432
+ keepLast: {
433
+ type: "number",
434
+ description: "Number of recent results to keep (default: 50)",
435
+ },
436
+ deleteScreenshots: {
437
+ type: "boolean",
438
+ description: "Whether to delete old screenshots (default: true)",
439
+ },
440
+ },
441
+ },
442
+ },
443
+
444
+ // === App Context/Training Tools ===
445
+ {
446
+ name: "register_elements",
447
+ description:
448
+ "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.",
449
+ inputSchema: {
450
+ type: "object",
451
+ properties: {
452
+ appId: {
453
+ type: "string",
454
+ description: "App package ID (e.g., 'com.myapp')",
455
+ },
456
+ elements: {
457
+ type: "object",
458
+ description:
459
+ "Object containing element definitions. Each key is the element name, value contains: testId, accessibilityLabel, text, type, description",
460
+ },
461
+ },
462
+ required: ["appId", "elements"],
463
+ },
464
+ },
465
+ {
466
+ name: "register_screen",
467
+ description:
468
+ "Register a screen structure for an app. Define what elements and actions are available on each screen.",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ appId: {
473
+ type: "string",
474
+ description: "App package ID",
475
+ },
476
+ screenName: {
477
+ type: "string",
478
+ description: "Name of the screen (e.g., 'LoginScreen', 'Dashboard')",
479
+ },
480
+ screenData: {
481
+ type: "object",
482
+ description:
483
+ "Screen data including: description, elements (array of element names), actions (array of possible actions)",
484
+ },
485
+ },
486
+ required: ["appId", "screenName", "screenData"],
487
+ },
488
+ },
489
+ {
490
+ name: "save_successful_flow",
491
+ description:
492
+ "Save a successful test flow as a pattern for future reference. Call this after a test passes to help AI learn from successful patterns.",
493
+ inputSchema: {
494
+ type: "object",
495
+ properties: {
496
+ appId: {
497
+ type: "string",
498
+ description: "App package ID",
499
+ },
500
+ flowName: {
501
+ type: "string",
502
+ description: "Name for this flow pattern",
503
+ },
504
+ yamlContent: {
505
+ type: "string",
506
+ description: "The successful Maestro YAML content",
507
+ },
508
+ description: {
509
+ type: "string",
510
+ description: "Optional: Description of what this flow does",
511
+ },
512
+ },
513
+ required: ["appId", "flowName", "yamlContent"],
514
+ },
515
+ },
516
+ {
517
+ name: "get_saved_flows",
518
+ description:
519
+ "Get all saved successful flows for an app. Use these as patterns when generating new tests.",
520
+ inputSchema: {
521
+ type: "object",
522
+ properties: {
523
+ appId: {
524
+ type: "string",
525
+ description: "App package ID",
526
+ },
527
+ },
528
+ required: ["appId"],
529
+ },
530
+ },
531
+ {
532
+ name: "delete_flow",
533
+ description: "Delete a saved flow pattern.",
534
+ inputSchema: {
535
+ type: "object",
536
+ properties: {
537
+ appId: {
538
+ type: "string",
539
+ description: "App package ID",
540
+ },
541
+ flowName: {
542
+ type: "string",
543
+ description: "Name of the flow to delete",
544
+ },
545
+ },
546
+ required: ["appId", "flowName"],
547
+ },
548
+ },
549
+ {
550
+ name: "get_ai_context",
551
+ description:
552
+ "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.",
553
+ inputSchema: {
554
+ type: "object",
555
+ properties: {
556
+ appId: {
557
+ type: "string",
558
+ description: "App package ID",
559
+ },
560
+ },
561
+ required: ["appId"],
562
+ },
563
+ },
564
+ {
565
+ name: "get_full_context",
566
+ description:
567
+ "Get the complete raw app context including all elements, screens, and flows.",
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: "clear_app_context",
581
+ description:
582
+ "Clear all saved context for an app (elements, screens, 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: "list_app_contexts",
596
+ description: "List all apps that have saved context data.",
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {},
600
+ },
601
+ },
602
+
603
+ // === YAML Generation Tools (CRITICAL) ===
604
+ {
605
+ name: "get_yaml_instructions",
606
+ description:
607
+ "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.",
608
+ inputSchema: {
609
+ type: "object",
610
+ properties: {
611
+ appId: {
612
+ type: "string",
613
+ description: "App package ID to get app-specific context",
614
+ },
615
+ },
616
+ },
617
+ },
618
+ {
619
+ name: "validate_yaml_structure",
620
+ description:
621
+ "Validate YAML structure before running a test. Checks for common issues like missing 'tapOn' before 'inputText' which causes text to go to wrong fields.",
622
+ inputSchema: {
623
+ type: "object",
624
+ properties: {
625
+ yamlContent: {
626
+ type: "string",
627
+ description: "The Maestro YAML content to validate",
628
+ },
629
+ },
630
+ required: ["yamlContent"],
631
+ },
632
+ },
633
+ {
634
+ name: "get_test_pattern",
635
+ description:
636
+ "Get a standard test pattern template. Available: login, form, search, navigation, list, settings, logout. Use these as starting points.",
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ patternName: {
641
+ type: "string",
642
+ description:
643
+ "Pattern name: login, form, search, navigation, list, settings, or logout",
644
+ enum: [
645
+ "login",
646
+ "form",
647
+ "search",
648
+ "navigation",
649
+ "list",
650
+ "settings",
651
+ "logout",
652
+ ],
653
+ },
654
+ },
655
+ required: ["patternName"],
656
+ },
657
+ },
658
+ {
659
+ name: "get_screen_analysis_help",
660
+ description:
661
+ "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.",
662
+ inputSchema: {
663
+ type: "object",
664
+ properties: {},
665
+ },
666
+ },
667
+ ];
668
+
669
+ // ============================================
670
+ // HANDLERS
671
+ // ============================================
672
+
673
+ // List available tools
674
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
675
+ logger.info("Listing available tools");
676
+ return { tools: TOOLS };
677
+ });
678
+
679
+ // ============================================
680
+ // VALIDATION MIDDLEWARE
681
+ // ============================================
682
+
683
+ /**
684
+ * Create a validation error response
685
+ */
686
+ function createValidationErrorResponse(toolName, validationResult) {
687
+ logSecurityEvent("TOOL_VALIDATION_FAILED", {
688
+ tool: toolName,
689
+ errors: validationResult.errors,
690
+ });
691
+
692
+ return {
693
+ content: [
694
+ {
695
+ type: "text",
696
+ text: JSON.stringify({
697
+ success: false,
698
+ error: "Input validation failed",
699
+ validationErrors: validationResult.errors,
700
+ message: validationResult.message,
701
+ hint: "Please check the input parameters and try again.",
702
+ }),
703
+ },
704
+ ],
705
+ };
706
+ }
707
+
708
+ /**
709
+ * Create a security error response
710
+ */
711
+ function createSecurityErrorResponse(error) {
712
+ return {
713
+ content: [
714
+ {
715
+ type: "text",
716
+ text: JSON.stringify({
717
+ success: false,
718
+ error: "Security violation",
719
+ code: error.code,
720
+ message: error.message,
721
+ details: error.details,
722
+ }),
723
+ },
724
+ ],
725
+ };
726
+ }
727
+
728
+ // Handle tool calls
729
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
730
+ const { name, arguments: args } = request.params;
731
+ const startTime = Date.now();
732
+
733
+ logger.info(`Tool called: ${name}`, { args });
734
+
735
+ // ============================================
736
+ // STEP 1: Validate input with Zod schema
737
+ // ============================================
738
+ const validationResult = validateToolInput(name, args);
739
+
740
+ if (!validationResult.success) {
741
+ logger.warn(`Validation failed for tool: ${name}`, {
742
+ errors: validationResult.errors,
743
+ });
744
+ return createValidationErrorResponse(name, validationResult);
745
+ }
746
+
747
+ // Use validated/sanitized data
748
+ const validatedArgs = validationResult.data;
749
+
750
+ // ============================================
751
+ // STEP 2: Log security event for audit trail
752
+ // ============================================
753
+ logSecurityEvent("TOOL_EXECUTION_START", {
754
+ tool: name,
755
+ safeMode: isSafeModeEnabled(),
756
+ hasArgs: Object.keys(validatedArgs || {}).length > 0,
757
+ });
758
+
759
+ try {
760
+ // ============================================
761
+ // STEP 3: Execute tool with validated args
762
+ // ============================================
763
+ let result;
764
+
765
+ switch (name) {
766
+ // Prompt tools
767
+ case "read_prompt_file":
768
+ result = await readPromptFile(validatedArgs.file);
769
+ break;
770
+
771
+ case "list_prompt_files":
772
+ result = await listPromptFiles(validatedArgs.directory);
773
+ break;
774
+
775
+ // Device management tools
776
+ case "list_devices":
777
+ result = await listDevices();
778
+ break;
779
+
780
+ case "select_device":
781
+ result = await selectDevice(validatedArgs.deviceId);
782
+ break;
783
+
784
+ case "clear_device":
785
+ result = await clearDevice();
786
+ break;
787
+
788
+ case "check_device":
789
+ result = await checkDevice();
790
+ break;
791
+
792
+ case "check_app":
793
+ result = await checkApp(validatedArgs.appId);
794
+ break;
795
+
796
+ // Config tools
797
+ case "get_app_config":
798
+ result = await getAppConfig();
799
+ break;
800
+
801
+ // Validation tools
802
+ case "validate_maestro_yaml":
803
+ result = await validateMaestroYaml(validatedArgs.yaml);
804
+ break;
805
+
806
+ // Execution tools
807
+ case "run_test":
808
+ result = await runTest(validatedArgs.yaml, validatedArgs.name, {
809
+ retries: validatedArgs.retries,
810
+ });
811
+ break;
812
+
813
+ case "run_test_suite":
814
+ result = await runTestSuite(validatedArgs.tests, {
815
+ retries: validatedArgs.retries,
816
+ });
817
+ break;
818
+
819
+ // Results & reporting tools
820
+ case "run_tests_with_report":
821
+ result = await runTestSuiteWithReport(validatedArgs.tests, {
822
+ promptFile: validatedArgs.promptFile,
823
+ appId: validatedArgs.appId,
824
+ retries: validatedArgs.retries,
825
+ });
826
+ break;
827
+
828
+ case "generate_report":
829
+ result = await generateTestReport(validatedArgs.results, {
830
+ promptFile: validatedArgs.promptFile,
831
+ appId: validatedArgs.appId,
832
+ });
833
+ break;
834
+
835
+ case "list_reports":
836
+ result = await listTestReports();
837
+ break;
838
+
839
+ case "get_test_results":
840
+ result = await getTestResults(validatedArgs.runId);
841
+ break;
842
+
843
+ case "take_screenshot":
844
+ result = await takeScreenshot(validatedArgs.name);
845
+ break;
846
+
847
+ case "cleanup_results":
848
+ result = await cleanupResults({
849
+ keepLast: validatedArgs.keepLast,
850
+ deleteScreenshots: validatedArgs.deleteScreenshots,
851
+ });
852
+ break;
853
+
854
+ // App context/training tools
855
+ case "register_elements":
856
+ result = await registerAppElements(
857
+ validatedArgs.appId,
858
+ validatedArgs.elements
859
+ );
860
+ break;
861
+
862
+ case "register_screen":
863
+ result = await registerAppScreen(
864
+ validatedArgs.appId,
865
+ validatedArgs.screenName,
866
+ validatedArgs.screenData
867
+ );
868
+ break;
869
+
870
+ case "save_successful_flow":
871
+ result = await saveFlow(
872
+ validatedArgs.appId,
873
+ validatedArgs.flowName,
874
+ validatedArgs.yamlContent,
875
+ validatedArgs.description
876
+ );
877
+ break;
878
+
879
+ case "get_saved_flows":
880
+ result = await getFlows(validatedArgs.appId);
881
+ break;
882
+
883
+ case "delete_flow":
884
+ result = await removeFlow(validatedArgs.appId, validatedArgs.flowName);
885
+ break;
886
+
887
+ case "get_ai_context":
888
+ result = await getAIContext(validatedArgs.appId);
889
+ break;
890
+
891
+ case "get_full_context":
892
+ result = await getAppContext(validatedArgs.appId);
893
+ break;
894
+
895
+ case "clear_app_context":
896
+ result = await clearContext(validatedArgs.appId);
897
+ break;
898
+
899
+ case "list_app_contexts":
900
+ result = await listContexts();
901
+ break;
902
+
903
+ // YAML generation tools
904
+ case "get_yaml_instructions":
905
+ result = await getYamlInstructions(validatedArgs.appId);
906
+ break;
907
+
908
+ case "validate_yaml_structure":
909
+ result = await validateYamlBeforeRun(validatedArgs.yamlContent);
910
+ break;
911
+
912
+ case "get_test_pattern":
913
+ result = await getTestPattern(validatedArgs.patternName);
914
+ break;
915
+
916
+ case "get_screen_analysis_help":
917
+ result = await getScreenAnalysis();
918
+ break;
919
+
920
+ default:
921
+ throw new Error(`Unknown tool: ${name}`);
922
+ }
923
+
924
+ // ============================================
925
+ // STEP 4: Log successful execution
926
+ // ============================================
927
+ const duration = Date.now() - startTime;
928
+ logSecurityEvent("TOOL_EXECUTION_SUCCESS", {
929
+ tool: name,
930
+ duration: `${duration}ms`,
931
+ });
932
+
933
+ return result;
934
+ } catch (error) {
935
+ // ============================================
936
+ // ERROR HANDLING
937
+ // ============================================
938
+ const duration = Date.now() - startTime;
939
+
940
+ // Handle SecurityError specifically
941
+ if (error instanceof SecurityError) {
942
+ logger.error(`Security error in tool: ${name}`, {
943
+ code: error.code,
944
+ message: error.message,
945
+ });
946
+
947
+ logSecurityEvent("TOOL_SECURITY_ERROR", {
948
+ tool: name,
949
+ code: error.code,
950
+ duration: `${duration}ms`,
951
+ });
952
+
953
+ return createSecurityErrorResponse(error);
954
+ }
955
+
956
+ // Handle general errors
957
+ logger.error(`Tool error: ${name}`, { error: error.message });
958
+
959
+ logSecurityEvent("TOOL_EXECUTION_ERROR", {
960
+ tool: name,
961
+ error: error.message,
962
+ duration: `${duration}ms`,
963
+ });
964
+
965
+ return {
966
+ content: [
967
+ {
968
+ type: "text",
969
+ text: JSON.stringify({
970
+ success: false,
971
+ error: error.message,
972
+ }),
973
+ },
974
+ ],
975
+ };
976
+ }
977
+ });
978
+
979
+ // List available resources (prompt files)
980
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
981
+ const result = await listPromptFiles("prompts");
982
+ const files = JSON.parse(result.content[0].text).files || [];
983
+
984
+ return {
985
+ resources: files.map((file) => ({
986
+ uri: `prompts://${file}`,
987
+ name: file,
988
+ mimeType: "text/plain",
989
+ description: `Prompt file: ${file}`,
990
+ })),
991
+ };
992
+ });
993
+
994
+ // Read a resource
995
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
996
+ const uri = request.params.uri;
997
+ const file = uri.replace("prompts://", "");
998
+ const result = await readPromptFile(`prompts/${file}`);
999
+
1000
+ return {
1001
+ contents: [
1002
+ {
1003
+ uri,
1004
+ mimeType: "text/plain",
1005
+ text: result.content[0].text,
1006
+ },
1007
+ ],
1008
+ };
1009
+ });
1010
+
1011
+ // ============================================
1012
+ // START SERVER
1013
+ // ============================================
1014
+
1015
+ async function main() {
1016
+ logger.info(`Starting MCP Maestro Mobile AI v${SERVER_VERSION}...`);
1017
+ logger.info("");
1018
+
1019
+ // Log security configuration
1020
+ const securityConfig = getSecurityConfig();
1021
+ logger.info("Security Configuration:", securityConfig);
1022
+ logger.info(
1023
+ ` Safe Mode: ${securityConfig.safeMode ? "ENABLED ✓" : "DISABLED ⚠"}`
1024
+ );
1025
+ logger.info(` Security Mode: ${securityConfig.mode}`);
1026
+ logger.info(
1027
+ ` Security Logging: ${
1028
+ securityConfig.logSecurityEvents ? "ENABLED" : "DISABLED"
1029
+ }`
1030
+ );
1031
+ logger.info("");
1032
+
1033
+ // Validate prerequisites before starting
1034
+ // This will exit with code 2 if critical prerequisites are missing
1035
+ await validatePrerequisites({
1036
+ exitOnError: true,
1037
+ checkDevice: false, // Don't require device at startup
1038
+ });
1039
+
1040
+ logger.info("");
1041
+ logger.info("Prerequisites validated. Starting server...");
1042
+
1043
+ const transport = new StdioServerTransport();
1044
+ await server.connect(transport);
1045
+
1046
+ logger.info(
1047
+ `MCP Maestro Mobile AI server v${SERVER_VERSION} running on stdio`
1048
+ );
1049
+
1050
+ logSecurityEvent("SERVER_STARTED", {
1051
+ version: SERVER_VERSION,
1052
+ safeMode: securityConfig.safeMode,
1053
+ });
1054
+ }
1055
+
1056
+ main().catch((error) => {
1057
+ logger.error("Failed to start server", { error: error.message });
1058
+ process.exit(1);
1059
+ });