@runtimescope/mcp-server 0.7.2 → 0.9.0
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 +640 -344
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { execSync as execSync2 } from "child_process";
|
|
5
4
|
import { existsSync as existsSync2 } from "fs";
|
|
6
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -23,24 +22,44 @@ import {
|
|
|
23
22
|
Redactor,
|
|
24
23
|
resolveTlsConfig,
|
|
25
24
|
PmStore,
|
|
26
|
-
ProjectDiscovery
|
|
25
|
+
ProjectDiscovery,
|
|
26
|
+
getPidsOnPort
|
|
27
27
|
} from "@runtimescope/collector";
|
|
28
28
|
|
|
29
29
|
// src/tools/network.ts
|
|
30
|
+
import { z as z2 } from "zod";
|
|
31
|
+
|
|
32
|
+
// src/tools/shared.ts
|
|
30
33
|
import { z } from "zod";
|
|
34
|
+
var projectIdParam = z.string().optional().describe(
|
|
35
|
+
"Scope to a specific project by its project ID (proj_xxx). Omit to query all sessions."
|
|
36
|
+
);
|
|
37
|
+
function resolveSessionContext(store, projectId) {
|
|
38
|
+
const all = store.getSessionInfo();
|
|
39
|
+
const sessions = projectId ? all.filter((s) => s.projectId === projectId) : all;
|
|
40
|
+
return {
|
|
41
|
+
sessions,
|
|
42
|
+
sessionId: sessions[0]?.sessionId ?? null,
|
|
43
|
+
projectId
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/tools/network.ts
|
|
31
48
|
function registerNetworkTools(server, store) {
|
|
32
49
|
server.tool(
|
|
33
50
|
"get_network_requests",
|
|
34
51
|
"Get captured network (fetch) requests from the running web app. Returns URL, method, status, timing, and optional GraphQL operation info.",
|
|
35
52
|
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
project_id: projectIdParam,
|
|
54
|
+
since_seconds: z2.number().optional().describe("Only return requests from the last N seconds"),
|
|
55
|
+
url_pattern: z2.string().optional().describe("Filter by URL substring match"),
|
|
56
|
+
status: z2.number().optional().describe("Filter by HTTP status code"),
|
|
57
|
+
method: z2.string().optional().describe("Filter by HTTP method (GET, POST, etc.)"),
|
|
58
|
+
limit: z2.number().optional().describe("Max results to return (default 200, max 1000)")
|
|
41
59
|
},
|
|
42
|
-
async ({ since_seconds, url_pattern, status, method, limit }) => {
|
|
60
|
+
async ({ project_id, since_seconds, url_pattern, status, method, limit }) => {
|
|
43
61
|
const allEvents = store.getNetworkRequests({
|
|
62
|
+
projectId: project_id,
|
|
44
63
|
sinceSeconds: since_seconds,
|
|
45
64
|
urlPattern: url_pattern,
|
|
46
65
|
status,
|
|
@@ -50,8 +69,7 @@ function registerNetworkTools(server, store) {
|
|
|
50
69
|
const truncated = allEvents.length > maxLimit;
|
|
51
70
|
const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
|
|
52
71
|
const timeRange = events.length > 0 ? { from: events[events.length - 1].timestamp, to: events[0].timestamp } : { from: 0, to: 0 };
|
|
53
|
-
const
|
|
54
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
72
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
55
73
|
const failedCount = events.filter((e) => e.status >= 400).length;
|
|
56
74
|
const avgDuration = events.length > 0 ? (events.reduce((s, e) => s + e.duration, 0) / events.length).toFixed(0) : "0";
|
|
57
75
|
const issues = [];
|
|
@@ -89,7 +107,7 @@ function registerNetworkTools(server, store) {
|
|
|
89
107
|
timestamp: new Date(e.timestamp).toISOString()
|
|
90
108
|
})),
|
|
91
109
|
issues,
|
|
92
|
-
metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId }
|
|
110
|
+
metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId, projectId: project_id ?? null }
|
|
93
111
|
};
|
|
94
112
|
return {
|
|
95
113
|
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
@@ -99,19 +117,21 @@ function registerNetworkTools(server, store) {
|
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
// src/tools/console.ts
|
|
102
|
-
import { z as
|
|
120
|
+
import { z as z3 } from "zod";
|
|
103
121
|
function registerConsoleTools(server, store) {
|
|
104
122
|
server.tool(
|
|
105
123
|
"get_console_messages",
|
|
106
124
|
"Get captured console messages (log, warn, error, info, debug, trace) from the running web app. Includes message text, args, and stack traces for errors.",
|
|
107
125
|
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
project_id: projectIdParam,
|
|
127
|
+
level: z3.enum(["log", "warn", "error", "info", "debug", "trace"]).optional().describe("Filter by console level"),
|
|
128
|
+
since_seconds: z3.number().optional().describe("Only return messages from the last N seconds"),
|
|
129
|
+
search: z3.string().optional().describe("Search message text (case-insensitive substring match)"),
|
|
130
|
+
limit: z3.number().optional().describe("Max results to return (default 200, max 1000)")
|
|
112
131
|
},
|
|
113
|
-
async ({ level, since_seconds, search, limit }) => {
|
|
132
|
+
async ({ project_id, level, since_seconds, search, limit }) => {
|
|
114
133
|
const allEvents = store.getConsoleMessages({
|
|
134
|
+
projectId: project_id,
|
|
115
135
|
level,
|
|
116
136
|
sinceSeconds: since_seconds,
|
|
117
137
|
search
|
|
@@ -120,8 +140,7 @@ function registerConsoleTools(server, store) {
|
|
|
120
140
|
const truncated = allEvents.length > maxLimit;
|
|
121
141
|
const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
|
|
122
142
|
const timeRange = events.length > 0 ? { from: events[events.length - 1].timestamp, to: events[0].timestamp } : { from: 0, to: 0 };
|
|
123
|
-
const
|
|
124
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
143
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
125
144
|
const levelCounts = {};
|
|
126
145
|
for (const e of events) {
|
|
127
146
|
levelCounts[e.level] = (levelCounts[e.level] || 0) + 1;
|
|
@@ -158,7 +177,7 @@ function registerConsoleTools(server, store) {
|
|
|
158
177
|
timestamp: new Date(e.timestamp).toISOString()
|
|
159
178
|
})),
|
|
160
179
|
issues,
|
|
161
|
-
metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId }
|
|
180
|
+
metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId, projectId: project_id ?? null }
|
|
162
181
|
};
|
|
163
182
|
return {
|
|
164
183
|
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
@@ -172,13 +191,16 @@ function registerSessionTools(server, store) {
|
|
|
172
191
|
server.tool(
|
|
173
192
|
"get_session_info",
|
|
174
193
|
"Get information about connected browser sessions and overall event statistics. Use this to check if the SDK is connected.",
|
|
175
|
-
{
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
{
|
|
195
|
+
project_id: projectIdParam
|
|
196
|
+
},
|
|
197
|
+
async ({ project_id }) => {
|
|
198
|
+
const { sessions, sessionId } = resolveSessionContext(store, project_id);
|
|
178
199
|
const response = {
|
|
179
200
|
summary: sessions.length > 0 ? `${sessions.length} session(s) connected. Total events captured: ${store.eventCount}.` : "No active sessions. Make sure the RuntimeScope SDK is injected in your app and connected to ws://localhost:9090.",
|
|
180
201
|
data: sessions.map((s) => ({
|
|
181
202
|
sessionId: s.sessionId,
|
|
203
|
+
projectId: s.projectId ?? null,
|
|
182
204
|
appName: s.appName,
|
|
183
205
|
sdkVersion: s.sdkVersion,
|
|
184
206
|
connectedAt: new Date(s.connectedAt).toISOString(),
|
|
@@ -189,7 +211,8 @@ function registerSessionTools(server, store) {
|
|
|
189
211
|
metadata: {
|
|
190
212
|
timeRange: { from: 0, to: Date.now() },
|
|
191
213
|
eventCount: store.eventCount,
|
|
192
|
-
sessionId
|
|
214
|
+
sessionId,
|
|
215
|
+
projectId: project_id ?? null
|
|
193
216
|
}
|
|
194
217
|
};
|
|
195
218
|
return {
|
|
@@ -221,18 +244,19 @@ function registerSessionTools(server, store) {
|
|
|
221
244
|
}
|
|
222
245
|
|
|
223
246
|
// src/tools/issues.ts
|
|
224
|
-
import { z as
|
|
247
|
+
import { z as z4 } from "zod";
|
|
225
248
|
import { detectIssues } from "@runtimescope/collector";
|
|
226
249
|
function registerIssueTools(server, store, apiDiscovery, processMonitor) {
|
|
227
250
|
server.tool(
|
|
228
251
|
"detect_issues",
|
|
229
252
|
"Run all pattern detectors against captured runtime data and return prioritized issues. Detects: failed requests, slow requests (>3s), N+1 request patterns, console error spam, high error rates, slow DB queries (>500ms), N+1 DB queries, API degradation, high latency endpoints, orphaned processes, and more. Use this as the first tool when investigating performance problems.",
|
|
230
253
|
{
|
|
231
|
-
|
|
232
|
-
|
|
254
|
+
project_id: projectIdParam,
|
|
255
|
+
since_seconds: z4.number().optional().describe("Analyze events from the last N seconds (default: all events)"),
|
|
256
|
+
severity_filter: z4.enum(["high", "medium", "low"]).optional().describe("Only return issues at this severity or above")
|
|
233
257
|
},
|
|
234
|
-
async ({ since_seconds, severity_filter }) => {
|
|
235
|
-
const events = store.getAllEvents(since_seconds);
|
|
258
|
+
async ({ project_id, since_seconds, severity_filter }) => {
|
|
259
|
+
const events = store.getAllEvents(since_seconds, void 0, project_id);
|
|
236
260
|
const allIssues = [...detectIssues(events)];
|
|
237
261
|
if (apiDiscovery) {
|
|
238
262
|
try {
|
|
@@ -252,8 +276,7 @@ function registerIssueTools(server, store, apiDiscovery, processMonitor) {
|
|
|
252
276
|
const issues = allIssues.filter(
|
|
253
277
|
(i) => severityOrder[i.severity] <= filterThreshold
|
|
254
278
|
);
|
|
255
|
-
const
|
|
256
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
279
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
257
280
|
const highCount = issues.filter((i) => i.severity === "high").length;
|
|
258
281
|
const mediumCount = issues.filter((i) => i.severity === "medium").length;
|
|
259
282
|
const lowCount = issues.filter((i) => i.severity === "low").length;
|
|
@@ -284,7 +307,8 @@ function registerIssueTools(server, store, apiDiscovery, processMonitor) {
|
|
|
284
307
|
to: events.length > 0 ? events[events.length - 1].timestamp : 0
|
|
285
308
|
},
|
|
286
309
|
eventCount: events.length,
|
|
287
|
-
sessionId
|
|
310
|
+
sessionId,
|
|
311
|
+
projectId: project_id ?? null
|
|
288
312
|
}
|
|
289
313
|
};
|
|
290
314
|
return {
|
|
@@ -295,26 +319,27 @@ function registerIssueTools(server, store, apiDiscovery, processMonitor) {
|
|
|
295
319
|
}
|
|
296
320
|
|
|
297
321
|
// src/tools/timeline.ts
|
|
298
|
-
import { z as
|
|
322
|
+
import { z as z5 } from "zod";
|
|
299
323
|
function registerTimelineTools(server, store) {
|
|
300
324
|
server.tool(
|
|
301
325
|
"get_event_timeline",
|
|
302
326
|
"Get a chronological view of ALL events (network requests, console messages) interleaved by timestamp. Essential for understanding causal chains \u2014 e.g. seeing that an API call failed, then an error was logged, then another retry fired. Events are in chronological order (oldest first).",
|
|
303
327
|
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
328
|
+
project_id: projectIdParam,
|
|
329
|
+
since_seconds: z5.number().optional().describe("Only return events from the last N seconds (default: 60)"),
|
|
330
|
+
event_types: z5.array(z5.enum(["network", "console", "session", "state", "render", "performance", "dom_snapshot", "database", "custom"])).optional().describe("Filter by event types (default: all)"),
|
|
331
|
+
limit: z5.number().optional().describe("Max events to return (default: 200, max: 1000)")
|
|
307
332
|
},
|
|
308
|
-
async ({ since_seconds, event_types, limit }) => {
|
|
333
|
+
async ({ project_id, since_seconds, event_types, limit }) => {
|
|
309
334
|
const sinceSeconds = since_seconds ?? 60;
|
|
310
335
|
const maxEvents = Math.min(limit ?? 200, 1e3);
|
|
311
336
|
const events = store.getEventTimeline({
|
|
337
|
+
projectId: project_id,
|
|
312
338
|
sinceSeconds,
|
|
313
339
|
eventTypes: event_types
|
|
314
340
|
});
|
|
315
341
|
const trimmed = events.length > maxEvents ? events.slice(events.length - maxEvents) : events;
|
|
316
|
-
const
|
|
317
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
342
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
318
343
|
const typeCounts = {};
|
|
319
344
|
for (const e of trimmed) {
|
|
320
345
|
typeCounts[e.eventType] = (typeCounts[e.eventType] || 0) + 1;
|
|
@@ -331,7 +356,8 @@ function registerTimelineTools(server, store) {
|
|
|
331
356
|
},
|
|
332
357
|
eventCount: trimmed.length,
|
|
333
358
|
totalInWindow: events.length,
|
|
334
|
-
sessionId
|
|
359
|
+
sessionId,
|
|
360
|
+
projectId: project_id ?? null
|
|
335
361
|
}
|
|
336
362
|
};
|
|
337
363
|
return {
|
|
@@ -439,26 +465,27 @@ function formatTimelineEvent(event) {
|
|
|
439
465
|
}
|
|
440
466
|
|
|
441
467
|
// src/tools/state.ts
|
|
442
|
-
import { z as
|
|
468
|
+
import { z as z6 } from "zod";
|
|
443
469
|
function registerStateTools(server, store) {
|
|
444
470
|
server.tool(
|
|
445
471
|
"get_state_snapshots",
|
|
446
472
|
"Get state store snapshots and diffs from Zustand or Redux stores. Shows state changes over time with action history, mutation frequency, and shallow diffs showing which keys changed.",
|
|
447
473
|
{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
474
|
+
project_id: projectIdParam,
|
|
475
|
+
store_name: z6.string().optional().describe("Filter by store name/ID"),
|
|
476
|
+
since_seconds: z6.number().optional().describe("Only return events from the last N seconds"),
|
|
477
|
+
limit: z6.number().optional().describe("Max results to return (default 200, max 1000)")
|
|
451
478
|
},
|
|
452
|
-
async ({ store_name, since_seconds, limit }) => {
|
|
479
|
+
async ({ project_id, store_name, since_seconds, limit }) => {
|
|
453
480
|
const allEvents = store.getStateEvents({
|
|
481
|
+
projectId: project_id,
|
|
454
482
|
storeId: store_name,
|
|
455
483
|
sinceSeconds: since_seconds
|
|
456
484
|
});
|
|
457
485
|
const maxLimit = Math.min(limit ?? 200, 1e3);
|
|
458
486
|
const truncated = allEvents.length > maxLimit;
|
|
459
487
|
const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
|
|
460
|
-
const
|
|
461
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
488
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
462
489
|
const issues = [];
|
|
463
490
|
const storeUpdates = /* @__PURE__ */ new Map();
|
|
464
491
|
for (const e of events) {
|
|
@@ -497,7 +524,8 @@ function registerStateTools(server, store) {
|
|
|
497
524
|
eventCount: events.length,
|
|
498
525
|
totalCount: allEvents.length,
|
|
499
526
|
truncated,
|
|
500
|
-
sessionId
|
|
527
|
+
sessionId,
|
|
528
|
+
projectId: project_id ?? null
|
|
501
529
|
}
|
|
502
530
|
};
|
|
503
531
|
return {
|
|
@@ -508,22 +536,23 @@ function registerStateTools(server, store) {
|
|
|
508
536
|
}
|
|
509
537
|
|
|
510
538
|
// src/tools/renders.ts
|
|
511
|
-
import { z as
|
|
539
|
+
import { z as z7 } from "zod";
|
|
512
540
|
function registerRenderTools(server, store) {
|
|
513
541
|
server.tool(
|
|
514
542
|
"get_render_profile",
|
|
515
543
|
"Get React component render profiles showing render counts, velocity, average duration, and render causes. Flags suspicious components that are re-rendering excessively. Requires captureRenders: true in the SDK config and React dev mode for accurate timing data.",
|
|
516
544
|
{
|
|
517
|
-
|
|
518
|
-
|
|
545
|
+
project_id: projectIdParam,
|
|
546
|
+
component_name: z7.string().optional().describe("Filter by component name (substring match)"),
|
|
547
|
+
since_seconds: z7.number().optional().describe("Only return events from the last N seconds")
|
|
519
548
|
},
|
|
520
|
-
async ({ component_name, since_seconds }) => {
|
|
549
|
+
async ({ project_id, component_name, since_seconds }) => {
|
|
521
550
|
const events = store.getRenderEvents({
|
|
551
|
+
projectId: project_id,
|
|
522
552
|
componentName: component_name,
|
|
523
553
|
sinceSeconds: since_seconds
|
|
524
554
|
});
|
|
525
|
-
const
|
|
526
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
555
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
527
556
|
const issues = [];
|
|
528
557
|
const merged = /* @__PURE__ */ new Map();
|
|
529
558
|
const allSuspicious = /* @__PURE__ */ new Set();
|
|
@@ -572,7 +601,8 @@ function registerRenderTools(server, store) {
|
|
|
572
601
|
to: events.length > 0 ? events[events.length - 1].timestamp : 0
|
|
573
602
|
},
|
|
574
603
|
eventCount: events.length,
|
|
575
|
-
sessionId
|
|
604
|
+
sessionId,
|
|
605
|
+
projectId: project_id ?? null
|
|
576
606
|
}
|
|
577
607
|
};
|
|
578
608
|
return {
|
|
@@ -583,7 +613,7 @@ function registerRenderTools(server, store) {
|
|
|
583
613
|
}
|
|
584
614
|
|
|
585
615
|
// src/tools/performance.ts
|
|
586
|
-
import { z as
|
|
616
|
+
import { z as z8 } from "zod";
|
|
587
617
|
var WEB_VITAL_METRICS = ["LCP", "FCP", "CLS", "TTFB", "FID", "INP"];
|
|
588
618
|
var SERVER_METRICS = [
|
|
589
619
|
"memory.rss",
|
|
@@ -609,12 +639,14 @@ function registerPerformanceTools(server, store) {
|
|
|
609
639
|
"get_performance_metrics",
|
|
610
640
|
"Get performance metrics from browser (Web Vitals: LCP, FCP, CLS, TTFB, FID, INP) and/or server (memory, event loop lag, GC pauses, CPU usage). Browser metrics include quality ratings. Server metrics require capturePerformance: true in the server-SDK config.",
|
|
611
641
|
{
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
642
|
+
project_id: projectIdParam,
|
|
643
|
+
metric_name: z8.enum(ALL_METRICS).optional().describe("Filter by specific metric name"),
|
|
644
|
+
source: z8.enum(["browser", "server", "all"]).optional().default("all").describe("Filter by metric source: browser (Web Vitals), server (Node.js runtime), or all"),
|
|
645
|
+
since_seconds: z8.number().optional().describe("Only return metrics from the last N seconds")
|
|
615
646
|
},
|
|
616
|
-
async ({ metric_name, source, since_seconds }) => {
|
|
647
|
+
async ({ project_id, metric_name, source, since_seconds }) => {
|
|
617
648
|
let events = store.getPerformanceMetrics({
|
|
649
|
+
projectId: project_id,
|
|
618
650
|
metricName: metric_name,
|
|
619
651
|
sinceSeconds: since_seconds
|
|
620
652
|
});
|
|
@@ -623,8 +655,7 @@ function registerPerformanceTools(server, store) {
|
|
|
623
655
|
} else if (source === "server") {
|
|
624
656
|
events = events.filter((e) => !isWebVital(e.metricName));
|
|
625
657
|
}
|
|
626
|
-
const
|
|
627
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
658
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
628
659
|
const issues = [];
|
|
629
660
|
const poor = events.filter((e) => e.rating === "poor");
|
|
630
661
|
const needsImprovement = events.filter((e) => e.rating === "needs-improvement");
|
|
@@ -676,7 +707,8 @@ function registerPerformanceTools(server, store) {
|
|
|
676
707
|
to: events.length > 0 ? events[events.length - 1].timestamp : 0
|
|
677
708
|
},
|
|
678
709
|
eventCount: events.length,
|
|
679
|
-
sessionId
|
|
710
|
+
sessionId,
|
|
711
|
+
projectId: project_id ?? null
|
|
680
712
|
}
|
|
681
713
|
};
|
|
682
714
|
return {
|
|
@@ -687,7 +719,7 @@ function registerPerformanceTools(server, store) {
|
|
|
687
719
|
}
|
|
688
720
|
|
|
689
721
|
// src/tools/dom-snapshot.ts
|
|
690
|
-
import { z as
|
|
722
|
+
import { z as z9 } from "zod";
|
|
691
723
|
function generateRequestId() {
|
|
692
724
|
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
693
725
|
}
|
|
@@ -696,10 +728,11 @@ function registerDomSnapshotTools(server, store, collector) {
|
|
|
696
728
|
"get_dom_snapshot",
|
|
697
729
|
"Capture a live DOM snapshot from the running web app. Sends a command to the SDK which serializes document.documentElement.outerHTML and returns it along with the current URL, viewport dimensions, scroll position, and element count. Useful for understanding what the user sees.",
|
|
698
730
|
{
|
|
699
|
-
|
|
731
|
+
project_id: projectIdParam,
|
|
732
|
+
max_size: z9.number().optional().describe("Maximum HTML size in bytes (default: 500000). Larger pages will be truncated.")
|
|
700
733
|
},
|
|
701
|
-
async ({ max_size }) => {
|
|
702
|
-
const sessions = store
|
|
734
|
+
async ({ project_id, max_size }) => {
|
|
735
|
+
const { sessions, sessionId: resolvedSessionId } = resolveSessionContext(store, project_id);
|
|
703
736
|
const sessionId = collector.getFirstSessionId();
|
|
704
737
|
const activeSession = sessions[0] ?? null;
|
|
705
738
|
if (!sessionId || !activeSession?.isConnected) {
|
|
@@ -736,7 +769,8 @@ function registerDomSnapshotTools(server, store, collector) {
|
|
|
736
769
|
metadata: {
|
|
737
770
|
timeRange: { from: Date.now(), to: Date.now() },
|
|
738
771
|
eventCount: 1,
|
|
739
|
-
sessionId
|
|
772
|
+
sessionId,
|
|
773
|
+
projectId: project_id ?? null
|
|
740
774
|
}
|
|
741
775
|
};
|
|
742
776
|
return {
|
|
@@ -751,7 +785,7 @@ function registerDomSnapshotTools(server, store, collector) {
|
|
|
751
785
|
summary: `Failed to capture DOM snapshot: ${errorMsg}`,
|
|
752
786
|
data: null,
|
|
753
787
|
issues: [errorMsg],
|
|
754
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
788
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
755
789
|
}, null, 2)
|
|
756
790
|
}]
|
|
757
791
|
};
|
|
@@ -761,24 +795,25 @@ function registerDomSnapshotTools(server, store, collector) {
|
|
|
761
795
|
}
|
|
762
796
|
|
|
763
797
|
// src/tools/har.ts
|
|
764
|
-
import { z as
|
|
798
|
+
import { z as z10 } from "zod";
|
|
765
799
|
function registerHarTools(server, store) {
|
|
766
800
|
server.tool(
|
|
767
801
|
"capture_har",
|
|
768
802
|
"Export captured network requests as a HAR (HTTP Archive) 1.2 JSON file. This is the standard format used by browser DevTools, Charles Proxy, and other tools. Includes request/response headers, body content (if captureBody was enabled in the SDK), and timing data.",
|
|
769
803
|
{
|
|
770
|
-
|
|
771
|
-
|
|
804
|
+
project_id: projectIdParam,
|
|
805
|
+
since_seconds: z10.number().optional().describe("Only include requests from the last N seconds"),
|
|
806
|
+
limit: z10.number().optional().describe("Max entries to include (default 200, max 1000)")
|
|
772
807
|
},
|
|
773
|
-
async ({ since_seconds, limit }) => {
|
|
808
|
+
async ({ project_id, since_seconds, limit }) => {
|
|
774
809
|
const allEvents = store.getNetworkRequests({
|
|
810
|
+
projectId: project_id,
|
|
775
811
|
sinceSeconds: since_seconds
|
|
776
812
|
});
|
|
777
813
|
const maxLimit = Math.min(limit ?? 200, 1e3);
|
|
778
814
|
const truncated = allEvents.length > maxLimit;
|
|
779
815
|
const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
|
|
780
|
-
const
|
|
781
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
816
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
782
817
|
const har = buildHar(events);
|
|
783
818
|
const response = {
|
|
784
819
|
summary: `HAR export: ${events.length} request(s)${truncated ? ` (showing ${maxLimit} of ${allEvents.length})` : ""}${since_seconds ? ` from the last ${since_seconds}s` : ""}. Import into Chrome DevTools or any HAR viewer.`,
|
|
@@ -792,7 +827,8 @@ function registerHarTools(server, store) {
|
|
|
792
827
|
eventCount: events.length,
|
|
793
828
|
totalCount: allEvents.length,
|
|
794
829
|
truncated,
|
|
795
|
-
sessionId
|
|
830
|
+
sessionId,
|
|
831
|
+
projectId: project_id ?? null
|
|
796
832
|
}
|
|
797
833
|
};
|
|
798
834
|
return {
|
|
@@ -900,25 +936,26 @@ function statusText(status) {
|
|
|
900
936
|
}
|
|
901
937
|
|
|
902
938
|
// src/tools/errors.ts
|
|
903
|
-
import { z as
|
|
939
|
+
import { z as z11 } from "zod";
|
|
904
940
|
function registerErrorTools(server, store) {
|
|
905
941
|
server.tool(
|
|
906
942
|
"get_errors_with_source_context",
|
|
907
943
|
"Get console errors with parsed stack traces and surrounding source code lines. Fetches the actual source files from the dev server (e.g. http://localhost:3000/src/...) and shows the lines around the error. This gives you the same context as clicking a stack frame in DevTools.",
|
|
908
944
|
{
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
945
|
+
project_id: projectIdParam,
|
|
946
|
+
since_seconds: z11.number().optional().describe("Only return errors from the last N seconds"),
|
|
947
|
+
fetch_source: z11.boolean().optional().describe("Whether to fetch source files for context (default: true). Set false for faster results."),
|
|
948
|
+
context_lines: z11.number().optional().describe("Number of source lines to show above and below the error line (default: 5)")
|
|
912
949
|
},
|
|
913
|
-
async ({ since_seconds, fetch_source, context_lines }) => {
|
|
950
|
+
async ({ project_id, since_seconds, fetch_source, context_lines }) => {
|
|
914
951
|
const shouldFetch = fetch_source !== false;
|
|
915
952
|
const contextSize = context_lines ?? 5;
|
|
916
953
|
const events = store.getConsoleMessages({
|
|
954
|
+
projectId: project_id,
|
|
917
955
|
level: "error",
|
|
918
956
|
sinceSeconds: since_seconds
|
|
919
957
|
});
|
|
920
|
-
const
|
|
921
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
958
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
922
959
|
const limited = events.slice(0, 50);
|
|
923
960
|
const sourceCache = /* @__PURE__ */ new Map();
|
|
924
961
|
const errors = [];
|
|
@@ -958,7 +995,8 @@ function registerErrorTools(server, store) {
|
|
|
958
995
|
to: limited.length > 0 ? limited[limited.length - 1].timestamp : 0
|
|
959
996
|
},
|
|
960
997
|
eventCount: limited.length,
|
|
961
|
-
sessionId
|
|
998
|
+
sessionId,
|
|
999
|
+
projectId: project_id ?? null
|
|
962
1000
|
}
|
|
963
1001
|
};
|
|
964
1002
|
return {
|
|
@@ -1021,20 +1059,20 @@ function extractContext(source, targetLine, contextSize) {
|
|
|
1021
1059
|
}
|
|
1022
1060
|
|
|
1023
1061
|
// src/tools/api-discovery.ts
|
|
1024
|
-
import { z as
|
|
1062
|
+
import { z as z12 } from "zod";
|
|
1025
1063
|
function registerApiDiscoveryTools(server, store, engine) {
|
|
1026
1064
|
server.tool(
|
|
1027
1065
|
"get_api_catalog",
|
|
1028
1066
|
"Discover all API endpoints the app is communicating with, auto-grouped by service. Shows normalized paths, call counts, auth patterns, and inferred response shapes.",
|
|
1029
1067
|
{
|
|
1030
|
-
|
|
1031
|
-
|
|
1068
|
+
project_id: projectIdParam,
|
|
1069
|
+
service: z12.string().optional().describe('Filter by service name (e.g. "Supabase", "Your API")'),
|
|
1070
|
+
min_calls: z12.number().optional().describe("Only show endpoints with at least N calls")
|
|
1032
1071
|
},
|
|
1033
|
-
async ({ service, min_calls }) => {
|
|
1072
|
+
async ({ project_id, service, min_calls }) => {
|
|
1034
1073
|
const catalog = engine.getCatalog({ service, minCalls: min_calls });
|
|
1035
1074
|
const services = engine.getServiceMap();
|
|
1036
|
-
const
|
|
1037
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1075
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1038
1076
|
const response = {
|
|
1039
1077
|
summary: `Discovered ${catalog.length} API endpoint(s) across ${services.length} service(s).`,
|
|
1040
1078
|
data: {
|
|
@@ -1064,7 +1102,8 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1064
1102
|
metadata: {
|
|
1065
1103
|
timeRange: catalog.length > 0 ? { from: Math.min(...catalog.map((e) => e.firstSeen)), to: Math.max(...catalog.map((e) => e.lastSeen)) } : { from: 0, to: 0 },
|
|
1066
1104
|
eventCount: catalog.reduce((s, e) => s + e.callCount, 0),
|
|
1067
|
-
sessionId
|
|
1105
|
+
sessionId,
|
|
1106
|
+
projectId: project_id ?? null
|
|
1068
1107
|
}
|
|
1069
1108
|
};
|
|
1070
1109
|
return {
|
|
@@ -1076,13 +1115,13 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1076
1115
|
"get_api_health",
|
|
1077
1116
|
"Get health metrics for discovered API endpoints: success rate, latency percentiles (p50/p95), error rates and error codes.",
|
|
1078
1117
|
{
|
|
1079
|
-
|
|
1080
|
-
|
|
1118
|
+
project_id: projectIdParam,
|
|
1119
|
+
endpoint: z12.string().optional().describe("Filter by endpoint path substring"),
|
|
1120
|
+
since_seconds: z12.number().optional().describe("Only consider requests from the last N seconds")
|
|
1081
1121
|
},
|
|
1082
|
-
async ({ endpoint, since_seconds }) => {
|
|
1122
|
+
async ({ project_id, endpoint, since_seconds }) => {
|
|
1083
1123
|
const health = engine.getHealth({ endpoint, sinceSeconds: since_seconds });
|
|
1084
|
-
const
|
|
1085
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1124
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1086
1125
|
const issues = [];
|
|
1087
1126
|
for (const ep of health) {
|
|
1088
1127
|
if (ep.errorRate > 0.5) issues.push(`${ep.method} ${ep.normalizedPath}: ${(ep.errorRate * 100).toFixed(0)}% error rate`);
|
|
@@ -1106,7 +1145,8 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1106
1145
|
metadata: {
|
|
1107
1146
|
timeRange: { from: 0, to: Date.now() },
|
|
1108
1147
|
eventCount: health.reduce((s, e) => s + e.callCount, 0),
|
|
1109
|
-
sessionId
|
|
1148
|
+
sessionId,
|
|
1149
|
+
projectId: project_id ?? null
|
|
1110
1150
|
}
|
|
1111
1151
|
};
|
|
1112
1152
|
return {
|
|
@@ -1118,9 +1158,10 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1118
1158
|
"get_api_documentation",
|
|
1119
1159
|
"Generate API documentation from observed network traffic. Shows endpoints, auth, latency, and inferred response shapes in markdown format.",
|
|
1120
1160
|
{
|
|
1121
|
-
|
|
1161
|
+
project_id: projectIdParam,
|
|
1162
|
+
service: z12.string().optional().describe("Generate docs for a specific service only")
|
|
1122
1163
|
},
|
|
1123
|
-
async ({ service }) => {
|
|
1164
|
+
async ({ project_id, service }) => {
|
|
1124
1165
|
const docs = engine.getDocumentation({ service });
|
|
1125
1166
|
return {
|
|
1126
1167
|
content: [{ type: "text", text: docs }]
|
|
@@ -1130,11 +1171,12 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1130
1171
|
server.tool(
|
|
1131
1172
|
"get_service_map",
|
|
1132
1173
|
"Get a topology map of all external services the app communicates with, including detected platforms (Supabase, Vercel, Stripe, etc.), call counts, and latency.",
|
|
1133
|
-
{
|
|
1134
|
-
|
|
1174
|
+
{
|
|
1175
|
+
project_id: projectIdParam
|
|
1176
|
+
},
|
|
1177
|
+
async ({ project_id }) => {
|
|
1135
1178
|
const services = engine.getServiceMap();
|
|
1136
|
-
const
|
|
1137
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1179
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1138
1180
|
const response = {
|
|
1139
1181
|
summary: `${services.length} service(s) detected from network traffic.`,
|
|
1140
1182
|
data: services.map((s) => ({
|
|
@@ -1151,7 +1193,8 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1151
1193
|
metadata: {
|
|
1152
1194
|
timeRange: { from: 0, to: Date.now() },
|
|
1153
1195
|
eventCount: services.reduce((s, e) => s + e.totalCalls, 0),
|
|
1154
|
-
sessionId
|
|
1196
|
+
sessionId,
|
|
1197
|
+
projectId: project_id ?? null
|
|
1155
1198
|
}
|
|
1156
1199
|
};
|
|
1157
1200
|
return {
|
|
@@ -1163,13 +1206,13 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1163
1206
|
"get_api_changes",
|
|
1164
1207
|
"Compare API endpoints between two sessions. Detects added/removed endpoints and response shape changes.",
|
|
1165
1208
|
{
|
|
1166
|
-
|
|
1167
|
-
|
|
1209
|
+
project_id: projectIdParam,
|
|
1210
|
+
session_a: z12.string().describe("First session ID"),
|
|
1211
|
+
session_b: z12.string().describe("Second session ID")
|
|
1168
1212
|
},
|
|
1169
|
-
async ({ session_a, session_b }) => {
|
|
1213
|
+
async ({ project_id, session_a, session_b }) => {
|
|
1170
1214
|
const changes = engine.getApiChanges(session_a, session_b);
|
|
1171
|
-
const
|
|
1172
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1215
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1173
1216
|
const added = changes.filter((c) => c.changeType === "added").length;
|
|
1174
1217
|
const removed = changes.filter((c) => c.changeType === "removed").length;
|
|
1175
1218
|
const modified = changes.filter((c) => c.changeType === "modified").length;
|
|
@@ -1180,7 +1223,8 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1180
1223
|
metadata: {
|
|
1181
1224
|
timeRange: { from: 0, to: Date.now() },
|
|
1182
1225
|
eventCount: changes.length,
|
|
1183
|
-
sessionId
|
|
1226
|
+
sessionId,
|
|
1227
|
+
projectId: project_id ?? null
|
|
1184
1228
|
}
|
|
1185
1229
|
};
|
|
1186
1230
|
return {
|
|
@@ -1191,7 +1235,7 @@ function registerApiDiscoveryTools(server, store, engine) {
|
|
|
1191
1235
|
}
|
|
1192
1236
|
|
|
1193
1237
|
// src/tools/database.ts
|
|
1194
|
-
import { z as
|
|
1238
|
+
import { z as z13 } from "zod";
|
|
1195
1239
|
import {
|
|
1196
1240
|
aggregateQueryStats,
|
|
1197
1241
|
detectN1Queries,
|
|
@@ -1203,20 +1247,21 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1203
1247
|
"get_query_log",
|
|
1204
1248
|
"Get captured database queries with SQL, timing, rows returned, and source ORM. Requires server-side SDK instrumentation.",
|
|
1205
1249
|
{
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1250
|
+
project_id: projectIdParam,
|
|
1251
|
+
since_seconds: z13.number().optional().describe("Only return queries from the last N seconds"),
|
|
1252
|
+
table: z13.string().optional().describe("Filter by table name"),
|
|
1253
|
+
min_duration_ms: z13.number().optional().describe("Only return queries slower than N ms"),
|
|
1254
|
+
search: z13.string().optional().describe("Search query text")
|
|
1210
1255
|
},
|
|
1211
|
-
async ({ since_seconds, table, min_duration_ms, search }) => {
|
|
1256
|
+
async ({ project_id, since_seconds, table, min_duration_ms, search }) => {
|
|
1212
1257
|
const events = store.getDatabaseEvents({
|
|
1258
|
+
projectId: project_id,
|
|
1213
1259
|
sinceSeconds: since_seconds,
|
|
1214
1260
|
table,
|
|
1215
1261
|
minDurationMs: min_duration_ms,
|
|
1216
1262
|
search
|
|
1217
1263
|
});
|
|
1218
|
-
const
|
|
1219
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1264
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1220
1265
|
const totalDuration = events.reduce((s, e) => s + e.duration, 0);
|
|
1221
1266
|
const avgDuration = events.length > 0 ? totalDuration / events.length : 0;
|
|
1222
1267
|
const errorCount = events.filter((e) => e.error).length;
|
|
@@ -1243,7 +1288,8 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1243
1288
|
metadata: {
|
|
1244
1289
|
timeRange: events.length > 0 ? { from: events[0].timestamp, to: events[events.length - 1].timestamp } : { from: 0, to: 0 },
|
|
1245
1290
|
eventCount: events.length,
|
|
1246
|
-
sessionId
|
|
1291
|
+
sessionId,
|
|
1292
|
+
projectId: project_id ?? null
|
|
1247
1293
|
}
|
|
1248
1294
|
};
|
|
1249
1295
|
return {
|
|
@@ -1255,15 +1301,15 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1255
1301
|
"get_query_performance",
|
|
1256
1302
|
"Get aggregated database query performance stats: avg/max/p95 duration, call counts, N+1 detection, and slow query analysis.",
|
|
1257
1303
|
{
|
|
1258
|
-
|
|
1304
|
+
project_id: projectIdParam,
|
|
1305
|
+
since_seconds: z13.number().optional().describe("Analyze queries from the last N seconds")
|
|
1259
1306
|
},
|
|
1260
|
-
async ({ since_seconds }) => {
|
|
1261
|
-
const events = store.getDatabaseEvents({ sinceSeconds: since_seconds });
|
|
1307
|
+
async ({ project_id, since_seconds }) => {
|
|
1308
|
+
const events = store.getDatabaseEvents({ projectId: project_id, sinceSeconds: since_seconds });
|
|
1262
1309
|
const stats = aggregateQueryStats(events);
|
|
1263
1310
|
const n1Issues = detectN1Queries(events);
|
|
1264
1311
|
const slowIssues = detectSlowQueries(events);
|
|
1265
|
-
const
|
|
1266
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1312
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1267
1313
|
const issues = [
|
|
1268
1314
|
...n1Issues.map((i) => i.title),
|
|
1269
1315
|
...slowIssues.map((i) => i.title)
|
|
@@ -1288,7 +1334,8 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1288
1334
|
metadata: {
|
|
1289
1335
|
timeRange: { from: 0, to: Date.now() },
|
|
1290
1336
|
eventCount: events.length,
|
|
1291
|
-
sessionId
|
|
1337
|
+
sessionId,
|
|
1338
|
+
projectId: project_id ?? null
|
|
1292
1339
|
}
|
|
1293
1340
|
};
|
|
1294
1341
|
return {
|
|
@@ -1300,8 +1347,8 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1300
1347
|
"get_schema_map",
|
|
1301
1348
|
"Get the full database schema: tables, columns, types, foreign keys, and indexes. Requires a configured database connection.",
|
|
1302
1349
|
{
|
|
1303
|
-
connection_id:
|
|
1304
|
-
table:
|
|
1350
|
+
connection_id: z13.string().optional().describe("Connection ID (defaults to first available)"),
|
|
1351
|
+
table: z13.string().optional().describe("Introspect a specific table only")
|
|
1305
1352
|
},
|
|
1306
1353
|
async ({ connection_id, table }) => {
|
|
1307
1354
|
const connections = connectionManager.listConnections();
|
|
@@ -1371,12 +1418,12 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1371
1418
|
"get_table_data",
|
|
1372
1419
|
"Read rows from a database table with pagination. Requires a configured database connection.",
|
|
1373
1420
|
{
|
|
1374
|
-
table:
|
|
1375
|
-
connection_id:
|
|
1376
|
-
limit:
|
|
1377
|
-
offset:
|
|
1378
|
-
where:
|
|
1379
|
-
order_by:
|
|
1421
|
+
table: z13.string().describe("Table name to read"),
|
|
1422
|
+
connection_id: z13.string().optional().describe("Connection ID"),
|
|
1423
|
+
limit: z13.number().optional().describe("Max rows (default 50, max 1000)"),
|
|
1424
|
+
offset: z13.number().optional().describe("Pagination offset"),
|
|
1425
|
+
where: z13.string().optional().describe("SQL WHERE clause (without WHERE keyword)"),
|
|
1426
|
+
order_by: z13.string().optional().describe("SQL ORDER BY clause (without ORDER BY keyword)")
|
|
1380
1427
|
},
|
|
1381
1428
|
async ({ table, connection_id, limit, offset, where, order_by }) => {
|
|
1382
1429
|
const connections = connectionManager.listConnections();
|
|
@@ -1429,11 +1476,11 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1429
1476
|
"modify_table_data",
|
|
1430
1477
|
"Insert, update, or delete rows in a LOCAL DEV database. Safety guarded: localhost only, WHERE required for update/delete, max 100 affected rows, wrapped in transaction.",
|
|
1431
1478
|
{
|
|
1432
|
-
table:
|
|
1433
|
-
operation:
|
|
1434
|
-
connection_id:
|
|
1435
|
-
data:
|
|
1436
|
-
where:
|
|
1479
|
+
table: z13.string().describe("Table name"),
|
|
1480
|
+
operation: z13.enum(["insert", "update", "delete"]).describe("Operation type"),
|
|
1481
|
+
connection_id: z13.string().optional().describe("Connection ID"),
|
|
1482
|
+
data: z13.record(z13.unknown()).optional().describe("Row data (for insert/update)"),
|
|
1483
|
+
where: z13.string().optional().describe("WHERE clause (required for update/delete)")
|
|
1437
1484
|
},
|
|
1438
1485
|
async ({ table, operation, connection_id, data, where }) => {
|
|
1439
1486
|
const connections = connectionManager.listConnections();
|
|
@@ -1512,13 +1559,13 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1512
1559
|
"suggest_indexes",
|
|
1513
1560
|
"Analyze captured database queries and suggest missing indexes based on WHERE/ORDER BY columns and query performance.",
|
|
1514
1561
|
{
|
|
1515
|
-
|
|
1562
|
+
project_id: projectIdParam,
|
|
1563
|
+
since_seconds: z13.number().optional().describe("Analyze queries from the last N seconds")
|
|
1516
1564
|
},
|
|
1517
|
-
async ({ since_seconds }) => {
|
|
1518
|
-
const events = store.getDatabaseEvents({ sinceSeconds: since_seconds });
|
|
1565
|
+
async ({ project_id, since_seconds }) => {
|
|
1566
|
+
const events = store.getDatabaseEvents({ projectId: project_id, sinceSeconds: since_seconds });
|
|
1519
1567
|
const suggestions = suggestIndexes(events);
|
|
1520
|
-
const
|
|
1521
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
1568
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
1522
1569
|
const response = {
|
|
1523
1570
|
summary: `${suggestions.length} index suggestion(s) based on ${events.length} captured queries.`,
|
|
1524
1571
|
data: suggestions.map((s) => ({
|
|
@@ -1533,7 +1580,8 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1533
1580
|
metadata: {
|
|
1534
1581
|
timeRange: { from: 0, to: Date.now() },
|
|
1535
1582
|
eventCount: events.length,
|
|
1536
|
-
sessionId
|
|
1583
|
+
sessionId,
|
|
1584
|
+
projectId: project_id ?? null
|
|
1537
1585
|
}
|
|
1538
1586
|
};
|
|
1539
1587
|
return {
|
|
@@ -1544,7 +1592,7 @@ function registerDatabaseTools(server, store, connectionManager, schemaIntrospec
|
|
|
1544
1592
|
}
|
|
1545
1593
|
|
|
1546
1594
|
// src/tools/process-monitor.ts
|
|
1547
|
-
import { z as
|
|
1595
|
+
import { z as z14 } from "zod";
|
|
1548
1596
|
import { execSync, spawn } from "child_process";
|
|
1549
1597
|
import { existsSync, statSync, rmSync } from "fs";
|
|
1550
1598
|
import { join } from "path";
|
|
@@ -1553,8 +1601,8 @@ function registerProcessMonitorTools(server, processMonitor) {
|
|
|
1553
1601
|
"get_dev_processes",
|
|
1554
1602
|
"List all running dev processes (Next.js, Vite, Prisma, Docker, databases, etc.) with PID, port, memory, and CPU usage.",
|
|
1555
1603
|
{
|
|
1556
|
-
type:
|
|
1557
|
-
project:
|
|
1604
|
+
type: z14.string().optional().describe("Filter by process type (next, vite, docker, postgres, etc.)"),
|
|
1605
|
+
project: z14.string().optional().describe("Filter by project name")
|
|
1558
1606
|
},
|
|
1559
1607
|
async ({ type, project }) => {
|
|
1560
1608
|
processMonitor.scan();
|
|
@@ -1592,8 +1640,8 @@ function registerProcessMonitorTools(server, processMonitor) {
|
|
|
1592
1640
|
"kill_process",
|
|
1593
1641
|
"Terminate a dev process by PID. Default signal is SIGTERM; use SIGKILL for force kill.",
|
|
1594
1642
|
{
|
|
1595
|
-
pid:
|
|
1596
|
-
signal:
|
|
1643
|
+
pid: z14.number().describe("Process ID to kill"),
|
|
1644
|
+
signal: z14.enum(["SIGTERM", "SIGKILL"]).optional().describe("Signal to send (default: SIGTERM)")
|
|
1597
1645
|
},
|
|
1598
1646
|
async ({ pid, signal }) => {
|
|
1599
1647
|
if (pid < 2 || pid === process.pid) {
|
|
@@ -1626,7 +1674,7 @@ function registerProcessMonitorTools(server, processMonitor) {
|
|
|
1626
1674
|
"get_port_usage",
|
|
1627
1675
|
"Show which dev processes are bound to which ports. Useful for debugging port conflicts.",
|
|
1628
1676
|
{
|
|
1629
|
-
port:
|
|
1677
|
+
port: z14.number().optional().describe("Filter by specific port number")
|
|
1630
1678
|
},
|
|
1631
1679
|
async ({ port }) => {
|
|
1632
1680
|
processMonitor.scan();
|
|
@@ -1656,8 +1704,8 @@ function registerProcessMonitorTools(server, processMonitor) {
|
|
|
1656
1704
|
"purge_caches",
|
|
1657
1705
|
"Delete common build/dev cache directories (.next/cache, node_modules/.cache, .vite, .turbo, .swc, .parcel-cache, etc.) for a project directory. Reports size freed per cache.",
|
|
1658
1706
|
{
|
|
1659
|
-
directory:
|
|
1660
|
-
dryRun:
|
|
1707
|
+
directory: z14.string().describe("Absolute path to the project directory"),
|
|
1708
|
+
dryRun: z14.boolean().optional().describe("If true, report what would be deleted without actually deleting (default: false)")
|
|
1661
1709
|
},
|
|
1662
1710
|
async ({ directory, dryRun }) => {
|
|
1663
1711
|
const CACHE_TARGETS = [
|
|
@@ -1714,10 +1762,10 @@ function registerProcessMonitorTools(server, processMonitor) {
|
|
|
1714
1762
|
"restart_dev_server",
|
|
1715
1763
|
"Kill a dev server process, purge build caches in its working directory, and restart it with the same or a custom command. Combines kill_process + purge_caches + spawn into one operation.",
|
|
1716
1764
|
{
|
|
1717
|
-
pid:
|
|
1718
|
-
command:
|
|
1719
|
-
skipCachePurge:
|
|
1720
|
-
signal:
|
|
1765
|
+
pid: z14.number().describe("PID of the dev server process to restart"),
|
|
1766
|
+
command: z14.string().optional().describe('Custom start command (e.g. "npm run dev"). If omitted, infers from process type.'),
|
|
1767
|
+
skipCachePurge: z14.boolean().optional().describe("If true, skip cache purging (default: false)"),
|
|
1768
|
+
signal: z14.enum(["SIGTERM", "SIGKILL"]).optional().describe("Kill signal (default: SIGTERM)")
|
|
1721
1769
|
},
|
|
1722
1770
|
async ({ pid, command, skipCachePurge, signal }) => {
|
|
1723
1771
|
if (pid < 2 || pid === process.pid) {
|
|
@@ -1867,15 +1915,15 @@ function inferStartCommand(type, rawCommand) {
|
|
|
1867
1915
|
}
|
|
1868
1916
|
|
|
1869
1917
|
// src/tools/infra-connector.ts
|
|
1870
|
-
import { z as
|
|
1918
|
+
import { z as z15 } from "zod";
|
|
1871
1919
|
function registerInfraTools(server, infraConnector) {
|
|
1872
1920
|
server.tool(
|
|
1873
1921
|
"get_deploy_logs",
|
|
1874
1922
|
"Get deployment history from connected platforms (Vercel, Cloudflare, Railway). Shows build status, branch, commit, and timing.",
|
|
1875
1923
|
{
|
|
1876
|
-
project:
|
|
1877
|
-
platform:
|
|
1878
|
-
deploy_id:
|
|
1924
|
+
project: z15.string().optional().describe("Project name"),
|
|
1925
|
+
platform: z15.string().optional().describe("Filter by platform (vercel, cloudflare, railway)"),
|
|
1926
|
+
deploy_id: z15.string().optional().describe("Get details for a specific deployment")
|
|
1879
1927
|
},
|
|
1880
1928
|
async ({ project, platform, deploy_id }) => {
|
|
1881
1929
|
const logs = await infraConnector.getDeployLogs(project ?? "default", platform, deploy_id);
|
|
@@ -1908,10 +1956,10 @@ function registerInfraTools(server, infraConnector) {
|
|
|
1908
1956
|
"get_runtime_logs",
|
|
1909
1957
|
"Get runtime error/info logs from connected deployment platforms.",
|
|
1910
1958
|
{
|
|
1911
|
-
project:
|
|
1912
|
-
platform:
|
|
1913
|
-
level:
|
|
1914
|
-
since_seconds:
|
|
1959
|
+
project: z15.string().optional().describe("Project name"),
|
|
1960
|
+
platform: z15.string().optional().describe("Filter by platform"),
|
|
1961
|
+
level: z15.string().optional().describe("Filter by log level (info, warn, error)"),
|
|
1962
|
+
since_seconds: z15.number().optional().describe("Only return logs from the last N seconds")
|
|
1915
1963
|
},
|
|
1916
1964
|
async ({ project, platform, level, since_seconds }) => {
|
|
1917
1965
|
const since = since_seconds ? Date.now() - since_seconds * 1e3 : void 0;
|
|
@@ -1941,7 +1989,7 @@ function registerInfraTools(server, infraConnector) {
|
|
|
1941
1989
|
"get_build_status",
|
|
1942
1990
|
"Get the current deployment status for each connected platform.",
|
|
1943
1991
|
{
|
|
1944
|
-
project:
|
|
1992
|
+
project: z15.string().optional().describe("Project name")
|
|
1945
1993
|
},
|
|
1946
1994
|
async ({ project }) => {
|
|
1947
1995
|
const statuses = await infraConnector.getBuildStatus(project ?? "default");
|
|
@@ -1971,7 +2019,7 @@ function registerInfraTools(server, infraConnector) {
|
|
|
1971
2019
|
"get_infra_overview",
|
|
1972
2020
|
"Overview of which platforms a project uses, combining explicit configuration with auto-detection from network traffic.",
|
|
1973
2021
|
{
|
|
1974
|
-
project:
|
|
2022
|
+
project: z15.string().optional().describe("Project name")
|
|
1975
2023
|
},
|
|
1976
2024
|
async ({ project }) => {
|
|
1977
2025
|
const overview = infraConnector.getInfraOverview(project);
|
|
@@ -1993,18 +2041,19 @@ function registerInfraTools(server, infraConnector) {
|
|
|
1993
2041
|
}
|
|
1994
2042
|
|
|
1995
2043
|
// src/tools/session-diff.ts
|
|
1996
|
-
import { z as
|
|
2044
|
+
import { z as z16 } from "zod";
|
|
1997
2045
|
import { compareSessions } from "@runtimescope/collector";
|
|
1998
|
-
function registerSessionDiffTools(server, sessionManager, collector) {
|
|
2046
|
+
function registerSessionDiffTools(server, sessionManager, collector, projectManager) {
|
|
1999
2047
|
server.tool(
|
|
2000
2048
|
"create_session_snapshot",
|
|
2001
2049
|
"Capture a point-in-time snapshot of a live or recent session. Use before/after code changes to compare how your app behaves at different moments. Each session can have multiple snapshots.",
|
|
2002
2050
|
{
|
|
2003
|
-
session_id:
|
|
2004
|
-
label:
|
|
2005
|
-
project:
|
|
2051
|
+
session_id: z16.string().optional().describe("Session ID (defaults to first active session)"),
|
|
2052
|
+
label: z16.string().optional().describe('Label for this snapshot (e.g., "before-fix", "baseline", "after-deploy")'),
|
|
2053
|
+
project: z16.string().optional().describe("Project name"),
|
|
2054
|
+
project_id: projectIdParam
|
|
2006
2055
|
},
|
|
2007
|
-
async ({ session_id, label, project }) => {
|
|
2056
|
+
async ({ session_id, label, project, project_id }) => {
|
|
2008
2057
|
const sessionId = session_id ?? collector.getFirstSessionId();
|
|
2009
2058
|
if (!sessionId) {
|
|
2010
2059
|
return {
|
|
@@ -2016,7 +2065,7 @@ function registerSessionDiffTools(server, sessionManager, collector) {
|
|
|
2016
2065
|
}, null, 2) }]
|
|
2017
2066
|
};
|
|
2018
2067
|
}
|
|
2019
|
-
const projectName = project ?? collector.getProjectForSession(sessionId) ?? "default";
|
|
2068
|
+
const projectName = project ?? (project_id && projectManager ? projectManager.getAppForProjectId(project_id) : void 0) ?? collector.getProjectForSession(sessionId) ?? "default";
|
|
2020
2069
|
const snapshot = sessionManager.createSnapshot(sessionId, projectName, label);
|
|
2021
2070
|
const response = {
|
|
2022
2071
|
summary: `Snapshot captured for session ${sessionId.slice(0, 8)}${label ? ` (label: "${label}")` : ""}. ${snapshot.metrics.totalEvents} events, ${snapshot.metrics.errorCount} errors.`,
|
|
@@ -2051,11 +2100,12 @@ function registerSessionDiffTools(server, sessionManager, collector) {
|
|
|
2051
2100
|
"get_session_snapshots",
|
|
2052
2101
|
"List all snapshots for a session. Use with compare_sessions to track how your app changed over time within a single session.",
|
|
2053
2102
|
{
|
|
2054
|
-
session_id:
|
|
2055
|
-
project:
|
|
2103
|
+
session_id: z16.string().describe("Session ID"),
|
|
2104
|
+
project: z16.string().optional().describe("Project name"),
|
|
2105
|
+
project_id: projectIdParam
|
|
2056
2106
|
},
|
|
2057
|
-
async ({ session_id, project }) => {
|
|
2058
|
-
const projectName = project ?? collector.getProjectForSession(session_id) ?? "default";
|
|
2107
|
+
async ({ session_id, project, project_id }) => {
|
|
2108
|
+
const projectName = project ?? (project_id && projectManager ? projectManager.getAppForProjectId(project_id) : void 0) ?? collector.getProjectForSession(session_id) ?? "default";
|
|
2059
2109
|
const snapshots = sessionManager.getSessionSnapshots(projectName, session_id);
|
|
2060
2110
|
const response = {
|
|
2061
2111
|
summary: `${snapshots.length} snapshot(s) for session ${session_id.slice(0, 8)}.`,
|
|
@@ -2085,14 +2135,15 @@ function registerSessionDiffTools(server, sessionManager, collector) {
|
|
|
2085
2135
|
"compare_sessions",
|
|
2086
2136
|
"Compare two sessions or two snapshots: render counts, API latency, errors, Web Vitals, and query performance. Shows regressions and improvements. Use snapshot_a/snapshot_b to compare specific snapshots within or across sessions.",
|
|
2087
2137
|
{
|
|
2088
|
-
session_a:
|
|
2089
|
-
session_b:
|
|
2090
|
-
snapshot_a:
|
|
2091
|
-
snapshot_b:
|
|
2092
|
-
project:
|
|
2138
|
+
session_a: z16.string().optional().describe("First session ID (baseline) \u2014 used when comparing sessions"),
|
|
2139
|
+
session_b: z16.string().optional().describe("Second session ID (comparison) \u2014 used when comparing sessions"),
|
|
2140
|
+
snapshot_a: z16.number().optional().describe("First snapshot ID (baseline) \u2014 used when comparing snapshots"),
|
|
2141
|
+
snapshot_b: z16.number().optional().describe("Second snapshot ID (comparison) \u2014 used when comparing snapshots"),
|
|
2142
|
+
project: z16.string().optional().describe("Project name"),
|
|
2143
|
+
project_id: projectIdParam
|
|
2093
2144
|
},
|
|
2094
|
-
async ({ session_a, session_b, snapshot_a, snapshot_b, project }) => {
|
|
2095
|
-
const projectName = project ?? "default";
|
|
2145
|
+
async ({ session_a, session_b, snapshot_a, snapshot_b, project, project_id }) => {
|
|
2146
|
+
const projectName = project ?? (project_id && projectManager ? projectManager.getAppForProjectId(project_id) : void 0) ?? "default";
|
|
2096
2147
|
let metricsA = null;
|
|
2097
2148
|
let metricsB = null;
|
|
2098
2149
|
let labelA = "";
|
|
@@ -2202,8 +2253,8 @@ function registerSessionDiffTools(server, sessionManager, collector) {
|
|
|
2202
2253
|
"get_session_history",
|
|
2203
2254
|
"List past sessions with build metadata, event counts, and timestamps. Requires SQLite persistence.",
|
|
2204
2255
|
{
|
|
2205
|
-
project:
|
|
2206
|
-
limit:
|
|
2256
|
+
project: z16.string().optional().describe("Project name"),
|
|
2257
|
+
limit: z16.number().optional().describe("Max sessions to return (default 20)")
|
|
2207
2258
|
},
|
|
2208
2259
|
async ({ project, limit }) => {
|
|
2209
2260
|
const projectName = project ?? "default";
|
|
@@ -2235,19 +2286,20 @@ function registerSessionDiffTools(server, sessionManager, collector) {
|
|
|
2235
2286
|
}
|
|
2236
2287
|
|
|
2237
2288
|
// src/tools/recon-metadata.ts
|
|
2238
|
-
import { z as
|
|
2289
|
+
import { z as z17 } from "zod";
|
|
2239
2290
|
function registerReconMetadataTools(server, store, collector) {
|
|
2240
2291
|
server.tool(
|
|
2241
2292
|
"get_page_metadata",
|
|
2242
2293
|
"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.",
|
|
2243
2294
|
{
|
|
2244
|
-
|
|
2245
|
-
|
|
2295
|
+
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")
|
|
2246
2298
|
},
|
|
2247
|
-
async ({ url, force_refresh }) => {
|
|
2299
|
+
async ({ project_id, url, force_refresh }) => {
|
|
2248
2300
|
if (force_refresh) {
|
|
2249
|
-
const
|
|
2250
|
-
const activeSession =
|
|
2301
|
+
const sessions = store.getSessionInfo();
|
|
2302
|
+
const activeSession = sessions.find((s) => s.isConnected);
|
|
2251
2303
|
if (activeSession) {
|
|
2252
2304
|
try {
|
|
2253
2305
|
await collector.sendCommand(activeSession.sessionId, {
|
|
@@ -2260,8 +2312,7 @@ function registerReconMetadataTools(server, store, collector) {
|
|
|
2260
2312
|
}
|
|
2261
2313
|
}
|
|
2262
2314
|
const event = store.getReconMetadata({ url });
|
|
2263
|
-
const
|
|
2264
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
2315
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2265
2316
|
if (!event) {
|
|
2266
2317
|
return {
|
|
2267
2318
|
content: [{
|
|
@@ -2270,7 +2321,7 @@ function registerReconMetadataTools(server, store, collector) {
|
|
|
2270
2321
|
summary: "No page metadata captured yet. Ensure the RuntimeScope extension is connected and has scanned a page.",
|
|
2271
2322
|
data: null,
|
|
2272
2323
|
issues: ["No recon_metadata events found in the event store"],
|
|
2273
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
2324
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2274
2325
|
}, null, 2)
|
|
2275
2326
|
}]
|
|
2276
2327
|
};
|
|
@@ -2313,7 +2364,8 @@ function registerReconMetadataTools(server, store, collector) {
|
|
|
2313
2364
|
metadata: {
|
|
2314
2365
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2315
2366
|
eventCount: 1,
|
|
2316
|
-
sessionId: event.sessionId
|
|
2367
|
+
sessionId: event.sessionId,
|
|
2368
|
+
projectId: project_id ?? null
|
|
2317
2369
|
}
|
|
2318
2370
|
};
|
|
2319
2371
|
return {
|
|
@@ -2324,20 +2376,21 @@ function registerReconMetadataTools(server, store, collector) {
|
|
|
2324
2376
|
}
|
|
2325
2377
|
|
|
2326
2378
|
// src/tools/recon-design-tokens.ts
|
|
2327
|
-
import { z as
|
|
2379
|
+
import { z as z18 } from "zod";
|
|
2328
2380
|
function registerReconDesignTokenTools(server, store, collector) {
|
|
2329
2381
|
server.tool(
|
|
2330
2382
|
"get_design_tokens",
|
|
2331
2383
|
"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.",
|
|
2332
2384
|
{
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2385
|
+
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")
|
|
2336
2389
|
},
|
|
2337
|
-
async ({ url, category, force_refresh }) => {
|
|
2390
|
+
async ({ project_id, url, category, force_refresh }) => {
|
|
2338
2391
|
if (force_refresh) {
|
|
2339
|
-
const
|
|
2340
|
-
const activeSession =
|
|
2392
|
+
const sessions = store.getSessionInfo();
|
|
2393
|
+
const activeSession = sessions.find((s) => s.isConnected);
|
|
2341
2394
|
if (activeSession) {
|
|
2342
2395
|
try {
|
|
2343
2396
|
await collector.sendCommand(activeSession.sessionId, {
|
|
@@ -2350,8 +2403,7 @@ function registerReconDesignTokenTools(server, store, collector) {
|
|
|
2350
2403
|
}
|
|
2351
2404
|
}
|
|
2352
2405
|
const event = store.getReconDesignTokens({ url });
|
|
2353
|
-
const
|
|
2354
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
2406
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2355
2407
|
if (!event) {
|
|
2356
2408
|
return {
|
|
2357
2409
|
content: [{
|
|
@@ -2360,7 +2412,7 @@ function registerReconDesignTokenTools(server, store, collector) {
|
|
|
2360
2412
|
summary: "No design tokens captured yet. Ensure the RuntimeScope extension is connected and has scanned a page.",
|
|
2361
2413
|
data: null,
|
|
2362
2414
|
issues: ["No recon_design_tokens events found in the event store"],
|
|
2363
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
2415
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2364
2416
|
}, null, 2)
|
|
2365
2417
|
}]
|
|
2366
2418
|
};
|
|
@@ -2411,7 +2463,8 @@ function registerReconDesignTokenTools(server, store, collector) {
|
|
|
2411
2463
|
metadata: {
|
|
2412
2464
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2413
2465
|
eventCount: 1,
|
|
2414
|
-
sessionId: event.sessionId
|
|
2466
|
+
sessionId: event.sessionId,
|
|
2467
|
+
projectId: project_id ?? null
|
|
2415
2468
|
}
|
|
2416
2469
|
};
|
|
2417
2470
|
return {
|
|
@@ -2422,18 +2475,18 @@ function registerReconDesignTokenTools(server, store, collector) {
|
|
|
2422
2475
|
}
|
|
2423
2476
|
|
|
2424
2477
|
// src/tools/recon-fonts.ts
|
|
2425
|
-
import { z as
|
|
2478
|
+
import { z as z19 } from "zod";
|
|
2426
2479
|
function registerReconFontTools(server, store) {
|
|
2427
2480
|
server.tool(
|
|
2428
2481
|
"get_font_info",
|
|
2429
2482
|
"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.",
|
|
2430
2483
|
{
|
|
2431
|
-
|
|
2484
|
+
project_id: projectIdParam,
|
|
2485
|
+
url: z19.string().optional().describe("Filter by URL substring")
|
|
2432
2486
|
},
|
|
2433
|
-
async ({ url }) => {
|
|
2487
|
+
async ({ project_id, url }) => {
|
|
2434
2488
|
const event = store.getReconFonts({ url });
|
|
2435
|
-
const
|
|
2436
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
2489
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2437
2490
|
if (!event) {
|
|
2438
2491
|
return {
|
|
2439
2492
|
content: [{
|
|
@@ -2442,7 +2495,7 @@ function registerReconFontTools(server, store) {
|
|
|
2442
2495
|
summary: "No font data captured yet. Ensure the RuntimeScope extension is connected and has scanned a page.",
|
|
2443
2496
|
data: null,
|
|
2444
2497
|
issues: ["No recon_fonts events found in the event store"],
|
|
2445
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
2498
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2446
2499
|
}, null, 2)
|
|
2447
2500
|
}]
|
|
2448
2501
|
};
|
|
@@ -2469,7 +2522,8 @@ function registerReconFontTools(server, store) {
|
|
|
2469
2522
|
metadata: {
|
|
2470
2523
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2471
2524
|
eventCount: 1,
|
|
2472
|
-
sessionId: event.sessionId
|
|
2525
|
+
sessionId: event.sessionId,
|
|
2526
|
+
projectId: project_id ?? null
|
|
2473
2527
|
}
|
|
2474
2528
|
};
|
|
2475
2529
|
return {
|
|
@@ -2480,21 +2534,22 @@ function registerReconFontTools(server, store) {
|
|
|
2480
2534
|
}
|
|
2481
2535
|
|
|
2482
2536
|
// src/tools/recon-layout.ts
|
|
2483
|
-
import { z as
|
|
2537
|
+
import { z as z20 } from "zod";
|
|
2484
2538
|
function registerReconLayoutTools(server, store, collector) {
|
|
2485
2539
|
server.tool(
|
|
2486
2540
|
"get_layout_tree",
|
|
2487
2541
|
"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.",
|
|
2488
2542
|
{
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2543
|
+
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")
|
|
2493
2548
|
},
|
|
2494
|
-
async ({ selector, max_depth, url, force_refresh }) => {
|
|
2549
|
+
async ({ project_id, selector, max_depth, url, force_refresh }) => {
|
|
2495
2550
|
if (force_refresh) {
|
|
2496
|
-
const
|
|
2497
|
-
const activeSession =
|
|
2551
|
+
const sessions = store.getSessionInfo();
|
|
2552
|
+
const activeSession = sessions.find((s) => s.isConnected);
|
|
2498
2553
|
if (activeSession) {
|
|
2499
2554
|
try {
|
|
2500
2555
|
await collector.sendCommand(activeSession.sessionId, {
|
|
@@ -2507,8 +2562,7 @@ function registerReconLayoutTools(server, store, collector) {
|
|
|
2507
2562
|
}
|
|
2508
2563
|
}
|
|
2509
2564
|
const event = store.getReconLayoutTree({ url });
|
|
2510
|
-
const
|
|
2511
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
2565
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2512
2566
|
if (!event) {
|
|
2513
2567
|
return {
|
|
2514
2568
|
content: [{
|
|
@@ -2517,7 +2571,7 @@ function registerReconLayoutTools(server, store, collector) {
|
|
|
2517
2571
|
summary: "No layout tree captured yet. Ensure the RuntimeScope extension is connected and has scanned a page.",
|
|
2518
2572
|
data: null,
|
|
2519
2573
|
issues: ["No recon_layout_tree events found in the event store"],
|
|
2520
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
2574
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2521
2575
|
}, null, 2)
|
|
2522
2576
|
}]
|
|
2523
2577
|
};
|
|
@@ -2549,7 +2603,8 @@ function registerReconLayoutTools(server, store, collector) {
|
|
|
2549
2603
|
metadata: {
|
|
2550
2604
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2551
2605
|
eventCount: 1,
|
|
2552
|
-
sessionId: event.sessionId
|
|
2606
|
+
sessionId: event.sessionId,
|
|
2607
|
+
projectId: project_id ?? null
|
|
2553
2608
|
}
|
|
2554
2609
|
};
|
|
2555
2610
|
return {
|
|
@@ -2602,18 +2657,18 @@ function pruneTree(node, maxDepth, currentDepth = 0) {
|
|
|
2602
2657
|
}
|
|
2603
2658
|
|
|
2604
2659
|
// src/tools/recon-accessibility.ts
|
|
2605
|
-
import { z as
|
|
2660
|
+
import { z as z21 } from "zod";
|
|
2606
2661
|
function registerReconAccessibilityTools(server, store) {
|
|
2607
2662
|
server.tool(
|
|
2608
2663
|
"get_accessibility_tree",
|
|
2609
2664
|
"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.",
|
|
2610
2665
|
{
|
|
2611
|
-
|
|
2666
|
+
project_id: projectIdParam,
|
|
2667
|
+
url: z21.string().optional().describe("Filter by URL substring")
|
|
2612
2668
|
},
|
|
2613
|
-
async ({ url }) => {
|
|
2669
|
+
async ({ project_id, url }) => {
|
|
2614
2670
|
const event = store.getReconAccessibility({ url });
|
|
2615
|
-
const
|
|
2616
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
2671
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2617
2672
|
if (!event) {
|
|
2618
2673
|
return {
|
|
2619
2674
|
content: [{
|
|
@@ -2622,7 +2677,7 @@ function registerReconAccessibilityTools(server, store) {
|
|
|
2622
2677
|
summary: "No accessibility data captured yet. Ensure the RuntimeScope extension is connected and has scanned a page.",
|
|
2623
2678
|
data: null,
|
|
2624
2679
|
issues: ["No recon_accessibility events found in the event store"],
|
|
2625
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
2680
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2626
2681
|
}, null, 2)
|
|
2627
2682
|
}]
|
|
2628
2683
|
};
|
|
@@ -2664,7 +2719,8 @@ function registerReconAccessibilityTools(server, store) {
|
|
|
2664
2719
|
metadata: {
|
|
2665
2720
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2666
2721
|
eventCount: 1,
|
|
2667
|
-
sessionId: event.sessionId
|
|
2722
|
+
sessionId: event.sessionId,
|
|
2723
|
+
projectId: project_id ?? null
|
|
2668
2724
|
}
|
|
2669
2725
|
};
|
|
2670
2726
|
return {
|
|
@@ -2675,7 +2731,7 @@ function registerReconAccessibilityTools(server, store) {
|
|
|
2675
2731
|
}
|
|
2676
2732
|
|
|
2677
2733
|
// src/tools/recon-computed-styles.ts
|
|
2678
|
-
import { z as
|
|
2734
|
+
import { z as z22 } from "zod";
|
|
2679
2735
|
var PROPERTY_GROUPS = {
|
|
2680
2736
|
colors: [
|
|
2681
2737
|
"color",
|
|
@@ -2779,16 +2835,17 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
|
|
|
2779
2835
|
"get_computed_styles",
|
|
2780
2836
|
"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.",
|
|
2781
2837
|
{
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2838
|
+
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")
|
|
2786
2843
|
},
|
|
2787
|
-
async ({ selector, properties, specific_properties, force_refresh }) => {
|
|
2844
|
+
async ({ project_id, selector, properties, specific_properties, force_refresh }) => {
|
|
2788
2845
|
const propFilter = specific_properties ?? (properties !== "all" ? PROPERTY_GROUPS[properties] : void 0);
|
|
2789
2846
|
if (force_refresh) {
|
|
2790
|
-
const
|
|
2791
|
-
const activeSession =
|
|
2847
|
+
const sessions = store.getSessionInfo();
|
|
2848
|
+
const activeSession = sessions.find((s) => s.isConnected);
|
|
2792
2849
|
if (activeSession) {
|
|
2793
2850
|
try {
|
|
2794
2851
|
await collector.sendCommand(activeSession.sessionId, {
|
|
@@ -2823,8 +2880,7 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
|
|
|
2823
2880
|
} catch {
|
|
2824
2881
|
}
|
|
2825
2882
|
}
|
|
2826
|
-
const
|
|
2827
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
2883
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2828
2884
|
if (!event || event.entries.length === 0) {
|
|
2829
2885
|
const hint = scanner.getLastScannedUrl() ? `No elements matched "${selector}" on the scanned page. Check the selector and try again.` : `No computed styles captured for "${selector}". Run scan_website first to scan a page, then query selectors on it.`;
|
|
2830
2886
|
return {
|
|
@@ -2834,7 +2890,7 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
|
|
|
2834
2890
|
summary: hint,
|
|
2835
2891
|
data: null,
|
|
2836
2892
|
issues: ["No computed style data available for this selector"],
|
|
2837
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
2893
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2838
2894
|
}, null, 2)
|
|
2839
2895
|
}]
|
|
2840
2896
|
};
|
|
@@ -2882,7 +2938,8 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
|
|
|
2882
2938
|
metadata: {
|
|
2883
2939
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2884
2940
|
eventCount: 1,
|
|
2885
|
-
sessionId: event.sessionId
|
|
2941
|
+
sessionId: event.sessionId,
|
|
2942
|
+
projectId: project_id ?? null
|
|
2886
2943
|
}
|
|
2887
2944
|
};
|
|
2888
2945
|
return {
|
|
@@ -2893,20 +2950,21 @@ function registerReconComputedStyleTools(server, store, collector, scanner) {
|
|
|
2893
2950
|
}
|
|
2894
2951
|
|
|
2895
2952
|
// src/tools/recon-element-snapshot.ts
|
|
2896
|
-
import { z as
|
|
2953
|
+
import { z as z23 } from "zod";
|
|
2897
2954
|
function registerReconElementSnapshotTools(server, store, collector, scanner) {
|
|
2898
2955
|
server.tool(
|
|
2899
2956
|
"get_element_snapshot",
|
|
2900
2957
|
'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.',
|
|
2901
2958
|
{
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2959
|
+
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")
|
|
2905
2963
|
},
|
|
2906
|
-
async ({ selector, depth, force_refresh }) => {
|
|
2964
|
+
async ({ project_id, selector, depth, force_refresh }) => {
|
|
2907
2965
|
if (force_refresh) {
|
|
2908
|
-
const
|
|
2909
|
-
const activeSession =
|
|
2966
|
+
const sessions = store.getSessionInfo();
|
|
2967
|
+
const activeSession = sessions.find((s) => s.isConnected);
|
|
2910
2968
|
if (activeSession) {
|
|
2911
2969
|
try {
|
|
2912
2970
|
await collector.sendCommand(activeSession.sessionId, {
|
|
@@ -2942,8 +3000,7 @@ function registerReconElementSnapshotTools(server, store, collector, scanner) {
|
|
|
2942
3000
|
} catch {
|
|
2943
3001
|
}
|
|
2944
3002
|
}
|
|
2945
|
-
const
|
|
2946
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
3003
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
2947
3004
|
if (!event) {
|
|
2948
3005
|
const hint = scanner.getLastScannedUrl() ? `No element found matching "${selector}" on the scanned page. Check the selector and try again.` : `No element snapshot captured for "${selector}". Run scan_website first to scan a page, then query selectors on it.`;
|
|
2949
3006
|
return {
|
|
@@ -2953,7 +3010,7 @@ function registerReconElementSnapshotTools(server, store, collector, scanner) {
|
|
|
2953
3010
|
summary: hint,
|
|
2954
3011
|
data: null,
|
|
2955
3012
|
issues: ["No element snapshot data available for this selector"],
|
|
2956
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
3013
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
2957
3014
|
}, null, 2)
|
|
2958
3015
|
}]
|
|
2959
3016
|
};
|
|
@@ -2975,7 +3032,8 @@ function registerReconElementSnapshotTools(server, store, collector, scanner) {
|
|
|
2975
3032
|
metadata: {
|
|
2976
3033
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
2977
3034
|
eventCount: 1,
|
|
2978
|
-
sessionId: event.sessionId
|
|
3035
|
+
sessionId: event.sessionId,
|
|
3036
|
+
projectId: project_id ?? null
|
|
2979
3037
|
}
|
|
2980
3038
|
};
|
|
2981
3039
|
return {
|
|
@@ -2986,19 +3044,19 @@ function registerReconElementSnapshotTools(server, store, collector, scanner) {
|
|
|
2986
3044
|
}
|
|
2987
3045
|
|
|
2988
3046
|
// src/tools/recon-assets.ts
|
|
2989
|
-
import { z as
|
|
3047
|
+
import { z as z24 } from "zod";
|
|
2990
3048
|
function registerReconAssetTools(server, store) {
|
|
2991
3049
|
server.tool(
|
|
2992
3050
|
"get_asset_inventory",
|
|
2993
3051
|
"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.",
|
|
2994
3052
|
{
|
|
2995
|
-
|
|
2996
|
-
|
|
3053
|
+
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")
|
|
2997
3056
|
},
|
|
2998
|
-
async ({ category, url }) => {
|
|
3057
|
+
async ({ project_id, category, url }) => {
|
|
2999
3058
|
const event = store.getReconAssetInventory({ url });
|
|
3000
|
-
const
|
|
3001
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
3059
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
3002
3060
|
if (!event) {
|
|
3003
3061
|
return {
|
|
3004
3062
|
content: [{
|
|
@@ -3007,7 +3065,7 @@ function registerReconAssetTools(server, store) {
|
|
|
3007
3065
|
summary: "No asset inventory captured yet. Ensure the RuntimeScope extension is connected and has scanned a page.",
|
|
3008
3066
|
data: null,
|
|
3009
3067
|
issues: ["No recon_asset_inventory events found in the event store"],
|
|
3010
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
3068
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
3011
3069
|
}, null, 2)
|
|
3012
3070
|
}]
|
|
3013
3071
|
};
|
|
@@ -3073,7 +3131,8 @@ function registerReconAssetTools(server, store) {
|
|
|
3073
3131
|
metadata: {
|
|
3074
3132
|
timeRange: { from: event.timestamp, to: event.timestamp },
|
|
3075
3133
|
eventCount: 1,
|
|
3076
|
-
sessionId: event.sessionId
|
|
3134
|
+
sessionId: event.sessionId,
|
|
3135
|
+
projectId: project_id ?? null
|
|
3077
3136
|
}
|
|
3078
3137
|
};
|
|
3079
3138
|
return {
|
|
@@ -3084,7 +3143,7 @@ function registerReconAssetTools(server, store) {
|
|
|
3084
3143
|
}
|
|
3085
3144
|
|
|
3086
3145
|
// src/tools/recon-style-diff.ts
|
|
3087
|
-
import { z as
|
|
3146
|
+
import { z as z25 } from "zod";
|
|
3088
3147
|
var VISUAL_PROPERTIES = [
|
|
3089
3148
|
"color",
|
|
3090
3149
|
"background-color",
|
|
@@ -3128,15 +3187,15 @@ function registerReconStyleDiffTools(server, store) {
|
|
|
3128
3187
|
"get_style_diff",
|
|
3129
3188
|
"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.",
|
|
3130
3189
|
{
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3190
|
+
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)")
|
|
3135
3195
|
},
|
|
3136
|
-
async ({ source_selector, target_selector, properties, specific_properties }) => {
|
|
3196
|
+
async ({ project_id, source_selector, target_selector, properties, specific_properties }) => {
|
|
3137
3197
|
const events = store.getReconComputedStyles();
|
|
3138
|
-
const
|
|
3139
|
-
const sessionId = sessions[0]?.sessionId ?? null;
|
|
3198
|
+
const { sessionId } = resolveSessionContext(store, project_id);
|
|
3140
3199
|
const sourceEvent = events.find(
|
|
3141
3200
|
(e) => e.entries.some((entry) => entry.selector === source_selector)
|
|
3142
3201
|
);
|
|
@@ -3154,7 +3213,7 @@ function registerReconStyleDiffTools(server, store) {
|
|
|
3154
3213
|
summary: `Missing computed styles for ${missing.join(" and ")}. Capture computed styles for both selectors first using get_computed_styles with force_refresh=true.`,
|
|
3155
3214
|
data: null,
|
|
3156
3215
|
issues: [`No captured styles for: ${missing.join(", ")}`],
|
|
3157
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId }
|
|
3216
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId, projectId: project_id ?? null }
|
|
3158
3217
|
}, null, 2)
|
|
3159
3218
|
}]
|
|
3160
3219
|
};
|
|
@@ -3223,7 +3282,8 @@ function registerReconStyleDiffTools(server, store) {
|
|
|
3223
3282
|
to: Math.max(sourceEvent.timestamp, targetEvent.timestamp)
|
|
3224
3283
|
},
|
|
3225
3284
|
eventCount: 2,
|
|
3226
|
-
sessionId
|
|
3285
|
+
sessionId,
|
|
3286
|
+
projectId: project_id ?? null
|
|
3227
3287
|
}
|
|
3228
3288
|
};
|
|
3229
3289
|
return {
|
|
@@ -4389,23 +4449,28 @@ var PlaywrightScanner = class _PlaywrightScanner {
|
|
|
4389
4449
|
};
|
|
4390
4450
|
|
|
4391
4451
|
// src/tools/scanner.ts
|
|
4392
|
-
import { z as
|
|
4452
|
+
import { z as z26 } from "zod";
|
|
4453
|
+
import { getOrCreateProjectId } from "@runtimescope/collector";
|
|
4393
4454
|
var COLLECTOR_PORT = process.env.RUNTIMESCOPE_PORT ?? "9090";
|
|
4394
4455
|
var HTTP_PORT = process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091";
|
|
4395
|
-
function registerScannerTools(server, store, scanner) {
|
|
4456
|
+
function registerScannerTools(server, store, scanner, projectManager) {
|
|
4396
4457
|
server.tool(
|
|
4397
4458
|
"get_sdk_snippet",
|
|
4398
4459
|
"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.",
|
|
4399
4460
|
{
|
|
4400
|
-
app_name:
|
|
4401
|
-
framework:
|
|
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.")
|
|
4402
4464
|
},
|
|
4403
|
-
async ({ app_name, framework }) => {
|
|
4465
|
+
async ({ app_name, framework, project_id }) => {
|
|
4466
|
+
const resolvedProjectId = project_id ?? (projectManager ? getOrCreateProjectId(projectManager, app_name) : void 0);
|
|
4467
|
+
const projectIdLine = resolvedProjectId ? `
|
|
4468
|
+
projectId: '${resolvedProjectId}',` : "";
|
|
4404
4469
|
const scriptTagSnippet = `<!-- RuntimeScope \u2014 paste before </body> -->
|
|
4405
4470
|
<script src="http://localhost:${HTTP_PORT}/runtimescope.js"></script>
|
|
4406
4471
|
<script>
|
|
4407
4472
|
RuntimeScope.init({
|
|
4408
|
-
appName: '${app_name}'
|
|
4473
|
+
appName: '${app_name}',${projectIdLine}
|
|
4409
4474
|
endpoint: 'ws://localhost:${COLLECTOR_PORT}',
|
|
4410
4475
|
});
|
|
4411
4476
|
</script>`;
|
|
@@ -4413,11 +4478,37 @@ function registerScannerTools(server, store, scanner) {
|
|
|
4413
4478
|
import { RuntimeScope } from '@runtimescope/sdk';
|
|
4414
4479
|
|
|
4415
4480
|
RuntimeScope.init({
|
|
4416
|
-
appName: '${app_name}'
|
|
4481
|
+
appName: '${app_name}',${projectIdLine}
|
|
4417
4482
|
endpoint: 'ws://localhost:${COLLECTOR_PORT}',
|
|
4418
4483
|
});`;
|
|
4419
|
-
const
|
|
4420
|
-
|
|
4484
|
+
const workersSnippet = `// npm install @runtimescope/workers-sdk
|
|
4485
|
+
import { withRuntimeScope, scopeD1, scopeKV, scopeR2, track, addBreadcrumb } from '@runtimescope/workers-sdk';
|
|
4486
|
+
|
|
4487
|
+
export default withRuntimeScope({
|
|
4488
|
+
async fetch(request, env, ctx) {
|
|
4489
|
+
// Instrument bindings (optional \u2014 use the ones you have)
|
|
4490
|
+
// const db = scopeD1(env.DB);
|
|
4491
|
+
// const kv = scopeKV(env.KV);
|
|
4492
|
+
// const bucket = scopeR2(env.BUCKET);
|
|
4493
|
+
|
|
4494
|
+
// Track custom events
|
|
4495
|
+
// track('request.processed', { path: new URL(request.url).pathname });
|
|
4496
|
+
|
|
4497
|
+
// Add breadcrumbs for debugging
|
|
4498
|
+
// addBreadcrumb('handler started', { method: request.method });
|
|
4499
|
+
|
|
4500
|
+
return new Response('Hello!');
|
|
4501
|
+
},
|
|
4502
|
+
}, {
|
|
4503
|
+
appName: '${app_name}',${projectIdLine}
|
|
4504
|
+
httpEndpoint: 'http://localhost:${HTTP_PORT}/api/events',
|
|
4505
|
+
// captureConsole: true, // Capture console.log/warn/error (default: true)
|
|
4506
|
+
// captureHeaders: false, // Include request/response headers (default: false)
|
|
4507
|
+
// sampleRate: 1.0, // 0.0-1.0 probabilistic sampling (default: 1.0)
|
|
4508
|
+
});`;
|
|
4509
|
+
const isWorkers = framework === "workers";
|
|
4510
|
+
const usesNpm = isWorkers || ["react", "vue", "angular", "svelte", "nextjs", "nuxt"].includes(framework);
|
|
4511
|
+
const primarySnippet = isWorkers ? workersSnippet : usesNpm ? npmSnippet : scriptTagSnippet;
|
|
4421
4512
|
const placementHints = {
|
|
4422
4513
|
html: "Paste the <script> tags before </body> in your HTML file(s).",
|
|
4423
4514
|
react: "Add the import to your entry file (src/index.tsx or src/main.tsx), before ReactDOM.render/createRoot.",
|
|
@@ -4431,31 +4522,47 @@ RuntimeScope.init({
|
|
|
4431
4522
|
rails: "Add the <script> tags to your application layout (app/views/layouts/application.html.erb) before </body>.",
|
|
4432
4523
|
php: "Add the <script> tags to your layout/footer file before </body>.",
|
|
4433
4524
|
wordpress: "Add the <script> tags to your theme's footer.php before </body>, or use a custom HTML plugin.",
|
|
4525
|
+
workers: "Wrap your default export with withRuntimeScope in your Worker entry file (src/index.ts). Enable nodejs_compat in wrangler.toml.",
|
|
4434
4526
|
other: "Add the <script> tags to your HTML template before </body>. Works in any HTML page."
|
|
4435
4527
|
};
|
|
4528
|
+
const workersCaptures = [
|
|
4529
|
+
"Incoming HTTP requests with timing, status, and Cloudflare properties",
|
|
4530
|
+
"Console logs, warnings, and errors with stack traces",
|
|
4531
|
+
"D1 database queries with SQL parsing, timing, and N+1 detection",
|
|
4532
|
+
"KV namespace operations (get/put/delete/list) with timing",
|
|
4533
|
+
"R2 bucket operations (get/put/delete/list/head) with size tracking",
|
|
4534
|
+
"Custom business events via track()",
|
|
4535
|
+
"Request breadcrumbs via addBreadcrumb()"
|
|
4536
|
+
];
|
|
4537
|
+
const browserCaptures = [
|
|
4538
|
+
"Network requests (fetch/XHR) with timing and headers",
|
|
4539
|
+
"Console logs, warnings, and errors with stack traces",
|
|
4540
|
+
"React/Vue/Svelte component renders (if applicable)",
|
|
4541
|
+
"State store changes (Redux, Zustand, Pinia)",
|
|
4542
|
+
"Web Vitals (LCP, FCP, CLS, TTFB, INP)",
|
|
4543
|
+
"Unhandled errors and promise rejections"
|
|
4544
|
+
];
|
|
4436
4545
|
const response = {
|
|
4437
|
-
summary: `SDK snippet for ${framework} project "${app_name}". ${usesNpm ? "Uses npm import." : "Uses <script> tag \u2014 no build system required."}`,
|
|
4546
|
+
summary: isWorkers ? `Workers SDK snippet for Cloudflare Worker "${app_name}". Captures requests, D1/KV/R2 operations, console, custom events, and breadcrumbs.` : `SDK snippet for ${framework} project "${app_name}". ${usesNpm ? "Uses npm import." : "Uses <script> tag \u2014 no build system required."}`,
|
|
4438
4547
|
data: {
|
|
4439
4548
|
snippet: primarySnippet,
|
|
4440
4549
|
placement: placementHints[framework] || placementHints.other,
|
|
4441
|
-
alternativeSnippet: usesNpm ? scriptTagSnippet : npmSnippet,
|
|
4442
|
-
alternativeNote: 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:",
|
|
4443
|
-
requirements: [
|
|
4550
|
+
alternativeSnippet: isWorkers ? void 0 : usesNpm ? scriptTagSnippet : npmSnippet,
|
|
4551
|
+
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
|
+
requirements: isWorkers ? [
|
|
4553
|
+
"RuntimeScope collector must be reachable from your Worker",
|
|
4554
|
+
`HTTP collector endpoint at http://localhost:${HTTP_PORT}/api/events`,
|
|
4555
|
+
"Add nodejs_compat to compatibility_flags in wrangler.toml",
|
|
4556
|
+
"For production: set httpEndpoint to your hosted collector URL"
|
|
4557
|
+
] : [
|
|
4444
4558
|
"RuntimeScope MCP server must be running (it starts automatically with Claude Code)",
|
|
4445
4559
|
`SDK bundle served at http://localhost:${HTTP_PORT}/runtimescope.js`,
|
|
4446
4560
|
`WebSocket collector at ws://localhost:${COLLECTOR_PORT}`
|
|
4447
4561
|
],
|
|
4448
|
-
whatItCaptures:
|
|
4449
|
-
"Network requests (fetch/XHR) with timing and headers",
|
|
4450
|
-
"Console logs, warnings, and errors with stack traces",
|
|
4451
|
-
"React/Vue/Svelte component renders (if applicable)",
|
|
4452
|
-
"State store changes (Redux, Zustand, Pinia)",
|
|
4453
|
-
"Web Vitals (LCP, FCP, CLS, TTFB, INP)",
|
|
4454
|
-
"Unhandled errors and promise rejections"
|
|
4455
|
-
]
|
|
4562
|
+
whatItCaptures: isWorkers ? workersCaptures : browserCaptures
|
|
4456
4563
|
},
|
|
4457
4564
|
issues: [],
|
|
4458
|
-
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
|
|
4565
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null, projectId: resolvedProjectId ?? null }
|
|
4459
4566
|
};
|
|
4460
4567
|
return {
|
|
4461
4568
|
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
@@ -4466,10 +4573,10 @@ RuntimeScope.init({
|
|
|
4466
4573
|
"scan_website",
|
|
4467
4574
|
"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.",
|
|
4468
4575
|
{
|
|
4469
|
-
url:
|
|
4470
|
-
viewport_width:
|
|
4471
|
-
viewport_height:
|
|
4472
|
-
wait_for:
|
|
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)")
|
|
4473
4580
|
},
|
|
4474
4581
|
async ({ url, viewport_width, viewport_height, wait_for }) => {
|
|
4475
4582
|
try {
|
|
@@ -4549,19 +4656,21 @@ RuntimeScope.init({
|
|
|
4549
4656
|
}
|
|
4550
4657
|
|
|
4551
4658
|
// src/tools/custom-events.ts
|
|
4552
|
-
import { z as
|
|
4659
|
+
import { z as z27 } from "zod";
|
|
4553
4660
|
function registerCustomEventTools(server, store) {
|
|
4554
4661
|
server.tool(
|
|
4555
4662
|
"get_custom_events",
|
|
4556
4663
|
"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.",
|
|
4557
4664
|
{
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4665
|
+
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")
|
|
4561
4669
|
},
|
|
4562
|
-
async ({ name, since_seconds, session_id }) => {
|
|
4670
|
+
async ({ project_id, name, since_seconds, session_id }) => {
|
|
4563
4671
|
const sinceSeconds = since_seconds ?? 300;
|
|
4564
4672
|
const events = store.getCustomEvents({
|
|
4673
|
+
projectId: project_id,
|
|
4565
4674
|
name,
|
|
4566
4675
|
sinceSeconds,
|
|
4567
4676
|
sessionId: session_id
|
|
@@ -4583,8 +4692,8 @@ function registerCustomEventTools(server, store) {
|
|
|
4583
4692
|
lastSeen: new Date(info.lastSeen).toISOString(),
|
|
4584
4693
|
sampleProperties: info.sampleProperties
|
|
4585
4694
|
}));
|
|
4586
|
-
const
|
|
4587
|
-
const sessionId = session_id ??
|
|
4695
|
+
const { sessionId: resolvedSessionId } = resolveSessionContext(store, project_id);
|
|
4696
|
+
const sessionId = session_id ?? resolvedSessionId;
|
|
4588
4697
|
const response = {
|
|
4589
4698
|
summary: `${events.length} custom event(s) across ${catalogList.length} unique event name(s) in the last ${sinceSeconds}s.${name ? ` Filtered by: "${name}".` : ""}`,
|
|
4590
4699
|
data: {
|
|
@@ -4603,7 +4712,8 @@ function registerCustomEventTools(server, store) {
|
|
|
4603
4712
|
to: events.length > 0 ? events[0].timestamp : 0
|
|
4604
4713
|
},
|
|
4605
4714
|
eventCount: events.length,
|
|
4606
|
-
sessionId
|
|
4715
|
+
sessionId,
|
|
4716
|
+
projectId: project_id ?? null
|
|
4607
4717
|
}
|
|
4608
4718
|
};
|
|
4609
4719
|
return {
|
|
@@ -4615,13 +4725,14 @@ function registerCustomEventTools(server, store) {
|
|
|
4615
4725
|
"get_event_flow",
|
|
4616
4726
|
"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.",
|
|
4617
4727
|
{
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4728
|
+
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)")
|
|
4621
4732
|
},
|
|
4622
|
-
async ({ steps, since_seconds, session_id }) => {
|
|
4733
|
+
async ({ project_id, steps, since_seconds, session_id }) => {
|
|
4623
4734
|
const sinceSeconds = since_seconds ?? 3600;
|
|
4624
|
-
const allCustom = store.getCustomEvents({ sinceSeconds, sessionId: session_id });
|
|
4735
|
+
const allCustom = store.getCustomEvents({ projectId: project_id, sinceSeconds, sessionId: session_id });
|
|
4625
4736
|
const bySession = /* @__PURE__ */ new Map();
|
|
4626
4737
|
for (const e of allCustom) {
|
|
4627
4738
|
if (!bySession.has(e.sessionId)) bySession.set(e.sessionId, []);
|
|
@@ -4630,9 +4741,9 @@ function registerCustomEventTools(server, store) {
|
|
|
4630
4741
|
for (const events of bySession.values()) {
|
|
4631
4742
|
events.sort((a, b) => a.timestamp - b.timestamp);
|
|
4632
4743
|
}
|
|
4633
|
-
const networkErrors = store.getNetworkRequests({ sinceSeconds, sessionId: session_id }).filter((e) => e.status >= 400 || e.errorPhase);
|
|
4634
|
-
const consoleErrors = store.getConsoleMessages({ sinceSeconds, sessionId: session_id }).filter((e) => e.level === "error");
|
|
4635
|
-
const dbErrors = store.getDatabaseEvents({ sinceSeconds, sessionId: session_id }).filter((e) => !!e.error);
|
|
4744
|
+
const networkErrors = store.getNetworkRequests({ projectId: project_id, sinceSeconds, sessionId: session_id }).filter((e) => e.status >= 400 || e.errorPhase);
|
|
4745
|
+
const consoleErrors = store.getConsoleMessages({ projectId: project_id, sinceSeconds, sessionId: session_id }).filter((e) => e.level === "error");
|
|
4746
|
+
const dbErrors = store.getDatabaseEvents({ projectId: project_id, sinceSeconds, sessionId: session_id }).filter((e) => !!e.error);
|
|
4636
4747
|
const stepResults = steps.map(() => ({
|
|
4637
4748
|
step: "",
|
|
4638
4749
|
sessionsReached: 0,
|
|
@@ -4729,7 +4840,8 @@ function registerCustomEventTools(server, store) {
|
|
|
4729
4840
|
to: allCustom.length > 0 ? allCustom[0].timestamp : 0
|
|
4730
4841
|
},
|
|
4731
4842
|
eventCount: allCustom.length,
|
|
4732
|
-
sessionId: session_id ?? null
|
|
4843
|
+
sessionId: session_id ?? null,
|
|
4844
|
+
projectId: project_id ?? null
|
|
4733
4845
|
}
|
|
4734
4846
|
};
|
|
4735
4847
|
return {
|
|
@@ -4781,8 +4893,170 @@ function dedup(arr, limit) {
|
|
|
4781
4893
|
return result;
|
|
4782
4894
|
}
|
|
4783
4895
|
|
|
4896
|
+
// src/tools/breadcrumbs.ts
|
|
4897
|
+
import { z as z28 } from "zod";
|
|
4898
|
+
function eventToBreadcrumb(event, anchorTs) {
|
|
4899
|
+
const base = {
|
|
4900
|
+
timestamp: new Date(event.timestamp).toISOString(),
|
|
4901
|
+
relativeMs: event.timestamp - anchorTs
|
|
4902
|
+
};
|
|
4903
|
+
switch (event.eventType) {
|
|
4904
|
+
case "navigation": {
|
|
4905
|
+
const nav = event;
|
|
4906
|
+
return {
|
|
4907
|
+
...base,
|
|
4908
|
+
category: "navigation",
|
|
4909
|
+
level: "info",
|
|
4910
|
+
message: `${nav.trigger}: ${nav.to}`,
|
|
4911
|
+
data: { from: nav.from }
|
|
4912
|
+
};
|
|
4913
|
+
}
|
|
4914
|
+
case "ui": {
|
|
4915
|
+
const ui = event;
|
|
4916
|
+
if (ui.action === "click") {
|
|
4917
|
+
return {
|
|
4918
|
+
...base,
|
|
4919
|
+
category: "ui.click",
|
|
4920
|
+
level: "info",
|
|
4921
|
+
message: ui.text ? `Click: ${ui.text}` : `Click: ${ui.target}`,
|
|
4922
|
+
data: { target: ui.target }
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
return {
|
|
4926
|
+
...base,
|
|
4927
|
+
category: "breadcrumb",
|
|
4928
|
+
level: "info",
|
|
4929
|
+
message: ui.text ?? ui.target,
|
|
4930
|
+
...ui.data && { data: ui.data }
|
|
4931
|
+
};
|
|
4932
|
+
}
|
|
4933
|
+
case "console": {
|
|
4934
|
+
const con = event;
|
|
4935
|
+
const level = con.level === "error" ? "error" : con.level === "warn" ? "warning" : con.level === "debug" || con.level === "trace" ? "debug" : "info";
|
|
4936
|
+
return {
|
|
4937
|
+
...base,
|
|
4938
|
+
category: `console.${con.level}`,
|
|
4939
|
+
level,
|
|
4940
|
+
message: con.message.slice(0, 200),
|
|
4941
|
+
...con.stackTrace && { data: { hasStack: true } }
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
case "network": {
|
|
4945
|
+
const net = event;
|
|
4946
|
+
const level = net.errorPhase ? "error" : net.status >= 400 ? "warning" : "info";
|
|
4947
|
+
const url = new URL(net.url, "http://localhost").pathname;
|
|
4948
|
+
return {
|
|
4949
|
+
...base,
|
|
4950
|
+
category: "http",
|
|
4951
|
+
level,
|
|
4952
|
+
message: `${net.method} ${url} \u2192 ${net.status || net.errorPhase || "pending"}`,
|
|
4953
|
+
data: { duration: net.duration, status: net.status }
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4956
|
+
case "state": {
|
|
4957
|
+
const st = event;
|
|
4958
|
+
if (st.phase === "init") return null;
|
|
4959
|
+
const changedKeys = st.diff ? Object.keys(st.diff).join(", ") : "unknown";
|
|
4960
|
+
return {
|
|
4961
|
+
...base,
|
|
4962
|
+
category: "state",
|
|
4963
|
+
level: "debug",
|
|
4964
|
+
message: `${st.storeId}: ${changedKeys}`,
|
|
4965
|
+
data: { library: st.library }
|
|
4966
|
+
};
|
|
4967
|
+
}
|
|
4968
|
+
case "custom": {
|
|
4969
|
+
const cust = event;
|
|
4970
|
+
return {
|
|
4971
|
+
...base,
|
|
4972
|
+
category: `custom.${cust.name}`,
|
|
4973
|
+
level: "info",
|
|
4974
|
+
message: cust.name,
|
|
4975
|
+
...cust.properties && { data: cust.properties }
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
default:
|
|
4979
|
+
return null;
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
var MAX_BREADCRUMBS = 200;
|
|
4983
|
+
function registerBreadcrumbTools(server, store) {
|
|
4984
|
+
server.tool(
|
|
4985
|
+
"get_breadcrumbs",
|
|
4986
|
+
"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
|
+
{
|
|
4988
|
+
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})`)
|
|
4995
|
+
},
|
|
4996
|
+
async ({ project_id, since_seconds, session_id, before_timestamp, categories, level, limit }) => {
|
|
4997
|
+
const sinceSeconds = since_seconds ?? 60;
|
|
4998
|
+
const maxItems = Math.min(limit ?? MAX_BREADCRUMBS, MAX_BREADCRUMBS);
|
|
4999
|
+
const allEvents = store.getEventTimeline({
|
|
5000
|
+
projectId: project_id,
|
|
5001
|
+
sinceSeconds,
|
|
5002
|
+
sessionId: session_id,
|
|
5003
|
+
eventTypes: ["navigation", "ui", "console", "network", "state", "custom"]
|
|
5004
|
+
});
|
|
5005
|
+
const anchor = before_timestamp ?? (allEvents.length > 0 ? allEvents[allEvents.length - 1].timestamp : Date.now());
|
|
5006
|
+
const filtered = before_timestamp ? allEvents.filter((e) => e.timestamp <= before_timestamp) : allEvents;
|
|
5007
|
+
let breadcrumbs = [];
|
|
5008
|
+
for (const event of filtered) {
|
|
5009
|
+
const bc = eventToBreadcrumb(event, anchor);
|
|
5010
|
+
if (bc) breadcrumbs.push(bc);
|
|
5011
|
+
}
|
|
5012
|
+
if (categories && categories.length > 0) {
|
|
5013
|
+
const catSet = new Set(categories);
|
|
5014
|
+
breadcrumbs = breadcrumbs.filter((bc) => {
|
|
5015
|
+
return catSet.has(bc.category) || Array.from(catSet).some((cat) => bc.category.startsWith(cat + "."));
|
|
5016
|
+
});
|
|
5017
|
+
}
|
|
5018
|
+
if (level) {
|
|
5019
|
+
const levelOrder = { debug: 0, info: 1, warning: 2, error: 3 };
|
|
5020
|
+
const minLevel = levelOrder[level];
|
|
5021
|
+
breadcrumbs = breadcrumbs.filter((bc) => levelOrder[bc.level] >= minLevel);
|
|
5022
|
+
}
|
|
5023
|
+
if (breadcrumbs.length > maxItems) {
|
|
5024
|
+
breadcrumbs = breadcrumbs.slice(-maxItems);
|
|
5025
|
+
}
|
|
5026
|
+
const lastError = breadcrumbs.findLast((bc) => bc.level === "error");
|
|
5027
|
+
const { sessionId: resolvedSessionId } = resolveSessionContext(store, project_id);
|
|
5028
|
+
const sessionId = session_id ?? resolvedSessionId;
|
|
5029
|
+
const response = {
|
|
5030
|
+
summary: `${breadcrumbs.length} breadcrumbs over the last ${sinceSeconds}s${lastError ? ` \u2014 last error: "${lastError.message.slice(0, 80)}"` : ""}`,
|
|
5031
|
+
data: breadcrumbs,
|
|
5032
|
+
metadata: {
|
|
5033
|
+
timeRange: {
|
|
5034
|
+
from: breadcrumbs.length > 0 ? breadcrumbs[0].relativeMs : 0,
|
|
5035
|
+
to: breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1].relativeMs : 0
|
|
5036
|
+
},
|
|
5037
|
+
eventCount: breadcrumbs.length,
|
|
5038
|
+
sessionId,
|
|
5039
|
+
projectId: project_id ?? null,
|
|
5040
|
+
anchor: new Date(anchor).toISOString(),
|
|
5041
|
+
categoryCounts: countCategories(breadcrumbs)
|
|
5042
|
+
}
|
|
5043
|
+
};
|
|
5044
|
+
return {
|
|
5045
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
);
|
|
5049
|
+
}
|
|
5050
|
+
function countCategories(breadcrumbs) {
|
|
5051
|
+
const counts = {};
|
|
5052
|
+
for (const bc of breadcrumbs) {
|
|
5053
|
+
counts[bc.category] = (counts[bc.category] ?? 0) + 1;
|
|
5054
|
+
}
|
|
5055
|
+
return counts;
|
|
5056
|
+
}
|
|
5057
|
+
|
|
4784
5058
|
// src/tools/history.ts
|
|
4785
|
-
import { z as
|
|
5059
|
+
import { z as z29 } from "zod";
|
|
4786
5060
|
var EVENT_TYPES = [
|
|
4787
5061
|
"network",
|
|
4788
5062
|
"console",
|
|
@@ -4821,24 +5095,38 @@ function registerHistoryTools(server, collector, projectManager) {
|
|
|
4821
5095
|
"get_historical_events",
|
|
4822
5096
|
"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.",
|
|
4823
5097
|
{
|
|
4824
|
-
project:
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
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")
|
|
4831
5106
|
},
|
|
4832
|
-
async ({ project, event_types, since, until, session_id, limit, offset }) => {
|
|
4833
|
-
const
|
|
5107
|
+
async ({ project, project_id, event_types, since, until, session_id, limit, offset }) => {
|
|
5108
|
+
const resolvedProject = project ?? (project_id ? projectManager.getAppForProjectId(project_id) : void 0);
|
|
5109
|
+
if (!resolvedProject) {
|
|
5110
|
+
const projects = projectManager.listProjects();
|
|
5111
|
+
const hint = projects.length > 0 ? ` Available projects: ${projects.join(", ")}` : " No projects have connected yet.";
|
|
5112
|
+
return {
|
|
5113
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
5114
|
+
summary: `No project specified or project_id "${project_id}" not found.${hint}`,
|
|
5115
|
+
data: null,
|
|
5116
|
+
issues: ["Provide either project (appName) or project_id (proj_xxx)."],
|
|
5117
|
+
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
|
|
5118
|
+
}, null, 2) }]
|
|
5119
|
+
};
|
|
5120
|
+
}
|
|
5121
|
+
const sqliteStore = collector.getSqliteStore(resolvedProject);
|
|
4834
5122
|
if (!sqliteStore) {
|
|
4835
5123
|
const projects = projectManager.listProjects();
|
|
4836
5124
|
const hint = projects.length > 0 ? ` Available projects: ${projects.join(", ")}` : " No projects have connected yet.";
|
|
4837
5125
|
return {
|
|
4838
5126
|
content: [{ type: "text", text: JSON.stringify({
|
|
4839
|
-
summary: `No historical data for project "${
|
|
5127
|
+
summary: `No historical data for project "${resolvedProject}".${hint}`,
|
|
4840
5128
|
data: null,
|
|
4841
|
-
issues: [`Project "${
|
|
5129
|
+
issues: [`Project "${resolvedProject}" has no SQLite store. Connect an SDK with appName: "${resolvedProject}" first.`],
|
|
4842
5130
|
metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
|
|
4843
5131
|
}, null, 2) }]
|
|
4844
5132
|
};
|
|
@@ -4847,7 +5135,7 @@ function registerHistoryTools(server, collector, projectManager) {
|
|
|
4847
5135
|
const untilMs = parseDateParam(until);
|
|
4848
5136
|
const cappedLimit = Math.min(limit, 1e3);
|
|
4849
5137
|
const events = sqliteStore.getEvents({
|
|
4850
|
-
project,
|
|
5138
|
+
project: resolvedProject,
|
|
4851
5139
|
sessionId: session_id,
|
|
4852
5140
|
eventTypes: event_types,
|
|
4853
5141
|
since: sinceMs,
|
|
@@ -4856,7 +5144,7 @@ function registerHistoryTools(server, collector, projectManager) {
|
|
|
4856
5144
|
offset
|
|
4857
5145
|
});
|
|
4858
5146
|
const totalCount = sqliteStore.getEventCount({
|
|
4859
|
-
project,
|
|
5147
|
+
project: resolvedProject,
|
|
4860
5148
|
sessionId: session_id,
|
|
4861
5149
|
eventTypes: event_types,
|
|
4862
5150
|
since: sinceMs,
|
|
@@ -4912,8 +5200,10 @@ function registerHistoryTools(server, collector, projectManager) {
|
|
|
4912
5200
|
const eventCount = sqliteStore.getEventCount({ project: name });
|
|
4913
5201
|
const sessions = sqliteStore.getSessions(name, 100);
|
|
4914
5202
|
const connectedSessions = sessions.filter((s) => s.isConnected);
|
|
5203
|
+
const config = projectManager.getProjectConfig(name);
|
|
4915
5204
|
return {
|
|
4916
5205
|
name,
|
|
5206
|
+
projectId: config?.projectId ?? null,
|
|
4917
5207
|
eventCount,
|
|
4918
5208
|
sessionCount: sessions.length,
|
|
4919
5209
|
activeSessions: connectedSessions.length,
|
|
@@ -4947,16 +5237,14 @@ var HTTP_PORT2 = parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
|
|
|
4947
5237
|
var BUFFER_SIZE = parseInt(process.env.RUNTIMESCOPE_BUFFER_SIZE ?? "10000", 10);
|
|
4948
5238
|
function killStaleProcess(port) {
|
|
4949
5239
|
try {
|
|
4950
|
-
const pids =
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
} catch {
|
|
4959
|
-
}
|
|
5240
|
+
const pids = getPidsOnPort(port);
|
|
5241
|
+
const myPid = process.pid;
|
|
5242
|
+
for (const pid of pids) {
|
|
5243
|
+
if (pid !== myPid) {
|
|
5244
|
+
console.error(`[RuntimeScope] Killing stale process ${pid} on port ${port}`);
|
|
5245
|
+
try {
|
|
5246
|
+
process.kill(pid, "SIGTERM");
|
|
5247
|
+
} catch {
|
|
4960
5248
|
}
|
|
4961
5249
|
}
|
|
4962
5250
|
}
|
|
@@ -5071,6 +5359,7 @@ async function main() {
|
|
|
5071
5359
|
rateLimiter: collector.getRateLimiter(),
|
|
5072
5360
|
pmStore,
|
|
5073
5361
|
discovery,
|
|
5362
|
+
projectManager,
|
|
5074
5363
|
getConnectedSessions: () => collector.getConnectedSessions()
|
|
5075
5364
|
});
|
|
5076
5365
|
try {
|
|
@@ -5078,8 +5367,14 @@ async function main() {
|
|
|
5078
5367
|
} catch (err) {
|
|
5079
5368
|
console.error("[RuntimeScope] HTTP API failed to start:", err.message);
|
|
5080
5369
|
}
|
|
5081
|
-
collector.onConnect((sessionId, projectName) => {
|
|
5370
|
+
collector.onConnect((sessionId, projectName, projectId) => {
|
|
5082
5371
|
httpServer.broadcastSessionChange("session_connected", sessionId, projectName);
|
|
5372
|
+
if (pmStore) {
|
|
5373
|
+
try {
|
|
5374
|
+
pmStore.autoLinkApp(projectName, projectId);
|
|
5375
|
+
} catch {
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5083
5378
|
});
|
|
5084
5379
|
collector.onDisconnect((sessionId, projectName) => {
|
|
5085
5380
|
httpServer.broadcastSessionChange("session_disconnected", sessionId, projectName);
|
|
@@ -5104,7 +5399,7 @@ async function main() {
|
|
|
5104
5399
|
registerDatabaseTools(mcp, store, connectionManager, schemaIntrospector, dataBrowser);
|
|
5105
5400
|
registerProcessMonitorTools(mcp, processMonitor);
|
|
5106
5401
|
registerInfraTools(mcp, infraConnector);
|
|
5107
|
-
registerSessionDiffTools(mcp, sessionManager, collector);
|
|
5402
|
+
registerSessionDiffTools(mcp, sessionManager, collector, projectManager);
|
|
5108
5403
|
registerReconMetadataTools(mcp, store, collector);
|
|
5109
5404
|
registerReconDesignTokenTools(mcp, store, collector);
|
|
5110
5405
|
registerReconFontTools(mcp, store);
|
|
@@ -5114,8 +5409,9 @@ async function main() {
|
|
|
5114
5409
|
registerReconElementSnapshotTools(mcp, store, collector, scanner);
|
|
5115
5410
|
registerReconAssetTools(mcp, store);
|
|
5116
5411
|
registerReconStyleDiffTools(mcp, store);
|
|
5117
|
-
registerScannerTools(mcp, store, scanner);
|
|
5412
|
+
registerScannerTools(mcp, store, scanner, projectManager);
|
|
5118
5413
|
registerCustomEventTools(mcp, store);
|
|
5414
|
+
registerBreadcrumbTools(mcp, store);
|
|
5119
5415
|
registerHistoryTools(mcp, collector, projectManager);
|
|
5120
5416
|
const transport = new StdioServerTransport();
|
|
5121
5417
|
await mcp.connect(transport);
|