@runtimescope/mcp-server 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -36,15 +36,19 @@ function registerNetworkTools(server, store) {
36
36
  since_seconds: z.number().optional().describe("Only return requests from the last N seconds"),
37
37
  url_pattern: z.string().optional().describe("Filter by URL substring match"),
38
38
  status: z.number().optional().describe("Filter by HTTP status code"),
39
- 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)")
40
41
  },
41
- async ({ since_seconds, url_pattern, status, method }) => {
42
- const events = store.getNetworkRequests({
42
+ async ({ since_seconds, url_pattern, status, method, limit }) => {
43
+ const allEvents = store.getNetworkRequests({
43
44
  sinceSeconds: since_seconds,
44
45
  urlPattern: url_pattern,
45
46
  status,
46
47
  method
47
48
  });
49
+ const maxLimit = Math.min(limit ?? 200, 1e3);
50
+ const truncated = allEvents.length > maxLimit;
51
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
48
52
  const timeRange = events.length > 0 ? { from: events[events.length - 1].timestamp, to: events[0].timestamp } : { from: 0, to: 0 };
49
53
  const sessions = store.getSessionInfo();
50
54
  const sessionId = sessions[0]?.sessionId ?? null;
@@ -72,7 +76,7 @@ function registerNetworkTools(server, store) {
72
76
  }
73
77
  }
74
78
  const response = {
75
- 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.`,
76
80
  data: events.map((e) => ({
77
81
  url: e.url,
78
82
  method: e.method,
@@ -85,7 +89,7 @@ function registerNetworkTools(server, store) {
85
89
  timestamp: new Date(e.timestamp).toISOString()
86
90
  })),
87
91
  issues,
88
- metadata: { timeRange, eventCount: events.length, sessionId }
92
+ metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId }
89
93
  };
90
94
  return {
91
95
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
@@ -103,14 +107,18 @@ function registerConsoleTools(server, store) {
103
107
  {
104
108
  level: z2.enum(["log", "warn", "error", "info", "debug", "trace"]).optional().describe("Filter by console level"),
105
109
  since_seconds: z2.number().optional().describe("Only return messages from the last N seconds"),
106
- 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)")
107
112
  },
108
- async ({ level, since_seconds, search }) => {
109
- const events = store.getConsoleMessages({
113
+ async ({ level, since_seconds, search, limit }) => {
114
+ const allEvents = store.getConsoleMessages({
110
115
  level,
111
116
  sinceSeconds: since_seconds,
112
117
  search
113
118
  });
119
+ const maxLimit = Math.min(limit ?? 200, 1e3);
120
+ const truncated = allEvents.length > maxLimit;
121
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
114
122
  const timeRange = events.length > 0 ? { from: events[events.length - 1].timestamp, to: events[0].timestamp } : { from: 0, to: 0 };
115
123
  const sessions = store.getSessionInfo();
116
124
  const sessionId = sessions[0]?.sessionId ?? null;
@@ -135,12 +143,12 @@ function registerConsoleTools(server, store) {
135
143
  }
136
144
  for (const [msg, info] of errorMessages) {
137
145
  if (info.count > 5 && info.last - info.first < 1e4) {
138
- const truncated = msg.length > 80 ? msg.slice(0, 80) + "..." : msg;
139
- 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`);
140
148
  }
141
149
  }
142
150
  const response = {
143
- 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}` : ""}.`,
144
152
  data: events.map((e) => ({
145
153
  level: e.level,
146
154
  message: e.message,
@@ -150,7 +158,7 @@ function registerConsoleTools(server, store) {
150
158
  timestamp: new Date(e.timestamp).toISOString()
151
159
  })),
152
160
  issues,
153
- metadata: { timeRange, eventCount: events.length, sessionId }
161
+ metadata: { timeRange, eventCount: events.length, totalCount: allEvents.length, truncated, sessionId }
154
162
  };
155
163
  return {
156
164
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
@@ -438,13 +446,17 @@ function registerStateTools(server, store) {
438
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.",
439
447
  {
440
448
  store_name: z5.string().optional().describe("Filter by store name/ID"),
441
- 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)")
442
451
  },
443
- async ({ store_name, since_seconds }) => {
444
- const events = store.getStateEvents({
452
+ async ({ store_name, since_seconds, limit }) => {
453
+ const allEvents = store.getStateEvents({
445
454
  storeId: store_name,
446
455
  sinceSeconds: since_seconds
447
456
  });
457
+ const maxLimit = Math.min(limit ?? 200, 1e3);
458
+ const truncated = allEvents.length > maxLimit;
459
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
448
460
  const sessions = store.getSessionInfo();
449
461
  const sessionId = sessions[0]?.sessionId ?? null;
450
462
  const issues = [];
@@ -465,7 +477,7 @@ function registerStateTools(server, store) {
465
477
  }
466
478
  }
467
479
  const response = {
468
- 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}"` : ""}.`,
469
481
  data: events.map((e) => ({
470
482
  storeId: e.storeId,
471
483
  library: e.library,
@@ -483,6 +495,8 @@ function registerStateTools(server, store) {
483
495
  to: events.length > 0 ? events[events.length - 1].timestamp : 0
484
496
  },
485
497
  eventCount: events.length,
498
+ totalCount: allEvents.length,
499
+ truncated,
486
500
  sessionId
487
501
  }
488
502
  };
@@ -753,17 +767,21 @@ function registerHarTools(server, store) {
753
767
  "capture_har",
754
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.",
755
769
  {
756
- 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)")
757
772
  },
758
- async ({ since_seconds }) => {
759
- const events = store.getNetworkRequests({
773
+ async ({ since_seconds, limit }) => {
774
+ const allEvents = store.getNetworkRequests({
760
775
  sinceSeconds: since_seconds
761
776
  });
777
+ const maxLimit = Math.min(limit ?? 200, 1e3);
778
+ const truncated = allEvents.length > maxLimit;
779
+ const events = truncated ? allEvents.slice(0, maxLimit) : allEvents;
762
780
  const sessions = store.getSessionInfo();
763
781
  const sessionId = sessions[0]?.sessionId ?? null;
764
782
  const har = buildHar(events);
765
783
  const response = {
766
- 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.`,
767
785
  data: har,
768
786
  issues: [],
769
787
  metadata: {
@@ -772,6 +790,8 @@ function registerHarTools(server, store) {
772
790
  to: events.length > 0 ? events[events.length - 1].timestamp : 0
773
791
  },
774
792
  eventCount: events.length,
793
+ totalCount: allEvents.length,
794
+ truncated,
775
795
  sessionId
776
796
  }
777
797
  };
@@ -4128,6 +4148,30 @@ function buildReconEvents(url, title, sessionId, techResults, tokens, layout, a1
4128
4148
  }
4129
4149
 
4130
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
+ };
4131
4175
  var PlaywrightScanner = class _PlaywrightScanner {
4132
4176
  db = null;
4133
4177
  jsGlobalPaths = [];
@@ -4137,6 +4181,7 @@ var PlaywrightScanner = class _PlaywrightScanner {
4137
4181
  static IDLE_TIMEOUT = 6e4;
4138
4182
  // Close browser after 60s idle
4139
4183
  lastScannedUrl = null;
4184
+ contextSemaphore = new Semaphore(2);
4140
4185
  /**
4141
4186
  * Lazily load the technology database.
4142
4187
  */
@@ -4201,12 +4246,14 @@ var PlaywrightScanner = class _PlaywrightScanner {
4201
4246
  const db = this.ensureDb();
4202
4247
  const { browser } = await this.ensureBrowser();
4203
4248
  const br = browser;
4204
- const context = await br.newContext({
4205
- viewport: { width: viewportWidth, height: viewportHeight },
4206
- 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"
4207
- });
4208
- const page = await context.newPage();
4249
+ await this.contextSemaphore.acquire();
4250
+ let context = null;
4209
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();
4210
4257
  let mainResponse = null;
4211
4258
  page.on("response", (response) => {
4212
4259
  if (!mainResponse && response.request().resourceType() === "document") {
@@ -4267,7 +4314,9 @@ var PlaywrightScanner = class _PlaywrightScanner {
4267
4314
  scanDurationMs
4268
4315
  };
4269
4316
  } finally {
4270
- await context.close();
4317
+ if (context) await context.close().catch(() => {
4318
+ });
4319
+ this.contextSemaphore.release();
4271
4320
  }
4272
4321
  }
4273
4322
  /**
@@ -4283,15 +4332,19 @@ var PlaywrightScanner = class _PlaywrightScanner {
4283
4332
  async queryComputedStyles(url, selector, propertyFilter) {
4284
4333
  const { browser } = await this.ensureBrowser();
4285
4334
  const br = browser;
4286
- const context = await br.newContext({
4287
- viewport: { width: 1280, height: 720 }
4288
- });
4289
- const page = await context.newPage();
4335
+ await this.contextSemaphore.acquire();
4336
+ let context = null;
4290
4337
  try {
4338
+ context = await br.newContext({
4339
+ viewport: { width: 1280, height: 720 }
4340
+ });
4341
+ const page = await context.newPage();
4291
4342
  await page.goto(url, { waitUntil: "networkidle", timeout: 6e4 });
4292
4343
  return await collectComputedStyles(page, selector, propertyFilter);
4293
4344
  } finally {
4294
- await context.close();
4345
+ if (context) await context.close().catch(() => {
4346
+ });
4347
+ this.contextSemaphore.release();
4295
4348
  }
4296
4349
  }
4297
4350
  /**
@@ -4301,15 +4354,19 @@ var PlaywrightScanner = class _PlaywrightScanner {
4301
4354
  async queryElementSnapshot(url, selector, depth = 5) {
4302
4355
  const { browser } = await this.ensureBrowser();
4303
4356
  const br = browser;
4304
- const context = await br.newContext({
4305
- viewport: { width: 1280, height: 720 }
4306
- });
4307
- const page = await context.newPage();
4357
+ await this.contextSemaphore.acquire();
4358
+ let context = null;
4308
4359
  try {
4360
+ context = await br.newContext({
4361
+ viewport: { width: 1280, height: 720 }
4362
+ });
4363
+ const page = await context.newPage();
4309
4364
  await page.goto(url, { waitUntil: "networkidle", timeout: 6e4 });
4310
4365
  return await collectElementSnapshot(page, selector, depth);
4311
4366
  } finally {
4312
- await context.close();
4367
+ if (context) await context.close().catch(() => {
4368
+ });
4369
+ this.contextSemaphore.release();
4313
4370
  }
4314
4371
  }
4315
4372
  /**
@@ -4341,7 +4398,7 @@ function registerScannerTools(server, store, scanner) {
4341
4398
  "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.",
4342
4399
  {
4343
4400
  app_name: z25.string().optional().default("my-app").describe('Name for the app in RuntimeScope (e.g., "echo-frontend", "dashboard")'),
4344
- framework: z25.enum(["html", "react", "vue", "angular", "svelte", "nextjs", "nuxt", "flask", "django", "rails", "php", "wordpress", "other"]).optional().default("html").describe('The framework/tech stack of the project. Use "html" for any plain HTML or server-rendered pages.')
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.')
4345
4402
  },
4346
4403
  async ({ app_name, framework }) => {
4347
4404
  const scriptTagSnippet = `<!-- RuntimeScope \u2014 paste before </body> -->
@@ -4359,8 +4416,34 @@ RuntimeScope.init({
4359
4416
  appName: '${app_name}',
4360
4417
  endpoint: 'ws://localhost:${COLLECTOR_PORT}',
4361
4418
  });`;
4362
- const usesNpm = ["react", "vue", "angular", "svelte", "nextjs", "nuxt"].includes(framework);
4363
- const primarySnippet = usesNpm ? npmSnippet : scriptTagSnippet;
4419
+ const workersSnippet = `// npm install @runtimescope/workers-sdk
4420
+ import { withRuntimeScope, scopeD1, scopeKV, scopeR2, track, addBreadcrumb } from '@runtimescope/workers-sdk';
4421
+
4422
+ export default withRuntimeScope({
4423
+ async fetch(request, env, ctx) {
4424
+ // Instrument bindings (optional \u2014 use the ones you have)
4425
+ // const db = scopeD1(env.DB);
4426
+ // const kv = scopeKV(env.KV);
4427
+ // const bucket = scopeR2(env.BUCKET);
4428
+
4429
+ // Track custom events
4430
+ // track('request.processed', { path: new URL(request.url).pathname });
4431
+
4432
+ // Add breadcrumbs for debugging
4433
+ // addBreadcrumb('handler started', { method: request.method });
4434
+
4435
+ return new Response('Hello!');
4436
+ },
4437
+ }, {
4438
+ appName: '${app_name}',
4439
+ httpEndpoint: 'http://localhost:${HTTP_PORT}/api/events',
4440
+ // captureConsole: true, // Capture console.log/warn/error (default: true)
4441
+ // captureHeaders: false, // Include request/response headers (default: false)
4442
+ // sampleRate: 1.0, // 0.0-1.0 probabilistic sampling (default: 1.0)
4443
+ });`;
4444
+ const isWorkers = framework === "workers";
4445
+ const usesNpm = isWorkers || ["react", "vue", "angular", "svelte", "nextjs", "nuxt"].includes(framework);
4446
+ const primarySnippet = isWorkers ? workersSnippet : usesNpm ? npmSnippet : scriptTagSnippet;
4364
4447
  const placementHints = {
4365
4448
  html: "Paste the <script> tags before </body> in your HTML file(s).",
4366
4449
  react: "Add the import to your entry file (src/index.tsx or src/main.tsx), before ReactDOM.render/createRoot.",
@@ -4374,28 +4457,44 @@ RuntimeScope.init({
4374
4457
  rails: "Add the <script> tags to your application layout (app/views/layouts/application.html.erb) before </body>.",
4375
4458
  php: "Add the <script> tags to your layout/footer file before </body>.",
4376
4459
  wordpress: "Add the <script> tags to your theme's footer.php before </body>, or use a custom HTML plugin.",
4460
+ workers: "Wrap your default export with withRuntimeScope in your Worker entry file (src/index.ts). Enable nodejs_compat in wrangler.toml.",
4377
4461
  other: "Add the <script> tags to your HTML template before </body>. Works in any HTML page."
4378
4462
  };
4463
+ const workersCaptures = [
4464
+ "Incoming HTTP requests with timing, status, and Cloudflare properties",
4465
+ "Console logs, warnings, and errors with stack traces",
4466
+ "D1 database queries with SQL parsing, timing, and N+1 detection",
4467
+ "KV namespace operations (get/put/delete/list) with timing",
4468
+ "R2 bucket operations (get/put/delete/list/head) with size tracking",
4469
+ "Custom business events via track()",
4470
+ "Request breadcrumbs via addBreadcrumb()"
4471
+ ];
4472
+ const browserCaptures = [
4473
+ "Network requests (fetch/XHR) with timing and headers",
4474
+ "Console logs, warnings, and errors with stack traces",
4475
+ "React/Vue/Svelte component renders (if applicable)",
4476
+ "State store changes (Redux, Zustand, Pinia)",
4477
+ "Web Vitals (LCP, FCP, CLS, TTFB, INP)",
4478
+ "Unhandled errors and promise rejections"
4479
+ ];
4379
4480
  const response = {
4380
- summary: `SDK snippet for ${framework} project "${app_name}". ${usesNpm ? "Uses npm import." : "Uses <script> tag \u2014 no build system required."}`,
4481
+ summary: isWorkers ? `Workers SDK snippet for Cloudflare Worker "${app_name}". Captures requests, D1/KV/R2 operations, console, custom events, and breadcrumbs.` : `SDK snippet for ${framework} project "${app_name}". ${usesNpm ? "Uses npm import." : "Uses <script> tag \u2014 no build system required."}`,
4381
4482
  data: {
4382
4483
  snippet: primarySnippet,
4383
4484
  placement: placementHints[framework] || placementHints.other,
4384
- alternativeSnippet: usesNpm ? scriptTagSnippet : npmSnippet,
4385
- alternativeNote: usesNpm ? "If you prefer, you can also use a <script> tag instead of npm:" : "If the project uses npm/Node.js, you can also install via:",
4386
- requirements: [
4485
+ alternativeSnippet: isWorkers ? void 0 : usesNpm ? scriptTagSnippet : npmSnippet,
4486
+ alternativeNote: isWorkers ? void 0 : usesNpm ? "If you prefer, you can also use a <script> tag instead of npm:" : "If the project uses npm/Node.js, you can also install via:",
4487
+ requirements: isWorkers ? [
4488
+ "RuntimeScope collector must be reachable from your Worker",
4489
+ `HTTP collector endpoint at http://localhost:${HTTP_PORT}/api/events`,
4490
+ "Add nodejs_compat to compatibility_flags in wrangler.toml",
4491
+ "For production: set httpEndpoint to your hosted collector URL"
4492
+ ] : [
4387
4493
  "RuntimeScope MCP server must be running (it starts automatically with Claude Code)",
4388
4494
  `SDK bundle served at http://localhost:${HTTP_PORT}/runtimescope.js`,
4389
4495
  `WebSocket collector at ws://localhost:${COLLECTOR_PORT}`
4390
4496
  ],
4391
- whatItCaptures: [
4392
- "Network requests (fetch/XHR) with timing and headers",
4393
- "Console logs, warnings, and errors with stack traces",
4394
- "React/Vue/Svelte component renders (if applicable)",
4395
- "State store changes (Redux, Zustand, Pinia)",
4396
- "Web Vitals (LCP, FCP, CLS, TTFB, INP)",
4397
- "Unhandled errors and promise rejections"
4398
- ]
4497
+ whatItCaptures: isWorkers ? workersCaptures : browserCaptures
4399
4498
  },
4400
4499
  issues: [],
4401
4500
  metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
@@ -4724,8 +4823,167 @@ function dedup(arr, limit) {
4724
4823
  return result;
4725
4824
  }
4726
4825
 
4727
- // src/tools/history.ts
4826
+ // src/tools/breadcrumbs.ts
4728
4827
  import { z as z27 } from "zod";
4828
+ function eventToBreadcrumb(event, anchorTs) {
4829
+ const base = {
4830
+ timestamp: new Date(event.timestamp).toISOString(),
4831
+ relativeMs: event.timestamp - anchorTs
4832
+ };
4833
+ switch (event.eventType) {
4834
+ case "navigation": {
4835
+ const nav = event;
4836
+ return {
4837
+ ...base,
4838
+ category: "navigation",
4839
+ level: "info",
4840
+ message: `${nav.trigger}: ${nav.to}`,
4841
+ data: { from: nav.from }
4842
+ };
4843
+ }
4844
+ case "ui": {
4845
+ const ui = event;
4846
+ if (ui.action === "click") {
4847
+ return {
4848
+ ...base,
4849
+ category: "ui.click",
4850
+ level: "info",
4851
+ message: ui.text ? `Click: ${ui.text}` : `Click: ${ui.target}`,
4852
+ data: { target: ui.target }
4853
+ };
4854
+ }
4855
+ return {
4856
+ ...base,
4857
+ category: "breadcrumb",
4858
+ level: "info",
4859
+ message: ui.text ?? ui.target,
4860
+ ...ui.data && { data: ui.data }
4861
+ };
4862
+ }
4863
+ case "console": {
4864
+ const con = event;
4865
+ const level = con.level === "error" ? "error" : con.level === "warn" ? "warning" : con.level === "debug" || con.level === "trace" ? "debug" : "info";
4866
+ return {
4867
+ ...base,
4868
+ category: `console.${con.level}`,
4869
+ level,
4870
+ message: con.message.slice(0, 200),
4871
+ ...con.stackTrace && { data: { hasStack: true } }
4872
+ };
4873
+ }
4874
+ case "network": {
4875
+ const net = event;
4876
+ const level = net.errorPhase ? "error" : net.status >= 400 ? "warning" : "info";
4877
+ const url = new URL(net.url, "http://localhost").pathname;
4878
+ return {
4879
+ ...base,
4880
+ category: "http",
4881
+ level,
4882
+ message: `${net.method} ${url} \u2192 ${net.status || net.errorPhase || "pending"}`,
4883
+ data: { duration: net.duration, status: net.status }
4884
+ };
4885
+ }
4886
+ case "state": {
4887
+ const st = event;
4888
+ if (st.phase === "init") return null;
4889
+ const changedKeys = st.diff ? Object.keys(st.diff).join(", ") : "unknown";
4890
+ return {
4891
+ ...base,
4892
+ category: "state",
4893
+ level: "debug",
4894
+ message: `${st.storeId}: ${changedKeys}`,
4895
+ data: { library: st.library }
4896
+ };
4897
+ }
4898
+ case "custom": {
4899
+ const cust = event;
4900
+ return {
4901
+ ...base,
4902
+ category: `custom.${cust.name}`,
4903
+ level: "info",
4904
+ message: cust.name,
4905
+ ...cust.properties && { data: cust.properties }
4906
+ };
4907
+ }
4908
+ default:
4909
+ return null;
4910
+ }
4911
+ }
4912
+ var MAX_BREADCRUMBS = 200;
4913
+ function registerBreadcrumbTools(server, store) {
4914
+ server.tool(
4915
+ "get_breadcrumbs",
4916
+ "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
+ {
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})`)
4924
+ },
4925
+ async ({ since_seconds, session_id, before_timestamp, categories, level, limit }) => {
4926
+ const sinceSeconds = since_seconds ?? 60;
4927
+ const maxItems = Math.min(limit ?? MAX_BREADCRUMBS, MAX_BREADCRUMBS);
4928
+ const allEvents = store.getEventTimeline({
4929
+ sinceSeconds,
4930
+ sessionId: session_id,
4931
+ eventTypes: ["navigation", "ui", "console", "network", "state", "custom"]
4932
+ });
4933
+ const anchor = before_timestamp ?? (allEvents.length > 0 ? allEvents[allEvents.length - 1].timestamp : Date.now());
4934
+ const filtered = before_timestamp ? allEvents.filter((e) => e.timestamp <= before_timestamp) : allEvents;
4935
+ let breadcrumbs = [];
4936
+ for (const event of filtered) {
4937
+ const bc = eventToBreadcrumb(event, anchor);
4938
+ if (bc) breadcrumbs.push(bc);
4939
+ }
4940
+ if (categories && categories.length > 0) {
4941
+ const catSet = new Set(categories);
4942
+ breadcrumbs = breadcrumbs.filter((bc) => {
4943
+ return catSet.has(bc.category) || Array.from(catSet).some((cat) => bc.category.startsWith(cat + "."));
4944
+ });
4945
+ }
4946
+ if (level) {
4947
+ const levelOrder = { debug: 0, info: 1, warning: 2, error: 3 };
4948
+ const minLevel = levelOrder[level];
4949
+ breadcrumbs = breadcrumbs.filter((bc) => levelOrder[bc.level] >= minLevel);
4950
+ }
4951
+ if (breadcrumbs.length > maxItems) {
4952
+ breadcrumbs = breadcrumbs.slice(-maxItems);
4953
+ }
4954
+ const lastError = breadcrumbs.findLast((bc) => bc.level === "error");
4955
+ const sessions = store.getSessionInfo();
4956
+ const sessionId = session_id ?? sessions[0]?.sessionId ?? null;
4957
+ const response = {
4958
+ summary: `${breadcrumbs.length} breadcrumbs over the last ${sinceSeconds}s${lastError ? ` \u2014 last error: "${lastError.message.slice(0, 80)}"` : ""}`,
4959
+ data: breadcrumbs,
4960
+ metadata: {
4961
+ timeRange: {
4962
+ from: breadcrumbs.length > 0 ? breadcrumbs[0].relativeMs : 0,
4963
+ to: breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1].relativeMs : 0
4964
+ },
4965
+ eventCount: breadcrumbs.length,
4966
+ sessionId,
4967
+ anchor: new Date(anchor).toISOString(),
4968
+ categoryCounts: countCategories(breadcrumbs)
4969
+ }
4970
+ };
4971
+ return {
4972
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
4973
+ };
4974
+ }
4975
+ );
4976
+ }
4977
+ function countCategories(breadcrumbs) {
4978
+ const counts = {};
4979
+ for (const bc of breadcrumbs) {
4980
+ counts[bc.category] = (counts[bc.category] ?? 0) + 1;
4981
+ }
4982
+ return counts;
4983
+ }
4984
+
4985
+ // src/tools/history.ts
4986
+ import { z as z28 } from "zod";
4729
4987
  var EVENT_TYPES = [
4730
4988
  "network",
4731
4989
  "console",
@@ -4764,13 +5022,13 @@ function registerHistoryTools(server, collector, projectManager) {
4764
5022
  "get_historical_events",
4765
5023
  "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.",
4766
5024
  {
4767
- project: z27.string().describe("Project/app name (the appName used in SDK init)"),
4768
- event_types: z27.array(z27.enum(EVENT_TYPES)).optional().describe('Filter by event types (e.g., ["network", "console"])'),
4769
- since: z27.string().optional().describe('Start time \u2014 relative ("2h", "7d", "30m") or ISO date string'),
4770
- until: z27.string().optional().describe("End time \u2014 relative or ISO date string"),
4771
- session_id: z27.string().optional().describe("Filter by specific session ID"),
4772
- limit: z27.number().optional().default(200).describe("Max events to return (default 200, max 1000)"),
4773
- offset: z27.number().optional().default(0).describe("Pagination offset")
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")
4774
5032
  },
4775
5033
  async ({ project, event_types, since, until, session_id, limit, offset }) => {
4776
5034
  const sqliteStore = collector.getSqliteStore(project);
@@ -5059,6 +5317,7 @@ async function main() {
5059
5317
  registerReconStyleDiffTools(mcp, store);
5060
5318
  registerScannerTools(mcp, store, scanner);
5061
5319
  registerCustomEventTools(mcp, store);
5320
+ registerBreadcrumbTools(mcp, store);
5062
5321
  registerHistoryTools(mcp, collector, projectManager);
5063
5322
  const transport = new StdioServerTransport();
5064
5323
  await mcp.connect(transport);