@runtimescope/mcp-server 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,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
- since_seconds: z.number().optional().describe("Only return requests from the last N seconds"),
37
- url_pattern: z.string().optional().describe("Filter by URL substring match"),
38
- status: z.number().optional().describe("Filter by HTTP status code"),
39
- method: z.string().optional().describe("Filter by HTTP method (GET, POST, etc.)"),
40
- limit: z.number().optional().describe("Max results to return (default 200, max 1000)")
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 sessions = store.getSessionInfo();
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 z2 } from "zod";
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
- level: z2.enum(["log", "warn", "error", "info", "debug", "trace"]).optional().describe("Filter by console level"),
109
- since_seconds: z2.number().optional().describe("Only return messages from the last N seconds"),
110
- search: z2.string().optional().describe("Search message text (case-insensitive substring match)"),
111
- limit: z2.number().optional().describe("Max results to return (default 200, max 1000)")
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 sessions = store.getSessionInfo();
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
- async () => {
177
- const sessions = store.getSessionInfo();
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: sessions[0]?.sessionId ?? null
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 z3 } from "zod";
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
- since_seconds: z3.number().optional().describe("Analyze events from the last N seconds (default: all events)"),
232
- severity_filter: z3.enum(["high", "medium", "low"]).optional().describe("Only return issues at this severity or above")
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 sessions = store.getSessionInfo();
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 z4 } from "zod";
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
- since_seconds: z4.number().optional().describe("Only return events from the last N seconds (default: 60)"),
305
- event_types: z4.array(z4.enum(["network", "console", "session", "state", "render", "performance", "dom_snapshot", "database", "custom"])).optional().describe("Filter by event types (default: all)"),
306
- limit: z4.number().optional().describe("Max events to return (default: 200, max: 1000)")
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 sessions = store.getSessionInfo();
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 z5 } from "zod";
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
- store_name: z5.string().optional().describe("Filter by store name/ID"),
449
- since_seconds: z5.number().optional().describe("Only return events from the last N seconds"),
450
- limit: z5.number().optional().describe("Max results to return (default 200, max 1000)")
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 sessions = store.getSessionInfo();
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 z6 } from "zod";
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
- component_name: z6.string().optional().describe("Filter by component name (substring match)"),
518
- since_seconds: z6.number().optional().describe("Only return events from the last N seconds")
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 sessions = store.getSessionInfo();
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 z7 } from "zod";
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
- metric_name: z7.enum(ALL_METRICS).optional().describe("Filter by specific metric name"),
613
- source: z7.enum(["browser", "server", "all"]).optional().default("all").describe("Filter by metric source: browser (Web Vitals), server (Node.js runtime), or all"),
614
- since_seconds: z7.number().optional().describe("Only return metrics from the last N seconds")
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 sessions = store.getSessionInfo();
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 z8 } from "zod";
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
- max_size: z8.number().optional().describe("Maximum HTML size in bytes (default: 500000). Larger pages will be truncated.")
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.getSessionInfo();
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 z9 } from "zod";
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
- since_seconds: z9.number().optional().describe("Only include requests from the last N seconds"),
771
- limit: z9.number().optional().describe("Max entries to include (default 200, max 1000)")
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 sessions = store.getSessionInfo();
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 z10 } from "zod";
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
- since_seconds: z10.number().optional().describe("Only return errors from the last N seconds"),
910
- fetch_source: z10.boolean().optional().describe("Whether to fetch source files for context (default: true). Set false for faster results."),
911
- context_lines: z10.number().optional().describe("Number of source lines to show above and below the error line (default: 5)")
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 sessions = store.getSessionInfo();
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 z11 } from "zod";
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
- service: z11.string().optional().describe('Filter by service name (e.g. "Supabase", "Your API")'),
1031
- min_calls: z11.number().optional().describe("Only show endpoints with at least N calls")
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 sessions = store.getSessionInfo();
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
- endpoint: z11.string().optional().describe("Filter by endpoint path substring"),
1080
- since_seconds: z11.number().optional().describe("Only consider requests from the last N seconds")
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 sessions = store.getSessionInfo();
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
- service: z11.string().optional().describe("Generate docs for a specific service only")
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
- async () => {
1174
+ {
1175
+ project_id: projectIdParam
1176
+ },
1177
+ async ({ project_id }) => {
1135
1178
  const services = engine.getServiceMap();
1136
- const sessions = store.getSessionInfo();
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
- session_a: z11.string().describe("First session ID"),
1167
- session_b: z11.string().describe("Second session ID")
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 sessions = store.getSessionInfo();
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 z12 } from "zod";
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
- since_seconds: z12.number().optional().describe("Only return queries from the last N seconds"),
1207
- table: z12.string().optional().describe("Filter by table name"),
1208
- min_duration_ms: z12.number().optional().describe("Only return queries slower than N ms"),
1209
- search: z12.string().optional().describe("Search query text")
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 sessions = store.getSessionInfo();
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
- since_seconds: z12.number().optional().describe("Analyze queries from the last N seconds")
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 sessions = store.getSessionInfo();
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: z12.string().optional().describe("Connection ID (defaults to first available)"),
1304
- table: z12.string().optional().describe("Introspect a specific table only")
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: z12.string().describe("Table name to read"),
1375
- connection_id: z12.string().optional().describe("Connection ID"),
1376
- limit: z12.number().optional().describe("Max rows (default 50, max 1000)"),
1377
- offset: z12.number().optional().describe("Pagination offset"),
1378
- where: z12.string().optional().describe("SQL WHERE clause (without WHERE keyword)"),
1379
- order_by: z12.string().optional().describe("SQL ORDER BY clause (without ORDER BY keyword)")
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: z12.string().describe("Table name"),
1433
- operation: z12.enum(["insert", "update", "delete"]).describe("Operation type"),
1434
- connection_id: z12.string().optional().describe("Connection ID"),
1435
- data: z12.record(z12.unknown()).optional().describe("Row data (for insert/update)"),
1436
- where: z12.string().optional().describe("WHERE clause (required for update/delete)")
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
- since_seconds: z12.number().optional().describe("Analyze queries from the last N seconds")
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 sessions = store.getSessionInfo();
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 z13 } from "zod";
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: z13.string().optional().describe("Filter by process type (next, vite, docker, postgres, etc.)"),
1557
- project: z13.string().optional().describe("Filter by project name")
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: z13.number().describe("Process ID to kill"),
1596
- signal: z13.enum(["SIGTERM", "SIGKILL"]).optional().describe("Signal to send (default: SIGTERM)")
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: z13.number().optional().describe("Filter by specific port number")
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: z13.string().describe("Absolute path to the project directory"),
1660
- dryRun: z13.boolean().optional().describe("If true, report what would be deleted without actually deleting (default: false)")
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: z13.number().describe("PID of the dev server process to restart"),
1718
- command: z13.string().optional().describe('Custom start command (e.g. "npm run dev"). If omitted, infers from process type.'),
1719
- skipCachePurge: z13.boolean().optional().describe("If true, skip cache purging (default: false)"),
1720
- signal: z13.enum(["SIGTERM", "SIGKILL"]).optional().describe("Kill signal (default: SIGTERM)")
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 z14 } from "zod";
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: z14.string().optional().describe("Project name"),
1877
- platform: z14.string().optional().describe("Filter by platform (vercel, cloudflare, railway)"),
1878
- deploy_id: z14.string().optional().describe("Get details for a specific deployment")
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: z14.string().optional().describe("Project name"),
1912
- platform: z14.string().optional().describe("Filter by platform"),
1913
- level: z14.string().optional().describe("Filter by log level (info, warn, error)"),
1914
- since_seconds: z14.number().optional().describe("Only return logs from the last N 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: z14.string().optional().describe("Project name")
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: z14.string().optional().describe("Project name")
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 z15 } from "zod";
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: z15.string().optional().describe("Session ID (defaults to first active session)"),
2004
- label: z15.string().optional().describe('Label for this snapshot (e.g., "before-fix", "baseline", "after-deploy")'),
2005
- project: z15.string().optional().describe("Project name")
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: z15.string().describe("Session ID"),
2055
- project: z15.string().optional().describe("Project name")
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: z15.string().optional().describe("First session ID (baseline) \u2014 used when comparing sessions"),
2089
- session_b: z15.string().optional().describe("Second session ID (comparison) \u2014 used when comparing sessions"),
2090
- snapshot_a: z15.number().optional().describe("First snapshot ID (baseline) \u2014 used when comparing snapshots"),
2091
- snapshot_b: z15.number().optional().describe("Second snapshot ID (comparison) \u2014 used when comparing snapshots"),
2092
- project: z15.string().optional().describe("Project name")
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: z15.string().optional().describe("Project name"),
2206
- limit: z15.number().optional().describe("Max sessions to return (default 20)")
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 z16 } from "zod";
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
- url: z16.string().optional().describe("Filter by URL substring"),
2245
- force_refresh: z16.boolean().optional().default(false).describe("Send a recon_scan command to the extension to capture fresh data")
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 sessions2 = store.getSessionInfo();
2250
- const activeSession = sessions2.find((s) => s.isConnected);
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 sessions = store.getSessionInfo();
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 z17 } from "zod";
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
- url: z17.string().optional().describe("Filter by URL substring"),
2334
- category: z17.enum(["all", "colors", "typography", "spacing", "custom_properties", "shadows"]).optional().default("all").describe("Return only a specific token category"),
2335
- force_refresh: z17.boolean().optional().default(false).describe("Send a recon_scan command to capture fresh data")
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 sessions2 = store.getSessionInfo();
2340
- const activeSession = sessions2.find((s) => s.isConnected);
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 sessions = store.getSessionInfo();
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 z18 } from "zod";
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
- url: z18.string().optional().describe("Filter by URL substring")
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 sessions = store.getSessionInfo();
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 z19 } from "zod";
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
- selector: z19.string().optional().describe('CSS selector to scope the tree (e.g., "nav", ".hero", "main"). Omit for full page.'),
2490
- max_depth: z19.number().optional().default(10).describe("Maximum depth of the tree to return (default 10)"),
2491
- url: z19.string().optional().describe("Filter by URL substring"),
2492
- force_refresh: z19.boolean().optional().default(false).describe("Request fresh capture from extension")
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 sessions2 = store.getSessionInfo();
2497
- const activeSession = sessions2.find((s) => s.isConnected);
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 sessions = store.getSessionInfo();
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 z20 } from "zod";
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
- url: z20.string().optional().describe("Filter by URL substring")
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 sessions = store.getSessionInfo();
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 z21 } from "zod";
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
- selector: z21.string().describe('CSS selector to query (e.g., ".btn-primary", "nav > ul > li", "[data-testid=hero]")'),
2783
- properties: z21.enum(["all", "colors", "typography", "spacing", "layout", "borders", "visual"]).optional().default("all").describe('Property group to return, or "all" for everything'),
2784
- specific_properties: z21.array(z21.string()).optional().describe("Specific CSS property names to return (overrides the properties group)"),
2785
- force_refresh: z21.boolean().optional().default(false).describe("Request fresh capture from extension or scanner for this selector")
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 sessions2 = store.getSessionInfo();
2791
- const activeSession = sessions2.find((s) => s.isConnected);
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 sessions = store.getSessionInfo();
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 z22 } from "zod";
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
- selector: z22.string().describe('CSS selector for the root element (e.g., ".card", "#hero", "[data-testid=checkout-form]")'),
2903
- depth: z22.number().optional().default(5).describe("How many levels deep to capture children (default 5)"),
2904
- force_refresh: z22.boolean().optional().default(false).describe("Request fresh capture from extension or scanner for this element")
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 sessions2 = store.getSessionInfo();
2909
- const activeSession = sessions2.find((s) => s.isConnected);
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 sessions = store.getSessionInfo();
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 z23 } from "zod";
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
- category: z23.enum(["all", "images", "svg", "sprites", "icon_fonts"]).optional().default("all").describe("Filter by asset category"),
2996
- url: z23.string().optional().describe("Filter by page URL substring")
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 sessions = store.getSessionInfo();
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 z24 } from "zod";
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
- source_selector: z24.string().describe("CSS selector for the source/original element"),
3132
- target_selector: z24.string().describe("CSS selector for the target/recreation element"),
3133
- properties: z24.enum(["visual", "all"]).optional().default("visual").describe('"visual" compares only visually-significant properties (colors, typography, spacing, layout). "all" compares everything.'),
3134
- specific_properties: z24.array(z24.string()).optional().describe("Specific CSS property names to compare (overrides properties group)")
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 sessions = store.getSessionInfo();
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 z25 } from "zod";
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: z25.string().optional().default("my-app").describe('Name for the app in RuntimeScope (e.g., "echo-frontend", "dashboard")'),
4401
- framework: z25.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.')
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,7 +4478,7 @@ 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
4484
  const workersSnippet = `// npm install @runtimescope/workers-sdk
@@ -4435,7 +4500,7 @@ export default withRuntimeScope({
4435
4500
  return new Response('Hello!');
4436
4501
  },
4437
4502
  }, {
4438
- appName: '${app_name}',
4503
+ appName: '${app_name}',${projectIdLine}
4439
4504
  httpEndpoint: 'http://localhost:${HTTP_PORT}/api/events',
4440
4505
  // captureConsole: true, // Capture console.log/warn/error (default: true)
4441
4506
  // captureHeaders: false, // Include request/response headers (default: false)
@@ -4497,7 +4562,7 @@ export default withRuntimeScope({
4497
4562
  whatItCaptures: isWorkers ? workersCaptures : browserCaptures
4498
4563
  },
4499
4564
  issues: [],
4500
- metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
4565
+ metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null, projectId: resolvedProjectId ?? null }
4501
4566
  };
4502
4567
  return {
4503
4568
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
@@ -4508,10 +4573,10 @@ export default withRuntimeScope({
4508
4573
  "scan_website",
4509
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.",
4510
4575
  {
4511
- url: z25.string().describe('The full URL to scan (e.g., "https://stripe.com")'),
4512
- viewport_width: z25.number().optional().default(1280).describe("Viewport width in pixels (default: 1280)"),
4513
- viewport_height: z25.number().optional().default(720).describe("Viewport height in pixels (default: 720)"),
4514
- wait_for: z25.enum(["load", "networkidle", "domcontentloaded"]).optional().default("networkidle").describe("Wait condition before scanning (default: networkidle)")
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)")
4515
4580
  },
4516
4581
  async ({ url, viewport_width, viewport_height, wait_for }) => {
4517
4582
  try {
@@ -4591,19 +4656,21 @@ export default withRuntimeScope({
4591
4656
  }
4592
4657
 
4593
4658
  // src/tools/custom-events.ts
4594
- import { z as z26 } from "zod";
4659
+ import { z as z27 } from "zod";
4595
4660
  function registerCustomEventTools(server, store) {
4596
4661
  server.tool(
4597
4662
  "get_custom_events",
4598
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.",
4599
4664
  {
4600
- name: z26.string().optional().describe("Filter by event name (exact match)"),
4601
- since_seconds: z26.number().optional().describe("Only events from the last N seconds (default: 300)"),
4602
- session_id: z26.string().optional().describe("Filter by session ID")
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")
4603
4669
  },
4604
- async ({ name, since_seconds, session_id }) => {
4670
+ async ({ project_id, name, since_seconds, session_id }) => {
4605
4671
  const sinceSeconds = since_seconds ?? 300;
4606
4672
  const events = store.getCustomEvents({
4673
+ projectId: project_id,
4607
4674
  name,
4608
4675
  sinceSeconds,
4609
4676
  sessionId: session_id
@@ -4625,8 +4692,8 @@ function registerCustomEventTools(server, store) {
4625
4692
  lastSeen: new Date(info.lastSeen).toISOString(),
4626
4693
  sampleProperties: info.sampleProperties
4627
4694
  }));
4628
- const sessions = store.getSessionInfo();
4629
- const sessionId = session_id ?? sessions[0]?.sessionId ?? null;
4695
+ const { sessionId: resolvedSessionId } = resolveSessionContext(store, project_id);
4696
+ const sessionId = session_id ?? resolvedSessionId;
4630
4697
  const response = {
4631
4698
  summary: `${events.length} custom event(s) across ${catalogList.length} unique event name(s) in the last ${sinceSeconds}s.${name ? ` Filtered by: "${name}".` : ""}`,
4632
4699
  data: {
@@ -4645,7 +4712,8 @@ function registerCustomEventTools(server, store) {
4645
4712
  to: events.length > 0 ? events[0].timestamp : 0
4646
4713
  },
4647
4714
  eventCount: events.length,
4648
- sessionId
4715
+ sessionId,
4716
+ projectId: project_id ?? null
4649
4717
  }
4650
4718
  };
4651
4719
  return {
@@ -4657,13 +4725,14 @@ function registerCustomEventTools(server, store) {
4657
4725
  "get_event_flow",
4658
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.",
4659
4727
  {
4660
- steps: z26.array(z26.string()).min(2).describe('Ordered list of custom event names representing the flow (e.g. ["create_profile", "generate_campaign", "export_ad"])'),
4661
- since_seconds: z26.number().optional().describe("Only analyze events from the last N seconds (default: 3600)"),
4662
- session_id: z26.string().optional().describe("Analyze a specific session (default: all sessions)")
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)")
4663
4732
  },
4664
- async ({ steps, since_seconds, session_id }) => {
4733
+ async ({ project_id, steps, since_seconds, session_id }) => {
4665
4734
  const sinceSeconds = since_seconds ?? 3600;
4666
- const allCustom = store.getCustomEvents({ sinceSeconds, sessionId: session_id });
4735
+ const allCustom = store.getCustomEvents({ projectId: project_id, sinceSeconds, sessionId: session_id });
4667
4736
  const bySession = /* @__PURE__ */ new Map();
4668
4737
  for (const e of allCustom) {
4669
4738
  if (!bySession.has(e.sessionId)) bySession.set(e.sessionId, []);
@@ -4672,9 +4741,9 @@ function registerCustomEventTools(server, store) {
4672
4741
  for (const events of bySession.values()) {
4673
4742
  events.sort((a, b) => a.timestamp - b.timestamp);
4674
4743
  }
4675
- const networkErrors = store.getNetworkRequests({ sinceSeconds, sessionId: session_id }).filter((e) => e.status >= 400 || e.errorPhase);
4676
- const consoleErrors = store.getConsoleMessages({ sinceSeconds, sessionId: session_id }).filter((e) => e.level === "error");
4677
- 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);
4678
4747
  const stepResults = steps.map(() => ({
4679
4748
  step: "",
4680
4749
  sessionsReached: 0,
@@ -4771,7 +4840,8 @@ function registerCustomEventTools(server, store) {
4771
4840
  to: allCustom.length > 0 ? allCustom[0].timestamp : 0
4772
4841
  },
4773
4842
  eventCount: allCustom.length,
4774
- sessionId: session_id ?? null
4843
+ sessionId: session_id ?? null,
4844
+ projectId: project_id ?? null
4775
4845
  }
4776
4846
  };
4777
4847
  return {
@@ -4824,7 +4894,7 @@ function dedup(arr, limit) {
4824
4894
  }
4825
4895
 
4826
4896
  // src/tools/breadcrumbs.ts
4827
- import { z as z27 } from "zod";
4897
+ import { z as z28 } from "zod";
4828
4898
  function eventToBreadcrumb(event, anchorTs) {
4829
4899
  const base = {
4830
4900
  timestamp: new Date(event.timestamp).toISOString(),
@@ -4915,17 +4985,19 @@ function registerBreadcrumbTools(server, store) {
4915
4985
  "get_breadcrumbs",
4916
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.",
4917
4987
  {
4918
- since_seconds: z27.number().optional().describe("How far back to look (default: 60 seconds)"),
4919
- session_id: z27.string().optional().describe("Filter to a specific session"),
4920
- before_timestamp: z27.number().optional().describe('Only show breadcrumbs before this Unix ms timestamp (useful for "what happened before this error")'),
4921
- categories: z27.array(z27.string()).optional().describe("Filter to specific categories: navigation, ui.click, breadcrumb, console.error, console.warn, console.log, http, state, custom.*"),
4922
- level: z27.enum(["debug", "info", "warning", "error"]).optional().describe("Minimum breadcrumb level to include (default: debug = show all)"),
4923
- limit: z27.number().optional().describe(`Max breadcrumbs to return (default/max: ${MAX_BREADCRUMBS})`)
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})`)
4924
4995
  },
4925
- async ({ since_seconds, session_id, before_timestamp, categories, level, limit }) => {
4996
+ async ({ project_id, since_seconds, session_id, before_timestamp, categories, level, limit }) => {
4926
4997
  const sinceSeconds = since_seconds ?? 60;
4927
4998
  const maxItems = Math.min(limit ?? MAX_BREADCRUMBS, MAX_BREADCRUMBS);
4928
4999
  const allEvents = store.getEventTimeline({
5000
+ projectId: project_id,
4929
5001
  sinceSeconds,
4930
5002
  sessionId: session_id,
4931
5003
  eventTypes: ["navigation", "ui", "console", "network", "state", "custom"]
@@ -4952,8 +5024,8 @@ function registerBreadcrumbTools(server, store) {
4952
5024
  breadcrumbs = breadcrumbs.slice(-maxItems);
4953
5025
  }
4954
5026
  const lastError = breadcrumbs.findLast((bc) => bc.level === "error");
4955
- const sessions = store.getSessionInfo();
4956
- const sessionId = session_id ?? sessions[0]?.sessionId ?? null;
5027
+ const { sessionId: resolvedSessionId } = resolveSessionContext(store, project_id);
5028
+ const sessionId = session_id ?? resolvedSessionId;
4957
5029
  const response = {
4958
5030
  summary: `${breadcrumbs.length} breadcrumbs over the last ${sinceSeconds}s${lastError ? ` \u2014 last error: "${lastError.message.slice(0, 80)}"` : ""}`,
4959
5031
  data: breadcrumbs,
@@ -4964,6 +5036,7 @@ function registerBreadcrumbTools(server, store) {
4964
5036
  },
4965
5037
  eventCount: breadcrumbs.length,
4966
5038
  sessionId,
5039
+ projectId: project_id ?? null,
4967
5040
  anchor: new Date(anchor).toISOString(),
4968
5041
  categoryCounts: countCategories(breadcrumbs)
4969
5042
  }
@@ -4983,7 +5056,7 @@ function countCategories(breadcrumbs) {
4983
5056
  }
4984
5057
 
4985
5058
  // src/tools/history.ts
4986
- import { z as z28 } from "zod";
5059
+ import { z as z29 } from "zod";
4987
5060
  var EVENT_TYPES = [
4988
5061
  "network",
4989
5062
  "console",
@@ -5022,24 +5095,38 @@ function registerHistoryTools(server, collector, projectManager) {
5022
5095
  "get_historical_events",
5023
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.",
5024
5097
  {
5025
- project: z28.string().describe("Project/app name (the appName used in SDK init)"),
5026
- event_types: z28.array(z28.enum(EVENT_TYPES)).optional().describe('Filter by event types (e.g., ["network", "console"])'),
5027
- since: z28.string().optional().describe('Start time \u2014 relative ("2h", "7d", "30m") or ISO date string'),
5028
- until: z28.string().optional().describe("End time \u2014 relative or ISO date string"),
5029
- session_id: z28.string().optional().describe("Filter by specific session ID"),
5030
- limit: z28.number().optional().default(200).describe("Max events to return (default 200, max 1000)"),
5031
- offset: z28.number().optional().default(0).describe("Pagination offset")
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")
5032
5106
  },
5033
- async ({ project, event_types, since, until, session_id, limit, offset }) => {
5034
- const sqliteStore = collector.getSqliteStore(project);
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);
5035
5122
  if (!sqliteStore) {
5036
5123
  const projects = projectManager.listProjects();
5037
5124
  const hint = projects.length > 0 ? ` Available projects: ${projects.join(", ")}` : " No projects have connected yet.";
5038
5125
  return {
5039
5126
  content: [{ type: "text", text: JSON.stringify({
5040
- summary: `No historical data for project "${project}".${hint}`,
5127
+ summary: `No historical data for project "${resolvedProject}".${hint}`,
5041
5128
  data: null,
5042
- issues: [`Project "${project}" has no SQLite store. Connect an SDK with appName: "${project}" first.`],
5129
+ issues: [`Project "${resolvedProject}" has no SQLite store. Connect an SDK with appName: "${resolvedProject}" first.`],
5043
5130
  metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
5044
5131
  }, null, 2) }]
5045
5132
  };
@@ -5048,7 +5135,7 @@ function registerHistoryTools(server, collector, projectManager) {
5048
5135
  const untilMs = parseDateParam(until);
5049
5136
  const cappedLimit = Math.min(limit, 1e3);
5050
5137
  const events = sqliteStore.getEvents({
5051
- project,
5138
+ project: resolvedProject,
5052
5139
  sessionId: session_id,
5053
5140
  eventTypes: event_types,
5054
5141
  since: sinceMs,
@@ -5057,7 +5144,7 @@ function registerHistoryTools(server, collector, projectManager) {
5057
5144
  offset
5058
5145
  });
5059
5146
  const totalCount = sqliteStore.getEventCount({
5060
- project,
5147
+ project: resolvedProject,
5061
5148
  sessionId: session_id,
5062
5149
  eventTypes: event_types,
5063
5150
  since: sinceMs,
@@ -5113,8 +5200,10 @@ function registerHistoryTools(server, collector, projectManager) {
5113
5200
  const eventCount = sqliteStore.getEventCount({ project: name });
5114
5201
  const sessions = sqliteStore.getSessions(name, 100);
5115
5202
  const connectedSessions = sessions.filter((s) => s.isConnected);
5203
+ const config = projectManager.getProjectConfig(name);
5116
5204
  return {
5117
5205
  name,
5206
+ projectId: config?.projectId ?? null,
5118
5207
  eventCount,
5119
5208
  sessionCount: sessions.length,
5120
5209
  activeSessions: connectedSessions.length,
@@ -5148,16 +5237,14 @@ var HTTP_PORT2 = parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
5148
5237
  var BUFFER_SIZE = parseInt(process.env.RUNTIMESCOPE_BUFFER_SIZE ?? "10000", 10);
5149
5238
  function killStaleProcess(port) {
5150
5239
  try {
5151
- const pids = execSync2(`lsof -ti :${port} 2>/dev/null`, { encoding: "utf-8" }).trim();
5152
- if (pids) {
5153
- const myPid = process.pid.toString();
5154
- for (const pid of pids.split("\n")) {
5155
- if (pid && pid !== myPid) {
5156
- console.error(`[RuntimeScope] Killing stale process ${pid} on port ${port}`);
5157
- try {
5158
- process.kill(parseInt(pid, 10), "SIGTERM");
5159
- } catch {
5160
- }
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 {
5161
5248
  }
5162
5249
  }
5163
5250
  }
@@ -5272,6 +5359,7 @@ async function main() {
5272
5359
  rateLimiter: collector.getRateLimiter(),
5273
5360
  pmStore,
5274
5361
  discovery,
5362
+ projectManager,
5275
5363
  getConnectedSessions: () => collector.getConnectedSessions()
5276
5364
  });
5277
5365
  try {
@@ -5279,8 +5367,14 @@ async function main() {
5279
5367
  } catch (err) {
5280
5368
  console.error("[RuntimeScope] HTTP API failed to start:", err.message);
5281
5369
  }
5282
- collector.onConnect((sessionId, projectName) => {
5370
+ collector.onConnect((sessionId, projectName, projectId) => {
5283
5371
  httpServer.broadcastSessionChange("session_connected", sessionId, projectName);
5372
+ if (pmStore) {
5373
+ try {
5374
+ pmStore.autoLinkApp(projectName, projectId);
5375
+ } catch {
5376
+ }
5377
+ }
5284
5378
  });
5285
5379
  collector.onDisconnect((sessionId, projectName) => {
5286
5380
  httpServer.broadcastSessionChange("session_disconnected", sessionId, projectName);
@@ -5305,7 +5399,7 @@ async function main() {
5305
5399
  registerDatabaseTools(mcp, store, connectionManager, schemaIntrospector, dataBrowser);
5306
5400
  registerProcessMonitorTools(mcp, processMonitor);
5307
5401
  registerInfraTools(mcp, infraConnector);
5308
- registerSessionDiffTools(mcp, sessionManager, collector);
5402
+ registerSessionDiffTools(mcp, sessionManager, collector, projectManager);
5309
5403
  registerReconMetadataTools(mcp, store, collector);
5310
5404
  registerReconDesignTokenTools(mcp, store, collector);
5311
5405
  registerReconFontTools(mcp, store);
@@ -5315,7 +5409,7 @@ async function main() {
5315
5409
  registerReconElementSnapshotTools(mcp, store, collector, scanner);
5316
5410
  registerReconAssetTools(mcp, store);
5317
5411
  registerReconStyleDiffTools(mcp, store);
5318
- registerScannerTools(mcp, store, scanner);
5412
+ registerScannerTools(mcp, store, scanner, projectManager);
5319
5413
  registerCustomEventTools(mcp, store);
5320
5414
  registerBreadcrumbTools(mcp, store);
5321
5415
  registerHistoryTools(mcp, collector, projectManager);