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.
@@ -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
- <span className="font-mono text-xs text-gray-600 px-3 py-1 whitespace-nowrap">
474
- {isInitialLoading && !currentLogFile ? (
475
- <div className="h-4 bg-gray-200 rounded animate-pulse" style={{width: '220px'}} />
476
- ) : (
477
- currentLogFile ? currentLogFile.split('/').pop() : 'dev3000.log'
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
- </span>
529
+ </div>
480
530
  )}
481
531
 
482
532
  {logs.length > 0 && (
@@ -28,7 +28,7 @@ export default function HomePage() {
28
28
  </a>
29
29
 
30
30
  <a
31
- href="/api/mcp/http"
31
+ href="/api/mcp/mcp"
32
32
  className="block w-full bg-green-500 text-white text-center py-3 px-4 rounded hover:bg-green-600 transition-colors"
33
33
  >
34
34
  🤖 MCP Endpoint
@@ -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
- "mcp-handler": "^1.0.2",
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
  }