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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Tools
|
|
3
|
+
* Validate Maestro YAML flows before execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate Maestro YAML syntax and structure
|
|
11
|
+
*/
|
|
12
|
+
export async function validateMaestroYaml(yamlContent) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Check if content is empty
|
|
18
|
+
if (!yamlContent || yamlContent.trim().length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
valid: false,
|
|
25
|
+
errors: ['YAML content is empty'],
|
|
26
|
+
warnings: [],
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Split by document separator (---)
|
|
34
|
+
const parts = yamlContent.split(/^---$/m);
|
|
35
|
+
let configPart = '';
|
|
36
|
+
let flowPart = '';
|
|
37
|
+
|
|
38
|
+
if (parts.length >= 2) {
|
|
39
|
+
configPart = parts[0];
|
|
40
|
+
flowPart = parts.slice(1).join('---');
|
|
41
|
+
} else {
|
|
42
|
+
flowPart = yamlContent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse config section
|
|
46
|
+
let config = {};
|
|
47
|
+
if (configPart.trim()) {
|
|
48
|
+
try {
|
|
49
|
+
config = yaml.load(configPart) || {};
|
|
50
|
+
} catch (e) {
|
|
51
|
+
errors.push(`Invalid YAML in config section: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for appId
|
|
56
|
+
if (!config.appId) {
|
|
57
|
+
errors.push('Missing required "appId" in config section. Example: appId: com.example.app');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse flow section
|
|
61
|
+
let flow = [];
|
|
62
|
+
if (flowPart.trim()) {
|
|
63
|
+
try {
|
|
64
|
+
flow = yaml.load(flowPart) || [];
|
|
65
|
+
} catch (e) {
|
|
66
|
+
errors.push(`Invalid YAML in flow section: ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate flow is an array
|
|
71
|
+
if (!Array.isArray(flow)) {
|
|
72
|
+
errors.push('Flow section must be an array of steps');
|
|
73
|
+
} else if (flow.length === 0) {
|
|
74
|
+
errors.push('Flow has no steps defined');
|
|
75
|
+
} else {
|
|
76
|
+
// Validate each step
|
|
77
|
+
flow.forEach((step, index) => {
|
|
78
|
+
validateStep(step, index, errors, warnings);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isValid = errors.length === 0;
|
|
83
|
+
|
|
84
|
+
logger.info(`YAML validation: ${isValid ? 'passed' : 'failed'}`, {
|
|
85
|
+
errors: errors.length,
|
|
86
|
+
warnings: warnings.length,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
valid: isValid,
|
|
95
|
+
errors,
|
|
96
|
+
warnings,
|
|
97
|
+
config: isValid ? config : undefined,
|
|
98
|
+
stepCount: Array.isArray(flow) ? flow.length : 0,
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.error('Validation error', { error: error.message });
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: JSON.stringify({
|
|
110
|
+
valid: false,
|
|
111
|
+
errors: [`Validation error: ${error.message}`],
|
|
112
|
+
warnings: [],
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate a single Maestro step
|
|
122
|
+
*/
|
|
123
|
+
function validateStep(step, index, errors, warnings) {
|
|
124
|
+
if (typeof step === 'string') {
|
|
125
|
+
// Simple command like "launchApp" or "- back"
|
|
126
|
+
const validSimpleCommands = [
|
|
127
|
+
'launchApp',
|
|
128
|
+
'stopApp',
|
|
129
|
+
'clearState',
|
|
130
|
+
'clearKeychain',
|
|
131
|
+
'back',
|
|
132
|
+
'home',
|
|
133
|
+
'hideKeyboard',
|
|
134
|
+
'scroll',
|
|
135
|
+
'scrollUntilVisible',
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
if (!validSimpleCommands.includes(step)) {
|
|
139
|
+
warnings.push(`Step ${index + 1}: "${step}" may not be a valid simple command`);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof step !== 'object' || step === null) {
|
|
145
|
+
errors.push(`Step ${index + 1}: Invalid step format`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get the command name (first key)
|
|
150
|
+
const keys = Object.keys(step);
|
|
151
|
+
if (keys.length === 0) {
|
|
152
|
+
errors.push(`Step ${index + 1}: Empty step object`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const command = keys[0];
|
|
157
|
+
const value = step[command];
|
|
158
|
+
|
|
159
|
+
// Validate known commands
|
|
160
|
+
const validCommands = [
|
|
161
|
+
'launchApp',
|
|
162
|
+
'stopApp',
|
|
163
|
+
'clearState',
|
|
164
|
+
'tapOn',
|
|
165
|
+
'longPressOn',
|
|
166
|
+
'doubleTapOn',
|
|
167
|
+
'inputText',
|
|
168
|
+
'eraseText',
|
|
169
|
+
'pressKey',
|
|
170
|
+
'swipe',
|
|
171
|
+
'scroll',
|
|
172
|
+
'scrollUntilVisible',
|
|
173
|
+
'assertVisible',
|
|
174
|
+
'assertNotVisible',
|
|
175
|
+
'waitForAnimationToEnd',
|
|
176
|
+
'extendedWaitUntil',
|
|
177
|
+
'takeScreenshot',
|
|
178
|
+
'runFlow',
|
|
179
|
+
'runScript',
|
|
180
|
+
'evalScript',
|
|
181
|
+
'assertTrue',
|
|
182
|
+
'back',
|
|
183
|
+
'home',
|
|
184
|
+
'hideKeyboard',
|
|
185
|
+
'openLink',
|
|
186
|
+
'repeat',
|
|
187
|
+
'copyTextFrom',
|
|
188
|
+
'pasteText',
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
if (!validCommands.includes(command)) {
|
|
192
|
+
warnings.push(`Step ${index + 1}: "${command}" is not a recognized Maestro command`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Specific validations
|
|
196
|
+
if (command === 'tapOn' || command === 'assertVisible') {
|
|
197
|
+
if (typeof value !== 'string' && typeof value !== 'object') {
|
|
198
|
+
errors.push(`Step ${index + 1}: "${command}" requires a string or object selector`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (command === 'inputText') {
|
|
203
|
+
if (typeof value !== 'string') {
|
|
204
|
+
errors.push(`Step ${index + 1}: "inputText" requires a string value`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (command === 'extendedWaitUntil') {
|
|
209
|
+
if (typeof value !== 'object') {
|
|
210
|
+
errors.push(`Step ${index + 1}: "extendedWaitUntil" requires an object with "visible" and optional "timeout"`);
|
|
211
|
+
} else if (!value.visible) {
|
|
212
|
+
errors.push(`Step ${index + 1}: "extendedWaitUntil" requires a "visible" property`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default {
|
|
218
|
+
validateMaestroYaml,
|
|
219
|
+
};
|
|
220
|
+
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Context Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides context/training data to help AI generate better YAML.
|
|
5
|
+
* Stores information about app elements, successful flows, and patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
|
|
13
|
+
// Context storage location
|
|
14
|
+
const USER_HOME = os.homedir();
|
|
15
|
+
const CONTEXT_DIR = path.join(USER_HOME, ".maestro-mcp", "context");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize context directory
|
|
19
|
+
*/
|
|
20
|
+
async function ensureContextDir() {
|
|
21
|
+
await fs.mkdir(CONTEXT_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the context file path for an app
|
|
26
|
+
*/
|
|
27
|
+
function getContextPath(appId) {
|
|
28
|
+
// Sanitize appId for filename
|
|
29
|
+
const safeAppId = appId.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
30
|
+
return path.join(CONTEXT_DIR, `${safeAppId}.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load app context
|
|
35
|
+
*/
|
|
36
|
+
export async function loadAppContext(appId) {
|
|
37
|
+
if (!appId) {
|
|
38
|
+
return { elements: {}, flows: [], patterns: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await ensureContextDir();
|
|
43
|
+
const contextPath = getContextPath(appId);
|
|
44
|
+
const content = await fs.readFile(contextPath, "utf8");
|
|
45
|
+
return JSON.parse(content);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// No context exists yet
|
|
48
|
+
return {
|
|
49
|
+
appId,
|
|
50
|
+
elements: {},
|
|
51
|
+
screens: {},
|
|
52
|
+
flows: [],
|
|
53
|
+
patterns: [],
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save app context
|
|
62
|
+
*/
|
|
63
|
+
export async function saveAppContext(appId, context) {
|
|
64
|
+
await ensureContextDir();
|
|
65
|
+
const contextPath = getContextPath(appId);
|
|
66
|
+
context.updatedAt = new Date().toISOString();
|
|
67
|
+
await fs.writeFile(contextPath, JSON.stringify(context, null, 2), "utf8");
|
|
68
|
+
logger.info(`App context saved: ${appId}`);
|
|
69
|
+
return { success: true, path: contextPath };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register UI elements for an app
|
|
74
|
+
* These are the testIDs, accessibilityLabels, and text that can be used
|
|
75
|
+
*/
|
|
76
|
+
export async function registerElements(appId, elements) {
|
|
77
|
+
const context = await loadAppContext(appId);
|
|
78
|
+
|
|
79
|
+
// Merge new elements
|
|
80
|
+
context.elements = {
|
|
81
|
+
...context.elements,
|
|
82
|
+
...elements,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await saveAppContext(appId, context);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
elementsCount: Object.keys(context.elements).length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register screen structure
|
|
95
|
+
* Defines what elements exist on each screen
|
|
96
|
+
*/
|
|
97
|
+
export async function registerScreen(appId, screenName, screenData) {
|
|
98
|
+
const context = await loadAppContext(appId);
|
|
99
|
+
|
|
100
|
+
context.screens = context.screens || {};
|
|
101
|
+
context.screens[screenName] = {
|
|
102
|
+
...screenData,
|
|
103
|
+
updatedAt: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await saveAppContext(appId, context);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
screen: screenName,
|
|
111
|
+
screensCount: Object.keys(context.screens).length,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Save a successful flow as a pattern for future reference
|
|
117
|
+
*/
|
|
118
|
+
export async function saveSuccessfulFlow(appId, flowName, yamlContent, description) {
|
|
119
|
+
const context = await loadAppContext(appId);
|
|
120
|
+
|
|
121
|
+
context.flows = context.flows || [];
|
|
122
|
+
|
|
123
|
+
// Remove existing flow with same name
|
|
124
|
+
context.flows = context.flows.filter(f => f.name !== flowName);
|
|
125
|
+
|
|
126
|
+
// Add new flow
|
|
127
|
+
context.flows.push({
|
|
128
|
+
name: flowName,
|
|
129
|
+
description: description || "",
|
|
130
|
+
yaml: yamlContent,
|
|
131
|
+
savedAt: new Date().toISOString(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Keep only last 50 flows
|
|
135
|
+
if (context.flows.length > 50) {
|
|
136
|
+
context.flows = context.flows.slice(-50);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await saveAppContext(appId, context);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
flowName,
|
|
144
|
+
totalFlows: context.flows.length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get all saved flows for an app
|
|
150
|
+
*/
|
|
151
|
+
export async function getSavedFlows(appId) {
|
|
152
|
+
const context = await loadAppContext(appId);
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
flows: context.flows || [],
|
|
156
|
+
count: (context.flows || []).length,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Delete a saved flow
|
|
162
|
+
*/
|
|
163
|
+
export async function deleteFlow(appId, flowName) {
|
|
164
|
+
const context = await loadAppContext(appId);
|
|
165
|
+
|
|
166
|
+
const before = (context.flows || []).length;
|
|
167
|
+
context.flows = (context.flows || []).filter(f => f.name !== flowName);
|
|
168
|
+
const after = context.flows.length;
|
|
169
|
+
|
|
170
|
+
await saveAppContext(appId, context);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: before !== after,
|
|
174
|
+
deleted: before !== after,
|
|
175
|
+
message: before !== after ? `Flow "${flowName}" deleted` : `Flow "${flowName}" not found`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Generate AI prompt context from app context
|
|
181
|
+
* This is the key function that provides training data to AI
|
|
182
|
+
*/
|
|
183
|
+
export async function generateAIPromptContext(appId) {
|
|
184
|
+
const context = await loadAppContext(appId);
|
|
185
|
+
|
|
186
|
+
let promptContext = `## App Context for ${appId}\n\n`;
|
|
187
|
+
|
|
188
|
+
// Add elements information
|
|
189
|
+
if (Object.keys(context.elements || {}).length > 0) {
|
|
190
|
+
promptContext += `### Available UI Elements (use these exact IDs/labels):\n\n`;
|
|
191
|
+
for (const [key, element] of Object.entries(context.elements)) {
|
|
192
|
+
promptContext += `- **${key}**: `;
|
|
193
|
+
if (element.testId) promptContext += `testID="${element.testId}" `;
|
|
194
|
+
if (element.accessibilityLabel) promptContext += `accessibilityLabel="${element.accessibilityLabel}" `;
|
|
195
|
+
if (element.text) promptContext += `text="${element.text}" `;
|
|
196
|
+
if (element.type) promptContext += `(${element.type})`;
|
|
197
|
+
if (element.description) promptContext += ` - ${element.description}`;
|
|
198
|
+
promptContext += `\n`;
|
|
199
|
+
}
|
|
200
|
+
promptContext += `\n`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add screen information
|
|
204
|
+
if (Object.keys(context.screens || {}).length > 0) {
|
|
205
|
+
promptContext += `### Screen Structure:\n\n`;
|
|
206
|
+
for (const [screenName, screen] of Object.entries(context.screens)) {
|
|
207
|
+
promptContext += `#### ${screenName}\n`;
|
|
208
|
+
if (screen.description) promptContext += `${screen.description}\n`;
|
|
209
|
+
if (screen.elements && screen.elements.length > 0) {
|
|
210
|
+
promptContext += `Elements: ${screen.elements.join(", ")}\n`;
|
|
211
|
+
}
|
|
212
|
+
if (screen.actions && screen.actions.length > 0) {
|
|
213
|
+
promptContext += `Available actions: ${screen.actions.join(", ")}\n`;
|
|
214
|
+
}
|
|
215
|
+
promptContext += `\n`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Add example flows (patterns)
|
|
220
|
+
if ((context.flows || []).length > 0) {
|
|
221
|
+
promptContext += `### Example Flows (reference these patterns):\n\n`;
|
|
222
|
+
// Show last 5 flows as examples
|
|
223
|
+
const recentFlows = context.flows.slice(-5);
|
|
224
|
+
for (const flow of recentFlows) {
|
|
225
|
+
promptContext += `#### ${flow.name}\n`;
|
|
226
|
+
if (flow.description) promptContext += `${flow.description}\n`;
|
|
227
|
+
promptContext += `\`\`\`yaml\n${flow.yaml}\n\`\`\`\n\n`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Add general instructions
|
|
232
|
+
promptContext += `### Generation Instructions:\n`;
|
|
233
|
+
promptContext += `- Use the exact testID or accessibilityLabel values shown above\n`;
|
|
234
|
+
promptContext += `- Follow the patterns from example flows\n`;
|
|
235
|
+
promptContext += `- Add appropriate waits between actions\n`;
|
|
236
|
+
promptContext += `- Use assertVisible to verify screens before interacting\n`;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
success: true,
|
|
240
|
+
context: promptContext,
|
|
241
|
+
hasElements: Object.keys(context.elements || {}).length > 0,
|
|
242
|
+
hasScreens: Object.keys(context.screens || {}).length > 0,
|
|
243
|
+
hasFlows: (context.flows || []).length > 0,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Clear all context for an app
|
|
249
|
+
*/
|
|
250
|
+
export async function clearAppContext(appId) {
|
|
251
|
+
try {
|
|
252
|
+
await ensureContextDir();
|
|
253
|
+
const contextPath = getContextPath(appId);
|
|
254
|
+
await fs.unlink(contextPath);
|
|
255
|
+
logger.info(`App context cleared: ${appId}`);
|
|
256
|
+
return { success: true, message: `Context for ${appId} cleared` };
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return { success: false, error: error.message };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* List all apps with saved context
|
|
264
|
+
*/
|
|
265
|
+
export async function listAppContexts() {
|
|
266
|
+
try {
|
|
267
|
+
await ensureContextDir();
|
|
268
|
+
const files = await fs.readdir(CONTEXT_DIR);
|
|
269
|
+
const contexts = files
|
|
270
|
+
.filter(f => f.endsWith(".json"))
|
|
271
|
+
.map(f => f.replace(".json", "").replace(/_/g, "."));
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
apps: contexts,
|
|
276
|
+
count: contexts.length,
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return { success: true, apps: [], count: 0 };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default {
|
|
284
|
+
loadAppContext,
|
|
285
|
+
saveAppContext,
|
|
286
|
+
registerElements,
|
|
287
|
+
registerScreen,
|
|
288
|
+
saveSuccessfulFlow,
|
|
289
|
+
getSavedFlows,
|
|
290
|
+
deleteFlow,
|
|
291
|
+
generateAIPromptContext,
|
|
292
|
+
clearAppContext,
|
|
293
|
+
listAppContexts,
|
|
294
|
+
};
|
|
295
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility for MCP Server
|
|
3
|
+
* Logs to file only (stderr is used by MCP protocol)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import winston from 'winston';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
// Ensure logs directory exists
|
|
16
|
+
const logsDir = process.env.LOGS_DIR || join(__dirname, '../../../output/logs');
|
|
17
|
+
if (!fs.existsSync(logsDir)) {
|
|
18
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// File format for logs
|
|
22
|
+
const fileFormat = winston.format.combine(
|
|
23
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
24
|
+
winston.format.errors({ stack: true }),
|
|
25
|
+
winston.format.json()
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Create logger instance - FILE ONLY (MCP uses stdio)
|
|
29
|
+
export const logger = winston.createLogger({
|
|
30
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
31
|
+
transports: [
|
|
32
|
+
// File transport for all logs
|
|
33
|
+
new winston.transports.File({
|
|
34
|
+
filename: path.join(logsDir, 'mcp-server.log'),
|
|
35
|
+
format: fileFormat,
|
|
36
|
+
maxsize: 5242880, // 5MB
|
|
37
|
+
maxFiles: 5,
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
// Separate file for errors
|
|
41
|
+
new winston.transports.File({
|
|
42
|
+
filename: path.join(logsDir, 'error.log'),
|
|
43
|
+
format: fileFormat,
|
|
44
|
+
level: 'error',
|
|
45
|
+
maxsize: 5242880,
|
|
46
|
+
maxFiles: 5,
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default logger;
|
|
52
|
+
|