@runtimescope/mcp-server 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  SessionManager,
19
19
  HttpServer,
20
20
  SqliteStore,
21
+ isSqliteAvailable,
21
22
  AuthManager,
22
23
  Redactor,
23
24
  resolveTlsConfig,
@@ -35,15 +36,19 @@ function registerNetworkTools(server, store) {
35
36
  since_seconds: z.number().optional().describe("Only return requests from the last N seconds"),
36
37
  url_pattern: z.string().optional().describe("Filter by URL substring match"),
37
38
  status: z.number().optional().describe("Filter by HTTP status code"),
38
- method: z.string().optional().describe("Filter by HTTP method (GET, POST, etc.)")
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)")
39
41
  },
40
- async ({ since_seconds, url_pattern, status, method }) => {
41
- const events = store.getNetworkRequests({
42
+ async ({ since_seconds, url_pattern, status, method, limit }) => {
43
+ const allEvents = store.getNetworkRequests({
42
44
  sinceSeconds: since_seconds,
43
45
  urlPattern: url_pattern,
44
46
  status,
45
47
  method
46
48
  });
49
+ const maxLimit = Math.min(limit ?? 200, 1e3);
50
+ const truncated = allEvents.length > maxLimit;
51
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
47
52
  const timeRange = events.length > 0 ? { from: events[events.length - 1].timestamp, to: events[0].timestamp } : { from: 0, to: 0 };
48
53
  const sessions = store.getSessionInfo();
49
54
  const sessionId = sessions[0]?.sessionId ?? null;
@@ -71,7 +76,7 @@ function registerNetworkTools(server, store) {
71
76
  }
72
77
  }
73
78
  const response = {
74
- summary: `Found ${events.length} network request(s)${since_seconds ? ` in the last ${since_seconds}s` : ""}. Average duration: ${avgDuration}ms.`,
79
+ summary: `Found ${events.length} network request(s)${truncated ? ` (showing ${maxLimit} of ${allEvents.length})` : ""}${since_seconds ? ` in the last ${since_seconds}s` : ""}. Average duration: ${avgDuration}ms.`,
75
80
  data: events.map((e) => ({
76
81
  url: e.url,
77
82
  method: e.method,
@@ -84,7 +89,7 @@ function registerNetworkTools(server, store) {
84
89
  timestamp: new Date(e.timestamp).toISOString()
85
90
  })),
86
91
  issues,
87
- metadata: { timeRange, eventCount: events.length, sessionId }
92
+ metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId }
88
93
  };
89
94
  return {
90
95
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
@@ -102,14 +107,18 @@ function registerConsoleTools(server, store) {
102
107
  {
103
108
  level: z2.enum(["log", "warn", "error", "info", "debug", "trace"]).optional().describe("Filter by console level"),
104
109
  since_seconds: z2.number().optional().describe("Only return messages from the last N seconds"),
105
- search: z2.string().optional().describe("Search message text (case-insensitive substring match)")
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)")
106
112
  },
107
- async ({ level, since_seconds, search }) => {
108
- const events = store.getConsoleMessages({
113
+ async ({ level, since_seconds, search, limit }) => {
114
+ const allEvents = store.getConsoleMessages({
109
115
  level,
110
116
  sinceSeconds: since_seconds,
111
117
  search
112
118
  });
119
+ const maxLimit = Math.min(limit ?? 200, 1e3);
120
+ const truncated = allEvents.length > maxLimit;
121
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
113
122
  const timeRange = events.length > 0 ? { from: events[events.length - 1].timestamp, to: events[0].timestamp } : { from: 0, to: 0 };
114
123
  const sessions = store.getSessionInfo();
115
124
  const sessionId = sessions[0]?.sessionId ?? null;
@@ -134,12 +143,12 @@ function registerConsoleTools(server, store) {
134
143
  }
135
144
  for (const [msg, info] of errorMessages) {
136
145
  if (info.count > 5 && info.last - info.first < 1e4) {
137
- const truncated = msg.length > 80 ? msg.slice(0, 80) + "..." : msg;
138
- issues.push(`Error spam: "${truncated}" repeated ${info.count} times in ${((info.last - info.first) / 1e3).toFixed(1)}s`);
146
+ const truncated2 = msg.length > 80 ? msg.slice(0, 80) + "..." : msg;
147
+ issues.push(`Error spam: "${truncated2}" repeated ${info.count} times in ${((info.last - info.first) / 1e3).toFixed(1)}s`);
139
148
  }
140
149
  }
141
150
  const response = {
142
- summary: `Found ${events.length} console message(s)${since_seconds ? ` in the last ${since_seconds}s` : ""}${levelSummary ? `. Breakdown: ${levelSummary}` : ""}.`,
151
+ summary: `Found ${events.length} console message(s)${truncated ? ` (showing ${maxLimit} of ${allEvents.length})` : ""}${since_seconds ? ` in the last ${since_seconds}s` : ""}${levelSummary ? `. Breakdown: ${levelSummary}` : ""}.`,
143
152
  data: events.map((e) => ({
144
153
  level: e.level,
145
154
  message: e.message,
@@ -149,7 +158,7 @@ function registerConsoleTools(server, store) {
149
158
  timestamp: new Date(e.timestamp).toISOString()
150
159
  })),
151
160
  issues,
152
- metadata: { timeRange, eventCount: events.length, sessionId }
161
+ metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId }
153
162
  };
154
163
  return {
155
164
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
@@ -227,7 +236,7 @@ function registerIssueTools(server, store, apiDiscovery, processMonitor) {
227
236
  const allIssues = [...detectIssues(events)];
228
237
  if (apiDiscovery) {
229
238
  try {
230
- allIssues.push(...apiDiscovery.detectIssues(events));
239
+ allIssues.push(...apiDiscovery.detectIssues());
231
240
  } catch {
232
241
  }
233
242
  }
@@ -437,13 +446,17 @@ function registerStateTools(server, store) {
437
446
  "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.",
438
447
  {
439
448
  store_name: z5.string().optional().describe("Filter by store name/ID"),
440
- since_seconds: z5.number().optional().describe("Only return events from the last N seconds")
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)")
441
451
  },
442
- async ({ store_name, since_seconds }) => {
443
- const events = store.getStateEvents({
452
+ async ({ store_name, since_seconds, limit }) => {
453
+ const allEvents = store.getStateEvents({
444
454
  storeId: store_name,
445
455
  sinceSeconds: since_seconds
446
456
  });
457
+ const maxLimit = Math.min(limit ?? 200, 1e3);
458
+ const truncated = allEvents.length > maxLimit;
459
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
447
460
  const sessions = store.getSessionInfo();
448
461
  const sessionId = sessions[0]?.sessionId ?? null;
449
462
  const issues = [];
@@ -464,7 +477,7 @@ function registerStateTools(server, store) {
464
477
  }
465
478
  }
466
479
  const response = {
467
- summary: `Found ${events.length} state event(s)${since_seconds ? ` in the last ${since_seconds}s` : ""}${store_name ? ` for store "${store_name}"` : ""}.`,
480
+ summary: `Found ${events.length} state event(s)${truncated ? ` (showing ${maxLimit} of ${allEvents.length})` : ""}${since_seconds ? ` in the last ${since_seconds}s` : ""}${store_name ? ` for store "${store_name}"` : ""}.`,
468
481
  data: events.map((e) => ({
469
482
  storeId: e.storeId,
470
483
  library: e.library,
@@ -482,6 +495,8 @@ function registerStateTools(server, store) {
482
495
  to: events.length > 0 ? events[events.length - 1].timestamp : 0
483
496
  },
484
497
  eventCount: events.length,
498
+ totalCount: allEvents.length,
499
+ truncated,
485
500
  sessionId
486
501
  }
487
502
  };
@@ -752,17 +767,21 @@ function registerHarTools(server, store) {
752
767
  "capture_har",
753
768
  "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.",
754
769
  {
755
- since_seconds: z9.number().optional().describe("Only include requests from the last N seconds")
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)")
756
772
  },
757
- async ({ since_seconds }) => {
758
- const events = store.getNetworkRequests({
773
+ async ({ since_seconds, limit }) => {
774
+ const allEvents = store.getNetworkRequests({
759
775
  sinceSeconds: since_seconds
760
776
  });
777
+ const maxLimit = Math.min(limit ?? 200, 1e3);
778
+ const truncated = allEvents.length > maxLimit;
779
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
761
780
  const sessions = store.getSessionInfo();
762
781
  const sessionId = sessions[0]?.sessionId ?? null;
763
782
  const har = buildHar(events);
764
783
  const response = {
765
- summary: `HAR export: ${events.length} request(s)${since_seconds ? ` from the last ${since_seconds}s` : ""}. Import into Chrome DevTools or any HAR viewer.`,
784
+ 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.`,
766
785
  data: har,
767
786
  issues: [],
768
787
  metadata: {
@@ -771,6 +790,8 @@ function registerHarTools(server, store) {
771
790
  to: events.length > 0 ? events[events.length - 1].timestamp : 0
772
791
  },
773
792
  eventCount: events.length,
793
+ totalCount: allEvents.length,
794
+ truncated,
774
795
  sessionId
775
796
  }
776
797
  };
@@ -4127,6 +4148,30 @@ function buildReconEvents(url, title, sessionId, techResults, tokens, layout, a1
4127
4148
  }
4128
4149
 
4129
4150
  // src/scanner/index.ts
4151
+ var Semaphore = class {
4152
+ constructor(max) {
4153
+ this.max = max;
4154
+ }
4155
+ queue = [];
4156
+ active = 0;
4157
+ async acquire() {
4158
+ if (this.active < this.max) {
4159
+ this.active++;
4160
+ return;
4161
+ }
4162
+ return new Promise((resolve2) => {
4163
+ this.queue.push(resolve2);
4164
+ });
4165
+ }
4166
+ release() {
4167
+ this.active--;
4168
+ const next = this.queue.shift();
4169
+ if (next) {
4170
+ this.active++;
4171
+ next();
4172
+ }
4173
+ }
4174
+ };
4130
4175
  var PlaywrightScanner = class _PlaywrightScanner {
4131
4176
  db = null;
4132
4177
  jsGlobalPaths = [];
@@ -4136,6 +4181,7 @@ var PlaywrightScanner = class _PlaywrightScanner {
4136
4181
  static IDLE_TIMEOUT = 6e4;
4137
4182
  // Close browser after 60s idle
4138
4183
  lastScannedUrl = null;
4184
+ contextSemaphore = new Semaphore(2);
4139
4185
  /**
4140
4186
  * Lazily load the technology database.
4141
4187
  */
@@ -4200,12 +4246,14 @@ var PlaywrightScanner = class _PlaywrightScanner {
4200
4246
  const db = this.ensureDb();
4201
4247
  const { browser } = await this.ensureBrowser();
4202
4248
  const br = browser;
4203
- const context = await br.newContext({
4204
- viewport: { width: viewportWidth, height: viewportHeight },
4205
- userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
4206
- });
4207
- const page = await context.newPage();
4249
+ await this.contextSemaphore.acquire();
4250
+ let context = null;
4208
4251
  try {
4252
+ context = await br.newContext({
4253
+ viewport: { width: viewportWidth, height: viewportHeight },
4254
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
4255
+ });
4256
+ const page = await context.newPage();
4209
4257
  let mainResponse = null;
4210
4258
  page.on("response", (response) => {
4211
4259
  if (!mainResponse && response.request().resourceType() === "document") {
@@ -4266,7 +4314,9 @@ var PlaywrightScanner = class _PlaywrightScanner {
4266
4314
  scanDurationMs
4267
4315
  };
4268
4316
  } finally {
4269
- await context.close();
4317
+ if (context) await context.close().catch(() => {
4318
+ });
4319
+ this.contextSemaphore.release();
4270
4320
  }
4271
4321
  }
4272
4322
  /**
@@ -4282,15 +4332,19 @@ var PlaywrightScanner = class _PlaywrightScanner {
4282
4332
  async queryComputedStyles(url, selector, propertyFilter) {
4283
4333
  const { browser } = await this.ensureBrowser();
4284
4334
  const br = browser;
4285
- const context = await br.newContext({
4286
- viewport: { width: 1280, height: 720 }
4287
- });
4288
- const page = await context.newPage();
4335
+ await this.contextSemaphore.acquire();
4336
+ let context = null;
4289
4337
  try {
4338
+ context = await br.newContext({
4339
+ viewport: { width: 1280, height: 720 }
4340
+ });
4341
+ const page = await context.newPage();
4290
4342
  await page.goto(url, { waitUntil: "networkidle", timeout: 6e4 });
4291
4343
  return await collectComputedStyles(page, selector, propertyFilter);
4292
4344
  } finally {
4293
- await context.close();
4345
+ if (context) await context.close().catch(() => {
4346
+ });
4347
+ this.contextSemaphore.release();
4294
4348
  }
4295
4349
  }
4296
4350
  /**
@@ -4300,15 +4354,19 @@ var PlaywrightScanner = class _PlaywrightScanner {
4300
4354
  async queryElementSnapshot(url, selector, depth = 5) {
4301
4355
  const { browser } = await this.ensureBrowser();
4302
4356
  const br = browser;
4303
- const context = await br.newContext({
4304
- viewport: { width: 1280, height: 720 }
4305
- });
4306
- const page = await context.newPage();
4357
+ await this.contextSemaphore.acquire();
4358
+ let context = null;
4307
4359
  try {
4360
+ context = await br.newContext({
4361
+ viewport: { width: 1280, height: 720 }
4362
+ });
4363
+ const page = await context.newPage();
4308
4364
  await page.goto(url, { waitUntil: "networkidle", timeout: 6e4 });
4309
4365
  return await collectElementSnapshot(page, selector, depth);
4310
4366
  } finally {
4311
- await context.close();
4367
+ if (context) await context.close().catch(() => {
4368
+ });
4369
+ this.contextSemaphore.release();
4312
4370
  }
4313
4371
  }
4314
4372
  /**
@@ -4977,36 +5035,43 @@ async function main() {
4977
5035
  }
4978
5036
  }
4979
5037
  }, AUTO_SNAPSHOT_INTERVAL_MS);
4980
- const RETENTION_DAYS = parseInt(process.env.RUNTIMESCOPE_RETENTION_DAYS ?? "30", 10);
4981
- const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
4982
- for (const projectName of projectManager.listProjects()) {
4983
- const dbPath = projectManager.getProjectDbPath(projectName);
4984
- if (existsSync2(dbPath)) {
4985
- try {
4986
- const tempStore = new SqliteStore({ dbPath });
4987
- const deleted = tempStore.deleteOldEvents(cutoffMs);
4988
- if (deleted > 0) {
4989
- console.error(`[RuntimeScope] Pruned ${deleted} events older than ${RETENTION_DAYS}d from "${projectName}"`);
5038
+ if (isSqliteAvailable()) {
5039
+ const RETENTION_DAYS = parseInt(process.env.RUNTIMESCOPE_RETENTION_DAYS ?? "30", 10);
5040
+ const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
5041
+ for (const projectName of projectManager.listProjects()) {
5042
+ const dbPath = projectManager.getProjectDbPath(projectName);
5043
+ if (existsSync2(dbPath)) {
5044
+ try {
5045
+ const tempStore = new SqliteStore({ dbPath });
5046
+ const deleted = tempStore.deleteOldEvents(cutoffMs);
5047
+ if (deleted > 0) {
5048
+ console.error(`[RuntimeScope] Pruned ${deleted} events older than ${RETENTION_DAYS}d from "${projectName}"`);
5049
+ }
5050
+ tempStore.close();
5051
+ } catch {
4990
5052
  }
4991
- tempStore.close();
4992
- } catch {
4993
5053
  }
4994
5054
  }
4995
5055
  }
4996
- const pmDbPath = join2(projectManager.rootDir, "pm.db");
4997
- const pmStore = new PmStore({ dbPath: pmDbPath });
4998
- const discovery = new ProjectDiscovery(pmStore, projectManager);
4999
- discovery.discoverAll().then((result) => {
5000
- console.error(`[RuntimeScope] PM: ${result.projectsDiscovered} projects, ${result.sessionsDiscovered} sessions discovered`);
5001
- }).catch((err) => {
5002
- console.error("[RuntimeScope] PM discovery error:", err.message);
5003
- });
5056
+ let pmStore;
5057
+ let discovery;
5058
+ if (isSqliteAvailable()) {
5059
+ const pmDbPath = join2(projectManager.rootDir, "pm.db");
5060
+ pmStore = new PmStore({ dbPath: pmDbPath });
5061
+ discovery = new ProjectDiscovery(pmStore, projectManager);
5062
+ discovery.discoverAll().then((result) => {
5063
+ console.error(`[RuntimeScope] PM: ${result.projectsDiscovered} projects, ${result.sessionsDiscovered} sessions discovered`);
5064
+ }).catch((err) => {
5065
+ console.error("[RuntimeScope] PM discovery error:", err.message);
5066
+ });
5067
+ }
5004
5068
  const httpServer = new HttpServer(store, processMonitor, {
5005
5069
  authManager,
5006
5070
  allowedOrigins: corsOrigins,
5007
5071
  rateLimiter: collector.getRateLimiter(),
5008
5072
  pmStore,
5009
- discovery
5073
+ discovery,
5074
+ getConnectedSessions: () => collector.getConnectedSessions()
5010
5075
  });
5011
5076
  try {
5012
5077
  await httpServer.start({ port: HTTP_PORT2, tls: tlsConfig });
@@ -5068,7 +5133,7 @@ async function main() {
5068
5133
  await connectionManager.closeAll();
5069
5134
  await httpServer.stop();
5070
5135
  collector.stop();
5071
- pmStore.close();
5136
+ pmStore?.close();
5072
5137
  process.exit(0);
5073
5138
  };
5074
5139
  process.on("SIGINT", () => {