@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 +318 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
139
|
-
issues.push(`Error spam: "${
|
|
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
|
|
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
|
|
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
|
-
|
|
4205
|
-
|
|
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
|
-
|
|
4287
|
-
|
|
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
|
-
|
|
4305
|
-
|
|
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
|
|
4363
|
-
|
|
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/
|
|
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:
|
|
4768
|
-
event_types:
|
|
4769
|
-
since:
|
|
4770
|
-
until:
|
|
4771
|
-
session_id:
|
|
4772
|
-
limit:
|
|
4773
|
-
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);
|