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.
- package/CHANGELOG.md +114 -0
- package/CONTRIBUTING.md +417 -0
- package/LICENSE +22 -0
- package/README.md +719 -0
- package/ROADMAP.md +239 -0
- package/docs/ENTERPRISE_READINESS.md +545 -0
- package/docs/MCP_SETUP.md +180 -0
- package/docs/PRIVACY.md +198 -0
- package/docs/REACT_NATIVE_AUTOMATION_GUIDELINES.md +584 -0
- package/docs/SECURITY.md +573 -0
- package/package.json +69 -0
- package/prompts/example-login-tests.txt +9 -0
- package/prompts/example-youtube-tests.txt +8 -0
- package/src/mcp-server/index.js +625 -0
- package/src/mcp-server/tools/contextTools.js +194 -0
- package/src/mcp-server/tools/promptTools.js +191 -0
- package/src/mcp-server/tools/runTools.js +357 -0
- package/src/mcp-server/tools/utilityTools.js +721 -0
- package/src/mcp-server/tools/validateTools.js +220 -0
- package/src/mcp-server/utils/appContext.js +295 -0
- package/src/mcp-server/utils/logger.js +52 -0
- package/src/mcp-server/utils/maestro.js +508 -0
- package/templates/mcp-config-claude-desktop.json +15 -0
- package/templates/mcp-config-cursor.json +15 -0
- package/templates/mcp-config-vscode.json +13 -0
|
@@ -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
|
+
|