dev3000 0.0.26 → 0.0.32
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/README.md +15 -5
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +17 -1
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/dev-environment.d.ts +1 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +34 -13
- package/dist/dev-environment.js.map +1 -1
- package/mcp-server/app/api/logs/rotate/route.ts +63 -0
- package/mcp-server/app/api/mcp/[transport]/route.ts +221 -0
- package/mcp-server/app/api/replay/route.ts +4 -4
- package/mcp-server/app/logs/LogsClient.tsx +56 -6
- package/mcp-server/app/page.tsx +1 -1
- package/mcp-server/package.json +3 -1
- package/mcp-server/pnpm-lock.yaml +1491 -0
- package/package.json +4 -2
|
@@ -2,8 +2,28 @@ import { createMcpHandler } from "mcp-handler";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { readFileSync, existsSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
import { WebSocket } from "ws";
|
|
5
6
|
|
|
6
7
|
const handler = createMcpHandler((server) => {
|
|
8
|
+
// Healthcheck tool
|
|
9
|
+
server.tool(
|
|
10
|
+
"healthcheck",
|
|
11
|
+
"Simple healthcheck to verify MCP server is working",
|
|
12
|
+
{
|
|
13
|
+
message: z.string().optional().describe("Optional message to echo back")
|
|
14
|
+
},
|
|
15
|
+
async ({ message = "MCP server is healthy!" }) => {
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: `✅ ${message} - Timestamp: ${new Date().toISOString()}`
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
7
27
|
// Tool to read consolidated logs
|
|
8
28
|
server.tool(
|
|
9
29
|
"read_consolidated_logs",
|
|
@@ -183,6 +203,207 @@ const handler = createMcpHandler((server) => {
|
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
);
|
|
206
|
+
|
|
207
|
+
// Tool to execute browser actions via CDP
|
|
208
|
+
server.tool(
|
|
209
|
+
"execute_browser_action",
|
|
210
|
+
"Execute safe browser actions via Chrome DevTools Protocol",
|
|
211
|
+
{
|
|
212
|
+
action: z.enum(["click", "navigate", "screenshot", "evaluate", "scroll", "type"]).describe("Action to perform"),
|
|
213
|
+
params: z.object({
|
|
214
|
+
x: z.number().optional().describe("X coordinate for click/scroll"),
|
|
215
|
+
y: z.number().optional().describe("Y coordinate for click/scroll"),
|
|
216
|
+
url: z.string().optional().describe("URL for navigation"),
|
|
217
|
+
selector: z.string().optional().describe("CSS selector for element targeting"),
|
|
218
|
+
text: z.string().optional().describe("Text to type"),
|
|
219
|
+
expression: z.string().optional().describe("JavaScript expression to evaluate (safe expressions only)"),
|
|
220
|
+
deltaX: z.number().optional().describe("Horizontal scroll amount"),
|
|
221
|
+
deltaY: z.number().optional().describe("Vertical scroll amount")
|
|
222
|
+
}).describe("Parameters for the action")
|
|
223
|
+
},
|
|
224
|
+
async ({ action, params }) => {
|
|
225
|
+
try {
|
|
226
|
+
// Connect to CDP on port 9222
|
|
227
|
+
const targetsResponse = await fetch('http://localhost:9222/json');
|
|
228
|
+
const targets = await targetsResponse.json();
|
|
229
|
+
|
|
230
|
+
const pageTarget = targets.find((target: any) => target.type === 'page');
|
|
231
|
+
if (!pageTarget) {
|
|
232
|
+
throw new Error('No browser tab found. Make sure dev3000 is running with CDP monitoring.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const wsUrl = pageTarget.webSocketDebuggerUrl;
|
|
236
|
+
|
|
237
|
+
const result = await new Promise((resolve, reject) => {
|
|
238
|
+
// WebSocket imported at top of file
|
|
239
|
+
const ws = new WebSocket(wsUrl);
|
|
240
|
+
let messageId = 1;
|
|
241
|
+
|
|
242
|
+
ws.on('open', async () => {
|
|
243
|
+
try {
|
|
244
|
+
let cdpResult;
|
|
245
|
+
|
|
246
|
+
switch (action) {
|
|
247
|
+
case 'click':
|
|
248
|
+
if (!params.x || !params.y) {
|
|
249
|
+
throw new Error('Click action requires x and y coordinates');
|
|
250
|
+
}
|
|
251
|
+
// Send mouse down and up events
|
|
252
|
+
await sendCDPCommand(ws, messageId++, 'Input.dispatchMouseEvent', {
|
|
253
|
+
type: 'mousePressed',
|
|
254
|
+
x: params.x,
|
|
255
|
+
y: params.y,
|
|
256
|
+
button: 'left',
|
|
257
|
+
clickCount: 1
|
|
258
|
+
});
|
|
259
|
+
await sendCDPCommand(ws, messageId++, 'Input.dispatchMouseEvent', {
|
|
260
|
+
type: 'mouseReleased',
|
|
261
|
+
x: params.x,
|
|
262
|
+
y: params.y,
|
|
263
|
+
button: 'left',
|
|
264
|
+
clickCount: 1
|
|
265
|
+
});
|
|
266
|
+
cdpResult = { action: 'click', coordinates: { x: params.x, y: params.y } };
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'navigate':
|
|
270
|
+
if (!params.url) {
|
|
271
|
+
throw new Error('Navigate action requires url parameter');
|
|
272
|
+
}
|
|
273
|
+
// Basic URL validation
|
|
274
|
+
if (!params.url.startsWith('http://') && !params.url.startsWith('https://')) {
|
|
275
|
+
throw new Error('Only http:// and https:// URLs are allowed');
|
|
276
|
+
}
|
|
277
|
+
cdpResult = await sendCDPCommand(ws, messageId++, 'Page.navigate', { url: params.url });
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case 'screenshot':
|
|
281
|
+
cdpResult = await sendCDPCommand(ws, messageId++, 'Page.captureScreenshot', {
|
|
282
|
+
format: 'png',
|
|
283
|
+
quality: 80
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
case 'evaluate':
|
|
288
|
+
if (!params.expression) {
|
|
289
|
+
throw new Error('Evaluate action requires expression parameter');
|
|
290
|
+
}
|
|
291
|
+
// Whitelist safe expressions only
|
|
292
|
+
const safeExpressions = [
|
|
293
|
+
/^document\.title$/,
|
|
294
|
+
/^window\.location\.href$/,
|
|
295
|
+
/^document\.querySelector\(['"][^'"]*['"]\)\.textContent$/,
|
|
296
|
+
/^document\.body\.scrollHeight$/,
|
|
297
|
+
/^window\.scrollY$/,
|
|
298
|
+
/^window\.scrollX$/
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
if (!safeExpressions.some(regex => regex.test(params.expression!))) {
|
|
302
|
+
throw new Error('Expression not in whitelist. Only safe read-only expressions allowed.');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
cdpResult = await sendCDPCommand(ws, messageId++, 'Runtime.evaluate', {
|
|
306
|
+
expression: params.expression,
|
|
307
|
+
returnByValue: true
|
|
308
|
+
});
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case 'scroll':
|
|
312
|
+
const scrollX = params.deltaX || 0;
|
|
313
|
+
const scrollY = params.deltaY || 0;
|
|
314
|
+
cdpResult = await sendCDPCommand(ws, messageId++, 'Input.dispatchMouseEvent', {
|
|
315
|
+
type: 'mouseWheel',
|
|
316
|
+
x: params.x || 500,
|
|
317
|
+
y: params.y || 500,
|
|
318
|
+
deltaX: scrollX,
|
|
319
|
+
deltaY: scrollY
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
|
|
323
|
+
case 'type':
|
|
324
|
+
if (!params.text) {
|
|
325
|
+
throw new Error('Type action requires text parameter');
|
|
326
|
+
}
|
|
327
|
+
// Type each character
|
|
328
|
+
for (const char of params.text) {
|
|
329
|
+
await sendCDPCommand(ws, messageId++, 'Input.dispatchKeyEvent', {
|
|
330
|
+
type: 'char',
|
|
331
|
+
text: char
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
cdpResult = { action: 'type', text: params.text };
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
default:
|
|
338
|
+
throw new Error(`Unsupported action: ${action}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
ws.close();
|
|
342
|
+
resolve(cdpResult);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
ws.close();
|
|
345
|
+
reject(error);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
ws.on('error', reject);
|
|
350
|
+
|
|
351
|
+
// Helper function to send CDP commands
|
|
352
|
+
async function sendCDPCommand(ws: any, id: number, method: string, params: any): Promise<any> {
|
|
353
|
+
return new Promise((cmdResolve, cmdReject) => {
|
|
354
|
+
const command = { id, method, params };
|
|
355
|
+
|
|
356
|
+
const messageHandler = (data: any) => {
|
|
357
|
+
const message = JSON.parse(data.toString());
|
|
358
|
+
if (message.id === id) {
|
|
359
|
+
ws.removeListener('message', messageHandler);
|
|
360
|
+
if (message.error) {
|
|
361
|
+
cmdReject(new Error(message.error.message));
|
|
362
|
+
} else {
|
|
363
|
+
cmdResolve(message.result);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
ws.on('message', messageHandler);
|
|
369
|
+
ws.send(JSON.stringify(command));
|
|
370
|
+
|
|
371
|
+
// Command timeout
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
ws.removeListener('message', messageHandler);
|
|
374
|
+
cmdReject(new Error(`CDP command timeout: ${method}`));
|
|
375
|
+
}, 5000);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: `Browser action '${action}' executed successfully. Result: ${JSON.stringify(result, null, 2)}`
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: `Browser action failed: ${error instanceof Error ? error.message : String(error)}`
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}, {
|
|
402
|
+
// Server options
|
|
403
|
+
}, {
|
|
404
|
+
basePath: "/api/mcp",
|
|
405
|
+
maxDuration: 60,
|
|
406
|
+
verboseLogs: true
|
|
186
407
|
});
|
|
187
408
|
|
|
188
409
|
export { handler as GET, handler as POST };
|
|
@@ -273,7 +273,7 @@ function generateCDPCommands(replayData: ReplayData, speed: number): CDPCommand[
|
|
|
273
273
|
description: `Navigate to ${event.url}`
|
|
274
274
|
});
|
|
275
275
|
} else if (event.eventType === 'interaction') {
|
|
276
|
-
if (event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
|
|
276
|
+
if ('type' in event && event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
|
|
277
277
|
// Mouse down
|
|
278
278
|
commands.push({
|
|
279
279
|
method: 'Input.dispatchMouseEvent',
|
|
@@ -301,7 +301,7 @@ function generateCDPCommands(replayData: ReplayData, speed: number): CDPCommand[
|
|
|
301
301
|
delay: 50, // 50ms between down and up
|
|
302
302
|
description: `Release click at (${event.x}, ${event.y})`
|
|
303
303
|
});
|
|
304
|
-
} else if (event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
|
|
304
|
+
} else if ('type' in event && event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
|
|
305
305
|
commands.push({
|
|
306
306
|
method: 'Runtime.evaluate',
|
|
307
307
|
params: {
|
|
@@ -310,7 +310,7 @@ function generateCDPCommands(replayData: ReplayData, speed: number): CDPCommand[
|
|
|
310
310
|
delay: delay,
|
|
311
311
|
description: `Scroll to (${event.x}, ${event.y})`
|
|
312
312
|
});
|
|
313
|
-
} else if (event.type === 'KEY' && event.key) {
|
|
313
|
+
} else if ('type' in event && event.type === 'KEY' && event.key) {
|
|
314
314
|
// Key down
|
|
315
315
|
commands.push({
|
|
316
316
|
method: 'Input.dispatchKeyEvent',
|
|
@@ -382,7 +382,7 @@ async function executeCDPCommands(commands: CDPCommand[]): Promise<any> {
|
|
|
382
382
|
executeNext();
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
-
ws.on('message', (data) => {
|
|
385
|
+
ws.on('message', (data: any) => {
|
|
386
386
|
try {
|
|
387
387
|
const response = JSON.parse(data.toString());
|
|
388
388
|
if (response.id) {
|
|
@@ -164,6 +164,7 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
164
164
|
const [showFilters, setShowFilters] = useState(false);
|
|
165
165
|
const [showReplayPreview, setShowReplayPreview] = useState(false);
|
|
166
166
|
const [replayEvents, setReplayEvents] = useState<any[]>([]);
|
|
167
|
+
const [isRotatingLog, setIsRotatingLog] = useState(false);
|
|
167
168
|
const [filters, setFilters] = useState({
|
|
168
169
|
browser: true,
|
|
169
170
|
server: true,
|
|
@@ -381,6 +382,43 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
381
382
|
}
|
|
382
383
|
};
|
|
383
384
|
|
|
385
|
+
const handleRotateLog = async () => {
|
|
386
|
+
if (!currentLogFile || isRotatingLog) return;
|
|
387
|
+
|
|
388
|
+
setIsRotatingLog(true);
|
|
389
|
+
try {
|
|
390
|
+
const response = await fetch('/api/logs/rotate', {
|
|
391
|
+
method: 'POST',
|
|
392
|
+
headers: {
|
|
393
|
+
'Content-Type': 'application/json',
|
|
394
|
+
},
|
|
395
|
+
body: JSON.stringify({ currentLogPath: currentLogFile }),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (response.ok) {
|
|
399
|
+
// Clear current logs from UI
|
|
400
|
+
setLogs([]);
|
|
401
|
+
setLastLogCount(0);
|
|
402
|
+
setLastFetched(null);
|
|
403
|
+
|
|
404
|
+
// Reload available logs to show the new archived file
|
|
405
|
+
await loadAvailableLogs();
|
|
406
|
+
|
|
407
|
+
// Start fresh polling
|
|
408
|
+
await loadInitialLogs();
|
|
409
|
+
} else {
|
|
410
|
+
const error = await response.json();
|
|
411
|
+
console.error('Failed to rotate log:', error);
|
|
412
|
+
alert('Failed to rotate log: ' + error.error);
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('Error rotating log:', error);
|
|
416
|
+
alert('Error rotating log');
|
|
417
|
+
} finally {
|
|
418
|
+
setIsRotatingLog(false);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
384
422
|
const filteredLogs = useMemo(() => {
|
|
385
423
|
return logs.filter(entry => {
|
|
386
424
|
// Check specific message types first (these override source filtering)
|
|
@@ -470,13 +508,25 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
470
508
|
)}
|
|
471
509
|
</div>
|
|
472
510
|
) : (
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
511
|
+
<div className="flex items-center gap-2">
|
|
512
|
+
<span className="font-mono text-xs text-gray-600 px-3 py-1 whitespace-nowrap">
|
|
513
|
+
{isInitialLoading && !currentLogFile ? (
|
|
514
|
+
<div className="h-4 bg-gray-200 rounded animate-pulse" style={{width: '220px'}} />
|
|
515
|
+
) : (
|
|
516
|
+
currentLogFile ? currentLogFile.split('/').pop() : 'dev3000.log'
|
|
517
|
+
)}
|
|
518
|
+
</span>
|
|
519
|
+
{currentLogFile && !isInitialLoading && (
|
|
520
|
+
<button
|
|
521
|
+
onClick={handleRotateLog}
|
|
522
|
+
disabled={isRotatingLog}
|
|
523
|
+
className="px-2 py-1 text-xs bg-orange-100 text-orange-700 hover:bg-orange-200 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
524
|
+
title="Clear logs (rotate current log to archive and start fresh)"
|
|
525
|
+
>
|
|
526
|
+
{isRotatingLog ? '...' : 'Clear'}
|
|
527
|
+
</button>
|
|
478
528
|
)}
|
|
479
|
-
</
|
|
529
|
+
</div>
|
|
480
530
|
)}
|
|
481
531
|
|
|
482
532
|
{logs.length > 0 && (
|
package/mcp-server/app/page.tsx
CHANGED
package/mcp-server/package.json
CHANGED
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
"start": "next start"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
+
"mcp-handler": "^1.0.2",
|
|
11
12
|
"next": "^15.5.1-canary.13",
|
|
12
13
|
"react": "^18.0.0",
|
|
13
14
|
"react-dom": "^18.0.0",
|
|
14
|
-
"
|
|
15
|
+
"ws": "^8.18.3",
|
|
15
16
|
"zod": "^3.22.4"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@types/node": "^22.0.0",
|
|
19
20
|
"@types/react": "^18.0.0",
|
|
20
21
|
"@types/react-dom": "^18.0.0",
|
|
22
|
+
"@types/ws": "^8.5.12",
|
|
21
23
|
"typescript": "^5.0.0"
|
|
22
24
|
}
|
|
23
25
|
}
|