@runtimescope/mcp-server 0.7.2 → 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
@@ -4398,7 +4398,7 @@ function registerScannerTools(server, store, scanner) {
4398
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.",
4399
4399
  {
4400
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", "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.')
4402
4402
  },
4403
4403
  async ({ app_name, framework }) => {
4404
4404
  const scriptTagSnippet = `<!-- RuntimeScope \u2014 paste before </body> -->
@@ -4416,8 +4416,34 @@ RuntimeScope.init({
4416
4416
  appName: '${app_name}',
4417
4417
  endpoint: 'ws://localhost:${COLLECTOR_PORT}',
4418
4418
  });`;
4419
- const usesNpm = ["react", "vue", "angular", "svelte", "nextjs", "nuxt"].includes(framework);
4420
- 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;
4421
4447
  const placementHints = {
4422
4448
  html: "Paste the <script> tags before </body> in your HTML file(s).",
4423
4449
  react: "Add the import to your entry file (src/index.tsx or src/main.tsx), before ReactDOM.render/createRoot.",
@@ -4431,28 +4457,44 @@ RuntimeScope.init({
4431
4457
  rails: "Add the <script> tags to your application layout (app/views/layouts/application.html.erb) before </body>.",
4432
4458
  php: "Add the <script> tags to your layout/footer file before </body>.",
4433
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.",
4434
4461
  other: "Add the <script> tags to your HTML template before </body>. Works in any HTML page."
4435
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
+ ];
4436
4480
  const response = {
4437
- 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."}`,
4438
4482
  data: {
4439
4483
  snippet: primarySnippet,
4440
4484
  placement: placementHints[framework] || placementHints.other,
4441
- alternativeSnippet: usesNpm ? scriptTagSnippet : npmSnippet,
4442
- alternativeNote: usesNpm ? "If you prefer, you can also use a <script> tag instead of npm:" : "If the project uses npm/Node.js, you can also install via:",
4443
- requirements: [
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
+ ] : [
4444
4493
  "RuntimeScope MCP server must be running (it starts automatically with Claude Code)",
4445
4494
  `SDK bundle served at http://localhost:${HTTP_PORT}/runtimescope.js`,
4446
4495
  `WebSocket collector at ws://localhost:${COLLECTOR_PORT}`
4447
4496
  ],
4448
- whatItCaptures: [
4449
- "Network requests (fetch/XHR) with timing and headers",
4450
- "Console logs, warnings, and errors with stack traces",
4451
- "React/Vue/Svelte component renders (if applicable)",
4452
- "State store changes (Redux, Zustand, Pinia)",
4453
- "Web Vitals (LCP, FCP, CLS, TTFB, INP)",
4454
- "Unhandled errors and promise rejections"
4455
- ]
4497
+ whatItCaptures: isWorkers ? workersCaptures : browserCaptures
4456
4498
  },
4457
4499
  issues: [],
4458
4500
  metadata: { timeRange: { from: 0, to: 0 }, eventCount: 0, sessionId: null }
@@ -4781,8 +4823,167 @@ function dedup(arr, limit) {
4781
4823
  return result;
4782
4824
  }
4783
4825
 
4784
- // src/tools/history.ts
4826
+ // src/tools/breadcrumbs.ts
4785
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";
4786
4987
  var EVENT_TYPES = [
4787
4988
  "network",
4788
4989
  "console",
@@ -4821,13 +5022,13 @@ function registerHistoryTools(server, collector, projectManager) {
4821
5022
  "get_historical_events",
4822
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.",
4823
5024
  {
4824
- project: z27.string().describe("Project/app name (the appName used in SDK init)"),
4825
- event_types: z27.array(z27.enum(EVENT_TYPES)).optional().describe('Filter by event types (e.g., ["network", "console"])'),
4826
- since: z27.string().optional().describe('Start time \u2014 relative ("2h", "7d", "30m") or ISO date string'),
4827
- until: z27.string().optional().describe("End time \u2014 relative or ISO date string"),
4828
- session_id: z27.string().optional().describe("Filter by specific session ID"),
4829
- limit: z27.number().optional().default(200).describe("Max events to return (default 200, max 1000)"),
4830
- 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")
4831
5032
  },
4832
5033
  async ({ project, event_types, since, until, session_id, limit, offset }) => {
4833
5034
  const sqliteStore = collector.getSqliteStore(project);
@@ -5116,6 +5317,7 @@ async function main() {
5116
5317
  registerReconStyleDiffTools(mcp, store);
5117
5318
  registerScannerTools(mcp, store, scanner);
5118
5319
  registerCustomEventTools(mcp, store);
5320
+ registerBreadcrumbTools(mcp, store);
5119
5321
  registerHistoryTools(mcp, collector, projectManager);
5120
5322
  const transport = new StdioServerTransport();
5121
5323
  await mcp.connect(transport);