@runtimescope/mcp-server 0.9.1 → 0.9.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/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync2 } from "fs";
4
+ import { existsSync as existsSync3 } from "fs";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { join as join2 } from "path";
7
+ import { join as join3 } from "path";
8
8
  import {
9
9
  CollectorServer,
10
10
  ProjectManager,
@@ -2285,16 +2285,412 @@ function registerSessionDiffTools(server, sessionManager, collector, projectMana
2285
2285
  );
2286
2286
  }
2287
2287
 
2288
- // src/tools/recon-metadata.ts
2288
+ // src/tools/qa-check.ts
2289
2289
  import { z as z17 } from "zod";
2290
+ import { detectIssues as detectIssues2 } from "@runtimescope/collector";
2291
+ function registerQaCheckTools(server, store, sessionManager, collector, apiDiscovery) {
2292
+ server.tool(
2293
+ "runtime_qa_check",
2294
+ "Quick health check \u2014 snapshots the current session state and runs all issue detectors in one call. Use after making code changes to verify nothing is broken. Returns a snapshot (for later comparison) plus any detected issues. Combines create_session_snapshot + detect_issues into a single action.",
2295
+ {
2296
+ project_id: projectIdParam,
2297
+ label: z17.string().optional().describe('Label for the snapshot (e.g., "after-fix", "pre-deploy", "baseline")'),
2298
+ since_seconds: z17.number().optional().describe("Only detect issues from events in the last N seconds (default: all)")
2299
+ },
2300
+ async ({ project_id, label, since_seconds }) => {
2301
+ const { sessionId } = resolveSessionContext(store, project_id);
2302
+ if (!sessionId) {
2303
+ return {
2304
+ content: [{
2305
+ type: "text",
2306
+ text: JSON.stringify({
2307
+ summary: "No active session found. Connect an SDK first.",
2308
+ data: null,
2309
+ issues: ["No active sessions \u2014 connect an SDK with RuntimeScope.init()"],
2310
+ metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null, projectId: project_id ?? null }
2311
+ }, null, 2)
2312
+ }]
2313
+ };
2314
+ }
2315
+ const projectName = collector.getProjectForSession(sessionId) ?? "default";
2316
+ const snapshot = sessionManager.createSnapshot(sessionId, projectName, label ?? "qa-check");
2317
+ const events = store.getAllEvents(since_seconds, void 0, project_id);
2318
+ const allIssues = [...detectIssues2(events)];
2319
+ if (apiDiscovery) {
2320
+ try {
2321
+ allIssues.push(...apiDiscovery.detectIssues());
2322
+ } catch {
2323
+ }
2324
+ }
2325
+ const severityOrder = { high: 0, medium: 1, low: 2 };
2326
+ allIssues.sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
2327
+ const highCount = allIssues.filter((i) => i.severity === "high").length;
2328
+ const medCount = allIssues.filter((i) => i.severity === "medium").length;
2329
+ const lowCount = allIssues.filter((i) => i.severity === "low").length;
2330
+ const issuesSummary = allIssues.length === 0 ? "No issues detected." : `${allIssues.length} issue(s): ${highCount} high, ${medCount} medium, ${lowCount} low.`;
2331
+ const metricsSummary = [
2332
+ `${snapshot.metrics.totalEvents} events`,
2333
+ `${snapshot.metrics.errorCount} errors`,
2334
+ `${Object.keys(snapshot.metrics.endpoints).length} endpoints`,
2335
+ `${Object.keys(snapshot.metrics.components).length} components`
2336
+ ].join(", ");
2337
+ const webVitalsSummary = Object.entries(snapshot.metrics.webVitals).map(([name, v]) => `${name}: ${typeof v.value === "number" ? v.value.toFixed(1) : v.value} (${v.rating})`).join(", ");
2338
+ const response = {
2339
+ summary: `QA Check complete. Snapshot saved${label ? ` as "${label}"` : ""}. ${metricsSummary}. ${issuesSummary}`,
2340
+ data: {
2341
+ snapshot: {
2342
+ id: snapshot.id,
2343
+ sessionId: snapshot.sessionId,
2344
+ project: snapshot.project,
2345
+ label: snapshot.label ?? null,
2346
+ createdAt: new Date(snapshot.createdAt).toISOString(),
2347
+ metrics: {
2348
+ totalEvents: snapshot.metrics.totalEvents,
2349
+ errorCount: snapshot.metrics.errorCount,
2350
+ endpointCount: Object.keys(snapshot.metrics.endpoints).length,
2351
+ componentCount: Object.keys(snapshot.metrics.components).length,
2352
+ webVitals: snapshot.metrics.webVitals,
2353
+ queryCount: Object.keys(snapshot.metrics.queries).length
2354
+ }
2355
+ },
2356
+ issues: allIssues.map((i) => ({
2357
+ severity: i.severity,
2358
+ pattern: i.pattern,
2359
+ title: i.title,
2360
+ description: i.description,
2361
+ evidence: i.evidence,
2362
+ suggestion: i.suggestion
2363
+ })),
2364
+ nextSteps: allIssues.length > 0 ? "Fix the issues above, then run runtime_qa_check again to compare. Use compare_sessions with the snapshot ID to see what changed." : "All clear! Use compare_sessions later to track regressions."
2365
+ },
2366
+ issues: allIssues.map((i) => `[${i.severity.toUpperCase()}] ${i.title}`),
2367
+ metadata: {
2368
+ timeRange: {
2369
+ from: snapshot.metrics.connectedAt,
2370
+ to: snapshot.metrics.disconnectedAt || Date.now()
2371
+ },
2372
+ eventCount: snapshot.metrics.totalEvents,
2373
+ sessionId,
2374
+ projectId: project_id ?? null,
2375
+ webVitals: webVitalsSummary || null
2376
+ }
2377
+ };
2378
+ return {
2379
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
2380
+ };
2381
+ }
2382
+ );
2383
+ }
2384
+
2385
+ // src/tools/setup.ts
2386
+ import { z as z18 } from "zod";
2387
+ import { readFileSync, existsSync as existsSync2, writeFileSync } from "fs";
2388
+ import { join as join2, basename } from "path";
2389
+ import { homedir } from "os";
2390
+ import {
2391
+ scaffoldProjectConfig
2392
+ } from "@runtimescope/collector";
2393
+ var COLLECTOR_PORT = process.env.RUNTIMESCOPE_PORT ?? "9090";
2394
+ var HTTP_PORT = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
2395
+ function detectFrameworks(projectDir) {
2396
+ const detected = [];
2397
+ let pkg = {};
2398
+ try {
2399
+ pkg = JSON.parse(readFileSync(join2(projectDir, "package.json"), "utf-8"));
2400
+ } catch {
2401
+ }
2402
+ const allDeps = {
2403
+ ...pkg.dependencies ?? {},
2404
+ ...pkg.devDependencies ?? {}
2405
+ };
2406
+ if (existsSync2(join2(projectDir, "wrangler.toml")) || existsSync2(join2(projectDir, "wrangler.jsonc")) || allDeps["@cloudflare/workers-types"] || allDeps["wrangler"]) {
2407
+ const entryFile = existsSync2(join2(projectDir, "src/index.ts")) ? "src/index.ts" : existsSync2(join2(projectDir, "src/index.js")) ? "src/index.js" : void 0;
2408
+ detected.push({ framework: "workers", sdkType: "workers", entryFile, installCmd: "npm install @runtimescope/workers-sdk" });
2409
+ }
2410
+ if (allDeps["express"] || allDeps["fastify"] || allDeps["hono"] || allDeps["koa"]) {
2411
+ detected.push({ framework: "other", sdkType: "server", installCmd: "npm install @runtimescope/server-sdk" });
2412
+ }
2413
+ if (allDeps["next"]) {
2414
+ const entryFile = existsSync2(join2(projectDir, "app/providers.tsx")) ? "app/providers.tsx" : existsSync2(join2(projectDir, "src/app/providers.tsx")) ? "src/app/providers.tsx" : existsSync2(join2(projectDir, "app/layout.tsx")) ? "app/layout.tsx" : existsSync2(join2(projectDir, "pages/_app.tsx")) ? "pages/_app.tsx" : void 0;
2415
+ detected.push({ framework: "nextjs", sdkType: "browser", entryFile, installCmd: "npm install @runtimescope/sdk" });
2416
+ detected.push({ framework: "nextjs", sdkType: "server", installCmd: "npm install @runtimescope/server-sdk" });
2417
+ } else if (allDeps["react"] || allDeps["react-dom"]) {
2418
+ const entryFile = existsSync2(join2(projectDir, "src/main.tsx")) ? "src/main.tsx" : existsSync2(join2(projectDir, "src/index.tsx")) ? "src/index.tsx" : existsSync2(join2(projectDir, "src/main.jsx")) ? "src/main.jsx" : void 0;
2419
+ detected.push({ framework: "react", sdkType: "browser", entryFile, installCmd: "npm install @runtimescope/sdk" });
2420
+ } else if (allDeps["vue"]) {
2421
+ const entryFile = existsSync2(join2(projectDir, "src/main.ts")) ? "src/main.ts" : void 0;
2422
+ detected.push({ framework: "vue", sdkType: "browser", entryFile, installCmd: "npm install @runtimescope/sdk" });
2423
+ } else if (allDeps["nuxt"]) {
2424
+ detected.push({ framework: "nuxt", sdkType: "browser", installCmd: "npm install @runtimescope/sdk" });
2425
+ } else if (allDeps["svelte"] || allDeps["@sveltejs/kit"]) {
2426
+ detected.push({ framework: "svelte", sdkType: "browser", installCmd: "npm install @runtimescope/sdk" });
2427
+ } else if (allDeps["@angular/core"]) {
2428
+ detected.push({ framework: "angular", sdkType: "browser", installCmd: "npm install @runtimescope/sdk" });
2429
+ }
2430
+ if (existsSync2(join2(projectDir, "requirements.txt")) || existsSync2(join2(projectDir, "pyproject.toml"))) {
2431
+ if (existsSync2(join2(projectDir, "manage.py"))) {
2432
+ detected.push({ framework: "django", sdkType: "browser" });
2433
+ } else {
2434
+ detected.push({ framework: "flask", sdkType: "browser" });
2435
+ }
2436
+ }
2437
+ if (existsSync2(join2(projectDir, "Gemfile"))) {
2438
+ detected.push({ framework: "rails", sdkType: "browser" });
2439
+ }
2440
+ if (existsSync2(join2(projectDir, "composer.json"))) {
2441
+ detected.push({ framework: "php", sdkType: "browser" });
2442
+ }
2443
+ if (existsSync2(join2(projectDir, "wp-config.php"))) {
2444
+ detected.push({ framework: "wordpress", sdkType: "browser" });
2445
+ }
2446
+ if (detected.length === 0 && Object.keys(pkg).length > 0) {
2447
+ detected.push({ framework: "html", sdkType: "browser", installCmd: "npm install @runtimescope/sdk" });
2448
+ }
2449
+ if (detected.length === 0) {
2450
+ detected.push({ framework: "html", sdkType: "browser" });
2451
+ }
2452
+ return detected;
2453
+ }
2454
+ function generateSnippet(framework, sdkType, appName, projectId) {
2455
+ const projectIdLine = `
2456
+ projectId: '${projectId}',`;
2457
+ if (sdkType === "workers") {
2458
+ return {
2459
+ snippet: `import { withRuntimeScope, scopeD1, scopeKV, scopeR2 } from '@runtimescope/workers-sdk';
2460
+
2461
+ export default withRuntimeScope({
2462
+ async fetch(request, env, ctx) {
2463
+ // const db = scopeD1(env.DB);
2464
+ // const kv = scopeKV(env.KV);
2465
+ return yourApp.fetch(request, env, ctx);
2466
+ },
2467
+ }, {
2468
+ appName: '${appName}',${projectIdLine}
2469
+ httpEndpoint: 'http://localhost:${HTTP_PORT}/api/events',
2470
+ });`,
2471
+ placement: "Wrap your default export in the Worker entry file (src/index.ts).",
2472
+ installCmd: "npm install @runtimescope/workers-sdk"
2473
+ };
2474
+ }
2475
+ if (sdkType === "server") {
2476
+ return {
2477
+ snippet: `import { RuntimeScope } from '@runtimescope/server-sdk';
2478
+
2479
+ RuntimeScope.connect({
2480
+ appName: '${appName}',${projectIdLine}
2481
+ captureConsole: true,
2482
+ captureHttp: true,
2483
+ capturePerformance: true,
2484
+ });
2485
+
2486
+ // Instrument your ORM:
2487
+ // RuntimeScope.instrumentPrisma(prisma);
2488
+ // RuntimeScope.instrumentDrizzle(db);`,
2489
+ placement: framework === "nextjs" ? "Add to instrumentation.ts (Next.js instrumentation hook)." : "Add to your server entry file before starting the server.",
2490
+ installCmd: "npm install @runtimescope/server-sdk"
2491
+ };
2492
+ }
2493
+ const usesNpm = ["react", "vue", "angular", "svelte", "nextjs", "nuxt"].includes(framework);
2494
+ if (usesNpm) {
2495
+ return {
2496
+ snippet: `import { RuntimeScope } from '@runtimescope/sdk';
2497
+
2498
+ RuntimeScope.init({
2499
+ appName: '${appName}',${projectIdLine}
2500
+ endpoint: 'ws://localhost:${COLLECTOR_PORT}',
2501
+ capturePerformance: true,
2502
+ captureRenders: true,
2503
+ });`,
2504
+ placement: framework === "nextjs" ? "Add to app/providers.tsx (client component) or pages/_app.tsx." : framework === "react" ? "Add to src/main.tsx before createRoot()." : framework === "vue" ? "Add to src/main.ts before createApp()." : "Add to your entry file before the app initializes.",
2505
+ installCmd: "npm install @runtimescope/sdk"
2506
+ };
2507
+ }
2508
+ const placements = {
2509
+ flask: "Add to templates/base.html before </body>.",
2510
+ django: "Add to templates/base.html before </body>.",
2511
+ rails: "Add to app/views/layouts/application.html.erb before </body>.",
2512
+ php: "Add to your layout/footer file before </body>.",
2513
+ wordpress: "Add to your theme's footer.php before </body>.",
2514
+ html: "Add before </body> in your HTML files."
2515
+ };
2516
+ return {
2517
+ snippet: `<script src="http://localhost:${HTTP_PORT}/runtimescope.js"></script>
2518
+ <script>
2519
+ RuntimeScope.init({
2520
+ appName: '${appName}',${projectIdLine}
2521
+ endpoint: 'ws://localhost:${COLLECTOR_PORT}',
2522
+ });
2523
+ </script>`,
2524
+ placement: placements[framework] ?? placements.html
2525
+ };
2526
+ }
2527
+ function checkAndRegisterHooks() {
2528
+ const settingsPath = join2(homedir(), ".claude", "settings.json");
2529
+ let settings = {};
2530
+ try {
2531
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
2532
+ } catch {
2533
+ }
2534
+ const hooks = settings.hooks;
2535
+ if (hooks?.PostToolUse) {
2536
+ const existing = JSON.stringify(hooks.PostToolUse);
2537
+ if (existing.includes("9091") || existing.includes("runtimescope")) {
2538
+ return { registered: true, alreadyExists: true, message: "RuntimeScope hooks already registered." };
2539
+ }
2540
+ }
2541
+ if (!settings.hooks) settings.hooks = {};
2542
+ const h = settings.hooks;
2543
+ if (!h.PostToolUse) h.PostToolUse = [];
2544
+ h.PostToolUse.push({
2545
+ matcher: ".*",
2546
+ hooks: [{
2547
+ type: "command",
2548
+ command: `mkdir -p ~/.runtimescope/hooks && _RS_DIR=$(basename "$PWD") && _RS_PID=$(cat "$PWD/.runtimescope/config.json" 2>/dev/null | grep -o '"projectId":"[^"]*"' | cut -d'"' -f4) && echo '{"ts":'$(date +%s)',"tool":"'"$CLAUDE_TOOL_NAME"'","exit":"'"$CLAUDE_TOOL_EXIT_CODE"'","dir":"'"$_RS_DIR"'","projectId":"'"$_RS_PID"'"}' >> ~/.runtimescope/hooks/tool-events.jsonl && curl -s -X POST http://localhost:${HTTP_PORT}/api/events -H 'Content-Type: application/json' -d '{"sessionId":"claude-hooks","appName":"'"$_RS_DIR"'","events":[{"eventId":"hook-'$(date +%s%N)'","sessionId":"claude-hooks","timestamp":'$(date +%s000)',"eventType":"custom","name":"tool_use","properties":{"tool":"'"$CLAUDE_TOOL_NAME"'","exitCode":"'"$CLAUDE_TOOL_EXIT_CODE"'","projectId":"'"$_RS_PID"'"}}]}' >/dev/null 2>&1 &`
2549
+ }]
2550
+ });
2551
+ try {
2552
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2553
+ return { registered: true, alreadyExists: false, message: "RuntimeScope hooks registered in ~/.claude/settings.json." };
2554
+ } catch (err) {
2555
+ return { registered: false, alreadyExists: false, message: `Failed to register hooks: ${err.message}` };
2556
+ }
2557
+ }
2558
+ function registerSetupTools(server, store, collector, projectManager) {
2559
+ server.tool(
2560
+ "setup_project",
2561
+ "Set up RuntimeScope in a project \u2014 detects framework, creates .runtimescope/config.json, generates SDK snippets, and registers Claude hooks. Returns everything needed to install the SDK in one call. Use this instead of manual setup steps.",
2562
+ {
2563
+ project_dir: z18.string().describe("Absolute path to the project root directory"),
2564
+ app_name: z18.string().optional().describe("App name for RuntimeScope (defaults to directory name or package.json name)"),
2565
+ register_hooks: z18.boolean().optional().default(true).describe("Register Claude Code hooks for tool timing (default: true)")
2566
+ },
2567
+ async ({ project_dir, app_name, register_hooks }) => {
2568
+ const issues = [];
2569
+ if (!existsSync2(project_dir)) {
2570
+ return {
2571
+ content: [{
2572
+ type: "text",
2573
+ text: JSON.stringify({
2574
+ summary: `Directory not found: ${project_dir}`,
2575
+ data: null,
2576
+ issues: [`Project directory does not exist: ${project_dir}`],
2577
+ metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
2578
+ }, null, 2)
2579
+ }]
2580
+ };
2581
+ }
2582
+ let resolvedAppName = app_name;
2583
+ if (!resolvedAppName) {
2584
+ try {
2585
+ const pkg = JSON.parse(readFileSync(join2(project_dir, "package.json"), "utf-8"));
2586
+ resolvedAppName = pkg.name;
2587
+ } catch {
2588
+ }
2589
+ }
2590
+ if (!resolvedAppName) {
2591
+ resolvedAppName = basename(project_dir);
2592
+ }
2593
+ const frameworks = detectFrameworks(project_dir);
2594
+ const primaryFramework = frameworks[0];
2595
+ const config = scaffoldProjectConfig(project_dir, {
2596
+ appName: resolvedAppName,
2597
+ framework: primaryFramework.framework,
2598
+ sdkType: primaryFramework.sdkType
2599
+ });
2600
+ for (const fw of frameworks.slice(1)) {
2601
+ if (!config.sdks.some((s) => s.type === fw.sdkType)) {
2602
+ config.sdks.push({ type: fw.sdkType, framework: fw.framework, entryFile: fw.entryFile });
2603
+ }
2604
+ }
2605
+ projectManager.ensureProjectDir(resolvedAppName);
2606
+ projectManager.setProjectIdForApp(resolvedAppName, config.projectId);
2607
+ const snippets = frameworks.map((fw) => ({
2608
+ sdkType: fw.sdkType,
2609
+ framework: fw.framework,
2610
+ entryFile: fw.entryFile,
2611
+ ...generateSnippet(fw.framework, fw.sdkType, resolvedAppName, config.projectId)
2612
+ }));
2613
+ const uniqueSnippets = snippets.filter((s, i, arr) => arr.findIndex((x) => x.sdkType === s.sdkType) === i);
2614
+ const sessions = store.getSessionInfo();
2615
+ const projectSessions = sessions.filter((s) => s.projectId === config.projectId || s.appName === resolvedAppName);
2616
+ const isConnected = projectSessions.some((s) => s.isConnected);
2617
+ let hookResult = { registered: false, alreadyExists: false, message: "Hooks not requested." };
2618
+ if (register_hooks) {
2619
+ hookResult = checkAndRegisterHooks();
2620
+ }
2621
+ const sdkInstalled = existsSync2(join2(project_dir, "node_modules", "@runtimescope"));
2622
+ const phase = isConnected ? "connected" : sdkInstalled ? "installed_not_connected" : "awaiting_installation";
2623
+ const nextSteps = [];
2624
+ if (phase === "awaiting_installation") {
2625
+ for (const s of uniqueSnippets) {
2626
+ if (s.installCmd) nextSteps.push(`Run: ${s.installCmd}`);
2627
+ nextSteps.push(`Add SDK init to ${s.entryFile ?? "your entry file"}: ${s.placement}`);
2628
+ }
2629
+ nextSteps.push("Start your app and verify with get_session_info");
2630
+ } else if (phase === "installed_not_connected") {
2631
+ nextSteps.push("Start your app to establish the WebSocket connection");
2632
+ nextSteps.push("Verify with get_session_info");
2633
+ }
2634
+ if (!hookResult.alreadyExists && hookResult.registered) {
2635
+ nextSteps.push("Hooks registered \u2014 tool timing will be tracked automatically");
2636
+ }
2637
+ const response = {
2638
+ summary: phase === "connected" ? `${resolvedAppName} is set up and connected (${config.projectId}). ${projectSessions.length} active session(s).` : `${resolvedAppName} set up (${config.projectId}). ${uniqueSnippets.length} SDK snippet(s) generated. ${phase === "awaiting_installation" ? "Install the SDK to connect." : "Start your app to connect."}`,
2639
+ data: {
2640
+ phase,
2641
+ project: {
2642
+ projectId: config.projectId,
2643
+ appName: resolvedAppName,
2644
+ configPath: join2(project_dir, ".runtimescope", "config.json"),
2645
+ frameworks: frameworks.map((f) => ({ framework: f.framework, sdkType: f.sdkType, entryFile: f.entryFile }))
2646
+ },
2647
+ snippets: uniqueSnippets.map((s) => ({
2648
+ sdkType: s.sdkType,
2649
+ framework: s.framework,
2650
+ snippet: s.snippet,
2651
+ placement: s.placement,
2652
+ installCmd: s.installCmd,
2653
+ entryFile: s.entryFile
2654
+ })),
2655
+ connection: {
2656
+ connected: isConnected,
2657
+ sessionCount: projectSessions.length,
2658
+ sessions: projectSessions.map((s) => ({
2659
+ sessionId: s.sessionId,
2660
+ appName: s.appName,
2661
+ sdkVersion: s.sdkVersion,
2662
+ eventCount: s.eventCount
2663
+ }))
2664
+ },
2665
+ hooks: hookResult,
2666
+ sdkInstalled,
2667
+ nextSteps
2668
+ },
2669
+ issues,
2670
+ metadata: {
2671
+ timeRange: { from: 0, to: Date.now() },
2672
+ eventCount: 0,
2673
+ sessionId: projectSessions[0]?.sessionId ?? null,
2674
+ projectId: config.projectId
2675
+ }
2676
+ };
2677
+ return {
2678
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
2679
+ };
2680
+ }
2681
+ );
2682
+ }
2683
+
2684
+ // src/tools/recon-metadata.ts
2685
+ import { z as z19 } from "zod";
2290
2686
  function registerReconMetadataTools(server, store, collector) {
2291
2687
  server.tool(
2292
2688
  "get_page_metadata",
2293
2689
  "Get page metadata and tech stack detection for the current page. Returns URL, viewport, meta tags, detected framework/UI library/build tool/hosting, external stylesheets and scripts. Requires the RuntimeScope extension to be connected.",
2294
2690
  {
2295
2691
  project_id: projectIdParam,
2296
- url: z17.string().optional().describe("Filter by URL substring"),
2297
- force_refresh: z17.boolean().optional().default(false).describe("Send a recon_scan command to the extension to capture fresh data")
2692
+ url: z19.string().optional().describe("Filter by URL substring"),
2693
+ force_refresh: z19.boolean().optional().default(false).describe("Send a recon_scan command to the extension to capture fresh data")
2298
2694
  },
2299
2695
  async ({ project_id, url, force_refresh }) => {
2300
2696
  if (force_refresh) {
@@ -2376,16 +2772,16 @@ function registerReconMetadataTools(server, store, collector) {
2376
2772
  }
2377
2773
 
2378
2774
  // src/tools/recon-design-tokens.ts
2379
- import { z as z18 } from "zod";
2775
+ import { z as z20 } from "zod";
2380
2776
  function registerReconDesignTokenTools(server, store, collector) {
2381
2777
  server.tool(
2382
2778
  "get_design_tokens",
2383
2779
  "Extract the design system from the current page: CSS custom properties (--variables), color palette, typography scale, spacing scale, border radii, box shadows, and CSS architecture detection. Essential for matching a site's visual style when recreating UI.",
2384
2780
  {
2385
2781
  project_id: projectIdParam,
2386
- url: z18.string().optional().describe("Filter by URL substring"),
2387
- category: z18.enum(["all", "colors", "typography", "spacing", "custom_properties", "shadows"]).optional().default("all").describe("Return only a specific token category"),
2388
- force_refresh: z18.boolean().optional().default(false).describe("Send a recon_scan command to capture fresh data")
2782
+ url: z20.string().optional().describe("Filter by URL substring"),
2783
+ category: z20.enum(["all", "colors", "typography", "spacing", "custom_properties", "shadows"]).optional().default("all").describe("Return only a specific token category"),
2784
+ force_refresh: z20.boolean().optional().default(false).describe("Send a recon_scan command to capture fresh data")
2389
2785
  },
2390
2786
  async ({ project_id, url, category, force_refresh }) => {
2391
2787
  if (force_refresh) {
@@ -2475,14 +2871,14 @@ function registerReconDesignTokenTools(server, store, collector) {
2475
2871
  }
2476
2872
 
2477
2873
  // src/tools/recon-fonts.ts
2478
- import { z as z19 } from "zod";
2874
+ import { z as z21 } from "zod";
2479
2875
  function registerReconFontTools(server, store) {
2480
2876
  server.tool(
2481
2877
  "get_font_info",
2482
2878
  "Get typography details for the current page: @font-face declarations, font families actually used in computed styles, icon fonts with glyph usage, and font loading strategy. Critical for matching typography when recreating UI.",
2483
2879
  {
2484
2880
  project_id: projectIdParam,
2485
- url: z19.string().optional().describe("Filter by URL substring")
2881
+ url: z21.string().optional().describe("Filter by URL substring")
2486
2882
  },
2487
2883
  async ({ project_id, url }) => {
2488
2884
  const event = store.getReconFonts({ url });
@@ -2534,17 +2930,17 @@ function registerReconFontTools(server, store) {
2534
2930
  }
2535
2931
 
2536
2932
  // src/tools/recon-layout.ts
2537
- import { z as z20 } from "zod";
2933
+ import { z as z22 } from "zod";
2538
2934
  function registerReconLayoutTools(server, store, collector) {
2539
2935
  server.tool(
2540
2936
  "get_layout_tree",
2541
2937
  "Get the DOM structure with layout information: element tags, classes, bounding rects, display mode (flex/grid/block), flex/grid properties (direction, justify, align, gap, template columns/rows), position, and z-index. Optionally scoped to a CSS selector. Essential for understanding page structure when recreating UI.",
2542
2938
  {
2543
2939
  project_id: projectIdParam,
2544
- selector: z20.string().optional().describe('CSS selector to scope the tree (e.g., "nav", ".hero", "main"). Omit for full page.'),
2545
- max_depth: z20.number().optional().default(10).describe("Maximum depth of the tree to return (default 10)"),
2546
- url: z20.string().optional().describe("Filter by URL substring"),
2547
- force_refresh: z20.boolean().optional().default(false).describe("Request fresh capture from extension")
2940
+ selector: z22.string().optional().describe('CSS selector to scope the tree (e.g., "nav", ".hero", "main"). Omit for full page.'),
2941
+ max_depth: z22.number().optional().default(10).describe("Maximum depth of the tree to return (default 10)"),
2942
+ url: z22.string().optional().describe("Filter by URL substring"),
2943
+ force_refresh: z22.boolean().optional().default(false).describe("Request fresh capture from extension")
2548
2944
  },
2549
2945
  async ({ project_id, selector, max_depth, url, force_refresh }) => {
2550
2946
  if (force_refresh) {
@@ -2657,14 +3053,14 @@ function pruneTree(node, maxDepth, currentDepth = 0) {
2657
3053
  }
2658
3054
 
2659
3055
  // src/tools/recon-accessibility.ts
2660
- import { z as z21 } from "zod";
3056
+ import { z as z23 } from "zod";
2661
3057
  function registerReconAccessibilityTools(server, store) {
2662
3058
  server.tool(
2663
3059
  "get_accessibility_tree",
2664
3060
  "Get the accessibility structure of the current page: heading hierarchy (h1-h6), ARIA landmarks (nav, main, aside), form fields with labels, buttons, links, and images with alt text status. Useful for ensuring UI recreations maintain proper semantic HTML and accessibility.",
2665
3061
  {
2666
3062
  project_id: projectIdParam,
2667
- url: z21.string().optional().describe("Filter by URL substring")
3063
+ url: z23.string().optional().describe("Filter by URL substring")
2668
3064
  },
2669
3065
  async ({ project_id, url }) => {
2670
3066
  const event = store.getReconAccessibility({ url });
@@ -2731,7 +3127,7 @@ function registerReconAccessibilityTools(server, store) {
2731
3127
  }
2732
3128
 
2733
3129
  // src/tools/recon-computed-styles.ts
2734
- import { z as z22 } from "zod";
3130
+ import { z as z24 } from "zod";
2735
3131
  var PROPERTY_GROUPS = {
2736
3132
  colors: [
2737
3133
  "color",
@@ -2836,10 +3232,10 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
2836
3232
  "Get computed CSS styles for elements matching a selector. Returns the actual resolved values the browser uses to render each element. Can filter by property group (colors, typography, spacing, layout, borders, visual) or specific property names. When multiple elements match, highlights variations between them.",
2837
3233
  {
2838
3234
  project_id: projectIdParam,
2839
- selector: z22.string().describe('CSS selector to query (e.g., ".btn-primary", "nav > ul > li", "[data-testid=hero]")'),
2840
- properties: z22.enum(["all", "colors", "typography", "spacing", "layout", "borders", "visual"]).optional().default("all").describe('Property group to return, or "all" for everything'),
2841
- specific_properties: z22.array(z22.string()).optional().describe("Specific CSS property names to return (overrides the properties group)"),
2842
- force_refresh: z22.boolean().optional().default(false).describe("Request fresh capture from extension or scanner for this selector")
3235
+ selector: z24.string().describe('CSS selector to query (e.g., ".btn-primary", "nav > ul > li", "[data-testid=hero]")'),
3236
+ properties: z24.enum(["all", "colors", "typography", "spacing", "layout", "borders", "visual"]).optional().default("all").describe('Property group to return, or "all" for everything'),
3237
+ specific_properties: z24.array(z24.string()).optional().describe("Specific CSS property names to return (overrides the properties group)"),
3238
+ force_refresh: z24.boolean().optional().default(false).describe("Request fresh capture from extension or scanner for this selector")
2843
3239
  },
2844
3240
  async ({ project_id, selector, properties, specific_properties, force_refresh }) => {
2845
3241
  const propFilter = specific_properties ?? (properties !== "all" ? PROPERTY_GROUPS[properties] : void 0);
@@ -2950,16 +3346,16 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
2950
3346
  }
2951
3347
 
2952
3348
  // src/tools/recon-element-snapshot.ts
2953
- import { z as z23 } from "zod";
3349
+ import { z as z25 } from "zod";
2954
3350
  function registerReconElementSnapshotTools(server, store, collector, scanner) {
2955
3351
  server.tool(
2956
3352
  "get_element_snapshot",
2957
3353
  'Deep snapshot of a specific element and its children: structure, attributes, text content, bounding rects, and key computed styles for every node. This is the "zoom in" tool \u2014 use it when you need the full picture of a component (a card, a nav bar, a form) for recreation. More detailed than get_layout_tree, more targeted than get_computed_styles.',
2958
3354
  {
2959
3355
  project_id: projectIdParam,
2960
- selector: z23.string().describe('CSS selector for the root element (e.g., ".card", "#hero", "[data-testid=checkout-form]")'),
2961
- depth: z23.number().optional().default(5).describe("How many levels deep to capture children (default 5)"),
2962
- force_refresh: z23.boolean().optional().default(false).describe("Request fresh capture from extension or scanner for this element")
3356
+ selector: z25.string().describe('CSS selector for the root element (e.g., ".card", "#hero", "[data-testid=checkout-form]")'),
3357
+ depth: z25.number().optional().default(5).describe("How many levels deep to capture children (default 5)"),
3358
+ force_refresh: z25.boolean().optional().default(false).describe("Request fresh capture from extension or scanner for this element")
2963
3359
  },
2964
3360
  async ({ project_id, selector, depth, force_refresh }) => {
2965
3361
  if (force_refresh) {
@@ -3044,15 +3440,15 @@ function registerReconElementSnapshotTools(server, store, collector, scanner) {
3044
3440
  }
3045
3441
 
3046
3442
  // src/tools/recon-assets.ts
3047
- import { z as z24 } from "zod";
3443
+ import { z as z26 } from "zod";
3048
3444
  function registerReconAssetTools(server, store) {
3049
3445
  server.tool(
3050
3446
  "get_asset_inventory",
3051
3447
  "Sprite-aware asset inventory for the current page. Detects and extracts: standard images, inline SVGs, SVG sprite sheets (<symbol>/<use> references), CSS background sprites (with crop coordinates and extracted frames), CSS mask sprites, and icon fonts (with glyph codepoints). For CSS sprites, calculates the exact crop rectangle from background-position/size and can provide extracted individual frames as data URLs.",
3052
3448
  {
3053
3449
  project_id: projectIdParam,
3054
- category: z24.enum(["all", "images", "svg", "sprites", "icon_fonts"]).optional().default("all").describe("Filter by asset category"),
3055
- url: z24.string().optional().describe("Filter by page URL substring")
3450
+ category: z26.enum(["all", "images", "svg", "sprites", "icon_fonts"]).optional().default("all").describe("Filter by asset category"),
3451
+ url: z26.string().optional().describe("Filter by page URL substring")
3056
3452
  },
3057
3453
  async ({ project_id, category, url }) => {
3058
3454
  const event = store.getReconAssetInventory({ url });
@@ -3143,7 +3539,7 @@ function registerReconAssetTools(server, store) {
3143
3539
  }
3144
3540
 
3145
3541
  // src/tools/recon-style-diff.ts
3146
- import { z as z25 } from "zod";
3542
+ import { z as z27 } from "zod";
3147
3543
  var VISUAL_PROPERTIES = [
3148
3544
  "color",
3149
3545
  "background-color",
@@ -3188,10 +3584,10 @@ function registerReconStyleDiffTools(server, store) {
3188
3584
  "Compare computed styles between two captured element snapshots to check how closely a recreation matches the original. Compares two selectors from stored computed style events and reports property-by-property differences with a match percentage. Use this to verify UI recreation fidelity.",
3189
3585
  {
3190
3586
  project_id: projectIdParam,
3191
- source_selector: z25.string().describe("CSS selector for the source/original element"),
3192
- target_selector: z25.string().describe("CSS selector for the target/recreation element"),
3193
- properties: z25.enum(["visual", "all"]).optional().default("visual").describe('"visual" compares only visually-significant properties (colors, typography, spacing, layout). "all" compares everything.'),
3194
- specific_properties: z25.array(z25.string()).optional().describe("Specific CSS property names to compare (overrides properties group)")
3587
+ source_selector: z27.string().describe("CSS selector for the source/original element"),
3588
+ target_selector: z27.string().describe("CSS selector for the target/recreation element"),
3589
+ properties: z27.enum(["visual", "all"]).optional().default("visual").describe('"visual" compares only visually-significant properties (colors, typography, spacing, layout). "all" compares everything.'),
3590
+ specific_properties: z27.array(z27.string()).optional().describe("Specific CSS property names to compare (overrides properties group)")
3195
3591
  },
3196
3592
  async ({ project_id, source_selector, target_selector, properties, specific_properties }) => {
3197
3593
  const events = store.getReconComputedStyles();
@@ -3305,7 +3701,7 @@ function parseNumericValue(value) {
3305
3701
  }
3306
3702
 
3307
3703
  // src/scanner/index.ts
3308
- import { readFileSync } from "fs";
3704
+ import { readFileSync as readFileSync2 } from "fs";
3309
3705
  import { resolve, dirname } from "path";
3310
3706
  import { fileURLToPath } from "url";
3311
3707
  import { TechnologyDatabase, detect } from "@runtimescope/extension";
@@ -4256,8 +4652,8 @@ var PlaywrightScanner = class _PlaywrightScanner {
4256
4652
  let catData = null;
4257
4653
  for (const basePath of possiblePaths) {
4258
4654
  try {
4259
- techData = JSON.parse(readFileSync(resolve(basePath, "technologies.json"), "utf-8"));
4260
- catData = JSON.parse(readFileSync(resolve(basePath, "categories.json"), "utf-8"));
4655
+ techData = JSON.parse(readFileSync2(resolve(basePath, "technologies.json"), "utf-8"));
4656
+ catData = JSON.parse(readFileSync2(resolve(basePath, "categories.json"), "utf-8"));
4261
4657
  break;
4262
4658
  } catch {
4263
4659
  continue;
@@ -4449,29 +4845,42 @@ var PlaywrightScanner = class _PlaywrightScanner {
4449
4845
  };
4450
4846
 
4451
4847
  // src/tools/scanner.ts
4452
- import { z as z26 } from "zod";
4453
- import { getOrCreateProjectId } from "@runtimescope/collector";
4454
- var COLLECTOR_PORT = process.env.RUNTIMESCOPE_PORT ?? "9090";
4455
- var HTTP_PORT = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
4848
+ import { z as z28 } from "zod";
4849
+ import { getOrCreateProjectId, scaffoldProjectConfig as scaffoldProjectConfig2, readProjectConfig as readProjectConfig2 } from "@runtimescope/collector";
4850
+ var COLLECTOR_PORT2 = process.env.RUNTIMESCOPE_PORT ?? "9090";
4851
+ var HTTP_PORT2 = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
4456
4852
  function registerScannerTools(server, store, scanner, projectManager) {
4457
4853
  server.tool(
4458
4854
  "get_sdk_snippet",
4459
4855
  "Generate a ready-to-paste code snippet to connect any web application to RuntimeScope for live runtime monitoring. Works with ANY tech stack \u2014 React, Vue, Angular, Svelte, plain HTML, Flask/Django templates, Rails ERB, PHP, WordPress, etc. Returns the appropriate installation method based on the project type.",
4460
4856
  {
4461
- app_name: z26.string().optional().default("my-app").describe('Name for the app in RuntimeScope (e.g., "echo-frontend", "dashboard")'),
4462
- framework: z26.enum(["html", "react", "vue", "angular", "svelte", "nextjs", "nuxt", "flask", "django", "rails", "php", "wordpress", "workers", "other"]).optional().default("html").describe('The framework/tech stack of the project. Use "html" for any plain HTML or server-rendered pages. Use "workers" for Cloudflare Workers.'),
4463
- project_id: z26.string().optional().describe("Existing project ID to use (proj_xxx). If omitted, one is auto-generated and persisted.")
4857
+ app_name: z28.string().optional().default("my-app").describe('Name for the app in RuntimeScope (e.g., "echo-frontend", "dashboard")'),
4858
+ framework: z28.enum(["html", "react", "vue", "angular", "svelte", "nextjs", "nuxt", "flask", "django", "rails", "php", "wordpress", "workers", "other"]).optional().default("html").describe('The framework/tech stack of the project. Use "html" for any plain HTML or server-rendered pages. Use "workers" for Cloudflare Workers.'),
4859
+ project_id: z28.string().optional().describe("Existing project ID to use (proj_xxx). If omitted, one is auto-generated and persisted."),
4860
+ project_dir: z28.string().optional().describe("Absolute path to the project root directory. If provided, creates .runtimescope/config.json with the project config.")
4464
4861
  },
4465
- async ({ app_name, framework, project_id }) => {
4466
- const resolvedProjectId = project_id ?? (projectManager ? getOrCreateProjectId(projectManager, app_name) : void 0);
4862
+ async ({ app_name, framework, project_id, project_dir }) => {
4863
+ let resolvedProjectId = project_id;
4864
+ if (project_dir) {
4865
+ const sdkType = framework === "workers" ? "workers" : ["flask", "django", "rails", "php", "wordpress"].includes(framework) ? "browser" : "browser";
4866
+ const config = scaffoldProjectConfig2(project_dir, {
4867
+ appName: app_name,
4868
+ framework,
4869
+ sdkType
4870
+ });
4871
+ resolvedProjectId = resolvedProjectId ?? config.projectId;
4872
+ }
4873
+ if (!resolvedProjectId) {
4874
+ resolvedProjectId = projectManager ? getOrCreateProjectId(projectManager, app_name) : void 0;
4875
+ }
4467
4876
  const projectIdLine = resolvedProjectId ? `
4468
4877
  projectId: '${resolvedProjectId}',` : "";
4469
4878
  const scriptTagSnippet = `<!-- RuntimeScope \u2014 paste before </body> -->
4470
- <script src="http://localhost:${HTTP_PORT}/runtimescope.js"></script>
4879
+ <script src="http://localhost:${HTTP_PORT2}/runtimescope.js"></script>
4471
4880
  <script>
4472
4881
  RuntimeScope.init({
4473
4882
  appName: '${app_name}',${projectIdLine}
4474
- endpoint: 'ws://localhost:${COLLECTOR_PORT}',
4883
+ endpoint: 'ws://localhost:${COLLECTOR_PORT2}',
4475
4884
  });
4476
4885
  </script>`;
4477
4886
  const npmSnippet = `// npm install @runtimescope/sdk
@@ -4479,7 +4888,7 @@ import { RuntimeScope } from '@runtimescope/sdk';
4479
4888
 
4480
4889
  RuntimeScope.init({
4481
4890
  appName: '${app_name}',${projectIdLine}
4482
- endpoint: 'ws://localhost:${COLLECTOR_PORT}',
4891
+ endpoint: 'ws://localhost:${COLLECTOR_PORT2}',
4483
4892
  });`;
4484
4893
  const workersSnippet = `// npm install @runtimescope/workers-sdk
4485
4894
  import { withRuntimeScope, scopeD1, scopeKV, scopeR2, track, addBreadcrumb } from '@runtimescope/workers-sdk';
@@ -4501,7 +4910,7 @@ export default withRuntimeScope({
4501
4910
  },
4502
4911
  }, {
4503
4912
  appName: '${app_name}',${projectIdLine}
4504
- httpEndpoint: 'http://localhost:${HTTP_PORT}/api/events',
4913
+ httpEndpoint: 'http://localhost:${HTTP_PORT2}/api/events',
4505
4914
  // captureConsole: true, // Capture console.log/warn/error (default: true)
4506
4915
  // captureHeaders: false, // Include request/response headers (default: false)
4507
4916
  // sampleRate: 1.0, // 0.0-1.0 probabilistic sampling (default: 1.0)
@@ -4551,15 +4960,21 @@ export default withRuntimeScope({
4551
4960
  alternativeNote: isWorkers ? void 0 : usesNpm ? "If you prefer, you can also use a <script> tag instead of npm:" : "If the project uses npm/Node.js, you can also install via:",
4552
4961
  requirements: isWorkers ? [
4553
4962
  "RuntimeScope collector must be reachable from your Worker",
4554
- `HTTP collector endpoint at http://localhost:${HTTP_PORT}/api/events`,
4963
+ `HTTP collector endpoint at http://localhost:${HTTP_PORT2}/api/events`,
4555
4964
  "Add nodejs_compat to compatibility_flags in wrangler.toml",
4556
4965
  "For production: set httpEndpoint to your hosted collector URL"
4557
4966
  ] : [
4558
4967
  "RuntimeScope MCP server must be running (it starts automatically with Claude Code)",
4559
- `SDK bundle served at http://localhost:${HTTP_PORT}/runtimescope.js`,
4560
- `WebSocket collector at ws://localhost:${COLLECTOR_PORT}`
4968
+ `SDK bundle served at http://localhost:${HTTP_PORT2}/runtimescope.js`,
4969
+ `WebSocket collector at ws://localhost:${COLLECTOR_PORT2}`
4561
4970
  ],
4562
- whatItCaptures: isWorkers ? workersCaptures : browserCaptures
4971
+ whatItCaptures: isWorkers ? workersCaptures : browserCaptures,
4972
+ projectConfig: project_dir ? {
4973
+ created: true,
4974
+ path: `${project_dir}/.runtimescope/config.json`,
4975
+ projectId: resolvedProjectId,
4976
+ note: "Project config created. Commit .runtimescope/ to git to share settings across environments."
4977
+ } : void 0
4563
4978
  },
4564
4979
  issues: [],
4565
4980
  metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null, projectId: resolvedProjectId ?? null }
@@ -4569,14 +4984,49 @@ export default withRuntimeScope({
4569
4984
  };
4570
4985
  }
4571
4986
  );
4987
+ server.tool(
4988
+ "get_project_config",
4989
+ "Read the .runtimescope/config.json from a project directory. Returns the project ID, SDK entries, capture settings, and metadata. Use this to understand what RuntimeScope features are configured for a project.",
4990
+ {
4991
+ project_dir: z28.string().describe("Absolute path to the project root directory")
4992
+ },
4993
+ async ({ project_dir }) => {
4994
+ const config = readProjectConfig2(project_dir);
4995
+ if (!config) {
4996
+ return {
4997
+ content: [{
4998
+ type: "text",
4999
+ text: JSON.stringify({
5000
+ summary: `No .runtimescope/config.json found in ${project_dir}. Run get_sdk_snippet with project_dir to create one, or use /setup.`,
5001
+ data: null,
5002
+ issues: ["No project config found. Create one with get_sdk_snippet or /setup."],
5003
+ metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
5004
+ }, null, 2)
5005
+ }]
5006
+ };
5007
+ }
5008
+ const sdkSummary = config.sdks.length > 0 ? config.sdks.map((s) => `${s.type}${s.framework ? ` (${s.framework})` : ""}`).join(", ") : "none installed";
5009
+ return {
5010
+ content: [{
5011
+ type: "text",
5012
+ text: JSON.stringify({
5013
+ summary: `Project "${config.appName}" (${config.projectId}). SDKs: ${sdkSummary}. Phase: ${config.phase ?? "not set"}.`,
5014
+ data: config,
5015
+ issues: [],
5016
+ metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null, projectId: config.projectId }
5017
+ }, null, 2)
5018
+ }]
5019
+ };
5020
+ }
5021
+ );
4572
5022
  server.tool(
4573
5023
  "scan_website",
4574
5024
  "Visit a website with a headless browser and extract comprehensive data: tech stack (7,221 technologies), design tokens (colors, typography, spacing, CSS variables), layout tree (DOM with bounding rects, flex/grid), accessibility structure, fonts, and asset inventory (images, SVGs, sprites). After scanning, all recon tools (get_design_tokens, get_layout_tree, get_font_info, etc.) will return data from the scanned page. This is the primary way to analyze any website.",
4575
5025
  {
4576
- url: z26.string().describe('The full URL to scan (e.g., "https://stripe.com")'),
4577
- viewport_width: z26.number().optional().default(1280).describe("Viewport width in pixels (default: 1280)"),
4578
- viewport_height: z26.number().optional().default(720).describe("Viewport height in pixels (default: 720)"),
4579
- wait_for: z26.enum(["load", "networkidle", "domcontentloaded"]).optional().default("networkidle").describe("Wait condition before scanning (default: networkidle)")
5026
+ url: z28.string().describe('The full URL to scan (e.g., "https://stripe.com")'),
5027
+ viewport_width: z28.number().optional().default(1280).describe("Viewport width in pixels (default: 1280)"),
5028
+ viewport_height: z28.number().optional().default(720).describe("Viewport height in pixels (default: 720)"),
5029
+ wait_for: z28.enum(["load", "networkidle", "domcontentloaded"]).optional().default("networkidle").describe("Wait condition before scanning (default: networkidle)")
4580
5030
  },
4581
5031
  async ({ url, viewport_width, viewport_height, wait_for }) => {
4582
5032
  try {
@@ -4656,16 +5106,16 @@ export default withRuntimeScope({
4656
5106
  }
4657
5107
 
4658
5108
  // src/tools/custom-events.ts
4659
- import { z as z27 } from "zod";
5109
+ import { z as z29 } from "zod";
4660
5110
  function registerCustomEventTools(server, store) {
4661
5111
  server.tool(
4662
5112
  "get_custom_events",
4663
5113
  "Get custom business/product events tracked via RuntimeScope.track(). Shows event catalog (all unique event names with counts) and recent occurrences. Use this to see what events are being tracked and their frequency.",
4664
5114
  {
4665
5115
  project_id: projectIdParam,
4666
- name: z27.string().optional().describe("Filter by event name (exact match)"),
4667
- since_seconds: z27.number().optional().describe("Only events from the last N seconds (default: 300)"),
4668
- session_id: z27.string().optional().describe("Filter by session ID")
5116
+ name: z29.string().optional().describe("Filter by event name (exact match)"),
5117
+ since_seconds: z29.number().optional().describe("Only events from the last N seconds (default: 300)"),
5118
+ session_id: z29.string().optional().describe("Filter by session ID")
4669
5119
  },
4670
5120
  async ({ project_id, name, since_seconds, session_id }) => {
4671
5121
  const sinceSeconds = since_seconds ?? 300;
@@ -4726,9 +5176,9 @@ function registerCustomEventTools(server, store) {
4726
5176
  "Analyze a user flow as a funnel. Given an ordered list of custom event names (steps), shows how many sessions completed each step, where drop-offs happen, and what errors/failures occurred between steps. Each step includes correlated telemetry (network errors, console errors, failed DB queries) that happened between the previous step and this one \u2014 this is the key to finding WHY a step failed.",
4727
5177
  {
4728
5178
  project_id: projectIdParam,
4729
- steps: z27.array(z27.string()).min(2).describe('Ordered list of custom event names representing the flow (e.g. ["create_profile", "generate_campaign", "export_ad"])'),
4730
- since_seconds: z27.number().optional().describe("Only analyze events from the last N seconds (default: 3600)"),
4731
- session_id: z27.string().optional().describe("Analyze a specific session (default: all sessions)")
5179
+ steps: z29.array(z29.string()).min(2).describe('Ordered list of custom event names representing the flow (e.g. ["create_profile", "generate_campaign", "export_ad"])'),
5180
+ since_seconds: z29.number().optional().describe("Only analyze events from the last N seconds (default: 3600)"),
5181
+ session_id: z29.string().optional().describe("Analyze a specific session (default: all sessions)")
4732
5182
  },
4733
5183
  async ({ project_id, steps, since_seconds, session_id }) => {
4734
5184
  const sinceSeconds = since_seconds ?? 3600;
@@ -4894,7 +5344,7 @@ function dedup(arr, limit) {
4894
5344
  }
4895
5345
 
4896
5346
  // src/tools/breadcrumbs.ts
4897
- import { z as z28 } from "zod";
5347
+ import { z as z30 } from "zod";
4898
5348
  function eventToBreadcrumb(event, anchorTs) {
4899
5349
  const base = {
4900
5350
  timestamp: new Date(event.timestamp).toISOString(),
@@ -4986,12 +5436,12 @@ function registerBreadcrumbTools(server, store) {
4986
5436
  "Get the chronological trail of user actions, navigation, clicks, console logs, network requests, and state changes leading up to a point in time (or an error). This is the primary debugging context tool \u2014 use it when investigating errors, unexpected behavior, or user-reported issues.",
4987
5437
  {
4988
5438
  project_id: projectIdParam,
4989
- since_seconds: z28.number().optional().describe("How far back to look (default: 60 seconds)"),
4990
- session_id: z28.string().optional().describe("Filter to a specific session"),
4991
- before_timestamp: z28.number().optional().describe('Only show breadcrumbs before this Unix ms timestamp (useful for "what happened before this error")'),
4992
- categories: z28.array(z28.string()).optional().describe("Filter to specific categories: navigation, ui.click, breadcrumb, console.error, console.warn, console.log, http, state, custom.*"),
4993
- level: z28.enum(["debug", "info", "warning", "error"]).optional().describe("Minimum breadcrumb level to include (default: debug = show all)"),
4994
- limit: z28.number().optional().describe(`Max breadcrumbs to return (default/max: ${MAX_BREADCRUMBS})`)
5439
+ since_seconds: z30.number().optional().describe("How far back to look (default: 60 seconds)"),
5440
+ session_id: z30.string().optional().describe("Filter to a specific session"),
5441
+ before_timestamp: z30.number().optional().describe('Only show breadcrumbs before this Unix ms timestamp (useful for "what happened before this error")'),
5442
+ categories: z30.array(z30.string()).optional().describe("Filter to specific categories: navigation, ui.click, breadcrumb, console.error, console.warn, console.log, http, state, custom.*"),
5443
+ level: z30.enum(["debug", "info", "warning", "error"]).optional().describe("Minimum breadcrumb level to include (default: debug = show all)"),
5444
+ limit: z30.number().optional().describe(`Max breadcrumbs to return (default/max: ${MAX_BREADCRUMBS})`)
4995
5445
  },
4996
5446
  async ({ project_id, since_seconds, session_id, before_timestamp, categories, level, limit }) => {
4997
5447
  const sinceSeconds = since_seconds ?? 60;
@@ -5056,7 +5506,7 @@ function countCategories(breadcrumbs) {
5056
5506
  }
5057
5507
 
5058
5508
  // src/tools/history.ts
5059
- import { z as z29 } from "zod";
5509
+ import { z as z31 } from "zod";
5060
5510
  var EVENT_TYPES = [
5061
5511
  "network",
5062
5512
  "console",
@@ -5095,14 +5545,14 @@ function registerHistoryTools(server, collector, projectManager) {
5095
5545
  "get_historical_events",
5096
5546
  "Query past events from persistent SQLite storage. Use this to access events beyond the in-memory buffer (last 10K events). Events persist across Claude Code restarts. Filter by project, event type, time range, and session.",
5097
5547
  {
5098
- project: z29.string().optional().describe("Project/app name (the appName used in SDK init). Required unless project_id is provided."),
5099
- project_id: z29.string().optional().describe("Project ID (proj_xxx). Alternative to project name \u2014 resolves to the app name automatically."),
5100
- event_types: z29.array(z29.enum(EVENT_TYPES)).optional().describe('Filter by event types (e.g., ["network", "console"])'),
5101
- since: z29.string().optional().describe('Start time \u2014 relative ("2h", "7d", "30m") or ISO date string'),
5102
- until: z29.string().optional().describe("End time \u2014 relative or ISO date string"),
5103
- session_id: z29.string().optional().describe("Filter by specific session ID"),
5104
- limit: z29.number().optional().default(200).describe("Max events to return (default 200, max 1000)"),
5105
- offset: z29.number().optional().default(0).describe("Pagination offset")
5548
+ project: z31.string().optional().describe("Project/app name (the appName used in SDK init). Required unless project_id is provided."),
5549
+ project_id: z31.string().optional().describe("Project ID (proj_xxx). Alternative to project name \u2014 resolves to the app name automatically."),
5550
+ event_types: z31.array(z31.enum(EVENT_TYPES)).optional().describe('Filter by event types (e.g., ["network", "console"])'),
5551
+ since: z31.string().optional().describe('Start time \u2014 relative ("2h", "7d", "30m") or ISO date string'),
5552
+ until: z31.string().optional().describe("End time \u2014 relative or ISO date string"),
5553
+ session_id: z31.string().optional().describe("Filter by specific session ID"),
5554
+ limit: z31.number().optional().default(200).describe("Max events to return (default 200, max 1000)"),
5555
+ offset: z31.number().optional().default(0).describe("Pagination offset")
5106
5556
  },
5107
5557
  async ({ project, project_id, event_types, since, until, session_id, limit, offset }) => {
5108
5558
  const resolvedProject = project ?? (project_id ? projectManager.getAppForProjectId(project_id) : void 0);
@@ -5232,8 +5682,8 @@ function registerHistoryTools(server, collector, projectManager) {
5232
5682
  }
5233
5683
 
5234
5684
  // src/index.ts
5235
- var COLLECTOR_PORT2 = parseInt(process.env.RUNTIMESCOPE_PORT ?? "9090", 10);
5236
- var HTTP_PORT2 = parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
5685
+ var COLLECTOR_PORT3 = parseInt(process.env.RUNTIMESCOPE_PORT ?? "9090", 10);
5686
+ var HTTP_PORT3 = parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
5237
5687
  var BUFFER_SIZE = parseInt(process.env.RUNTIMESCOPE_BUFFER_SIZE ?? "10000", 10);
5238
5688
  function killStaleProcess(port) {
5239
5689
  try {
@@ -5279,8 +5729,8 @@ async function main() {
5279
5729
  if (redactor.isEnabled()) {
5280
5730
  console.error("[RuntimeScope] Payload redaction enabled");
5281
5731
  }
5282
- killStaleProcess(COLLECTOR_PORT2);
5283
- killStaleProcess(HTTP_PORT2);
5732
+ killStaleProcess(COLLECTOR_PORT3);
5733
+ killStaleProcess(HTTP_PORT3);
5284
5734
  const collector = new CollectorServer({
5285
5735
  bufferSize: BUFFER_SIZE,
5286
5736
  projectManager,
@@ -5288,7 +5738,7 @@ async function main() {
5288
5738
  rateLimits: globalConfig.rateLimits,
5289
5739
  tls: tlsConfig
5290
5740
  });
5291
- await collector.start({ port: COLLECTOR_PORT2, maxRetries: 5, retryDelayMs: 1e3 });
5741
+ await collector.start({ port: COLLECTOR_PORT3, maxRetries: 5, retryDelayMs: 1e3 });
5292
5742
  const store = collector.getStore();
5293
5743
  if (redactor.isEnabled()) {
5294
5744
  store.setRedactor(redactor);
@@ -5328,7 +5778,7 @@ async function main() {
5328
5778
  const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
5329
5779
  for (const projectName of projectManager.listProjects()) {
5330
5780
  const dbPath = projectManager.getProjectDbPath(projectName);
5331
- if (existsSync2(dbPath)) {
5781
+ if (existsSync3(dbPath)) {
5332
5782
  try {
5333
5783
  const tempStore = new SqliteStore({ dbPath });
5334
5784
  const deleted = tempStore.deleteOldEvents(cutoffMs);
@@ -5344,7 +5794,7 @@ async function main() {
5344
5794
  let pmStore;
5345
5795
  let discovery;
5346
5796
  if (isSqliteAvailable()) {
5347
- const pmDbPath = join2(projectManager.rootDir, "pm.db");
5797
+ const pmDbPath = join3(projectManager.rootDir, "pm.db");
5348
5798
  pmStore = new PmStore({ dbPath: pmDbPath });
5349
5799
  discovery = new ProjectDiscovery(pmStore, projectManager);
5350
5800
  discovery.discoverAll().then((result) => {
@@ -5363,7 +5813,7 @@ async function main() {
5363
5813
  getConnectedSessions: () => collector.getConnectedSessions()
5364
5814
  });
5365
5815
  try {
5366
- await httpServer.start({ port: HTTP_PORT2, tls: tlsConfig });
5816
+ await httpServer.start({ port: HTTP_PORT3, tls: tlsConfig });
5367
5817
  } catch (err) {
5368
5818
  console.error("[RuntimeScope] HTTP API failed to start:", err.message);
5369
5819
  }
@@ -5400,6 +5850,8 @@ async function main() {
5400
5850
  registerProcessMonitorTools(mcp, processMonitor);
5401
5851
  registerInfraTools(mcp, infraConnector);
5402
5852
  registerSessionDiffTools(mcp, sessionManager, collector, projectManager);
5853
+ registerQaCheckTools(mcp, store, sessionManager, collector, apiDiscovery);
5854
+ registerSetupTools(mcp, store, collector, projectManager);
5403
5855
  registerReconMetadataTools(mcp, store, collector);
5404
5856
  registerReconDesignTokenTools(mcp, store, collector);
5405
5857
  registerReconFontTools(mcp, store);
@@ -5416,9 +5868,9 @@ async function main() {
5416
5868
  const transport = new StdioServerTransport();
5417
5869
  await mcp.connect(transport);
5418
5870
  console.error("[RuntimeScope] MCP server running on stdio (v0.6.0 \u2014 46 tools)");
5419
- console.error(`[RuntimeScope] SDK snippet at http://127.0.0.1:${HTTP_PORT2}/snippet`);
5420
- console.error(`[RuntimeScope] SDK should connect to ws://127.0.0.1:${COLLECTOR_PORT2}`);
5421
- console.error(`[RuntimeScope] HTTP API at http://127.0.0.1:${HTTP_PORT2}`);
5871
+ console.error(`[RuntimeScope] SDK snippet at http://127.0.0.1:${HTTP_PORT3}/snippet`);
5872
+ console.error(`[RuntimeScope] SDK should connect to ws://127.0.0.1:${COLLECTOR_PORT3}`);
5873
+ console.error(`[RuntimeScope] HTTP API at http://127.0.0.1:${HTTP_PORT3}`);
5422
5874
  let shuttingDown = false;
5423
5875
  const shutdown = async () => {
5424
5876
  if (shuttingDown) return;