mcp-maestro-mobile-ai 1.1.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.
@@ -0,0 +1,194 @@
1
+ /**
2
+ * App Context Tools
3
+ * MCP tools for managing app context/training data
4
+ */
5
+
6
+ import {
7
+ loadAppContext,
8
+ registerElements,
9
+ registerScreen,
10
+ saveSuccessfulFlow,
11
+ getSavedFlows,
12
+ deleteFlow,
13
+ generateAIPromptContext,
14
+ clearAppContext,
15
+ listAppContexts,
16
+ } from "../utils/appContext.js";
17
+
18
+ /**
19
+ * Format result as MCP response
20
+ */
21
+ function formatResponse(result) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify(result, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Register UI elements for better AI generation
34
+ */
35
+ export async function registerAppElements(appId, elements) {
36
+ if (!appId) {
37
+ return formatResponse({
38
+ success: false,
39
+ error: "appId is required",
40
+ });
41
+ }
42
+
43
+ if (!elements || typeof elements !== "object") {
44
+ return formatResponse({
45
+ success: false,
46
+ error: "elements must be an object with element definitions",
47
+ example: {
48
+ loginButton: {
49
+ testId: "login-button",
50
+ type: "button",
51
+ description: "Main login button",
52
+ },
53
+ usernameInput: {
54
+ testId: "username-input",
55
+ accessibilityLabel: "Username",
56
+ type: "textfield",
57
+ },
58
+ },
59
+ });
60
+ }
61
+
62
+ const result = await registerElements(appId, elements);
63
+ return formatResponse(result);
64
+ }
65
+
66
+ /**
67
+ * Register a screen with its elements and actions
68
+ */
69
+ export async function registerAppScreen(appId, screenName, screenData) {
70
+ if (!appId || !screenName) {
71
+ return formatResponse({
72
+ success: false,
73
+ error: "appId and screenName are required",
74
+ });
75
+ }
76
+
77
+ const result = await registerScreen(appId, screenName, screenData);
78
+ return formatResponse(result);
79
+ }
80
+
81
+ /**
82
+ * Save a successful flow for future reference
83
+ * Call this after a test passes to help AI learn patterns
84
+ */
85
+ export async function saveFlow(appId, flowName, yamlContent, description) {
86
+ if (!appId || !flowName || !yamlContent) {
87
+ return formatResponse({
88
+ success: false,
89
+ error: "appId, flowName, and yamlContent are required",
90
+ });
91
+ }
92
+
93
+ const result = await saveSuccessfulFlow(appId, flowName, yamlContent, description);
94
+ return formatResponse(result);
95
+ }
96
+
97
+ /**
98
+ * Get saved flows for an app
99
+ */
100
+ export async function getFlows(appId) {
101
+ if (!appId) {
102
+ return formatResponse({
103
+ success: false,
104
+ error: "appId is required",
105
+ });
106
+ }
107
+
108
+ const result = await getSavedFlows(appId);
109
+ return formatResponse(result);
110
+ }
111
+
112
+ /**
113
+ * Delete a saved flow
114
+ */
115
+ export async function removeFlow(appId, flowName) {
116
+ if (!appId || !flowName) {
117
+ return formatResponse({
118
+ success: false,
119
+ error: "appId and flowName are required",
120
+ });
121
+ }
122
+
123
+ const result = await deleteFlow(appId, flowName);
124
+ return formatResponse(result);
125
+ }
126
+
127
+ /**
128
+ * Get AI prompt context - the formatted context for AI to use
129
+ */
130
+ export async function getAIContext(appId) {
131
+ if (!appId) {
132
+ return formatResponse({
133
+ success: false,
134
+ error: "appId is required",
135
+ });
136
+ }
137
+
138
+ const result = await generateAIPromptContext(appId);
139
+ return formatResponse(result);
140
+ }
141
+
142
+ /**
143
+ * Get the full app context
144
+ */
145
+ export async function getAppContext(appId) {
146
+ if (!appId) {
147
+ return formatResponse({
148
+ success: false,
149
+ error: "appId is required",
150
+ });
151
+ }
152
+
153
+ const context = await loadAppContext(appId);
154
+ return formatResponse({
155
+ success: true,
156
+ context,
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Clear app context
162
+ */
163
+ export async function clearContext(appId) {
164
+ if (!appId) {
165
+ return formatResponse({
166
+ success: false,
167
+ error: "appId is required",
168
+ });
169
+ }
170
+
171
+ const result = await clearAppContext(appId);
172
+ return formatResponse(result);
173
+ }
174
+
175
+ /**
176
+ * List all apps with saved context
177
+ */
178
+ export async function listContexts() {
179
+ const result = await listAppContexts();
180
+ return formatResponse(result);
181
+ }
182
+
183
+ export default {
184
+ registerAppElements,
185
+ registerAppScreen,
186
+ saveFlow,
187
+ getFlows,
188
+ removeFlow,
189
+ getAIContext,
190
+ getAppContext,
191
+ clearContext,
192
+ listContexts,
193
+ };
194
+
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Prompt File Tools
3
+ * Read and list prompt files for test generation
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+ import { logger } from '../utils/logger.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const PROJECT_ROOT = join(__dirname, '../../..');
15
+
16
+ /**
17
+ * Read prompts from a .txt or .md file
18
+ * Each non-empty line (excluding comments) is treated as a prompt
19
+ */
20
+ export async function readPromptFile(filePath) {
21
+ try {
22
+ const fullPath = path.isAbsolute(filePath)
23
+ ? filePath
24
+ : join(PROJECT_ROOT, filePath);
25
+
26
+ logger.info(`Reading prompt file: ${fullPath}`);
27
+
28
+ // Check if file exists
29
+ try {
30
+ await fs.access(fullPath);
31
+ } catch {
32
+ return {
33
+ content: [
34
+ {
35
+ type: 'text',
36
+ text: JSON.stringify({
37
+ success: false,
38
+ error: `File not found: ${filePath}`,
39
+ hint: 'Use list_prompt_files to see available prompt files',
40
+ }),
41
+ },
42
+ ],
43
+ };
44
+ }
45
+
46
+ // Read file content
47
+ const content = await fs.readFile(fullPath, 'utf8');
48
+
49
+ // Parse prompts (one per line, skip empty lines and comments)
50
+ const prompts = content
51
+ .split('\n')
52
+ .map(line => line.trim())
53
+ .filter(line => line.length > 0)
54
+ .filter(line => !line.startsWith('#'))
55
+ .filter(line => !line.startsWith('//'));
56
+
57
+ logger.info(`Found ${prompts.length} prompts in ${filePath}`);
58
+
59
+ return {
60
+ content: [
61
+ {
62
+ type: 'text',
63
+ text: JSON.stringify({
64
+ success: true,
65
+ file: filePath,
66
+ count: prompts.length,
67
+ prompts: prompts,
68
+ instructions: `
69
+ Found ${prompts.length} test prompts. For each prompt, you should:
70
+ 1. Generate a valid Maestro YAML flow based on the prompt
71
+ 2. Use the validate_maestro_yaml tool to verify the YAML
72
+ 3. Use the run_test tool to execute the test
73
+ 4. Report the results back to the user
74
+
75
+ IMPORTANT: When generating Maestro YAML, always include:
76
+ - appId at the top (use get_app_config to get the configured appId)
77
+ - Proper waits using extendedWaitUntil
78
+ - Clear step descriptions
79
+
80
+ Example Maestro YAML format:
81
+ \`\`\`yaml
82
+ appId: com.example.app
83
+ ---
84
+ - launchApp
85
+ - extendedWaitUntil:
86
+ visible: "Element Text"
87
+ timeout: 10000
88
+ - tapOn: "Button Text"
89
+ \`\`\`
90
+ `.trim(),
91
+ }),
92
+ },
93
+ ],
94
+ };
95
+ } catch (error) {
96
+ logger.error('Error reading prompt file', { error: error.message });
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: JSON.stringify({
102
+ success: false,
103
+ error: error.message,
104
+ }),
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * List available prompt files in a directory
113
+ */
114
+ export async function listPromptFiles(directory = 'prompts') {
115
+ try {
116
+ const fullPath = path.isAbsolute(directory)
117
+ ? directory
118
+ : join(PROJECT_ROOT, directory);
119
+
120
+ logger.info(`Listing prompt files in: ${fullPath}`);
121
+
122
+ // Check if directory exists
123
+ try {
124
+ await fs.access(fullPath);
125
+ } catch {
126
+ return {
127
+ content: [
128
+ {
129
+ type: 'text',
130
+ text: JSON.stringify({
131
+ success: true,
132
+ directory: directory,
133
+ files: [],
134
+ message: `Directory "${directory}" does not exist. Create it and add .txt or .md files with test prompts.`,
135
+ }),
136
+ },
137
+ ],
138
+ };
139
+ }
140
+
141
+ // Read directory
142
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
143
+
144
+ // Filter for .txt and .md files
145
+ const promptFiles = entries
146
+ .filter(entry => entry.isFile())
147
+ .filter(entry =>
148
+ entry.name.endsWith('.txt') ||
149
+ entry.name.endsWith('.md')
150
+ )
151
+ .map(entry => entry.name);
152
+
153
+ logger.info(`Found ${promptFiles.length} prompt files`);
154
+
155
+ return {
156
+ content: [
157
+ {
158
+ type: 'text',
159
+ text: JSON.stringify({
160
+ success: true,
161
+ directory: directory,
162
+ files: promptFiles,
163
+ count: promptFiles.length,
164
+ usage: promptFiles.length > 0
165
+ ? `Use read_prompt_file with one of these files to get the test prompts.`
166
+ : `No prompt files found. Create .txt or .md files in the "${directory}" folder.`,
167
+ }),
168
+ },
169
+ ],
170
+ };
171
+ } catch (error) {
172
+ logger.error('Error listing prompt files', { error: error.message });
173
+ return {
174
+ content: [
175
+ {
176
+ type: 'text',
177
+ text: JSON.stringify({
178
+ success: false,
179
+ error: error.message,
180
+ }),
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ }
186
+
187
+ export default {
188
+ readPromptFile,
189
+ listPromptFiles,
190
+ };
191
+