@harness-fe/mcp-server 3.1.0 → 3.2.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/mcp.js CHANGED
@@ -320,6 +320,48 @@ function registerTools(server, bridge) {
320
320
  const out = await bridge.sendCommand(COMMAND.STORAGE_TAIL, { n: n ?? 20, filter, match, which, op, key }, { tabId });
321
321
  return ok(out);
322
322
  });
323
+ server.registerTool(COMMAND.NAVIGATION_TAIL, {
324
+ description: 'Return the last N navigation events captured by the runtime: history.pushState / replaceState, popstate, hashchange, and location.href / location.hash / location.assign() / location.replace(). Each entry has `kind`, `url`, `replace`, and an `initiator.stack` for interceptable kinds. Filter via `filter` (against {kind, url, replace}) or narrow with `kind`. Buffer is in-memory and cleared on navigate — use `session.tail` (type=["navigation"]) for cross-navigate history.',
325
+ inputSchema: {
326
+ n: z.number().int().positive().default(20).optional(),
327
+ filter: filterParam,
328
+ match: matchParam,
329
+ kind: z.enum(['push', 'replace', 'pop', 'hash', 'assign']).optional(),
330
+ tabId: tabIdParam,
331
+ },
332
+ }, async ({ n, filter, match, kind, tabId }) => {
333
+ const out = await bridge.sendCommand(COMMAND.NAVIGATION_TAIL, { n: n ?? 20, filter, match, kind }, { tabId });
334
+ return ok(out);
335
+ });
336
+ server.registerTool(COMMAND.GLOBALS_TAIL, {
337
+ description: 'Return the last N read/writes to watched window globals. Only fires for keys registered via the install opts `globals.watch` list — global pollution detection or app-state debugging. Each entry has op=get|set|delete, key, value, previousValue (on set), and initiator.stack. Filter via `filter` or narrow with `op` / `key`. Buffer is in-memory; cross-navigate use `session.tail` (type=["globals"]).',
338
+ inputSchema: {
339
+ n: z.number().int().positive().default(20).optional(),
340
+ filter: filterParam,
341
+ match: matchParam,
342
+ op: z.enum(['get', 'set', 'delete']).optional(),
343
+ key: z.string().optional().describe('Exact window key match.'),
344
+ tabId: tabIdParam,
345
+ },
346
+ }, async ({ n, filter, match, op, key, tabId }) => {
347
+ const out = await bridge.sendCommand(COMMAND.GLOBALS_TAIL, { n: n ?? 20, filter, match, op, key }, { tabId });
348
+ return ok(out);
349
+ });
350
+ server.registerTool(COMMAND.INDEXEDDB_TAIL, {
351
+ description: 'Return the last N IndexedDB operations: open / put / add / get / getAll / delete / clear / cursor. Each entry has `op`, `store`, `key`, `value`, `db`, `version`, `success` and `initiator.stack`. Useful for tracking who reads/writes which IDB store key. Filter via `filter` (against {op, store, key}) or narrow with `op` / `store` / `db`. Buffer is in-memory; cross-navigate use `session.tail` (type=["indexeddb"]).',
352
+ inputSchema: {
353
+ n: z.number().int().positive().default(20).optional(),
354
+ filter: filterParam,
355
+ match: matchParam,
356
+ op: z.enum(['open', 'put', 'add', 'get', 'getAll', 'delete', 'clear', 'cursor']).optional(),
357
+ store: z.string().optional().describe('Exact object-store name.'),
358
+ db: z.string().optional().describe('Exact database name (open events only).'),
359
+ tabId: tabIdParam,
360
+ },
361
+ }, async ({ n, filter, match, op, store, db, tabId }) => {
362
+ const out = await bridge.sendCommand(COMMAND.INDEXEDDB_TAIL, { n: n ?? 20, filter, match, op, store, db }, { tabId });
363
+ return ok(out);
364
+ });
323
365
  server.registerTool(COMMAND.TAB_LIST, {
324
366
  description: 'List all currently connected browser tabs.',
325
367
  inputSchema: {},
@@ -19,7 +19,7 @@
19
19
  import type { Task, VisitorEnv } from '@harness-fe/protocol';
20
20
  export type { EventId, EventStore, StreamId, } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
21
21
  /** Short type codes used in JSONL lines to keep files compact. */
22
- export type EventType = 'log' | 'err' | 'req' | 'res' | 'cmd' | 'resp' | 'hmr' | 'task' | 'task:claim' | 'task:resolve' | 'rrweb' | 'node:log' | 'node:err' | 'note' | 'load' | 'storage' | 'ws' | 'server-log' | 'server-err' | 'server-action' | 'app-log' | string;
22
+ export type EventType = 'log' | 'err' | 'req' | 'res' | 'cmd' | 'resp' | 'hmr' | 'task' | 'task:claim' | 'task:resolve' | 'rrweb' | 'node:log' | 'node:err' | 'note' | 'load' | 'storage' | 'ws' | 'navigation' | 'globals' | 'indexeddb' | 'server-log' | 'server-err' | 'server-action' | 'app-log' | string;
23
23
  /** A single event line in a JSONL file. Carries row-level projectId/buildId tags. */
24
24
  export interface StoreEvent {
25
25
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-fe/mcp-server",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Unified MCP daemon: stdio MCP for AI agents + WS bridge for Vite plugin and runtime client.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "ws": "^8.18.0",
40
40
  "zod": "^4.4.3",
41
41
  "@harness-fe/dashboard-ui": "0.2.0",
42
- "@harness-fe/protocol": "3.1.0"
42
+ "@harness-fe/protocol": "3.2.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/ws": "^8.5.10",
package/src/mcp.ts CHANGED
@@ -505,6 +505,78 @@ function registerTools(server: McpServer, bridge: IBridge): void {
505
505
  },
506
506
  );
507
507
 
508
+ server.registerTool(
509
+ COMMAND.NAVIGATION_TAIL,
510
+ {
511
+ description:
512
+ 'Return the last N navigation events captured by the runtime: history.pushState / replaceState, popstate, hashchange, and location.href / location.hash / location.assign() / location.replace(). Each entry has `kind`, `url`, `replace`, and an `initiator.stack` for interceptable kinds. Filter via `filter` (against {kind, url, replace}) or narrow with `kind`. Buffer is in-memory and cleared on navigate — use `session.tail` (type=["navigation"]) for cross-navigate history.',
513
+ inputSchema: {
514
+ n: z.number().int().positive().default(20).optional(),
515
+ filter: filterParam,
516
+ match: matchParam,
517
+ kind: z.enum(['push', 'replace', 'pop', 'hash', 'assign']).optional(),
518
+ tabId: tabIdParam,
519
+ },
520
+ },
521
+ async ({ n, filter, match, kind, tabId }) => {
522
+ const out = await bridge.sendCommand(
523
+ COMMAND.NAVIGATION_TAIL,
524
+ { n: n ?? 20, filter, match, kind },
525
+ { tabId },
526
+ );
527
+ return ok(out);
528
+ },
529
+ );
530
+
531
+ server.registerTool(
532
+ COMMAND.GLOBALS_TAIL,
533
+ {
534
+ description:
535
+ 'Return the last N read/writes to watched window globals. Only fires for keys registered via the install opts `globals.watch` list — global pollution detection or app-state debugging. Each entry has op=get|set|delete, key, value, previousValue (on set), and initiator.stack. Filter via `filter` or narrow with `op` / `key`. Buffer is in-memory; cross-navigate use `session.tail` (type=["globals"]).',
536
+ inputSchema: {
537
+ n: z.number().int().positive().default(20).optional(),
538
+ filter: filterParam,
539
+ match: matchParam,
540
+ op: z.enum(['get', 'set', 'delete']).optional(),
541
+ key: z.string().optional().describe('Exact window key match.'),
542
+ tabId: tabIdParam,
543
+ },
544
+ },
545
+ async ({ n, filter, match, op, key, tabId }) => {
546
+ const out = await bridge.sendCommand(
547
+ COMMAND.GLOBALS_TAIL,
548
+ { n: n ?? 20, filter, match, op, key },
549
+ { tabId },
550
+ );
551
+ return ok(out);
552
+ },
553
+ );
554
+
555
+ server.registerTool(
556
+ COMMAND.INDEXEDDB_TAIL,
557
+ {
558
+ description:
559
+ 'Return the last N IndexedDB operations: open / put / add / get / getAll / delete / clear / cursor. Each entry has `op`, `store`, `key`, `value`, `db`, `version`, `success` and `initiator.stack`. Useful for tracking who reads/writes which IDB store key. Filter via `filter` (against {op, store, key}) or narrow with `op` / `store` / `db`. Buffer is in-memory; cross-navigate use `session.tail` (type=["indexeddb"]).',
560
+ inputSchema: {
561
+ n: z.number().int().positive().default(20).optional(),
562
+ filter: filterParam,
563
+ match: matchParam,
564
+ op: z.enum(['open', 'put', 'add', 'get', 'getAll', 'delete', 'clear', 'cursor']).optional(),
565
+ store: z.string().optional().describe('Exact object-store name.'),
566
+ db: z.string().optional().describe('Exact database name (open events only).'),
567
+ tabId: tabIdParam,
568
+ },
569
+ },
570
+ async ({ n, filter, match, op, store, db, tabId }) => {
571
+ const out = await bridge.sendCommand(
572
+ COMMAND.INDEXEDDB_TAIL,
573
+ { n: n ?? 20, filter, match, op, store, db },
574
+ { tabId },
575
+ );
576
+ return ok(out);
577
+ },
578
+ );
579
+
508
580
  server.registerTool(
509
581
  COMMAND.TAB_LIST,
510
582
  {
@@ -27,6 +27,20 @@ import type {
27
27
  WsEntry,
28
28
  } from '@harness-fe/protocol';
29
29
 
30
+ async function rmDirWithRetry(dir: string, attempts = 5): Promise<void> {
31
+ for (let i = 0; i < attempts; i++) {
32
+ try {
33
+ rmSync(dir, { recursive: true, force: true });
34
+ return;
35
+ } catch (err) {
36
+ const code = (err as NodeJS.ErrnoException).code;
37
+ if (code !== 'ENOTEMPTY' && code !== 'EBUSY' && code !== 'EPERM') throw err;
38
+ if (i === attempts - 1) throw err;
39
+ await new Promise((r) => setTimeout(r, 20 * (i + 1)));
40
+ }
41
+ }
42
+ }
43
+
30
44
  interface TestEnv {
31
45
  bridge: Bridge;
32
46
  store: JsonlStore;
@@ -68,8 +82,8 @@ async function setup(): Promise<TestEnv> {
68
82
  await client.close();
69
83
  await server.close();
70
84
  await bridge.stop();
71
- store.close();
72
- rmSync(dir, { recursive: true, force: true });
85
+ await store.close();
86
+ await rmDirWithRetry(dir);
73
87
  },
74
88
  };
75
89
  envs.push(env);
@@ -52,12 +52,26 @@ async function setup(): Promise<TestEnv> {
52
52
  return env;
53
53
  }
54
54
 
55
+ async function rmDirWithRetry(dir: string, attempts = 5): Promise<void> {
56
+ for (let i = 0; i < attempts; i++) {
57
+ try {
58
+ rmSync(dir, { recursive: true, force: true });
59
+ return;
60
+ } catch (err) {
61
+ const code = (err as NodeJS.ErrnoException).code;
62
+ if (code !== 'ENOTEMPTY' && code !== 'EBUSY' && code !== 'EPERM') throw err;
63
+ if (i === attempts - 1) throw err;
64
+ await new Promise((r) => setTimeout(r, 20 * (i + 1)));
65
+ }
66
+ }
67
+ }
68
+
55
69
  afterEach(async () => {
56
70
  while (envs.length > 0) {
57
71
  const env = envs.pop()!;
58
72
  await env.bridge.stop();
59
- env.store.close();
60
- rmSync(env.dir, { recursive: true, force: true });
73
+ await env.store.close();
74
+ await rmDirWithRetry(env.dir);
61
75
  }
62
76
  });
63
77
 
@@ -49,6 +49,9 @@ export type EventType =
49
49
  | 'load' // page-load initial snapshot
50
50
  | 'storage' // localStorage/sessionStorage/cookie mutation
51
51
  | 'ws' // WebSocket frame (open / send / recv / close)
52
+ | 'navigation' // history.pushState/replaceState/popstate/hashchange + location.* setters
53
+ | 'globals' // window.X get/set/delete (build-time-watched keys)
54
+ | 'indexeddb' // IDB open / put / add / get / delete / clear / cursor
52
55
  | 'server-log' // Node.js console log from node-runtime SDK
53
56
  | 'server-err' // Node.js uncaughtException / unhandledRejection from node-runtime SDK
54
57
  | 'server-action'// Route Handler / Server Action timing from withHarnessTracing()