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