chrometools-mcp 1.8.2 → 2.2.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 +757 -494
- package/README.md +219 -41
- package/browser/browser-manager.js +206 -0
- package/browser/page-manager.js +298 -0
- package/index.js +525 -1892
- package/package.json +55 -55
- package/recorder/page-object-generator.js +720 -0
- package/recorder/recorder-script.js +118 -12
- package/recorder/scenario-executor.js +970 -946
- package/recorder/scenario-storage.js +253 -29
- package/server/tool-definitions.js +620 -0
- package/server/tool-schemas.js +295 -0
- package/utils/code-generators/code-generator-base.js +61 -0
- package/utils/code-generators/file-appender.js +202 -0
- package/utils/code-generators/playwright-python.js +84 -0
- package/utils/code-generators/playwright-typescript.js +95 -0
- package/utils/code-generators/selenium-java.js +123 -0
- package/utils/code-generators/selenium-python.js +82 -0
- package/utils/css-utils.js +151 -0
- package/utils/image-processing.js +236 -0
- package/utils/platform-utils.js +62 -0
- package/utils/url-to-project.js +141 -0
- package/index.js.backup +0 -3674
- package/utils/project-detector.js +0 -87
package/index.js
CHANGED
|
@@ -1,1845 +1,228 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import pixelmatch from "pixelmatch";
|
|
13
|
-
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
|
|
14
|
-
import { dirname } from 'path';
|
|
15
|
-
import { spawn } from 'child_process';
|
|
16
|
-
import http from 'http';
|
|
17
|
-
import { fileURLToPath } from 'url';
|
|
18
|
-
import path from 'path';
|
|
19
|
-
|
|
20
|
-
// Figma token from environment variable (can be set in MCP config)
|
|
21
|
-
const FIGMA_TOKEN = process.env.FIGMA_TOKEN || null;
|
|
22
|
-
|
|
23
|
-
// Get current directory for loading utils
|
|
24
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
-
const __dirname = dirname(__filename);
|
|
26
|
-
|
|
27
|
-
// Load element finder utilities
|
|
28
|
-
const elementFinderUtils = readFileSync(path.join(__dirname, 'element-finder-utils.js'), 'utf-8');
|
|
29
|
-
|
|
30
|
-
// Import hints generator
|
|
31
|
-
import {
|
|
32
|
-
generateNavigationHints,
|
|
33
|
-
generateClickHints,
|
|
34
|
-
generateFormSubmitHints,
|
|
35
|
-
generatePageHints
|
|
36
|
-
} from './utils/hints-generator.js';
|
|
37
|
-
|
|
38
|
-
// Import Recorder modules
|
|
39
|
-
import { injectRecorder } from './recorder/recorder-script.js';
|
|
40
|
-
import { executeScenario } from './recorder/scenario-executor.js';
|
|
41
|
-
import {
|
|
42
|
-
initializeStorage,
|
|
43
|
-
saveScenario,
|
|
44
|
-
loadScenario,
|
|
45
|
-
listScenarios,
|
|
46
|
-
searchScenarios,
|
|
47
|
-
deleteScenario
|
|
48
|
-
} from './recorder/scenario-storage.js';
|
|
49
|
-
|
|
50
|
-
// Import Project Detection
|
|
51
|
-
import { detectProjectRoot } from './utils/project-detector.js';
|
|
52
|
-
|
|
53
|
-
// Import Code Generators
|
|
54
|
-
import { PlaywrightTypeScriptGenerator } from './utils/code-generators/playwright-typescript.js';
|
|
55
|
-
import { PlaywrightPythonGenerator } from './utils/code-generators/playwright-python.js';
|
|
56
|
-
import { SeleniumPythonGenerator } from './utils/code-generators/selenium-python.js';
|
|
57
|
-
import { SeleniumJavaGenerator } from './utils/code-generators/selenium-java.js';
|
|
58
|
-
|
|
59
|
-
// Global state for scenarios directory
|
|
60
|
-
let currentScenariosDirectory = null;
|
|
61
|
-
|
|
62
|
-
// Import Figma tools
|
|
63
|
-
import {
|
|
64
|
-
parseFigmaUrl,
|
|
65
|
-
normalizeFigmaNodeId,
|
|
66
|
-
fetchFigmaAPI,
|
|
67
|
-
getFigmaFile,
|
|
68
|
-
listFigmaPages,
|
|
69
|
-
searchFigmaFrames,
|
|
70
|
-
getFigmaComponents,
|
|
71
|
-
getFigmaStyles,
|
|
72
|
-
getFigmaColorPalette,
|
|
73
|
-
extractTextFromNode,
|
|
74
|
-
collectAllText
|
|
75
|
-
} from './figma-tools.js';
|
|
76
|
-
|
|
77
|
-
// Detect WSL environment
|
|
78
|
-
const isWSL = (() => {
|
|
79
|
-
try {
|
|
80
|
-
const fs = require('fs');
|
|
81
|
-
const proc_version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
82
|
-
return proc_version.includes('microsoft') || proc_version.includes('wsl');
|
|
83
|
-
} catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
})();
|
|
87
|
-
|
|
88
|
-
// Detect Windows environment (including WSL)
|
|
89
|
-
const isWindows = process.platform === 'win32' || isWSL;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get base directory for scenarios storage
|
|
93
|
-
* Uses cascade strategy: explicit param → remembered → auto-detect
|
|
94
|
-
* @param {string|null} explicitDir - Optional explicit directory path
|
|
95
|
-
* @returns {string} - Base directory path
|
|
96
|
-
*/
|
|
97
|
-
function getScenariosBaseDir(explicitDir = null) {
|
|
98
|
-
// 1. If explicitly provided - remember and return
|
|
99
|
-
if (explicitDir) {
|
|
100
|
-
currentScenariosDirectory = path.resolve(explicitDir);
|
|
101
|
-
console.log('[chrometools-mcp] Using explicit directory:', currentScenariosDirectory);
|
|
102
|
-
return currentScenariosDirectory;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 2. If already remembered - return
|
|
106
|
-
if (currentScenariosDirectory) {
|
|
107
|
-
console.log('[chrometools-mcp] Using remembered directory:', currentScenariosDirectory);
|
|
108
|
-
return currentScenariosDirectory;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 3. Auto-detect from environment
|
|
112
|
-
const detected = detectProjectRoot();
|
|
113
|
-
currentScenariosDirectory = detected;
|
|
114
|
-
return detected;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Get Chrome executable path based on platform
|
|
118
|
-
function getChromePath() {
|
|
119
|
-
if (process.platform === 'win32') {
|
|
120
|
-
// Native Windows
|
|
121
|
-
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
|
122
|
-
} else if (isWSL) {
|
|
123
|
-
// WSL - use Windows Chrome
|
|
124
|
-
return '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe';
|
|
125
|
-
} else {
|
|
126
|
-
// Linux
|
|
127
|
-
return '/usr/bin/google-chrome';
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Get temp directory based on platform
|
|
132
|
-
function getTempDir() {
|
|
133
|
-
if (process.platform === 'win32') {
|
|
134
|
-
return process.env.TEMP || 'C:\\Windows\\Temp';
|
|
135
|
-
} else if (isWSL) {
|
|
136
|
-
return '/mnt/c/Windows/Temp';
|
|
137
|
-
} else {
|
|
138
|
-
return process.env.TMPDIR || '/tmp';
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Global browser instance (persists between requests)
|
|
143
|
-
let browserPromise = null;
|
|
144
|
-
const openPages = new Map();
|
|
145
|
-
let lastPage = null;
|
|
146
|
-
let chromeProcess = null;
|
|
147
|
-
|
|
148
|
-
// Console logs storage
|
|
149
|
-
const consoleLogs = [];
|
|
150
|
-
|
|
151
|
-
// Network requests storage
|
|
152
|
-
const networkRequests = [];
|
|
153
|
-
|
|
154
|
-
// Page analysis cache (method 4)
|
|
155
|
-
const pageAnalysisCache = new Map();
|
|
156
|
-
|
|
157
|
-
// Track pages with recorder injected
|
|
158
|
-
const pagesWithRecorder = new WeakSet();
|
|
159
|
-
|
|
160
|
-
// Debug port for Chrome remote debugging
|
|
161
|
-
const CHROME_DEBUG_PORT = 9222;
|
|
162
|
-
|
|
163
|
-
// Helper function to get WebSocket endpoint from Chrome
|
|
164
|
-
async function getChromeWebSocketEndpoint(port = CHROME_DEBUG_PORT, maxRetries = 10) {
|
|
165
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
166
|
-
try {
|
|
167
|
-
const response = await new Promise((resolve, reject) => {
|
|
168
|
-
const req = http.get(`http://localhost:${port}/json/version`, (res) => {
|
|
169
|
-
let data = '';
|
|
170
|
-
res.on('data', chunk => data += chunk);
|
|
171
|
-
res.on('end', () => resolve(data));
|
|
172
|
-
});
|
|
173
|
-
req.on('error', reject);
|
|
174
|
-
req.setTimeout(1000);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const info = JSON.parse(response);
|
|
178
|
-
if (info.webSocketDebuggerUrl) {
|
|
179
|
-
return info.webSocketDebuggerUrl;
|
|
180
|
-
}
|
|
181
|
-
} catch (err) {
|
|
182
|
-
// Chrome might not be ready yet, wait and retry
|
|
183
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
throw new Error('Could not get Chrome WebSocket endpoint after multiple retries');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Initialize browser (singleton)
|
|
190
|
-
async function getBrowser() {
|
|
191
|
-
// Check if we have a cached browser and if it's still connected
|
|
192
|
-
if (browserPromise) {
|
|
193
|
-
try {
|
|
194
|
-
const cachedBrowser = await browserPromise;
|
|
195
|
-
if (cachedBrowser && cachedBrowser.isConnected()) {
|
|
196
|
-
return cachedBrowser;
|
|
197
|
-
}
|
|
198
|
-
// Browser disconnected, reset the promise
|
|
199
|
-
console.error("[chrometools-mcp] Browser disconnected, will reconnect...");
|
|
200
|
-
browserPromise = null;
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error("[chrometools-mcp] Error checking cached browser:", error.message);
|
|
203
|
-
browserPromise = null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (!browserPromise) {
|
|
208
|
-
browserPromise = (async () => {
|
|
209
|
-
try {
|
|
210
|
-
let browser;
|
|
211
|
-
let endpoint;
|
|
212
|
-
|
|
213
|
-
// Try to connect to existing Chrome with remote debugging
|
|
214
|
-
try {
|
|
215
|
-
endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 2);
|
|
216
|
-
browser = await puppeteer.connect({
|
|
217
|
-
browserWSEndpoint: endpoint,
|
|
218
|
-
defaultViewport: null,
|
|
219
|
-
});
|
|
220
|
-
console.error("[chrometools-mcp] Connected to existing Chrome instance");
|
|
221
|
-
console.error("[chrometools-mcp] WebSocket endpoint:", endpoint);
|
|
222
|
-
|
|
223
|
-
// Set up disconnect handler to reset browserPromise
|
|
224
|
-
browser.on('disconnected', () => {
|
|
225
|
-
console.error("[chrometools-mcp] Browser disconnected");
|
|
226
|
-
browserPromise = null;
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
return browser;
|
|
230
|
-
} catch (connectError) {
|
|
231
|
-
console.error("[chrometools-mcp] No existing Chrome found, launching new instance...");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Launch new Chrome with remote debugging enabled
|
|
235
|
-
const chromePath = getChromePath();
|
|
236
|
-
const userDataDir = `${getTempDir()}/chrome-mcp-profile`;
|
|
237
|
-
|
|
238
|
-
console.error("[chrometools-mcp] Chrome path:", chromePath);
|
|
239
|
-
console.error("[chrometools-mcp] User data dir:", userDataDir);
|
|
240
|
-
|
|
241
|
-
chromeProcess = spawn(chromePath, [
|
|
242
|
-
`--remote-debugging-port=${CHROME_DEBUG_PORT}`,
|
|
243
|
-
'--no-first-run',
|
|
244
|
-
'--no-default-browser-check',
|
|
245
|
-
`--user-data-dir=${userDataDir}`,
|
|
246
|
-
], {
|
|
247
|
-
detached: true,
|
|
248
|
-
stdio: 'ignore',
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
chromeProcess.unref(); // Allow Node to exit even if Chrome is running
|
|
252
|
-
|
|
253
|
-
console.error("[chrometools-mcp] Chrome launched with remote debugging on port", CHROME_DEBUG_PORT);
|
|
254
|
-
|
|
255
|
-
// Wait for Chrome to start and get the endpoint
|
|
256
|
-
endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 20);
|
|
257
|
-
|
|
258
|
-
// Connect to the Chrome instance
|
|
259
|
-
browser = await puppeteer.connect({
|
|
260
|
-
browserWSEndpoint: endpoint,
|
|
261
|
-
defaultViewport: null,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
console.error("[chrometools-mcp] Connected to Chrome instance");
|
|
265
|
-
console.error("[chrometools-mcp] WebSocket endpoint:", endpoint);
|
|
266
|
-
|
|
267
|
-
// Set up disconnect handler to reset browserPromise
|
|
268
|
-
browser.on('disconnected', () => {
|
|
269
|
-
console.error("[chrometools-mcp] Browser disconnected");
|
|
270
|
-
browserPromise = null;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
return browser;
|
|
274
|
-
} catch (error) {
|
|
275
|
-
// Check if it's a display-related error in WSL
|
|
276
|
-
if (isWSL && (
|
|
277
|
-
error.message.includes('DISPLAY') ||
|
|
278
|
-
error.message.includes('connect ECONNREFUSED') ||
|
|
279
|
-
error.message.includes('cannot open display')
|
|
280
|
-
)) {
|
|
281
|
-
const helpMessage = `
|
|
282
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
283
|
-
❌ WSL X Server Error Detected
|
|
284
|
-
|
|
285
|
-
You are running in WSL environment with headless:false mode.
|
|
286
|
-
This requires an X server to display the browser GUI.
|
|
287
|
-
|
|
288
|
-
🔧 Solution:
|
|
289
|
-
1. Start X server on Windows (e.g., VcXsrv, X410)
|
|
290
|
-
2. Set DISPLAY in your MCP config:
|
|
291
|
-
|
|
292
|
-
{
|
|
293
|
-
"mcpServers": {
|
|
294
|
-
"chrometools": {
|
|
295
|
-
"env": {
|
|
296
|
-
"DISPLAY": "172.25.96.1:0"
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
📚 For detailed setup instructions, see:
|
|
303
|
-
WSL_SETUP.md in chrometools-mcp package
|
|
304
|
-
|
|
305
|
-
💡 Alternative: Run in headless mode (modify index.js)
|
|
306
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
307
|
-
`;
|
|
308
|
-
console.error(helpMessage);
|
|
309
|
-
throw new Error(`WSL X Server not available. ${error.message}\n\nSee above for setup instructions.`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Re-throw other errors as-is
|
|
313
|
-
throw error;
|
|
314
|
-
}
|
|
315
|
-
})();
|
|
316
|
-
}
|
|
317
|
-
return browserPromise;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Setup navigation listener for recorder auto-reinjection
|
|
321
|
-
// Track pages with network monitoring to prevent duplicate setup
|
|
322
|
-
const pagesWithNetworkMonitoring = new WeakSet();
|
|
323
|
-
|
|
324
|
-
// Setup network monitoring with auto-reinitialization on navigation
|
|
325
|
-
async function setupNetworkMonitoring(page) {
|
|
326
|
-
// Prevent duplicate setup on the same page
|
|
327
|
-
if (pagesWithNetworkMonitoring.has(page)) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
pagesWithNetworkMonitoring.add(page);
|
|
331
|
-
|
|
332
|
-
const client = await page.target().createCDPSession();
|
|
333
|
-
await client.send('Network.enable');
|
|
334
|
-
|
|
335
|
-
client.on('Network.requestWillBeSent', (event) => {
|
|
336
|
-
const timestamp = new Date().toISOString();
|
|
337
|
-
networkRequests.push({
|
|
338
|
-
requestId: event.requestId,
|
|
339
|
-
url: event.request.url,
|
|
340
|
-
method: event.request.method,
|
|
341
|
-
headers: event.request.headers,
|
|
342
|
-
postData: event.request.postData,
|
|
343
|
-
timestamp,
|
|
344
|
-
type: event.type, // Document, Stylesheet, Image, Media, Font, Script, XHR, Fetch, etc.
|
|
345
|
-
initiator: event.initiator.type, // parser, script, other
|
|
346
|
-
status: 'pending',
|
|
347
|
-
documentURL: event.documentURL
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
client.on('Network.responseReceived', (event) => {
|
|
352
|
-
const req = networkRequests.find(r => r.requestId === event.requestId);
|
|
353
|
-
if (req) {
|
|
354
|
-
req.status = event.response.status;
|
|
355
|
-
req.statusText = event.response.statusText;
|
|
356
|
-
req.responseHeaders = event.response.headers;
|
|
357
|
-
req.mimeType = event.response.mimeType;
|
|
358
|
-
req.fromCache = event.response.fromDiskCache || event.response.fromServiceWorker;
|
|
359
|
-
req.timing = event.response.timing;
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
client.on('Network.loadingFinished', (event) => {
|
|
364
|
-
const req = networkRequests.find(r => r.requestId === event.requestId);
|
|
365
|
-
if (req && req.status === 'pending') {
|
|
366
|
-
req.status = 'completed';
|
|
367
|
-
}
|
|
368
|
-
if (req) {
|
|
369
|
-
req.encodedDataLength = event.encodedDataLength;
|
|
370
|
-
req.finishedTimestamp = new Date().toISOString();
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
client.on('Network.loadingFailed', (event) => {
|
|
375
|
-
const req = networkRequests.find(r => r.requestId === event.requestId);
|
|
376
|
-
if (req) {
|
|
377
|
-
req.status = 'failed';
|
|
378
|
-
req.errorText = event.errorText;
|
|
379
|
-
req.canceled = event.canceled;
|
|
380
|
-
req.finishedTimestamp = new Date().toISOString();
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Auto-reinitialize on navigation (CDP session is reset on navigation)
|
|
385
|
-
let lastUrl = page.url();
|
|
386
|
-
|
|
387
|
-
page.on('framenavigated', async (frame) => {
|
|
388
|
-
// Only handle main frame navigation
|
|
389
|
-
if (frame !== page.mainFrame()) return;
|
|
390
|
-
|
|
391
|
-
const currentUrl = frame.url();
|
|
392
|
-
|
|
393
|
-
// Skip if URL hasn't changed
|
|
394
|
-
if (currentUrl === lastUrl) return;
|
|
395
|
-
lastUrl = currentUrl;
|
|
396
|
-
|
|
397
|
-
// Remove from tracking set to allow re-setup
|
|
398
|
-
pagesWithNetworkMonitoring.delete(page);
|
|
399
|
-
|
|
400
|
-
// Small delay to let navigation settle
|
|
401
|
-
setTimeout(async () => {
|
|
402
|
-
try {
|
|
403
|
-
await setupNetworkMonitoring(page);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
console.error('[chrometools-mcp] Failed to reinitialize network monitoring:', error.message);
|
|
406
|
-
}
|
|
407
|
-
}, 100);
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async function setupRecorderAutoReinjection(page) {
|
|
412
|
-
let reinjectionTimeout = null;
|
|
413
|
-
let lastUrl = null;
|
|
414
|
-
|
|
415
|
-
// Handle navigation events (form submits, link clicks, history API)
|
|
416
|
-
page.on('framenavigated', async (frame) => {
|
|
417
|
-
// Only handle main frame navigation
|
|
418
|
-
if (frame !== page.mainFrame()) return;
|
|
419
|
-
|
|
420
|
-
// Get current URL
|
|
421
|
-
const currentUrl = frame.url();
|
|
422
|
-
|
|
423
|
-
// Skip if URL hasn't changed (prevents duplicate injections on same page)
|
|
424
|
-
if (currentUrl === lastUrl) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
lastUrl = currentUrl;
|
|
428
|
-
|
|
429
|
-
// Clear any pending reinjection
|
|
430
|
-
if (reinjectionTimeout) {
|
|
431
|
-
clearTimeout(reinjectionTimeout);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Debounce reinjection (wait 100ms for navigation to settle)
|
|
435
|
-
reinjectionTimeout = setTimeout(async () => {
|
|
436
|
-
// Check if this page had recorder before
|
|
437
|
-
if (pagesWithRecorder.has(page)) {
|
|
438
|
-
try {
|
|
439
|
-
const baseDir = getScenariosBaseDir();
|
|
440
|
-
await injectRecorder(page, baseDir);
|
|
441
|
-
} catch (error) {
|
|
442
|
-
console.error('[chrometools-mcp] Failed to re-inject recorder:', error.message);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}, 100);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Handle page reloads (F5, Ctrl+R) - use 'load' event
|
|
449
|
-
page.on('load', async () => {
|
|
450
|
-
// Check if this page had recorder before
|
|
451
|
-
if (pagesWithRecorder.has(page)) {
|
|
452
|
-
try {
|
|
453
|
-
const baseDir = getScenariosBaseDir();
|
|
454
|
-
await injectRecorder(page, baseDir);
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error('[chrometools-mcp] Failed to re-inject recorder after reload:', error.message);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Get or create page for URL
|
|
463
|
-
async function getOrCreatePage(url) {
|
|
464
|
-
const browser = await getBrowser();
|
|
465
|
-
|
|
466
|
-
// Check if page for this URL already exists
|
|
467
|
-
if (openPages.has(url)) {
|
|
468
|
-
const existingPage = openPages.get(url);
|
|
469
|
-
if (!existingPage.isClosed()) {
|
|
470
|
-
lastPage = existingPage;
|
|
471
|
-
return existingPage;
|
|
472
|
-
}
|
|
473
|
-
openPages.delete(url);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Create new page
|
|
477
|
-
const page = await browser.newPage();
|
|
478
|
-
|
|
479
|
-
// Set up console log capture
|
|
480
|
-
const client = await page.target().createCDPSession();
|
|
481
|
-
await client.send('Runtime.enable');
|
|
482
|
-
await client.send('Log.enable');
|
|
483
|
-
|
|
484
|
-
client.on('Runtime.consoleAPICalled', (event) => {
|
|
485
|
-
const timestamp = new Date().toISOString();
|
|
486
|
-
const args = event.args.map(arg => {
|
|
487
|
-
if (arg.value !== undefined) return arg.value;
|
|
488
|
-
if (arg.description) return arg.description;
|
|
489
|
-
return String(arg);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
consoleLogs.push({
|
|
493
|
-
type: event.type, // log, warn, error, info, debug
|
|
494
|
-
timestamp,
|
|
495
|
-
message: args.join(' '),
|
|
496
|
-
stackTrace: event.stackTrace
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
client.on('Log.entryAdded', (event) => {
|
|
501
|
-
const entry = event.entry;
|
|
502
|
-
consoleLogs.push({
|
|
503
|
-
type: entry.level, // verbose, info, warning, error
|
|
504
|
-
timestamp: new Date(entry.timestamp).toISOString(),
|
|
505
|
-
message: entry.text,
|
|
506
|
-
source: entry.source,
|
|
507
|
-
url: entry.url,
|
|
508
|
-
lineNumber: entry.lineNumber
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// Setup network monitoring with auto-reinitialization on navigation
|
|
513
|
-
await setupNetworkMonitoring(page);
|
|
514
|
-
|
|
515
|
-
// Setup recorder auto-reinjection on navigation
|
|
516
|
-
setupRecorderAutoReinjection(page);
|
|
517
|
-
|
|
518
|
-
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
519
|
-
openPages.set(url, page);
|
|
520
|
-
lastPage = page;
|
|
521
|
-
|
|
522
|
-
return page;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Get last opened page (for tools that don't need URL)
|
|
526
|
-
async function getLastOpenPage() {
|
|
527
|
-
if (!lastPage || lastPage.isClosed()) {
|
|
528
|
-
throw new Error('No page is currently open. Use openBrowser first to open a page.');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Setup recorder auto-reinjection if not already set up
|
|
532
|
-
// Check if page already has navigation listener
|
|
533
|
-
const listenerCount = lastPage.listenerCount('framenavigated');
|
|
534
|
-
if (listenerCount === 0) {
|
|
535
|
-
setupRecorderAutoReinjection(lastPage);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
return lastPage;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Helper function to normalize Figma node ID (convert URL format to API format)
|
|
542
|
-
// Figma helper functions moved to figma-tools.js
|
|
543
|
-
|
|
544
|
-
// Helper function to process screenshot with compression and scaling
|
|
545
|
-
async function processScreenshot(screenshotBuffer, options = {}) {
|
|
546
|
-
const {
|
|
547
|
-
maxWidth = 1024,
|
|
548
|
-
maxHeight = 8000, // API limit is 8000px
|
|
549
|
-
quality = 80,
|
|
550
|
-
format = 'auto',
|
|
551
|
-
maxFileSize = 3 * 1024 * 1024 // 3 MB limit
|
|
552
|
-
} = options;
|
|
553
|
-
|
|
554
|
-
// Load image with Jimp
|
|
555
|
-
const image = await Jimp.read(screenshotBuffer);
|
|
556
|
-
const originalWidth = image.bitmap.width;
|
|
557
|
-
const originalHeight = image.bitmap.height;
|
|
558
|
-
const originalSize = screenshotBuffer.length;
|
|
559
|
-
|
|
560
|
-
let processed = false;
|
|
561
|
-
|
|
562
|
-
// Apply scaling if needed to fit within maxWidth and maxHeight
|
|
563
|
-
if (maxWidth !== null || maxHeight !== null) {
|
|
564
|
-
let newWidth = originalWidth;
|
|
565
|
-
let newHeight = originalHeight;
|
|
566
|
-
|
|
567
|
-
// Calculate scale factors for both dimensions
|
|
568
|
-
let scaleWidth = 1.0;
|
|
569
|
-
let scaleHeight = 1.0;
|
|
570
|
-
|
|
571
|
-
if (maxWidth !== null && originalWidth > maxWidth) {
|
|
572
|
-
scaleWidth = maxWidth / originalWidth;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (maxHeight !== null && originalHeight > maxHeight) {
|
|
576
|
-
scaleHeight = maxHeight / originalHeight;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Use the smaller scale factor to ensure both dimensions fit
|
|
580
|
-
const scale = Math.min(scaleWidth, scaleHeight);
|
|
581
|
-
|
|
582
|
-
if (scale < 1.0) {
|
|
583
|
-
newWidth = Math.round(originalWidth * scale);
|
|
584
|
-
newHeight = Math.round(originalHeight * scale);
|
|
585
|
-
image.resize(newWidth, newHeight);
|
|
586
|
-
processed = true;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Determine output format
|
|
591
|
-
let outputFormat = format;
|
|
592
|
-
let mimeType = 'image/png';
|
|
593
|
-
|
|
594
|
-
if (format === 'auto') {
|
|
595
|
-
// Auto-select: use JPEG for large images, PNG for small
|
|
596
|
-
const estimatedSize = image.bitmap.width * image.bitmap.height * 4;
|
|
597
|
-
outputFormat = estimatedSize > 500000 ? 'jpeg' : 'png'; // ~500KB threshold
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Convert to buffer with appropriate format and quality
|
|
601
|
-
let currentQuality = quality;
|
|
602
|
-
let resultBuffer;
|
|
603
|
-
let compressionAttempts = 0;
|
|
604
|
-
const maxCompressionAttempts = 10;
|
|
605
|
-
|
|
606
|
-
if (outputFormat === 'jpeg') {
|
|
607
|
-
image.quality(currentQuality);
|
|
608
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
|
609
|
-
mimeType = 'image/jpeg';
|
|
610
|
-
processed = true;
|
|
611
|
-
|
|
612
|
-
// If file exceeds maxFileSize, reduce quality iteratively
|
|
613
|
-
while (resultBuffer.length > maxFileSize && compressionAttempts < maxCompressionAttempts) {
|
|
614
|
-
compressionAttempts++;
|
|
615
|
-
// Reduce quality by 10 points each iteration
|
|
616
|
-
currentQuality = Math.max(10, currentQuality - 10);
|
|
617
|
-
image.quality(currentQuality);
|
|
618
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
|
619
|
-
|
|
620
|
-
// If quality is already at minimum and still too large, scale down the image
|
|
621
|
-
if (currentQuality === 10 && resultBuffer.length > maxFileSize) {
|
|
622
|
-
const scaleFactor = Math.sqrt(maxFileSize / resultBuffer.length * 0.9); // 0.9 for safety margin
|
|
623
|
-
const newWidth = Math.round(image.bitmap.width * scaleFactor);
|
|
624
|
-
const newHeight = Math.round(image.bitmap.height * scaleFactor);
|
|
625
|
-
image.resize(newWidth, newHeight);
|
|
626
|
-
image.quality(currentQuality);
|
|
627
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
|
628
|
-
processed = true;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
} else {
|
|
632
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
|
633
|
-
mimeType = 'image/png';
|
|
634
|
-
|
|
635
|
-
// If PNG exceeds maxFileSize, convert to JPEG and compress
|
|
636
|
-
if (resultBuffer.length > maxFileSize) {
|
|
637
|
-
outputFormat = 'jpeg';
|
|
638
|
-
mimeType = 'image/jpeg';
|
|
639
|
-
currentQuality = quality;
|
|
640
|
-
image.quality(currentQuality);
|
|
641
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
|
642
|
-
processed = true;
|
|
643
|
-
|
|
644
|
-
// Reduce quality iteratively if still too large
|
|
645
|
-
while (resultBuffer.length > maxFileSize && compressionAttempts < maxCompressionAttempts) {
|
|
646
|
-
compressionAttempts++;
|
|
647
|
-
currentQuality = Math.max(10, currentQuality - 10);
|
|
648
|
-
image.quality(currentQuality);
|
|
649
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
|
650
|
-
|
|
651
|
-
// If quality is already at minimum and still too large, scale down the image
|
|
652
|
-
if (currentQuality === 10 && resultBuffer.length > maxFileSize) {
|
|
653
|
-
const scaleFactor = Math.sqrt(maxFileSize / resultBuffer.length * 0.9);
|
|
654
|
-
const newWidth = Math.round(image.bitmap.width * scaleFactor);
|
|
655
|
-
const newHeight = Math.round(image.bitmap.height * scaleFactor);
|
|
656
|
-
image.resize(newWidth, newHeight);
|
|
657
|
-
image.quality(currentQuality);
|
|
658
|
-
resultBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
|
659
|
-
processed = true;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Return original if no processing was needed and format is PNG
|
|
666
|
-
if (!processed && outputFormat === 'png' && resultBuffer.length <= maxFileSize) {
|
|
667
|
-
return {
|
|
668
|
-
buffer: screenshotBuffer,
|
|
669
|
-
mimeType: 'image/png',
|
|
670
|
-
metadata: {
|
|
671
|
-
width: originalWidth,
|
|
672
|
-
height: originalHeight,
|
|
673
|
-
originalSize,
|
|
674
|
-
finalSize: screenshotBuffer.length,
|
|
675
|
-
format: 'png',
|
|
676
|
-
compressed: false,
|
|
677
|
-
scaled: false
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return {
|
|
683
|
-
buffer: resultBuffer,
|
|
684
|
-
mimeType,
|
|
685
|
-
metadata: {
|
|
686
|
-
width: image.bitmap.width,
|
|
687
|
-
height: image.bitmap.height,
|
|
688
|
-
originalWidth,
|
|
689
|
-
originalHeight,
|
|
690
|
-
originalSize,
|
|
691
|
-
finalSize: resultBuffer.length,
|
|
692
|
-
format: outputFormat,
|
|
693
|
-
compressed: outputFormat === 'jpeg' || compressionAttempts > 0,
|
|
694
|
-
scaled: processed,
|
|
695
|
-
compressionRatio: Math.round((1 - resultBuffer.length / originalSize) * 100),
|
|
696
|
-
quality: outputFormat === 'jpeg' ? currentQuality : undefined,
|
|
697
|
-
compressionAttempts: compressionAttempts > 0 ? compressionAttempts : undefined,
|
|
698
|
-
autoCompressed: compressionAttempts > 0 || (outputFormat === 'jpeg' && format === 'png')
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Calculate SSIM (Structural Similarity Index) for image comparison
|
|
704
|
-
function calculateSSIM(img1Data, img2Data, width, height) {
|
|
705
|
-
if (img1Data.length !== img2Data.length) {
|
|
706
|
-
return 0;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const windowSize = 8;
|
|
710
|
-
const k1 = 0.01;
|
|
711
|
-
const k2 = 0.03;
|
|
712
|
-
const c1 = (k1 * 255) ** 2;
|
|
713
|
-
const c2 = (k2 * 255) ** 2;
|
|
714
|
-
|
|
715
|
-
let ssimSum = 0;
|
|
716
|
-
let validWindows = 0;
|
|
717
|
-
|
|
718
|
-
for (let y = 0; y <= height - windowSize; y += windowSize) {
|
|
719
|
-
for (let x = 0; x <= width - windowSize; x += windowSize) {
|
|
720
|
-
let sum1 = 0, sum2 = 0, sum1Sq = 0, sum2Sq = 0, sum12 = 0;
|
|
721
|
-
|
|
722
|
-
for (let dy = 0; dy < windowSize; dy++) {
|
|
723
|
-
for (let dx = 0; dx < windowSize; dx++) {
|
|
724
|
-
const idx = ((y + dy) * width + (x + dx)) * 4;
|
|
725
|
-
if (idx + 2 >= img1Data.length) continue;
|
|
726
|
-
|
|
727
|
-
const gray1 = (img1Data[idx] * 0.299 + img1Data[idx + 1] * 0.587 + img1Data[idx + 2] * 0.114);
|
|
728
|
-
const gray2 = (img2Data[idx] * 0.299 + img2Data[idx + 1] * 0.587 + img2Data[idx + 2] * 0.114);
|
|
729
|
-
|
|
730
|
-
sum1 += gray1;
|
|
731
|
-
sum2 += gray2;
|
|
732
|
-
sum1Sq += gray1 * gray1;
|
|
733
|
-
sum2Sq += gray2 * gray2;
|
|
734
|
-
sum12 += gray1 * gray2;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const n = windowSize * windowSize;
|
|
739
|
-
const mean1 = sum1 / n;
|
|
740
|
-
const mean2 = sum2 / n;
|
|
741
|
-
const variance1 = (sum1Sq / n) - (mean1 * mean1);
|
|
742
|
-
const variance2 = (sum2Sq / n) - (mean2 * mean2);
|
|
743
|
-
const covariance = (sum12 / n) - (mean1 * mean2);
|
|
744
|
-
|
|
745
|
-
const ssim = ((2 * mean1 * mean2 + c1) * (2 * covariance + c2)) /
|
|
746
|
-
((mean1 * mean1 + mean2 * mean2 + c1) * (variance1 + variance2 + c2));
|
|
747
|
-
|
|
748
|
-
ssimSum += ssim;
|
|
749
|
-
validWindows++;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return validWindows > 0 ? ssimSum / validWindows : 0;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Cleanup on exit
|
|
757
|
-
process.on("SIGINT", async () => {
|
|
758
|
-
if (browserPromise) {
|
|
759
|
-
const browser = await browserPromise;
|
|
760
|
-
await browser.close();
|
|
761
|
-
}
|
|
762
|
-
process.exit(0);
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
// Create MCP server
|
|
766
|
-
const server = new Server(
|
|
767
|
-
{
|
|
768
|
-
name: "chrometools-mcp",
|
|
769
|
-
version: "1.0.2",
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
capabilities: {
|
|
773
|
-
tools: {},
|
|
774
|
-
},
|
|
775
|
-
}
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
// CSS property categorization
|
|
779
|
-
const CSS_CATEGORIES = {
|
|
780
|
-
layout: [
|
|
781
|
-
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
|
|
782
|
-
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
783
|
-
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
784
|
-
'position', 'top', 'right', 'bottom', 'left', 'z-index',
|
|
785
|
-
'display', 'float', 'clear', 'overflow', 'overflow-x', 'overflow-y',
|
|
786
|
-
'flex', 'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',
|
|
787
|
-
'justify-content', 'align-items', 'align-content', 'align-self', 'order',
|
|
788
|
-
'grid', 'grid-template', 'grid-template-columns', 'grid-template-rows', 'grid-gap',
|
|
789
|
-
'gap', 'row-gap', 'column-gap',
|
|
790
|
-
'box-sizing', 'visibility', 'clip', 'clip-path'
|
|
791
|
-
],
|
|
792
|
-
typography: [
|
|
793
|
-
'font', 'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
|
|
794
|
-
'line-height', 'letter-spacing', 'word-spacing', 'text-align', 'text-decoration',
|
|
795
|
-
'text-transform', 'text-indent', 'text-overflow', 'white-space', 'word-break',
|
|
796
|
-
'word-wrap', 'overflow-wrap', 'hyphens', 'direction', 'unicode-bidi',
|
|
797
|
-
'writing-mode', 'vertical-align'
|
|
798
|
-
],
|
|
799
|
-
colors: [
|
|
800
|
-
'color', 'background', 'background-color', 'background-image', 'background-position',
|
|
801
|
-
'background-size', 'background-repeat', 'background-attachment', 'background-clip',
|
|
802
|
-
'background-origin', 'background-blend-mode',
|
|
803
|
-
'border-color', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
|
804
|
-
'outline-color', 'text-decoration-color', 'caret-color', 'column-rule-color'
|
|
805
|
-
],
|
|
806
|
-
visual: [
|
|
807
|
-
'opacity', 'transform', 'transform-origin', 'transform-style', 'perspective',
|
|
808
|
-
'perspective-origin', 'backface-visibility',
|
|
809
|
-
'transition', 'transition-property', 'transition-duration', 'transition-timing-function', 'transition-delay',
|
|
810
|
-
'animation', 'animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay',
|
|
811
|
-
'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state',
|
|
812
|
-
'filter', 'backdrop-filter', 'mix-blend-mode', 'isolation',
|
|
813
|
-
'box-shadow', 'text-shadow',
|
|
814
|
-
'border', 'border-width', 'border-style', 'border-radius',
|
|
815
|
-
'border-top', 'border-right', 'border-bottom', 'border-left',
|
|
816
|
-
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
|
817
|
-
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
|
|
818
|
-
'border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius',
|
|
819
|
-
'outline', 'outline-width', 'outline-style', 'outline-offset',
|
|
820
|
-
'cursor', 'pointer-events', 'user-select'
|
|
821
|
-
]
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
// Common default CSS values to filter out
|
|
825
|
-
const CSS_DEFAULTS = {
|
|
826
|
-
'display': 'inline',
|
|
827
|
-
'position': 'static',
|
|
828
|
-
'float': 'none',
|
|
829
|
-
'clear': 'none',
|
|
830
|
-
'visibility': 'visible',
|
|
831
|
-
'overflow': 'visible',
|
|
832
|
-
'overflow-x': 'visible',
|
|
833
|
-
'overflow-y': 'visible',
|
|
834
|
-
'z-index': 'auto',
|
|
835
|
-
'opacity': '1',
|
|
836
|
-
'transform': 'none',
|
|
837
|
-
'filter': 'none',
|
|
838
|
-
'backdrop-filter': 'none',
|
|
839
|
-
'box-shadow': 'none',
|
|
840
|
-
'text-shadow': 'none',
|
|
841
|
-
'border-style': 'none',
|
|
842
|
-
'border-width': '0px',
|
|
843
|
-
'outline-style': 'none',
|
|
844
|
-
'outline-width': '0px',
|
|
845
|
-
'margin': '0px',
|
|
846
|
-
'margin-top': '0px',
|
|
847
|
-
'margin-right': '0px',
|
|
848
|
-
'margin-bottom': '0px',
|
|
849
|
-
'margin-left': '0px',
|
|
850
|
-
'padding': '0px',
|
|
851
|
-
'padding-top': '0px',
|
|
852
|
-
'padding-right': '0px',
|
|
853
|
-
'padding-bottom': '0px',
|
|
854
|
-
'padding-left': '0px',
|
|
855
|
-
'background-image': 'none',
|
|
856
|
-
'transition': 'all 0s ease 0s',
|
|
857
|
-
'animation': 'none',
|
|
858
|
-
'pointer-events': 'auto',
|
|
859
|
-
'user-select': 'auto',
|
|
860
|
-
'cursor': 'auto',
|
|
861
|
-
'text-decoration': 'none',
|
|
862
|
-
'text-transform': 'none',
|
|
863
|
-
'font-weight': '400',
|
|
864
|
-
'font-style': 'normal',
|
|
865
|
-
'font-variant': 'normal',
|
|
866
|
-
'letter-spacing': 'normal',
|
|
867
|
-
'word-spacing': 'normal',
|
|
868
|
-
'text-align': 'start',
|
|
869
|
-
'white-space': 'normal',
|
|
870
|
-
'word-break': 'normal',
|
|
871
|
-
'overflow-wrap': 'normal',
|
|
872
|
-
'hyphens': 'manual'
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
// Filter computed CSS styles based on options
|
|
876
|
-
function filterCssStyles(computedStyle, options = {}) {
|
|
877
|
-
const { category, properties, includeDefaults = false } = options;
|
|
878
|
-
|
|
879
|
-
let filtered = computedStyle;
|
|
880
|
-
|
|
881
|
-
// Filter by specific properties (highest priority)
|
|
882
|
-
if (properties && properties.length > 0) {
|
|
883
|
-
filtered = filtered.filter(prop =>
|
|
884
|
-
properties.some(p => prop.name.toLowerCase() === p.toLowerCase())
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
// Filter by category
|
|
888
|
-
else if (category && category !== 'all') {
|
|
889
|
-
const categoryProps = CSS_CATEGORIES[category] || [];
|
|
890
|
-
filtered = filtered.filter(prop =>
|
|
891
|
-
categoryProps.some(p => prop.name.toLowerCase().startsWith(p.toLowerCase()))
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Filter out default values if requested
|
|
896
|
-
if (!includeDefaults) {
|
|
897
|
-
filtered = filtered.filter(prop => {
|
|
898
|
-
const defaultValue = CSS_DEFAULTS[prop.name];
|
|
899
|
-
if (!defaultValue) return true;
|
|
900
|
-
|
|
901
|
-
// Normalize values for comparison
|
|
902
|
-
const normalizedValue = prop.value.replace(/\s+/g, ' ').trim();
|
|
903
|
-
const normalizedDefault = defaultValue.replace(/\s+/g, ' ').trim();
|
|
904
|
-
|
|
905
|
-
return normalizedValue !== normalizedDefault;
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
return filtered;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Tool schemas
|
|
913
|
-
const PingSchema = z.object({
|
|
914
|
-
message: z.string().optional().describe("Optional message to send"),
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
const OpenBrowserSchema = z.object({
|
|
918
|
-
url: z.string().describe("URL to open in the browser"),
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
const ClickSchema = z.object({
|
|
922
|
-
selector: z.string().describe("CSS selector for element to click"),
|
|
923
|
-
waitAfter: z.number().optional().describe("Milliseconds to wait after click (default: 1500)"),
|
|
924
|
-
screenshot: z.boolean().optional().describe("Capture screenshot after click (default: false for performance)"),
|
|
925
|
-
timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
const TypeSchema = z.object({
|
|
929
|
-
selector: z.string().describe("CSS selector for input element"),
|
|
930
|
-
text: z.string().describe("Text to type"),
|
|
931
|
-
delay: z.number().optional().describe("Delay between keystrokes in ms (default: 0)"),
|
|
932
|
-
clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
const GetElementSchema = z.object({
|
|
936
|
-
selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
const GetComputedCssSchema = z.object({
|
|
940
|
-
selector: z.string().optional().describe("CSS selector (optional, defaults to body)"),
|
|
941
|
-
category: z.enum(['all', 'layout', 'typography', 'colors', 'visual']).optional().describe("Filter by CSS category: 'layout' (sizing, positioning), 'typography' (fonts, text), 'colors' (color schemes), 'visual' (effects, transforms), 'all' (default)"),
|
|
942
|
-
properties: z.array(z.string()).optional().describe("Specific CSS properties to return (e.g., ['color', 'font-size']). Overrides category filter."),
|
|
943
|
-
includeDefaults: z.boolean().optional().describe("Include properties with default values (default: false)"),
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
const GetBoxModelSchema = z.object({
|
|
947
|
-
selector: z.string().describe("CSS selector for element"),
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
const ScreenshotSchema = z.object({
|
|
951
|
-
selector: z.string().describe("CSS selector for element to screenshot"),
|
|
952
|
-
padding: z.number().optional().describe("Padding around element in pixels (default: 0)"),
|
|
953
|
-
maxWidth: z.number().nullable().optional().describe("Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)"),
|
|
954
|
-
maxHeight: z.number().nullable().optional().describe("Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)"),
|
|
955
|
-
quality: z.number().min(1).max(100).optional().describe("JPEG quality 1-100 (default: 80, only applies to JPEG format)"),
|
|
956
|
-
format: z.enum(['png', 'jpeg', 'auto']).optional().describe("Image format: 'png', 'jpeg', or 'auto' (default: 'auto' - chooses based on size)"),
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
const SaveScreenshotSchema = z.object({
|
|
960
|
-
selector: z.string().describe("CSS selector for element to screenshot"),
|
|
961
|
-
filePath: z.string().describe("Absolute path where to save file"),
|
|
962
|
-
padding: z.number().optional().describe("Padding around element in pixels (default: 0)"),
|
|
963
|
-
maxWidth: z.number().nullable().optional().describe("Maximum width in pixels, auto-scales if larger (default: 1024, set to null for original size)"),
|
|
964
|
-
maxHeight: z.number().nullable().optional().describe("Maximum height in pixels, auto-scales if larger (default: 8000 for API limit, set to null for original size)"),
|
|
965
|
-
quality: z.number().min(1).max(100).optional().describe("JPEG quality 1-100 (default: 80, only applies to JPEG format)"),
|
|
966
|
-
format: z.enum(['png', 'jpeg', 'auto']).optional().describe("Image format: 'png', 'jpeg', or 'auto' (default: 'auto' - chooses based on size)"),
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
const ScrollToSchema = z.object({
|
|
970
|
-
selector: z.string().describe("CSS selector for element to scroll to"),
|
|
971
|
-
behavior: z.enum(['auto', 'smooth']).optional().describe("Scroll behavior (default: auto)"),
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
const WaitForElementSchema = z.object({
|
|
975
|
-
selector: z.string().describe("CSS selector to wait for"),
|
|
976
|
-
timeout: z.number().optional().describe("Maximum time to wait in milliseconds (default: 5000)"),
|
|
977
|
-
visible: z.boolean().optional().describe("Wait for element to be visible (default: true)"),
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
const ExecuteScriptSchema = z.object({
|
|
981
|
-
script: z.string().describe("JavaScript code to execute in page context"),
|
|
982
|
-
waitAfter: z.number().optional().describe("Milliseconds to wait after execution (default: 500)"),
|
|
983
|
-
screenshot: z.boolean().optional().describe("Capture screenshot after execution (default: false for performance)"),
|
|
984
|
-
timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
// Phase 2 schemas
|
|
988
|
-
const GetConsoleLogsSchema = z.object({
|
|
989
|
-
types: z.array(z.enum(['log', 'warn', 'error', 'info', 'debug', 'verbose', 'warning']))
|
|
990
|
-
.optional()
|
|
991
|
-
.describe("Filter by log types (default: all)"),
|
|
992
|
-
clear: z.boolean().optional().describe("Clear logs after reading (default: false)"),
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
// Network tools schemas
|
|
996
|
-
const ListNetworkRequestsSchema = z.object({
|
|
997
|
-
types: z.array(z.enum(['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Script', 'XHR', 'Fetch', 'WebSocket', 'Other']))
|
|
998
|
-
.optional()
|
|
999
|
-
.default(['Fetch', 'XHR'])
|
|
1000
|
-
.describe("Filter by request types (default: Fetch, XHR)"),
|
|
1001
|
-
status: z.enum(['pending', 'completed', 'failed', 'all'])
|
|
1002
|
-
.optional()
|
|
1003
|
-
.describe("Filter by status (default: all)"),
|
|
1004
|
-
limit: z.number().min(1).max(500).optional().default(50).describe("Maximum number of requests to return (default: 50)"),
|
|
1005
|
-
offset: z.number().min(0).optional().default(0).describe("Number of requests to skip before returning results (default: 0)"),
|
|
1006
|
-
clear: z.boolean().optional().describe("Clear requests after reading (default: false)"),
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
const GetNetworkRequestSchema = z.object({
|
|
1010
|
-
requestId: z.string().describe("Request ID to get details for"),
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
const FilterNetworkRequestsSchema = z.object({
|
|
1014
|
-
urlPattern: z.string().describe("URL pattern to filter by (regex or partial match)"),
|
|
1015
|
-
types: z.array(z.enum(['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Script', 'XHR', 'Fetch', 'WebSocket', 'Other']))
|
|
1016
|
-
.optional()
|
|
1017
|
-
.default(['Fetch', 'XHR'])
|
|
1018
|
-
.describe("Filter by request types (default: Fetch, XHR)"),
|
|
1019
|
-
clear: z.boolean().optional().describe("Clear requests after reading (default: false)"),
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
const HoverSchema = z.object({
|
|
1023
|
-
selector: z.string().describe("CSS selector for element to hover"),
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
const SetStylesSchema = z.object({
|
|
1027
|
-
selector: z.string().describe("CSS selector for element to modify"),
|
|
1028
|
-
styles: z.array(z.object({
|
|
1029
|
-
name: z.string().describe("CSS property name (e.g., 'color')"),
|
|
1030
|
-
value: z.string().describe("CSS property value (e.g., 'red')")
|
|
1031
|
-
})).describe("Array of CSS property name-value pairs"),
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
const SetViewportSchema = z.object({
|
|
1035
|
-
width: z.number().min(320).max(4000).describe("Viewport width in pixels (320-4000)"),
|
|
1036
|
-
height: z.number().min(200).max(3000).describe("Viewport height in pixels (200-3000)"),
|
|
1037
|
-
deviceScaleFactor: z.number().min(0.5).max(3).optional().describe("Device pixel ratio (0.5-3, default: 1)"),
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
const GetViewportSchema = z.object({});
|
|
1041
|
-
|
|
1042
|
-
const NavigateToSchema = z.object({
|
|
1043
|
-
url: z.string().describe("URL to navigate to"),
|
|
1044
|
-
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2'])
|
|
1045
|
-
.optional()
|
|
1046
|
-
.describe("Wait until event (default: networkidle2)"),
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
// Figma tools schemas
|
|
1050
|
-
const GetFigmaFrameSchema = z.object({
|
|
1051
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1052
|
-
fileKey: z.string().describe("Figma file key (from URL: figma.com/file/FILE_KEY/...)"),
|
|
1053
|
-
nodeId: z.string().describe("Figma node ID (frame/component ID)"),
|
|
1054
|
-
scale: z.number().min(0.1).max(4).optional().describe("Export scale (0.1-4, default: 2)"),
|
|
1055
|
-
format: z.enum(['png', 'jpg', 'svg']).optional().describe("Export format (default: png)")
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
const CompareFigmaToElementSchema = z.object({
|
|
1059
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1060
|
-
fileKey: z.string().describe("Figma file key"),
|
|
1061
|
-
nodeId: z.string().describe("Figma frame/component ID"),
|
|
1062
|
-
selector: z.string().describe("CSS selector for page element"),
|
|
1063
|
-
threshold: z.number().min(0).max(1).optional().describe("Difference threshold (0-1, default: 0.05)"),
|
|
1064
|
-
figmaScale: z.number().min(0.1).max(4).optional().describe("Figma export scale (default: 2)")
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
const GetFigmaSpecsSchema = z.object({
|
|
1068
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1069
|
-
fileKey: z.string().describe("Figma file key"),
|
|
1070
|
-
nodeId: z.string().describe("Figma frame/component ID")
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
const ParseFigmaUrlSchema = z.object({
|
|
1074
|
-
url: z.string().describe("Full Figma URL or fileKey")
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
const ListFigmaPagesSchema = z.object({
|
|
1078
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1079
|
-
fileKey: z.string().describe("Figma file key or full Figma URL")
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
const SearchFigmaFramesSchema = z.object({
|
|
1083
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1084
|
-
fileKey: z.string().describe("Figma file key or full Figma URL"),
|
|
1085
|
-
searchQuery: z.string().describe("Search query")
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
const GetFigmaComponentsSchema = z.object({
|
|
1089
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1090
|
-
fileKey: z.string().describe("Figma file key or full Figma URL")
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
const GetFigmaStylesSchema = z.object({
|
|
1094
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1095
|
-
fileKey: z.string().describe("Figma file key or full Figma URL")
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
const GetFigmaColorPaletteSchema = z.object({
|
|
1099
|
-
figmaToken: z.string().optional().describe("Figma API token (optional if FIGMA_TOKEN env var is set)"),
|
|
1100
|
-
fileKey: z.string().describe("Figma file key or full Figma URL")
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
// New AI optimization tools schemas
|
|
1104
|
-
const SmartFindElementSchema = z.object({
|
|
1105
|
-
description: z.string().describe("Natural language description of element to find (e.g., 'login button', 'email field')"),
|
|
1106
|
-
maxResults: z.number().min(1).max(20).optional().describe("Maximum number of candidates to return (default: 5)"),
|
|
1107
|
-
action: z.object({
|
|
1108
|
-
type: z.enum(['click', 'type', 'scrollTo', 'screenshot', 'hover', 'setStyles']).describe("Action to perform on the best match"),
|
|
1109
|
-
text: z.string().optional().describe("Text to type (required for 'type' action)"),
|
|
1110
|
-
styles: z.array(z.object({
|
|
1111
|
-
name: z.string(),
|
|
1112
|
-
value: z.string()
|
|
1113
|
-
})).optional().describe("Styles to apply (required for 'setStyles' action)"),
|
|
1114
|
-
screenshot: z.boolean().optional().describe("Capture screenshot after action (default: false)"),
|
|
1115
|
-
waitAfter: z.number().optional().describe("Wait time in ms after action"),
|
|
1116
|
-
}).optional().describe("Optional action to perform on the best matching element"),
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
const AnalyzePageSchema = z.object({
|
|
1120
|
-
refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
const GetAllInteractiveElementsSchema = z.object({
|
|
1124
|
-
includeHidden: z.boolean().optional().describe("Include hidden elements (default: false)"),
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
const FindElementsByTextSchema = z.object({
|
|
1128
|
-
text: z.string().describe("Text to search for in elements"),
|
|
1129
|
-
exact: z.boolean().optional().describe("Exact match only (default: false)"),
|
|
1130
|
-
caseSensitive: z.boolean().optional().describe("Case sensitive search (default: false)"),
|
|
1131
|
-
action: z.object({
|
|
1132
|
-
type: z.enum(['click', 'type', 'scrollTo', 'screenshot', 'hover', 'setStyles']).describe("Action to perform on the first match"),
|
|
1133
|
-
text: z.string().optional().describe("Text to type (required for 'type' action)"),
|
|
1134
|
-
styles: z.array(z.object({
|
|
1135
|
-
name: z.string(),
|
|
1136
|
-
value: z.string()
|
|
1137
|
-
})).optional().describe("Styles to apply (required for 'setStyles' action)"),
|
|
1138
|
-
screenshot: z.boolean().optional().describe("Capture screenshot after action (default: false)"),
|
|
1139
|
-
waitAfter: z.number().optional().describe("Wait time in ms after action"),
|
|
1140
|
-
}).optional().describe("Optional action to perform on the first matching element"),
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
// List available tools
|
|
1144
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1145
|
-
return {
|
|
1146
|
-
tools: [
|
|
1147
|
-
{
|
|
1148
|
-
name: "ping",
|
|
1149
|
-
description: "Simple ping-pong tool for testing. Returns 'pong' with optional message.",
|
|
1150
|
-
inputSchema: {
|
|
1151
|
-
type: "object",
|
|
1152
|
-
properties: {
|
|
1153
|
-
message: { type: "string", description: "Optional message to include in response" },
|
|
1154
|
-
},
|
|
1155
|
-
},
|
|
1156
|
-
},
|
|
1157
|
-
{
|
|
1158
|
-
name: "openBrowser",
|
|
1159
|
-
description: "Open browser and navigate to URL. Window persists for further interactions.",
|
|
1160
|
-
inputSchema: {
|
|
1161
|
-
type: "object",
|
|
1162
|
-
properties: {
|
|
1163
|
-
url: { type: "string", description: "URL to navigate to" },
|
|
1164
|
-
},
|
|
1165
|
-
required: ["url"],
|
|
1166
|
-
},
|
|
1167
|
-
},
|
|
1168
|
-
{
|
|
1169
|
-
name: "click",
|
|
1170
|
-
description: "Click element. Waits for animations. Optional screenshot parameter.",
|
|
1171
|
-
inputSchema: {
|
|
1172
|
-
type: "object",
|
|
1173
|
-
properties: {
|
|
1174
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1175
|
-
waitAfter: { type: "number", description: "Wait ms (default: 1500)" },
|
|
1176
|
-
screenshot: { type: "boolean", description: "Screenshot (default: false)" },
|
|
1177
|
-
timeout: { type: "number", description: "Max wait ms (default: 30000)" },
|
|
1178
|
-
},
|
|
1179
|
-
required: ["selector"],
|
|
1180
|
-
},
|
|
1181
|
-
},
|
|
1182
|
-
{
|
|
1183
|
-
name: "type",
|
|
1184
|
-
description: "Type text into input field. Optional clear and typing delay.",
|
|
1185
|
-
inputSchema: {
|
|
1186
|
-
type: "object",
|
|
1187
|
-
properties: {
|
|
1188
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1189
|
-
text: { type: "string", description: "Text to type" },
|
|
1190
|
-
delay: { type: "number", description: "Keystroke delay ms (default: 0)" },
|
|
1191
|
-
clearFirst: { type: "boolean", description: "Clear first (default: true)" },
|
|
1192
|
-
},
|
|
1193
|
-
required: ["selector", "text"],
|
|
1194
|
-
},
|
|
1195
|
-
},
|
|
1196
|
-
{
|
|
1197
|
-
name: "getElement",
|
|
1198
|
-
description: "Get HTML markup of element. Prefer analyzePage for better efficiency.",
|
|
1199
|
-
inputSchema: {
|
|
1200
|
-
type: "object",
|
|
1201
|
-
properties: {
|
|
1202
|
-
selector: { type: "string", description: "CSS selector (default: body)" },
|
|
1203
|
-
},
|
|
1204
|
-
},
|
|
1205
|
-
},
|
|
1206
|
-
{
|
|
1207
|
-
name: "getComputedCss",
|
|
1208
|
-
description: "Get computed CSS styles for element. For layout debugging and responsive design.",
|
|
1209
|
-
inputSchema: {
|
|
1210
|
-
type: "object",
|
|
1211
|
-
properties: {
|
|
1212
|
-
selector: { type: "string", description: "CSS selector (default: body)" },
|
|
1213
|
-
category: {
|
|
1214
|
-
type: "string",
|
|
1215
|
-
enum: ["all", "layout", "typography", "colors", "visual"],
|
|
1216
|
-
description: "Filter: 'layout', 'typography', 'colors', 'visual', 'all' (default)"
|
|
1217
|
-
},
|
|
1218
|
-
properties: {
|
|
1219
|
-
type: "array",
|
|
1220
|
-
items: { type: "string" },
|
|
1221
|
-
description: "Specific properties. Overrides category."
|
|
1222
|
-
},
|
|
1223
|
-
includeDefaults: {
|
|
1224
|
-
type: "boolean",
|
|
1225
|
-
description: "Include defaults (default: false)"
|
|
1226
|
-
},
|
|
1227
|
-
},
|
|
1228
|
-
},
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
name: "getBoxModel",
|
|
1232
|
-
description: "Get element box model: dimensions, positioning, margins, padding, borders.",
|
|
1233
|
-
inputSchema: {
|
|
1234
|
-
type: "object",
|
|
1235
|
-
properties: {
|
|
1236
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1237
|
-
},
|
|
1238
|
-
required: ["selector"],
|
|
1239
|
-
},
|
|
1240
|
-
},
|
|
1241
|
-
{
|
|
1242
|
-
name: "screenshot",
|
|
1243
|
-
description: "Capture element image (15-25k tokens). For visual comparison. Use analyzePage for form data/validation (2-5k tokens).",
|
|
1244
|
-
inputSchema: {
|
|
1245
|
-
type: "object",
|
|
1246
|
-
properties: {
|
|
1247
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1248
|
-
padding: { type: "number", description: "Padding px (default: 0)" },
|
|
1249
|
-
maxWidth: { type: "number", description: "Max width px (default: 1024, null=original)" },
|
|
1250
|
-
maxHeight: { type: "number", description: "Max height px (default: 8000, null=original)" },
|
|
1251
|
-
quality: { type: "number", minimum: 1, maximum: 100, description: "JPEG quality (default: 80)" },
|
|
1252
|
-
format: { type: "string", enum: ["png", "jpeg", "auto"], description: "Format (default: auto)" },
|
|
1253
|
-
},
|
|
1254
|
-
required: ["selector"],
|
|
1255
|
-
},
|
|
1256
|
-
},
|
|
1257
|
-
{
|
|
1258
|
-
name: "saveScreenshot",
|
|
1259
|
-
description: "Save screenshot to file without returning in context. Auto-scales and compresses. Use maxWidth: null and format: 'png' for original quality.",
|
|
1260
|
-
inputSchema: {
|
|
1261
|
-
type: "object",
|
|
1262
|
-
properties: {
|
|
1263
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1264
|
-
filePath: { type: "string", description: "Save path (extension auto-adjusted)" },
|
|
1265
|
-
padding: { type: "number", description: "Padding px (default: 0)" },
|
|
1266
|
-
maxWidth: { type: "number", description: "Max width px (default: 1024, null=original)" },
|
|
1267
|
-
maxHeight: { type: "number", description: "Max height px (default: 8000, null=original)" },
|
|
1268
|
-
quality: { type: "number", minimum: 1, maximum: 100, description: "JPEG quality (default: 80)" },
|
|
1269
|
-
format: { type: "string", enum: ["png", "jpeg", "auto"], description: "Format (default: auto)" },
|
|
1270
|
-
},
|
|
1271
|
-
required: ["selector", "filePath"],
|
|
1272
|
-
},
|
|
1273
|
-
},
|
|
1274
|
-
{
|
|
1275
|
-
name: "scrollTo",
|
|
1276
|
-
description: "Scroll to element. For lazy loading and visibility testing.",
|
|
1277
|
-
inputSchema: {
|
|
1278
|
-
type: "object",
|
|
1279
|
-
properties: {
|
|
1280
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1281
|
-
behavior: { type: "string", enum: ["auto", "smooth"], description: "Behavior (default: auto)" },
|
|
1282
|
-
},
|
|
1283
|
-
required: ["selector"],
|
|
1284
|
-
},
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
name: "waitForElement",
|
|
1288
|
-
description: "Wait for element to appear. For dynamic content and lazy-loaded elements.",
|
|
1289
|
-
inputSchema: {
|
|
1290
|
-
type: "object",
|
|
1291
|
-
properties: {
|
|
1292
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1293
|
-
timeout: { type: "number", description: "Max wait ms (default: 5000)" },
|
|
1294
|
-
visible: { type: "boolean", description: "Wait for visible (default: true)" },
|
|
1295
|
-
},
|
|
1296
|
-
required: ["selector"],
|
|
1297
|
-
},
|
|
1298
|
-
},
|
|
1299
|
-
{
|
|
1300
|
-
name: "executeScript",
|
|
1301
|
-
description: "Execute JavaScript. Use only when specialized tools insufficient. Prefer analyzePage or findElementsByText.",
|
|
1302
|
-
inputSchema: {
|
|
1303
|
-
type: "object",
|
|
1304
|
-
properties: {
|
|
1305
|
-
script: { type: "string", description: "JavaScript code" },
|
|
1306
|
-
waitAfter: { type: "number", description: "Wait ms (default: 500)" },
|
|
1307
|
-
screenshot: { type: "boolean", description: "Screenshot (default: false)" },
|
|
1308
|
-
timeout: { type: "number", description: "Max wait ms (default: 30000)" },
|
|
1309
|
-
},
|
|
1310
|
-
required: ["script"],
|
|
1311
|
-
},
|
|
1312
|
-
},
|
|
1313
|
-
{
|
|
1314
|
-
name: "getConsoleLogs",
|
|
1315
|
-
description: "Get browser console messages. For debugging JS errors and tracking behavior.",
|
|
1316
|
-
inputSchema: {
|
|
1317
|
-
type: "object",
|
|
1318
|
-
properties: {
|
|
1319
|
-
types: { type: "array", items: { type: "string", enum: ["log", "warn", "error", "info", "debug", "verbose", "warning"] }, description: "Filter types (default: all)" },
|
|
1320
|
-
clear: { type: "boolean", description: "Clear after read (default: false)" },
|
|
1321
|
-
},
|
|
1322
|
-
},
|
|
1323
|
-
},
|
|
1324
|
-
{
|
|
1325
|
-
name: "listNetworkRequests",
|
|
1326
|
-
description: "List network requests (method, URL, status). Use getNetworkRequest for details. Supports pagination.",
|
|
1327
|
-
inputSchema: {
|
|
1328
|
-
type: "object",
|
|
1329
|
-
properties: {
|
|
1330
|
-
types: { type: "array", items: { type: "string", enum: ["Document", "Stylesheet", "Image", "Media", "Font", "Script", "XHR", "Fetch", "WebSocket", "Other"] }, description: "Filter types (default: Fetch, XHR)" },
|
|
1331
|
-
status: { type: "string", enum: ["pending", "completed", "failed", "all"], description: "Filter status (default: all)" },
|
|
1332
|
-
limit: { type: "number", description: "Max requests (default: 50)" },
|
|
1333
|
-
offset: { type: "number", description: "Skip requests (default: 0)" },
|
|
1334
|
-
clear: { type: "boolean", description: "Clear after read (default: false)" },
|
|
1335
|
-
},
|
|
1336
|
-
},
|
|
1337
|
-
},
|
|
1338
|
-
{
|
|
1339
|
-
name: "getNetworkRequest",
|
|
1340
|
-
description: "Get network request details (headers, payload, response). Use requestId from listNetworkRequests.",
|
|
1341
|
-
inputSchema: {
|
|
1342
|
-
type: "object",
|
|
1343
|
-
properties: {
|
|
1344
|
-
requestId: { type: "string", description: "Request ID" },
|
|
1345
|
-
},
|
|
1346
|
-
required: ["requestId"],
|
|
1347
|
-
},
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
name: "filterNetworkRequests",
|
|
1351
|
-
description: "Filter network requests by URL pattern. Returns matching requests with full details.",
|
|
1352
|
-
inputSchema: {
|
|
1353
|
-
type: "object",
|
|
1354
|
-
properties: {
|
|
1355
|
-
urlPattern: { type: "string", description: "URL pattern (regex or partial)" },
|
|
1356
|
-
types: { type: "array", items: { type: "string", enum: ["Document", "Stylesheet", "Image", "Media", "Font", "Script", "XHR", "Fetch", "WebSocket", "Other"] }, description: "Filter types (default: Fetch, XHR)" },
|
|
1357
|
-
clear: { type: "boolean", description: "Clear after read (default: false)" },
|
|
1358
|
-
},
|
|
1359
|
-
required: ["urlPattern"],
|
|
1360
|
-
},
|
|
1361
|
-
},
|
|
1362
|
-
{
|
|
1363
|
-
name: "hover",
|
|
1364
|
-
description: "Hover over element. For testing hover effects, tooltips, and CSS :hover states.",
|
|
1365
|
-
inputSchema: {
|
|
1366
|
-
type: "object",
|
|
1367
|
-
properties: {
|
|
1368
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1369
|
-
},
|
|
1370
|
-
required: ["selector"],
|
|
1371
|
-
},
|
|
1372
|
-
},
|
|
1373
|
-
{
|
|
1374
|
-
name: "setStyles",
|
|
1375
|
-
description: "Apply inline CSS to element. For live editing and prototyping.",
|
|
1376
|
-
inputSchema: {
|
|
1377
|
-
type: "object",
|
|
1378
|
-
properties: {
|
|
1379
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1380
|
-
styles: {
|
|
1381
|
-
type: "array",
|
|
1382
|
-
items: {
|
|
1383
|
-
type: "object",
|
|
1384
|
-
properties: {
|
|
1385
|
-
name: { type: "string", description: "Property name" },
|
|
1386
|
-
value: { type: "string", description: "Property value" },
|
|
1387
|
-
},
|
|
1388
|
-
required: ["name", "value"],
|
|
1389
|
-
},
|
|
1390
|
-
description: "CSS property name-value pairs",
|
|
1391
|
-
},
|
|
1392
|
-
},
|
|
1393
|
-
required: ["selector", "styles"],
|
|
1394
|
-
},
|
|
1395
|
-
},
|
|
1396
|
-
{
|
|
1397
|
-
name: "setViewport",
|
|
1398
|
-
description: "Change viewport dimensions. Test responsive layouts across screen sizes.",
|
|
1399
|
-
inputSchema: {
|
|
1400
|
-
type: "object",
|
|
1401
|
-
properties: {
|
|
1402
|
-
width: { type: "number", minimum: 320, maximum: 4000, description: "Width px" },
|
|
1403
|
-
height: { type: "number", minimum: 200, maximum: 3000, description: "Height px" },
|
|
1404
|
-
deviceScaleFactor: { type: "number", minimum: 0.5, maximum: 3, description: "Pixel ratio (default: 1)" },
|
|
1405
|
-
},
|
|
1406
|
-
required: ["width", "height"],
|
|
1407
|
-
},
|
|
1408
|
-
},
|
|
1409
|
-
{
|
|
1410
|
-
name: "getViewport",
|
|
1411
|
-
description: "Get viewport size and pixel ratio. For responsive design testing.",
|
|
1412
|
-
inputSchema: {
|
|
1413
|
-
type: "object",
|
|
1414
|
-
properties: {},
|
|
1415
|
-
},
|
|
1416
|
-
},
|
|
1417
|
-
{
|
|
1418
|
-
name: "navigateTo",
|
|
1419
|
-
description: "Navigate to new URL. Reuses browser instance.",
|
|
1420
|
-
inputSchema: {
|
|
1421
|
-
type: "object",
|
|
1422
|
-
properties: {
|
|
1423
|
-
url: { type: "string", description: "URL to navigate to" },
|
|
1424
|
-
waitUntil: { type: "string", enum: ["load", "domcontentloaded", "networkidle0", "networkidle2"], description: "Wait event (default: networkidle2)" },
|
|
1425
|
-
},
|
|
1426
|
-
required: ["url"],
|
|
1427
|
-
},
|
|
1428
|
-
},
|
|
1429
|
-
{
|
|
1430
|
-
name: "getFigmaFrame",
|
|
1431
|
-
description: "Export Figma frame as PNG. Requires API token and file/node IDs.",
|
|
1432
|
-
inputSchema: {
|
|
1433
|
-
type: "object",
|
|
1434
|
-
properties: {
|
|
1435
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1436
|
-
fileKey: { type: "string", description: "File key" },
|
|
1437
|
-
nodeId: { type: "string", description: "Frame/component ID" },
|
|
1438
|
-
scale: { type: "number", minimum: 0.1, maximum: 4, description: "Scale (default: 2)" },
|
|
1439
|
-
format: { type: "string", enum: ["png", "jpg", "svg"], description: "Format (default: png)" },
|
|
1440
|
-
},
|
|
1441
|
-
required: ["fileKey", "nodeId"],
|
|
1442
|
-
},
|
|
1443
|
-
},
|
|
1444
|
-
{
|
|
1445
|
-
name: "compareFigmaToElement",
|
|
1446
|
-
description: "Compare Figma design with browser element. Pixel-perfect validation.",
|
|
1447
|
-
inputSchema: {
|
|
1448
|
-
type: "object",
|
|
1449
|
-
properties: {
|
|
1450
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1451
|
-
fileKey: { type: "string", description: "File key" },
|
|
1452
|
-
nodeId: { type: "string", description: "Frame/component ID" },
|
|
1453
|
-
selector: { type: "string", description: "CSS selector" },
|
|
1454
|
-
threshold: { type: "number", minimum: 0, maximum: 1, description: "Diff threshold (default: 0.05)" },
|
|
1455
|
-
figmaScale: { type: "number", minimum: 0.1, maximum: 4, description: "Scale (default: 2)" },
|
|
1456
|
-
},
|
|
1457
|
-
required: ["fileKey", "nodeId", "selector"],
|
|
1458
|
-
},
|
|
1459
|
-
},
|
|
1460
|
-
{
|
|
1461
|
-
name: "getFigmaSpecs",
|
|
1462
|
-
description: "Extract design specs from Figma: colors, fonts, dimensions, spacing.",
|
|
1463
|
-
inputSchema: {
|
|
1464
|
-
type: "object",
|
|
1465
|
-
properties: {
|
|
1466
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1467
|
-
fileKey: { type: "string", description: "File key" },
|
|
1468
|
-
nodeId: { type: "string", description: "Frame/component ID" },
|
|
1469
|
-
},
|
|
1470
|
-
required: ["fileKey", "nodeId"],
|
|
1471
|
-
},
|
|
1472
|
-
},
|
|
1473
|
-
{
|
|
1474
|
-
name: "parseFigmaUrl",
|
|
1475
|
-
description: "Parse Figma URL to extract fileKey and nodeId.",
|
|
1476
|
-
inputSchema: {
|
|
1477
|
-
type: "object",
|
|
1478
|
-
properties: {
|
|
1479
|
-
url: { type: "string", description: "Figma URL or fileKey" },
|
|
1480
|
-
},
|
|
1481
|
-
required: ["url"],
|
|
1482
|
-
},
|
|
1483
|
-
},
|
|
1484
|
-
{
|
|
1485
|
-
name: "listFigmaPages",
|
|
1486
|
-
description: "Get file structure: all pages and frames. Use first to discover file contents.",
|
|
1487
|
-
inputSchema: {
|
|
1488
|
-
type: "object",
|
|
1489
|
-
properties: {
|
|
1490
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1491
|
-
fileKey: { type: "string", description: "File key or URL" },
|
|
1492
|
-
},
|
|
1493
|
-
required: ["fileKey"],
|
|
1494
|
-
},
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
name: "searchFigmaFrames",
|
|
1498
|
-
description: "Search frames/components by name. Case-insensitive across all pages.",
|
|
1499
|
-
inputSchema: {
|
|
1500
|
-
type: "object",
|
|
1501
|
-
properties: {
|
|
1502
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1503
|
-
fileKey: { type: "string", description: "File key or URL" },
|
|
1504
|
-
searchQuery: { type: "string", description: "Search query" },
|
|
1505
|
-
},
|
|
1506
|
-
required: ["fileKey", "searchQuery"],
|
|
1507
|
-
},
|
|
1508
|
-
},
|
|
1509
|
-
{
|
|
1510
|
-
name: "getFigmaComponents",
|
|
1511
|
-
description: "Get all components from file (Design System). For extracting design system.",
|
|
1512
|
-
inputSchema: {
|
|
1513
|
-
type: "object",
|
|
1514
|
-
properties: {
|
|
1515
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1516
|
-
fileKey: { type: "string", description: "File key or URL" },
|
|
1517
|
-
},
|
|
1518
|
-
required: ["fileKey"],
|
|
1519
|
-
},
|
|
1520
|
-
},
|
|
1521
|
-
{
|
|
1522
|
-
name: "getFigmaStyles",
|
|
1523
|
-
description: "Get all styles: color, text, effect, grid. For extracting design tokens.",
|
|
1524
|
-
inputSchema: {
|
|
1525
|
-
type: "object",
|
|
1526
|
-
properties: {
|
|
1527
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1528
|
-
fileKey: { type: "string", description: "File key or URL" },
|
|
1529
|
-
},
|
|
1530
|
-
required: ["fileKey"],
|
|
1531
|
-
},
|
|
1532
|
-
},
|
|
1533
|
-
{
|
|
1534
|
-
name: "getFigmaColorPalette",
|
|
1535
|
-
description: "Extract color palette. Returns unique colors with hex, rgba, usage count.",
|
|
1536
|
-
inputSchema: {
|
|
1537
|
-
type: "object",
|
|
1538
|
-
properties: {
|
|
1539
|
-
figmaToken: { type: "string", description: "API token (optional)" },
|
|
1540
|
-
fileKey: { type: "string", description: "File key or URL" },
|
|
1541
|
-
},
|
|
1542
|
-
required: ["fileKey"],
|
|
1543
|
-
},
|
|
1544
|
-
},
|
|
1545
|
-
{
|
|
1546
|
-
name: "smartFindElement",
|
|
1547
|
-
description: "Find elements with natural language. Returns ranked candidates. Prefer analyzePage for better performance.",
|
|
1548
|
-
inputSchema: {
|
|
1549
|
-
type: "object",
|
|
1550
|
-
properties: {
|
|
1551
|
-
description: { type: "string", description: "Natural language description" },
|
|
1552
|
-
maxResults: { type: "number", minimum: 1, maximum: 20, description: "Max candidates (default: 5)" },
|
|
1553
|
-
action: {
|
|
1554
|
-
type: "object",
|
|
1555
|
-
properties: {
|
|
1556
|
-
type: { type: "string", enum: ["click", "type", "scrollTo", "screenshot", "hover", "setStyles"], description: "Action type" },
|
|
1557
|
-
text: { type: "string", description: "Text for 'type'" },
|
|
1558
|
-
styles: { type: "array", items: { type: "object", properties: { name: { type: "string" }, value: { type: "string" } } }, description: "Styles for 'setStyles'" },
|
|
1559
|
-
screenshot: { type: "boolean", description: "Screenshot (default: false)" },
|
|
1560
|
-
waitAfter: { type: "number", description: "Wait ms" },
|
|
1561
|
-
},
|
|
1562
|
-
required: ["type"],
|
|
1563
|
-
description: "Optional action on element",
|
|
1564
|
-
},
|
|
1565
|
-
},
|
|
1566
|
-
required: ["description"],
|
|
1567
|
-
},
|
|
1568
|
-
},
|
|
1569
|
-
{
|
|
1570
|
-
name: "analyzePage",
|
|
1571
|
-
description: "Get page state: forms, inputs, buttons, links with values. Use refresh:true after interactions. Cached per URL. 2-5k tokens vs screenshot 15-25k.",
|
|
1572
|
-
inputSchema: {
|
|
1573
|
-
type: "object",
|
|
1574
|
-
properties: {
|
|
1575
|
-
refresh: { type: "boolean", description: "Refresh cache (default: false)" },
|
|
1576
|
-
},
|
|
1577
|
-
},
|
|
1578
|
-
},
|
|
1579
|
-
{
|
|
1580
|
-
name: "getAllInteractiveElements",
|
|
1581
|
-
description: "Get all interactive elements with selectors. For understanding available actions.",
|
|
1582
|
-
inputSchema: {
|
|
1583
|
-
type: "object",
|
|
1584
|
-
properties: {
|
|
1585
|
-
includeHidden: { type: "boolean", description: "Include hidden (default: false)" },
|
|
1586
|
-
},
|
|
1587
|
-
},
|
|
1588
|
-
},
|
|
1589
|
-
{
|
|
1590
|
-
name: "findElementsByText",
|
|
1591
|
-
description: "Find elements by text. Returns elements with selectors. Optional actions on first match.",
|
|
1592
|
-
inputSchema: {
|
|
1593
|
-
type: "object",
|
|
1594
|
-
properties: {
|
|
1595
|
-
text: { type: "string", description: "Search text" },
|
|
1596
|
-
exact: { type: "boolean", description: "Exact match (default: false)" },
|
|
1597
|
-
caseSensitive: { type: "boolean", description: "Case sensitive (default: false)" },
|
|
1598
|
-
action: {
|
|
1599
|
-
type: "object",
|
|
1600
|
-
properties: {
|
|
1601
|
-
type: { type: "string", enum: ["click", "type", "scrollTo", "screenshot", "hover", "setStyles"], description: "Action type" },
|
|
1602
|
-
text: { type: "string", description: "Text for 'type'" },
|
|
1603
|
-
styles: { type: "array", items: { type: "object", properties: { name: { type: "string" }, value: { type: "string" } } }, description: "Styles for 'setStyles'" },
|
|
1604
|
-
screenshot: { type: "boolean", description: "Screenshot (default: false)" },
|
|
1605
|
-
waitAfter: { type: "number", description: "Wait ms" },
|
|
1606
|
-
},
|
|
1607
|
-
required: ["type"],
|
|
1608
|
-
description: "Optional action on first match",
|
|
1609
|
-
},
|
|
1610
|
-
},
|
|
1611
|
-
required: ["text"],
|
|
1612
|
-
},
|
|
1613
|
-
},
|
|
1614
|
-
{
|
|
1615
|
-
name: "enableRecorder",
|
|
1616
|
-
description: "Inject recorder UI widget. Visual recording with start/stop/save controls.",
|
|
1617
|
-
inputSchema: {
|
|
1618
|
-
type: "object",
|
|
1619
|
-
properties: {
|
|
1620
|
-
directory: { type: "string", description: "Directory to save scenarios (optional, defaults to auto-detected project root)" },
|
|
1621
|
-
},
|
|
1622
|
-
},
|
|
1623
|
-
},
|
|
1624
|
-
{
|
|
1625
|
-
name: "executeScenario",
|
|
1626
|
-
description: "Execute recorded scenario by name. Runs actions with dependency resolution.",
|
|
1627
|
-
inputSchema: {
|
|
1628
|
-
type: "object",
|
|
1629
|
-
properties: {
|
|
1630
|
-
name: { type: "string", description: "Scenario name" },
|
|
1631
|
-
parameters: { type: "object", description: "Execution parameters" },
|
|
1632
|
-
executeDependencies: { type: "boolean", description: "Execute dependencies (default: true)" },
|
|
1633
|
-
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1634
|
-
},
|
|
1635
|
-
required: ["name"],
|
|
1636
|
-
},
|
|
1637
|
-
},
|
|
1638
|
-
{
|
|
1639
|
-
name: "listScenarios",
|
|
1640
|
-
description: "List all scenarios with metadata.",
|
|
1641
|
-
inputSchema: {
|
|
1642
|
-
type: "object",
|
|
1643
|
-
properties: {
|
|
1644
|
-
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1645
|
-
},
|
|
1646
|
-
},
|
|
1647
|
-
},
|
|
1648
|
-
{
|
|
1649
|
-
name: "searchScenarios",
|
|
1650
|
-
description: "Search scenarios by text or tags.",
|
|
1651
|
-
inputSchema: {
|
|
1652
|
-
type: "object",
|
|
1653
|
-
properties: {
|
|
1654
|
-
text: { type: "string", description: "Search text" },
|
|
1655
|
-
tags: { type: "array", items: { type: "string" }, description: "Filter tags" },
|
|
1656
|
-
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1657
|
-
},
|
|
1658
|
-
},
|
|
1659
|
-
},
|
|
1660
|
-
{
|
|
1661
|
-
name: "getScenarioInfo",
|
|
1662
|
-
description: "Get scenario details: actions, parameters, dependencies.",
|
|
1663
|
-
inputSchema: {
|
|
1664
|
-
type: "object",
|
|
1665
|
-
properties: {
|
|
1666
|
-
name: { type: "string", description: "Scenario name" },
|
|
1667
|
-
includeSecrets: { type: "boolean", description: "Include secrets (default: false)" },
|
|
1668
|
-
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1669
|
-
},
|
|
1670
|
-
required: ["name"],
|
|
1671
|
-
},
|
|
1672
|
-
},
|
|
1673
|
-
{
|
|
1674
|
-
name: "deleteScenario",
|
|
1675
|
-
description: "Delete scenario and secrets.",
|
|
1676
|
-
inputSchema: {
|
|
1677
|
-
type: "object",
|
|
1678
|
-
properties: {
|
|
1679
|
-
name: { type: "string", description: "Scenario name" },
|
|
1680
|
-
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1681
|
-
},
|
|
1682
|
-
required: ["name"],
|
|
1683
|
-
},
|
|
1684
|
-
},
|
|
1685
|
-
{
|
|
1686
|
-
name: "exportScenarioAsCode",
|
|
1687
|
-
description: "Export recorded scenario as executable test code for various frameworks. Automatically cleans unstable selectors (CSS modules, styled-components).",
|
|
1688
|
-
inputSchema: {
|
|
1689
|
-
type: "object",
|
|
1690
|
-
properties: {
|
|
1691
|
-
scenarioName: {
|
|
1692
|
-
type: "string",
|
|
1693
|
-
description: "Name of scenario to export"
|
|
1694
|
-
},
|
|
1695
|
-
language: {
|
|
1696
|
-
type: "string",
|
|
1697
|
-
enum: ["playwright-typescript", "playwright-python", "selenium-python", "selenium-java"],
|
|
1698
|
-
description: "Target test framework and language"
|
|
1699
|
-
},
|
|
1700
|
-
cleanSelectors: {
|
|
1701
|
-
type: "boolean",
|
|
1702
|
-
description: "Remove unstable CSS classes (default: true)"
|
|
1703
|
-
},
|
|
1704
|
-
includeComments: {
|
|
1705
|
-
type: "boolean",
|
|
1706
|
-
description: "Include descriptive comments (default: true)"
|
|
1707
|
-
},
|
|
1708
|
-
directory: {
|
|
1709
|
-
type: "string",
|
|
1710
|
-
description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)"
|
|
1711
|
-
},
|
|
1712
|
-
},
|
|
1713
|
-
required: ["scenarioName", "language"],
|
|
1714
|
-
},
|
|
1715
|
-
},
|
|
1716
|
-
],
|
|
1717
|
-
};
|
|
1718
|
-
});
|
|
3
|
+
import {Server} from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {CallToolRequestSchema, ListToolsRequestSchema,} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import Jimp from "jimp";
|
|
7
|
+
import pixelmatch from "pixelmatch";
|
|
8
|
+
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs';
|
|
9
|
+
import path, {dirname} from 'path';
|
|
10
|
+
import {fileURLToPath} from 'url';
|
|
11
|
+
import {homedir} from 'os';
|
|
1719
12
|
|
|
1720
|
-
//
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
13
|
+
// Import browser and platform utilities
|
|
14
|
+
import {closeBrowser} from './browser/browser-manager.js';
|
|
15
|
+
import {isWSL} from './utils/platform-utils.js';
|
|
16
|
+
|
|
17
|
+
// Import page management utilities
|
|
18
|
+
import {
|
|
19
|
+
consoleLogs,
|
|
20
|
+
getLastOpenPage,
|
|
21
|
+
getOrCreatePage,
|
|
22
|
+
networkRequests,
|
|
23
|
+
pageAnalysisCache,
|
|
24
|
+
pagesWithRecorder
|
|
25
|
+
} from './browser/page-manager.js';
|
|
26
|
+
|
|
27
|
+
// Import image processing utilities
|
|
28
|
+
import {calculateSSIM, processScreenshot} from './utils/image-processing.js';
|
|
29
|
+
|
|
30
|
+
// Import CSS utilities
|
|
31
|
+
import {filterCssStyles} from './utils/css-utils.js';
|
|
32
|
+
|
|
33
|
+
// Import tool schemas and definitions
|
|
34
|
+
import * as schemas from './server/tool-schemas.js';
|
|
35
|
+
import {toolDefinitions} from './server/tool-definitions.js';
|
|
36
|
+
|
|
37
|
+
// Import element actions helper
|
|
38
|
+
import {executeElementAction} from './utils/element-actions.js';
|
|
39
|
+
// Import hints generator
|
|
40
|
+
import {generateClickHints, generateNavigationHints} from './utils/hints-generator.js';
|
|
41
|
+
|
|
42
|
+
// Import Recorder modules
|
|
43
|
+
import {injectRecorder} from './recorder/recorder-script.js';
|
|
44
|
+
import {executeScenario} from './recorder/scenario-executor.js';
|
|
45
|
+
import {deleteScenario, listScenarios, loadScenario, searchScenarios} from './recorder/scenario-storage.js';
|
|
46
|
+
import {generatePageObject} from './recorder/page-object-generator.js';
|
|
47
|
+
|
|
48
|
+
// Import Code Generators
|
|
49
|
+
import {PlaywrightTypeScriptGenerator} from './utils/code-generators/playwright-typescript.js';
|
|
50
|
+
import {PlaywrightPythonGenerator} from './utils/code-generators/playwright-python.js';
|
|
51
|
+
import {SeleniumPythonGenerator} from './utils/code-generators/selenium-python.js';
|
|
52
|
+
import {SeleniumJavaGenerator} from './utils/code-generators/selenium-java.js';
|
|
53
|
+
import {FileAppender} from './utils/code-generators/file-appender.js';
|
|
54
|
+
// Import Figma tools
|
|
55
|
+
import {
|
|
56
|
+
collectAllText,
|
|
57
|
+
extractTextFromNode,
|
|
58
|
+
fetchFigmaAPI,
|
|
59
|
+
getFigmaColorPalette,
|
|
60
|
+
getFigmaComponents,
|
|
61
|
+
getFigmaStyles,
|
|
62
|
+
listFigmaPages,
|
|
63
|
+
normalizeFigmaNodeId,
|
|
64
|
+
parseFigmaUrl,
|
|
65
|
+
searchFigmaFrames
|
|
66
|
+
} from './figma-tools.js';
|
|
67
|
+
|
|
68
|
+
// Debug mode - only use stderr for actual errors, not debug info
|
|
69
|
+
// MCP uses STDIO for JSON-RPC, so console.log/error breaks the protocol
|
|
70
|
+
const DEBUG_MODE = process.env.CHROMETOOLS_DEBUG === 'true';
|
|
71
|
+
const debugLog = DEBUG_MODE ? console.error : () => {};
|
|
72
|
+
|
|
73
|
+
// Figma token from environment variable (can be set in MCP config)
|
|
74
|
+
const FIGMA_TOKEN = process.env.FIGMA_TOKEN || null;
|
|
75
|
+
|
|
76
|
+
// Get current directory for loading utils
|
|
77
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
78
|
+
const __dirname = dirname(__filename);
|
|
79
|
+
|
|
80
|
+
// Load element finder utilities
|
|
81
|
+
const elementFinderUtils = readFileSync(path.join(__dirname, 'element-finder-utils.js'), 'utf-8');
|
|
1725
82
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
83
|
+
// Base storage directory in user's home folder
|
|
84
|
+
const BASE_STORAGE_DIR = path.join(homedir(), '.config', 'chrometools-mcp');
|
|
85
|
+
const GLOBAL_INDEX_PATH = path.join(BASE_STORAGE_DIR, 'index.json');
|
|
86
|
+
const PROJECTS_DIR = path.join(BASE_STORAGE_DIR, 'projects');
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load global index from ~/.config/chrometools-mcp/index.json
|
|
90
|
+
* @returns {object} - Global index object
|
|
91
|
+
*/
|
|
92
|
+
function loadGlobalIndex() {
|
|
93
|
+
try {
|
|
94
|
+
const data = readFileSync(GLOBAL_INDEX_PATH, 'utf-8');
|
|
95
|
+
return JSON.parse(data);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Index doesn't exist yet, return empty structure
|
|
98
|
+
return {
|
|
99
|
+
version: '2.0',
|
|
100
|
+
projects: {}
|
|
101
|
+
};
|
|
1729
102
|
}
|
|
103
|
+
}
|
|
1730
104
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Save global index to ~/.config/chrometools-mcp/index.json
|
|
107
|
+
* @param {object} index - Global index object
|
|
108
|
+
*/
|
|
109
|
+
function saveGlobalIndex(index) {
|
|
110
|
+
mkdirSync(BASE_STORAGE_DIR, { recursive: true });
|
|
111
|
+
writeFileSync(GLOBAL_INDEX_PATH, JSON.stringify(index, null, 2), 'utf-8');
|
|
112
|
+
debugLog('[chrometools-mcp] Global index saved');
|
|
113
|
+
}
|
|
1736
114
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Get all project IDs from global index
|
|
117
|
+
* @returns {string[]} - Array of project IDs
|
|
118
|
+
*/
|
|
119
|
+
function getAllProjectIds() {
|
|
120
|
+
const index = loadGlobalIndex();
|
|
121
|
+
return Object.keys(index.projects || {});
|
|
122
|
+
}
|
|
1742
123
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
124
|
+
// Cleanup on exit
|
|
125
|
+
process.on("SIGINT", async () => {
|
|
126
|
+
await closeBrowser();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
});
|
|
1748
129
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
await element.click({ clickCount: 3 });
|
|
1754
|
-
await page.keyboard.press('Backspace');
|
|
1755
|
-
await element.type(action.text, { delay: 0 });
|
|
1756
|
-
await new Promise(resolve => setTimeout(resolve, action.waitAfter || 500));
|
|
1757
|
-
result.message = `Typed "${action.text}" into ${selector}`;
|
|
1758
|
-
|
|
1759
|
-
if (action.screenshot) {
|
|
1760
|
-
const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
|
|
1761
|
-
result.screenshot = screenshot;
|
|
1762
|
-
}
|
|
1763
|
-
break;
|
|
130
|
+
// Migration: Remove old project-based scenarios (v2.0) on first run with v2.1.0
|
|
131
|
+
// This migration runs once to clean up old scenarios and start fresh with URL-based organization
|
|
132
|
+
try {
|
|
133
|
+
const migrationFlagPath = path.join(BASE_STORAGE_DIR, '.migration-v2.1.0-done');
|
|
1764
134
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
x: window.scrollX,
|
|
1770
|
-
y: window.scrollY
|
|
1771
|
-
}));
|
|
1772
|
-
result.message = `Scrolled to ${selector}`;
|
|
1773
|
-
result.position = position;
|
|
1774
|
-
break;
|
|
135
|
+
if (!existsSync(migrationFlagPath)) {
|
|
136
|
+
// Check if old projects directory exists
|
|
137
|
+
if (existsSync(PROJECTS_DIR)) {
|
|
138
|
+
console.error('[chrometools-mcp] Migration v2.1.0: Removing old project-based scenarios...');
|
|
1775
139
|
|
|
1776
|
-
|
|
1777
|
-
const
|
|
1778
|
-
|
|
1779
|
-
throw new Error(`Element not visible: ${selector}`);
|
|
1780
|
-
}
|
|
1781
|
-
const clip = {
|
|
1782
|
-
x: Math.max(box.x, 0),
|
|
1783
|
-
y: Math.max(box.y, 0),
|
|
1784
|
-
width: Math.max(box.width, 1),
|
|
1785
|
-
height: Math.max(box.height, 1)
|
|
1786
|
-
};
|
|
1787
|
-
const screenshot = await page.screenshot({ clip, encoding: 'base64' });
|
|
1788
|
-
result.message = `Captured screenshot of ${selector}`;
|
|
1789
|
-
result.screenshot = screenshot;
|
|
1790
|
-
break;
|
|
140
|
+
// Remove all old scenarios
|
|
141
|
+
const { rmSync } = await import('fs');
|
|
142
|
+
rmSync(PROJECTS_DIR, { recursive: true, force: true });
|
|
1791
143
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
await new Promise(resolve => setTimeout(resolve, action.waitAfter || 100));
|
|
1795
|
-
result.message = `Hovered over ${selector}`;
|
|
144
|
+
console.error('[chrometools-mcp] Migration v2.1.0: Old scenarios removed. Starting fresh with URL-based organization.');
|
|
145
|
+
}
|
|
1796
146
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
147
|
+
// Remove old global index
|
|
148
|
+
if (existsSync(GLOBAL_INDEX_PATH)) {
|
|
149
|
+
const { unlinkSync } = await import('fs');
|
|
150
|
+
unlinkSync(GLOBAL_INDEX_PATH);
|
|
151
|
+
}
|
|
1802
152
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const el = document.querySelector(sel);
|
|
1813
|
-
if (el) {
|
|
1814
|
-
Object.entries(styles).forEach(([key, value]) => {
|
|
1815
|
-
el.style.setProperty(key, value);
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
}, selector, stylesObject);
|
|
1819
|
-
await new Promise(resolve => setTimeout(resolve, action.waitAfter || 100));
|
|
1820
|
-
result.message = `Applied styles to ${selector}`;
|
|
1821
|
-
result.styles = stylesObject;
|
|
1822
|
-
|
|
1823
|
-
if (action.screenshot) {
|
|
1824
|
-
const screenshot = await page.screenshot({ encoding: 'base64', fullPage: false });
|
|
1825
|
-
result.screenshot = screenshot;
|
|
1826
|
-
}
|
|
1827
|
-
break;
|
|
153
|
+
// Create migration flag
|
|
154
|
+
mkdirSync(BASE_STORAGE_DIR, { recursive: true });
|
|
155
|
+
writeFileSync(migrationFlagPath, new Date().toISOString(), 'utf-8');
|
|
156
|
+
console.error('[chrometools-mcp] Migration v2.1.0: Complete. New scenarios will be organized by website domain.');
|
|
157
|
+
}
|
|
158
|
+
} catch (migrationError) {
|
|
159
|
+
console.error('[chrometools-mcp] Migration v2.1.0 failed:', migrationError.message);
|
|
160
|
+
// Continue anyway - migration is optional
|
|
161
|
+
}
|
|
1828
162
|
|
|
1829
|
-
|
|
1830
|
-
|
|
163
|
+
// Create MCP server
|
|
164
|
+
const server = new Server(
|
|
165
|
+
{
|
|
166
|
+
name: "chrometools-mcp",
|
|
167
|
+
version: "2.1.0",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
capabilities: {
|
|
171
|
+
tools: {},
|
|
172
|
+
},
|
|
1831
173
|
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// List available tools
|
|
177
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
178
|
+
return {
|
|
179
|
+
tools: toolDefinitions,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
|
|
1832
184
|
|
|
1833
|
-
|
|
185
|
+
// Wrapper to add timeout protection for all tool calls
|
|
186
|
+
async function executeToolWithTimeout(toolName, toolFunction, timeoutMs = 120000) {
|
|
187
|
+
return Promise.race([
|
|
188
|
+
toolFunction(),
|
|
189
|
+
new Promise((_, reject) =>
|
|
190
|
+
setTimeout(() => reject(new Error(`Tool '${toolName}' timeout after ${timeoutMs/1000}s`)), timeoutMs)
|
|
191
|
+
)
|
|
192
|
+
]);
|
|
1834
193
|
}
|
|
1835
194
|
|
|
1836
195
|
// Handle tool calls
|
|
1837
196
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1838
197
|
const { name, arguments: args } = request.params;
|
|
1839
198
|
|
|
199
|
+
try {
|
|
200
|
+
// Execute with timeout wrapper (2 minutes default, 5 minutes for scenario execution)
|
|
201
|
+
const toolTimeout = name === 'executeScenario' ? 360000 : 120000; // 6 min for scenarios, 2 min for others
|
|
202
|
+
|
|
203
|
+
return await executeToolWithTimeout(name, async () => {
|
|
204
|
+
return await executeToolInternal(name, args);
|
|
205
|
+
}, toolTimeout);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: 'text',
|
|
210
|
+
text: JSON.stringify({
|
|
211
|
+
success: false,
|
|
212
|
+
error: error.message,
|
|
213
|
+
tool: name
|
|
214
|
+
}, null, 2)
|
|
215
|
+
}],
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Internal tool execution function
|
|
222
|
+
async function executeToolInternal(name, args) {
|
|
1840
223
|
try {
|
|
1841
224
|
if (name === "ping") {
|
|
1842
|
-
const validatedArgs = PingSchema.parse(args);
|
|
225
|
+
const validatedArgs = schemas.PingSchema.parse(args);
|
|
1843
226
|
const responseMessage = validatedArgs.message
|
|
1844
227
|
? `pong: ${validatedArgs.message}`
|
|
1845
228
|
: "pong";
|
|
@@ -1855,7 +238,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1855
238
|
}
|
|
1856
239
|
|
|
1857
240
|
if (name === "openBrowser") {
|
|
1858
|
-
const validatedArgs = OpenBrowserSchema.parse(args);
|
|
241
|
+
const validatedArgs = schemas.OpenBrowserSchema.parse(args);
|
|
1859
242
|
const page = await getOrCreatePage(validatedArgs.url);
|
|
1860
243
|
const title = await page.title();
|
|
1861
244
|
|
|
@@ -1873,7 +256,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1873
256
|
}
|
|
1874
257
|
|
|
1875
258
|
if (name === "click") {
|
|
1876
|
-
const validatedArgs = ClickSchema.parse(args);
|
|
259
|
+
const validatedArgs = schemas.ClickSchema.parse(args);
|
|
1877
260
|
const page = await getLastOpenPage();
|
|
1878
261
|
const timeout = validatedArgs.timeout || 30000;
|
|
1879
262
|
|
|
@@ -1921,7 +304,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1921
304
|
}
|
|
1922
305
|
|
|
1923
306
|
if (name === "type") {
|
|
1924
|
-
const validatedArgs = TypeSchema.parse(args);
|
|
307
|
+
const validatedArgs = schemas.TypeSchema.parse(args);
|
|
1925
308
|
const page = await getLastOpenPage();
|
|
1926
309
|
|
|
1927
310
|
const element = await page.$(validatedArgs.selector);
|
|
@@ -1945,7 +328,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1945
328
|
}
|
|
1946
329
|
|
|
1947
330
|
if (name === "getElement") {
|
|
1948
|
-
const validatedArgs = GetElementSchema.parse(args);
|
|
331
|
+
const validatedArgs = schemas.GetElementSchema.parse(args);
|
|
1949
332
|
const page = await getLastOpenPage();
|
|
1950
333
|
|
|
1951
334
|
const client = await page.target().createCDPSession();
|
|
@@ -1971,7 +354,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1971
354
|
}
|
|
1972
355
|
|
|
1973
356
|
if (name === "getComputedCss") {
|
|
1974
|
-
const validatedArgs = GetComputedCssSchema.parse(args);
|
|
357
|
+
const validatedArgs = schemas.GetComputedCssSchema.parse(args);
|
|
1975
358
|
const page = await getLastOpenPage();
|
|
1976
359
|
|
|
1977
360
|
const client = await page.target().createCDPSession();
|
|
@@ -2018,7 +401,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2018
401
|
}
|
|
2019
402
|
|
|
2020
403
|
if (name === "getBoxModel") {
|
|
2021
|
-
const validatedArgs = GetBoxModelSchema.parse(args);
|
|
404
|
+
const validatedArgs = schemas.GetBoxModelSchema.parse(args);
|
|
2022
405
|
const page = await getLastOpenPage();
|
|
2023
406
|
|
|
2024
407
|
const client = await page.target().createCDPSession();
|
|
@@ -2056,7 +439,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2056
439
|
}
|
|
2057
440
|
|
|
2058
441
|
if (name === "screenshot") {
|
|
2059
|
-
const validatedArgs = ScreenshotSchema.parse(args);
|
|
442
|
+
const validatedArgs = schemas.ScreenshotSchema.parse(args);
|
|
2060
443
|
const page = await getLastOpenPage();
|
|
2061
444
|
|
|
2062
445
|
const element = await page.$(validatedArgs.selector);
|
|
@@ -2112,7 +495,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2112
495
|
}
|
|
2113
496
|
|
|
2114
497
|
if (name === "saveScreenshot") {
|
|
2115
|
-
const validatedArgs = SaveScreenshotSchema.parse(args);
|
|
498
|
+
const validatedArgs = schemas.SaveScreenshotSchema.parse(args);
|
|
2116
499
|
const page = await getLastOpenPage();
|
|
2117
500
|
|
|
2118
501
|
const element = await page.$(validatedArgs.selector);
|
|
@@ -2169,7 +552,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2169
552
|
}
|
|
2170
553
|
|
|
2171
554
|
if (name === "scrollTo") {
|
|
2172
|
-
const validatedArgs = ScrollToSchema.parse(args);
|
|
555
|
+
const validatedArgs = schemas.ScrollToSchema.parse(args);
|
|
2173
556
|
const page = await getLastOpenPage();
|
|
2174
557
|
|
|
2175
558
|
// Check if element exists
|
|
@@ -2202,7 +585,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2202
585
|
}
|
|
2203
586
|
|
|
2204
587
|
if (name === "waitForElement") {
|
|
2205
|
-
const validatedArgs = WaitForElementSchema.parse(args);
|
|
588
|
+
const validatedArgs = schemas.WaitForElementSchema.parse(args);
|
|
2206
589
|
const page = await getLastOpenPage();
|
|
2207
590
|
const timeout = validatedArgs.timeout || 5000;
|
|
2208
591
|
const waitForVisible = validatedArgs.visible !== false;
|
|
@@ -2247,7 +630,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2247
630
|
}
|
|
2248
631
|
|
|
2249
632
|
if (name === "executeScript") {
|
|
2250
|
-
const validatedArgs = ExecuteScriptSchema.parse(args);
|
|
633
|
+
const validatedArgs = schemas.ExecuteScriptSchema.parse(args);
|
|
2251
634
|
const page = await getLastOpenPage();
|
|
2252
635
|
const timeout = validatedArgs.timeout || 30000;
|
|
2253
636
|
|
|
@@ -2292,7 +675,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2292
675
|
}
|
|
2293
676
|
|
|
2294
677
|
if (name === "getConsoleLogs") {
|
|
2295
|
-
const validatedArgs = GetConsoleLogsSchema.parse(args);
|
|
678
|
+
const validatedArgs = schemas.GetConsoleLogsSchema.parse(args);
|
|
2296
679
|
|
|
2297
680
|
let logs = consoleLogs;
|
|
2298
681
|
|
|
@@ -2325,7 +708,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2325
708
|
|
|
2326
709
|
// Tool 1: listNetworkRequests - compact summary
|
|
2327
710
|
if (name === "listNetworkRequests") {
|
|
2328
|
-
const validatedArgs = ListNetworkRequestsSchema.parse(args);
|
|
711
|
+
const validatedArgs = schemas.ListNetworkRequestsSchema.parse(args);
|
|
2329
712
|
|
|
2330
713
|
let requests = networkRequests;
|
|
2331
714
|
|
|
@@ -2377,7 +760,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2377
760
|
|
|
2378
761
|
// Tool 2: getNetworkRequest - full details of single request
|
|
2379
762
|
if (name === "getNetworkRequest") {
|
|
2380
|
-
const validatedArgs = GetNetworkRequestSchema.parse(args);
|
|
763
|
+
const validatedArgs = schemas.GetNetworkRequestSchema.parse(args);
|
|
2381
764
|
|
|
2382
765
|
const req = networkRequests.find(r => r.requestId === validatedArgs.requestId);
|
|
2383
766
|
if (!req) {
|
|
@@ -2425,7 +808,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2425
808
|
|
|
2426
809
|
// Tool 3: filterNetworkRequests - filter by URL pattern with full details
|
|
2427
810
|
if (name === "filterNetworkRequests") {
|
|
2428
|
-
const validatedArgs = FilterNetworkRequestsSchema.parse(args);
|
|
811
|
+
const validatedArgs = schemas.FilterNetworkRequestsSchema.parse(args);
|
|
2429
812
|
|
|
2430
813
|
let requests = networkRequests;
|
|
2431
814
|
|
|
@@ -2490,7 +873,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2490
873
|
}
|
|
2491
874
|
|
|
2492
875
|
if (name === "hover") {
|
|
2493
|
-
const validatedArgs = HoverSchema.parse(args);
|
|
876
|
+
const validatedArgs = schemas.HoverSchema.parse(args);
|
|
2494
877
|
const page = await getLastOpenPage();
|
|
2495
878
|
|
|
2496
879
|
const element = await page.$(validatedArgs.selector);
|
|
@@ -2510,7 +893,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2510
893
|
}
|
|
2511
894
|
|
|
2512
895
|
if (name === "setStyles") {
|
|
2513
|
-
const validatedArgs = SetStylesSchema.parse(args);
|
|
896
|
+
const validatedArgs = schemas.SetStylesSchema.parse(args);
|
|
2514
897
|
const page = await getLastOpenPage();
|
|
2515
898
|
|
|
2516
899
|
const stylesObject = {};
|
|
@@ -2540,7 +923,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2540
923
|
}
|
|
2541
924
|
|
|
2542
925
|
if (name === "setViewport") {
|
|
2543
|
-
const validatedArgs = SetViewportSchema.parse(args);
|
|
926
|
+
const validatedArgs = schemas.SetViewportSchema.parse(args);
|
|
2544
927
|
const page = await getLastOpenPage();
|
|
2545
928
|
|
|
2546
929
|
await page.setViewport({
|
|
@@ -2583,7 +966,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2583
966
|
}
|
|
2584
967
|
|
|
2585
968
|
if (name === "navigateTo") {
|
|
2586
|
-
const validatedArgs = NavigateToSchema.parse(args);
|
|
969
|
+
const validatedArgs = schemas.NavigateToSchema.parse(args);
|
|
2587
970
|
const page = await getLastOpenPage();
|
|
2588
971
|
|
|
2589
972
|
// Navigate to the new URL (always navigate, don't use cache)
|
|
@@ -2604,7 +987,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2604
987
|
|
|
2605
988
|
// Figma tools
|
|
2606
989
|
if (name === "getFigmaFrame") {
|
|
2607
|
-
const validatedArgs = GetFigmaFrameSchema.parse(args);
|
|
990
|
+
const validatedArgs = schemas.GetFigmaFrameSchema.parse(args);
|
|
2608
991
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
2609
992
|
if (!token) {
|
|
2610
993
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -2682,7 +1065,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2682
1065
|
}
|
|
2683
1066
|
|
|
2684
1067
|
if (name === "compareFigmaToElement") {
|
|
2685
|
-
const validatedArgs = CompareFigmaToElementSchema.parse(args);
|
|
1068
|
+
const validatedArgs = schemas.CompareFigmaToElementSchema.parse(args);
|
|
2686
1069
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
2687
1070
|
if (!token) {
|
|
2688
1071
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -2813,7 +1196,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2813
1196
|
}
|
|
2814
1197
|
|
|
2815
1198
|
if (name === "getFigmaSpecs") {
|
|
2816
|
-
const validatedArgs = GetFigmaSpecsSchema.parse(args);
|
|
1199
|
+
const validatedArgs = schemas.GetFigmaSpecsSchema.parse(args);
|
|
2817
1200
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
2818
1201
|
if (!token) {
|
|
2819
1202
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -2955,7 +1338,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2955
1338
|
}
|
|
2956
1339
|
|
|
2957
1340
|
if (name === "parseFigmaUrl") {
|
|
2958
|
-
const validatedArgs = ParseFigmaUrlSchema.parse(args);
|
|
1341
|
+
const validatedArgs = schemas.ParseFigmaUrlSchema.parse(args);
|
|
2959
1342
|
const result = parseFigmaUrl(validatedArgs.url);
|
|
2960
1343
|
|
|
2961
1344
|
return {
|
|
@@ -2966,7 +1349,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2966
1349
|
}
|
|
2967
1350
|
|
|
2968
1351
|
if (name === "listFigmaPages") {
|
|
2969
|
-
const validatedArgs = ListFigmaPagesSchema.parse(args);
|
|
1352
|
+
const validatedArgs = schemas.ListFigmaPagesSchema.parse(args);
|
|
2970
1353
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
2971
1354
|
if (!token) {
|
|
2972
1355
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -2986,7 +1369,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2986
1369
|
}
|
|
2987
1370
|
|
|
2988
1371
|
if (name === "searchFigmaFrames") {
|
|
2989
|
-
const validatedArgs = SearchFigmaFramesSchema.parse(args);
|
|
1372
|
+
const validatedArgs = schemas.SearchFigmaFramesSchema.parse(args);
|
|
2990
1373
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
2991
1374
|
if (!token) {
|
|
2992
1375
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -3006,7 +1389,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3006
1389
|
}
|
|
3007
1390
|
|
|
3008
1391
|
if (name === "getFigmaComponents") {
|
|
3009
|
-
const validatedArgs = GetFigmaComponentsSchema.parse(args);
|
|
1392
|
+
const validatedArgs = schemas.GetFigmaComponentsSchema.parse(args);
|
|
3010
1393
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
3011
1394
|
if (!token) {
|
|
3012
1395
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -3026,7 +1409,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3026
1409
|
}
|
|
3027
1410
|
|
|
3028
1411
|
if (name === "getFigmaStyles") {
|
|
3029
|
-
const validatedArgs = GetFigmaStylesSchema.parse(args);
|
|
1412
|
+
const validatedArgs = schemas.GetFigmaStylesSchema.parse(args);
|
|
3030
1413
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
3031
1414
|
if (!token) {
|
|
3032
1415
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -3046,7 +1429,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3046
1429
|
}
|
|
3047
1430
|
|
|
3048
1431
|
if (name === "getFigmaColorPalette") {
|
|
3049
|
-
const validatedArgs = GetFigmaColorPaletteSchema.parse(args);
|
|
1432
|
+
const validatedArgs = schemas.GetFigmaColorPaletteSchema.parse(args);
|
|
3050
1433
|
const token = validatedArgs.figmaToken || FIGMA_TOKEN;
|
|
3051
1434
|
if (!token) {
|
|
3052
1435
|
throw new Error('Figma token is required. Pass it as parameter or set FIGMA_TOKEN environment variable in MCP config.');
|
|
@@ -3067,7 +1450,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3067
1450
|
|
|
3068
1451
|
// New AI optimization tools
|
|
3069
1452
|
if (name === "smartFindElement") {
|
|
3070
|
-
const validatedArgs = SmartFindElementSchema.parse(args);
|
|
1453
|
+
const validatedArgs = schemas.SmartFindElementSchema.parse(args);
|
|
3071
1454
|
const page = await getLastOpenPage();
|
|
3072
1455
|
const maxResults = validatedArgs.maxResults || 5;
|
|
3073
1456
|
|
|
@@ -3180,7 +1563,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3180
1563
|
}
|
|
3181
1564
|
|
|
3182
1565
|
if (name === "analyzePage") {
|
|
3183
|
-
const validatedArgs = AnalyzePageSchema.parse(args);
|
|
1566
|
+
const validatedArgs = schemas.AnalyzePageSchema.parse(args);
|
|
3184
1567
|
const page = await getLastOpenPage();
|
|
3185
1568
|
const pageUrl = page.url();
|
|
3186
1569
|
|
|
@@ -3336,7 +1719,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3336
1719
|
}
|
|
3337
1720
|
|
|
3338
1721
|
if (name === "getAllInteractiveElements") {
|
|
3339
|
-
const validatedArgs = GetAllInteractiveElementsSchema.parse(args);
|
|
1722
|
+
const validatedArgs = schemas.GetAllInteractiveElementsSchema.parse(args);
|
|
3340
1723
|
const page = await getLastOpenPage();
|
|
3341
1724
|
|
|
3342
1725
|
const elements = await page.evaluate((includeHidden, utilsCode) => {
|
|
@@ -3384,7 +1767,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3384
1767
|
}
|
|
3385
1768
|
|
|
3386
1769
|
if (name === "findElementsByText") {
|
|
3387
|
-
const validatedArgs = FindElementsByTextSchema.parse(args);
|
|
1770
|
+
const validatedArgs = schemas.FindElementsByTextSchema.parse(args);
|
|
3388
1771
|
const page = await getLastOpenPage();
|
|
3389
1772
|
|
|
3390
1773
|
const elements = await page.evaluate((text, exact, caseSensitive, utilsCode) => {
|
|
@@ -3471,9 +1854,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3471
1854
|
}
|
|
3472
1855
|
|
|
3473
1856
|
if (name === "enableRecorder") {
|
|
3474
|
-
|
|
1857
|
+
// Project ID will be determined from URL in browser context
|
|
3475
1858
|
const page = await getLastOpenPage();
|
|
3476
|
-
const result = await injectRecorder(page
|
|
1859
|
+
const result = await injectRecorder(page);
|
|
3477
1860
|
|
|
3478
1861
|
// Track this page as having recorder enabled
|
|
3479
1862
|
if (result.success) {
|
|
@@ -3485,7 +1868,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3485
1868
|
type: 'text',
|
|
3486
1869
|
text: JSON.stringify(result.success ? {
|
|
3487
1870
|
success: true,
|
|
3488
|
-
message:
|
|
1871
|
+
message: 'Recorder UI injected into page. Click \'Start\' to begin recording. Scenarios will be organized by website domain in: ~/.config/chrometools-mcp/projects/'
|
|
3489
1872
|
} : {
|
|
3490
1873
|
success: false,
|
|
3491
1874
|
error: result.error
|
|
@@ -3495,55 +1878,109 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3495
1878
|
}
|
|
3496
1879
|
|
|
3497
1880
|
if (name === "executeScenario") {
|
|
3498
|
-
//
|
|
3499
|
-
const
|
|
1881
|
+
// Load and validate scenario first (check for collisions)
|
|
1882
|
+
const scenario = await loadScenario(args.name, false, args.projectId || null);
|
|
1883
|
+
|
|
1884
|
+
if (!scenario) {
|
|
1885
|
+
return {
|
|
1886
|
+
content: [{
|
|
1887
|
+
type: 'text',
|
|
1888
|
+
text: JSON.stringify({
|
|
1889
|
+
success: false,
|
|
1890
|
+
error: `Scenario "${args.name}" not found`
|
|
1891
|
+
}, null, 2)
|
|
1892
|
+
}]
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Check for name collision
|
|
1897
|
+
if (scenario.collision) {
|
|
1898
|
+
return {
|
|
1899
|
+
content: [{
|
|
1900
|
+
type: 'text',
|
|
1901
|
+
text: JSON.stringify({
|
|
1902
|
+
success: false,
|
|
1903
|
+
error: scenario.message,
|
|
1904
|
+
availableProjectIds: scenario.availableProjectIds,
|
|
1905
|
+
hint: `Use: executeScenario({ name: "${args.name}", projectId: "one-of-the-above" })`
|
|
1906
|
+
}, null, 2)
|
|
1907
|
+
}]
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
3500
1910
|
|
|
3501
1911
|
// Try to get existing page, or auto-open browser using scenario's entryUrl
|
|
3502
1912
|
let page;
|
|
3503
1913
|
try {
|
|
3504
1914
|
page = await getLastOpenPage();
|
|
3505
1915
|
} catch (error) {
|
|
3506
|
-
// No page is open -
|
|
3507
|
-
const
|
|
3508
|
-
if (!
|
|
1916
|
+
// No page is open - open browser at scenario's entry URL
|
|
1917
|
+
const entryUrl = scenario.metadata?.entryUrl;
|
|
1918
|
+
if (!entryUrl) {
|
|
3509
1919
|
return {
|
|
3510
1920
|
content: [{
|
|
3511
1921
|
type: 'text',
|
|
3512
1922
|
text: JSON.stringify({
|
|
3513
1923
|
success: false,
|
|
3514
|
-
error: `Scenario "${args.name}"
|
|
1924
|
+
error: `Scenario "${args.name}" has no entryUrl. Cannot auto-open browser. Please open a browser first or ensure scenario has entryUrl.`
|
|
3515
1925
|
}, null, 2)
|
|
3516
1926
|
}]
|
|
3517
1927
|
};
|
|
3518
1928
|
}
|
|
3519
1929
|
|
|
3520
|
-
|
|
3521
|
-
|
|
1930
|
+
// Auto-open browser at scenario's entry URL with timeout
|
|
1931
|
+
try {
|
|
1932
|
+
page = await Promise.race([
|
|
1933
|
+
getOrCreatePage(entryUrl),
|
|
1934
|
+
new Promise((_, reject) =>
|
|
1935
|
+
setTimeout(() => reject(new Error('Browser open timeout')), 30000)
|
|
1936
|
+
)
|
|
1937
|
+
]);
|
|
1938
|
+
} catch (openError) {
|
|
3522
1939
|
return {
|
|
3523
1940
|
content: [{
|
|
3524
1941
|
type: 'text',
|
|
3525
1942
|
text: JSON.stringify({
|
|
3526
1943
|
success: false,
|
|
3527
|
-
error: `
|
|
1944
|
+
error: `Failed to open browser: ${openError.message}`
|
|
3528
1945
|
}, null, 2)
|
|
3529
1946
|
}]
|
|
3530
1947
|
};
|
|
3531
1948
|
}
|
|
3532
|
-
|
|
3533
|
-
// Auto-open browser at scenario's entry URL
|
|
3534
|
-
page = await getOrCreatePage(entryUrl);
|
|
3535
1949
|
}
|
|
3536
1950
|
|
|
3537
|
-
const options = {
|
|
3538
|
-
baseDir: baseDir
|
|
3539
|
-
};
|
|
1951
|
+
const options = {};
|
|
3540
1952
|
|
|
3541
1953
|
// Pass executeDependencies option if provided
|
|
3542
1954
|
if (args.executeDependencies !== undefined) {
|
|
3543
1955
|
options.executeDependencies = args.executeDependencies;
|
|
3544
1956
|
}
|
|
3545
1957
|
|
|
3546
|
-
|
|
1958
|
+
// Pass projectId option if provided
|
|
1959
|
+
if (args.projectId) {
|
|
1960
|
+
options.projectId = args.projectId;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Execute scenario with timeout (5 minutes max)
|
|
1964
|
+
let result;
|
|
1965
|
+
try {
|
|
1966
|
+
result = await Promise.race([
|
|
1967
|
+
executeScenario(args.name, page, args.parameters || {}, options),
|
|
1968
|
+
new Promise((_, reject) =>
|
|
1969
|
+
setTimeout(() => reject(new Error('Scenario execution timeout (5 minutes)')), 300000)
|
|
1970
|
+
)
|
|
1971
|
+
]);
|
|
1972
|
+
} catch (executeError) {
|
|
1973
|
+
return {
|
|
1974
|
+
content: [{
|
|
1975
|
+
type: 'text',
|
|
1976
|
+
text: JSON.stringify({
|
|
1977
|
+
success: false,
|
|
1978
|
+
error: `Scenario execution failed: ${executeError.message}`,
|
|
1979
|
+
stack: executeError.stack
|
|
1980
|
+
}, null, 2)
|
|
1981
|
+
}]
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
3547
1984
|
|
|
3548
1985
|
return {
|
|
3549
1986
|
content: [{
|
|
@@ -3554,8 +1991,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3554
1991
|
}
|
|
3555
1992
|
|
|
3556
1993
|
if (name === "listScenarios") {
|
|
3557
|
-
|
|
3558
|
-
|
|
1994
|
+
// Return ALL scenarios from all projects (URL-based organization)
|
|
1995
|
+
// Agent can filter by projectId, entryUrl, exitUrl as needed
|
|
1996
|
+
const scenarios = await listScenarios(null, true);
|
|
3559
1997
|
|
|
3560
1998
|
return {
|
|
3561
1999
|
content: [{
|
|
@@ -3566,8 +2004,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3566
2004
|
}
|
|
3567
2005
|
|
|
3568
2006
|
if (name === "searchScenarios") {
|
|
3569
|
-
|
|
3570
|
-
|
|
2007
|
+
// Return ALL matching scenarios from all projects
|
|
2008
|
+
// Agent can filter by projectId, entryUrl, exitUrl as needed
|
|
2009
|
+
const results = await searchScenarios(args, null, true);
|
|
3571
2010
|
|
|
3572
2011
|
return {
|
|
3573
2012
|
content: [{
|
|
@@ -3578,8 +2017,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3578
2017
|
}
|
|
3579
2018
|
|
|
3580
2019
|
if (name === "getScenarioInfo") {
|
|
3581
|
-
const
|
|
3582
|
-
const scenario = await loadScenario(args.name, args.includeSecrets || false, baseDir);
|
|
2020
|
+
const scenario = await loadScenario(args.name, args.includeSecrets || false, null);
|
|
3583
2021
|
|
|
3584
2022
|
return {
|
|
3585
2023
|
content: [{
|
|
@@ -3590,20 +2028,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3590
2028
|
}
|
|
3591
2029
|
|
|
3592
2030
|
if (name === "deleteScenario") {
|
|
3593
|
-
const
|
|
3594
|
-
const result = await deleteScenario(args.name, baseDir);
|
|
2031
|
+
const result = await deleteScenario(args.name, null);
|
|
3595
2032
|
|
|
3596
2033
|
return {
|
|
3597
2034
|
content: [{
|
|
3598
2035
|
type: 'text',
|
|
3599
|
-
text: JSON.stringify(result, null, 2)
|
|
2036
|
+
text: JSON.stringify({ success: result }, null, 2)
|
|
3600
2037
|
}]
|
|
3601
2038
|
};
|
|
3602
2039
|
}
|
|
3603
2040
|
|
|
3604
2041
|
if (name === "exportScenarioAsCode") {
|
|
3605
|
-
const
|
|
3606
|
-
const scenario = await loadScenario(args.scenarioName, false, baseDir);
|
|
2042
|
+
const scenario = await loadScenario(args.scenarioName, false, null);
|
|
3607
2043
|
|
|
3608
2044
|
if (!scenario) {
|
|
3609
2045
|
return {
|
|
@@ -3649,17 +2085,221 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3649
2085
|
};
|
|
3650
2086
|
}
|
|
3651
2087
|
|
|
3652
|
-
//
|
|
3653
|
-
|
|
2088
|
+
// Check if append mode is requested
|
|
2089
|
+
if (args.appendToFile) {
|
|
2090
|
+
try {
|
|
2091
|
+
// Validate file path and extension
|
|
2092
|
+
FileAppender.validateFile(args.appendToFile, args.language);
|
|
2093
|
+
|
|
2094
|
+
// Read existing file content
|
|
2095
|
+
const existingContent = FileAppender.readFile(args.appendToFile);
|
|
2096
|
+
|
|
2097
|
+
// Check if file is empty - if so, generate full test with imports
|
|
2098
|
+
if (FileAppender.isEmpty(existingContent)) {
|
|
2099
|
+
const fullCode = generator.generate(scenario, options);
|
|
2100
|
+
FileAppender.writeFile(args.appendToFile, fullCode);
|
|
2101
|
+
|
|
2102
|
+
return {
|
|
2103
|
+
content: [{
|
|
2104
|
+
type: 'text',
|
|
2105
|
+
text: JSON.stringify({
|
|
2106
|
+
success: true,
|
|
2107
|
+
mode: 'append',
|
|
2108
|
+
file: args.appendToFile,
|
|
2109
|
+
testName: args.testName || scenario.metadata?.name,
|
|
2110
|
+
message: `Test written to empty file: ${args.appendToFile}`
|
|
2111
|
+
}, null, 2)
|
|
2112
|
+
}]
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Generate only test function (without imports)
|
|
2117
|
+
const testOnly = generator.generateTestOnly(scenario, {
|
|
2118
|
+
...options,
|
|
2119
|
+
testName: args.testName
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
// Append test to existing content
|
|
2123
|
+
const appendOptions = {
|
|
2124
|
+
insertPosition: args.insertPosition || 'end',
|
|
2125
|
+
referenceTestName: args.referenceTestName
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
const updatedContent = generator.appendTest(existingContent, testOnly, appendOptions);
|
|
2129
|
+
|
|
2130
|
+
// Write updated content to file
|
|
2131
|
+
FileAppender.writeFile(args.appendToFile, updatedContent);
|
|
2132
|
+
|
|
2133
|
+
return {
|
|
2134
|
+
content: [{
|
|
2135
|
+
type: 'text',
|
|
2136
|
+
text: JSON.stringify({
|
|
2137
|
+
success: true,
|
|
2138
|
+
mode: 'append',
|
|
2139
|
+
file: args.appendToFile,
|
|
2140
|
+
testName: args.testName || scenario.metadata?.name,
|
|
2141
|
+
insertPosition: appendOptions.insertPosition,
|
|
2142
|
+
message: `Test '${args.testName || scenario.metadata?.name}' successfully appended to ${args.appendToFile}`
|
|
2143
|
+
}, null, 2)
|
|
2144
|
+
}]
|
|
2145
|
+
};
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
return {
|
|
2148
|
+
content: [{
|
|
2149
|
+
type: 'text',
|
|
2150
|
+
text: JSON.stringify({
|
|
2151
|
+
success: false,
|
|
2152
|
+
mode: 'append',
|
|
2153
|
+
file: args.appendToFile,
|
|
2154
|
+
error: error.message
|
|
2155
|
+
}, null, 2)
|
|
2156
|
+
}],
|
|
2157
|
+
isError: true
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// Non-append mode: Generate test code
|
|
2163
|
+
const testCode = generator.generate(scenario, options);
|
|
2164
|
+
|
|
2165
|
+
// If generatePageObject is requested, also generate Page Object class
|
|
2166
|
+
if (args.generatePageObject) {
|
|
2167
|
+
try {
|
|
2168
|
+
// Get page - need to open at scenario's entry URL
|
|
2169
|
+
let page;
|
|
2170
|
+
const entryUrl = scenario.metadata?.entryUrl;
|
|
2171
|
+
|
|
2172
|
+
if (!entryUrl) {
|
|
2173
|
+
return {
|
|
2174
|
+
content: [{
|
|
2175
|
+
type: 'text',
|
|
2176
|
+
text: JSON.stringify({
|
|
2177
|
+
error: 'Cannot generate Page Object: scenario has no entryUrl in metadata'
|
|
2178
|
+
}, null, 2)
|
|
2179
|
+
}],
|
|
2180
|
+
isError: true
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Try to get existing page or open new one
|
|
2185
|
+
try {
|
|
2186
|
+
page = await getLastOpenPage();
|
|
2187
|
+
// Navigate to entry URL if current page is different
|
|
2188
|
+
const currentUrl = page.url();
|
|
2189
|
+
if (currentUrl !== entryUrl) {
|
|
2190
|
+
await page.goto(entryUrl, { waitUntil: 'networkidle2' });
|
|
2191
|
+
}
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
// No page open, create new one
|
|
2194
|
+
page = await getOrCreatePage(entryUrl);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// Generate Page Object
|
|
2198
|
+
const pageObjectOptions = {
|
|
2199
|
+
className: args.pageObjectClassName || null,
|
|
2200
|
+
framework: args.language, // Use same framework as test
|
|
2201
|
+
includeComments: args.includeComments !== false,
|
|
2202
|
+
groupElements: true
|
|
2203
|
+
};
|
|
2204
|
+
|
|
2205
|
+
const pageObjectResult = await generatePageObject(page, pageObjectOptions);
|
|
2206
|
+
|
|
2207
|
+
if (pageObjectResult.success) {
|
|
2208
|
+
// Return both test code and Page Object code
|
|
2209
|
+
return {
|
|
2210
|
+
content: [{
|
|
2211
|
+
type: 'text',
|
|
2212
|
+
text: JSON.stringify({
|
|
2213
|
+
success: true,
|
|
2214
|
+
testCode: testCode,
|
|
2215
|
+
pageObjectCode: pageObjectResult.code,
|
|
2216
|
+
pageObjectClassName: pageObjectResult.className,
|
|
2217
|
+
framework: args.language,
|
|
2218
|
+
scenarioName: args.scenarioName,
|
|
2219
|
+
url: pageObjectResult.url,
|
|
2220
|
+
elementCount: pageObjectResult.elementCount
|
|
2221
|
+
}, null, 2)
|
|
2222
|
+
}]
|
|
2223
|
+
};
|
|
2224
|
+
} else {
|
|
2225
|
+
// Page Object generation failed, return test code only with warning
|
|
2226
|
+
return {
|
|
2227
|
+
content: [{
|
|
2228
|
+
type: 'text',
|
|
2229
|
+
text: JSON.stringify({
|
|
2230
|
+
success: true,
|
|
2231
|
+
testCode: testCode,
|
|
2232
|
+
pageObjectCode: null,
|
|
2233
|
+
warning: 'Page Object generation failed: ' + (pageObjectResult.error || 'Unknown error')
|
|
2234
|
+
}, null, 2)
|
|
2235
|
+
}]
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
// Page Object generation failed, return test code only with error
|
|
2240
|
+
return {
|
|
2241
|
+
content: [{
|
|
2242
|
+
type: 'text',
|
|
2243
|
+
text: JSON.stringify({
|
|
2244
|
+
success: true,
|
|
2245
|
+
testCode: testCode,
|
|
2246
|
+
pageObjectCode: null,
|
|
2247
|
+
warning: 'Page Object generation error: ' + error.message
|
|
2248
|
+
}, null, 2)
|
|
2249
|
+
}]
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
3654
2253
|
|
|
2254
|
+
// Default: return test code only
|
|
3655
2255
|
return {
|
|
3656
2256
|
content: [{
|
|
3657
2257
|
type: 'text',
|
|
3658
|
-
text:
|
|
2258
|
+
text: testCode
|
|
3659
2259
|
}]
|
|
3660
2260
|
};
|
|
3661
2261
|
}
|
|
3662
2262
|
|
|
2263
|
+
if (name === "generatePageObject") {
|
|
2264
|
+
const page = await getLastOpenPage();
|
|
2265
|
+
|
|
2266
|
+
const options = {
|
|
2267
|
+
className: args.className || null,
|
|
2268
|
+
framework: args.framework || 'playwright-typescript',
|
|
2269
|
+
includeComments: args.includeComments !== false,
|
|
2270
|
+
groupElements: args.groupElements !== false
|
|
2271
|
+
};
|
|
2272
|
+
|
|
2273
|
+
const result = await generatePageObject(page, options);
|
|
2274
|
+
|
|
2275
|
+
if (result.success) {
|
|
2276
|
+
return {
|
|
2277
|
+
content: [{
|
|
2278
|
+
type: 'text',
|
|
2279
|
+
text: JSON.stringify({
|
|
2280
|
+
success: true,
|
|
2281
|
+
className: result.className,
|
|
2282
|
+
url: result.url,
|
|
2283
|
+
title: result.title,
|
|
2284
|
+
elementCount: result.elementCount,
|
|
2285
|
+
framework: result.framework,
|
|
2286
|
+
code: result.code
|
|
2287
|
+
}, null, 2)
|
|
2288
|
+
}]
|
|
2289
|
+
};
|
|
2290
|
+
} else {
|
|
2291
|
+
return {
|
|
2292
|
+
content: [{
|
|
2293
|
+
type: 'text',
|
|
2294
|
+
text: JSON.stringify({
|
|
2295
|
+
error: result.error || 'Failed to generate Page Object'
|
|
2296
|
+
}, null, 2)
|
|
2297
|
+
}],
|
|
2298
|
+
isError: true
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
3663
2303
|
return {
|
|
3664
2304
|
content: [
|
|
3665
2305
|
{
|
|
@@ -3670,17 +2310,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3670
2310
|
isError: true,
|
|
3671
2311
|
};
|
|
3672
2312
|
} catch (error) {
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
{
|
|
3676
|
-
type: "text",
|
|
3677
|
-
text: `Error: ${error.message}`,
|
|
3678
|
-
},
|
|
3679
|
-
],
|
|
3680
|
-
isError: true,
|
|
3681
|
-
};
|
|
2313
|
+
// Re-throw to be caught by outer executeToolWithTimeout wrapper
|
|
2314
|
+
throw error;
|
|
3682
2315
|
}
|
|
3683
|
-
}
|
|
2316
|
+
}
|
|
3684
2317
|
|
|
3685
2318
|
// Start server
|
|
3686
2319
|
async function main() {
|