@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 +545 -93
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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/
|
|
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:
|
|
2297
|
-
force_refresh:
|
|
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
|
|
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:
|
|
2387
|
-
category:
|
|
2388
|
-
force_refresh:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
2545
|
-
max_depth:
|
|
2546
|
-
url:
|
|
2547
|
-
force_refresh:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
2840
|
-
properties:
|
|
2841
|
-
specific_properties:
|
|
2842
|
-
force_refresh:
|
|
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
|
|
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:
|
|
2961
|
-
depth:
|
|
2962
|
-
force_refresh:
|
|
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
|
|
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:
|
|
3055
|
-
url:
|
|
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
|
|
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:
|
|
3192
|
-
target_selector:
|
|
3193
|
-
properties:
|
|
3194
|
-
specific_properties:
|
|
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(
|
|
4260
|
-
catData = JSON.parse(
|
|
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
|
|
4453
|
-
import { getOrCreateProjectId } from "@runtimescope/collector";
|
|
4454
|
-
var
|
|
4455
|
-
var
|
|
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:
|
|
4462
|
-
framework:
|
|
4463
|
-
project_id:
|
|
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
|
-
|
|
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:${
|
|
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:${
|
|
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:${
|
|
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:${
|
|
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:${
|
|
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:${
|
|
4560
|
-
`WebSocket collector at ws://localhost:${
|
|
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:
|
|
4577
|
-
viewport_width:
|
|
4578
|
-
viewport_height:
|
|
4579
|
-
wait_for:
|
|
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
|
|
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:
|
|
4667
|
-
since_seconds:
|
|
4668
|
-
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:
|
|
4730
|
-
since_seconds:
|
|
4731
|
-
session_id:
|
|
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
|
|
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:
|
|
4990
|
-
session_id:
|
|
4991
|
-
before_timestamp:
|
|
4992
|
-
categories:
|
|
4993
|
-
level:
|
|
4994
|
-
limit:
|
|
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
|
|
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:
|
|
5099
|
-
project_id:
|
|
5100
|
-
event_types:
|
|
5101
|
-
since:
|
|
5102
|
-
until:
|
|
5103
|
-
session_id:
|
|
5104
|
-
limit:
|
|
5105
|
-
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
|
|
5236
|
-
var
|
|
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(
|
|
5283
|
-
killStaleProcess(
|
|
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:
|
|
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 (
|
|
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 =
|
|
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:
|
|
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:${
|
|
5420
|
-
console.error(`[RuntimeScope] SDK should connect to ws://127.0.0.1:${
|
|
5421
|
-
console.error(`[RuntimeScope] HTTP API at http://127.0.0.1:${
|
|
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;
|