@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 +42 -0
- package/dist/store/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/mcp.ts +72 -0
- package/src/mcpLayer.e2e.test.ts +16 -2
- package/src/newCapabilities.e2e.test.ts +16 -2
- package/src/store/types.ts +3 -0
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: {},
|
package/dist/store/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
{
|
package/src/mcpLayer.e2e.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
73
|
+
await env.store.close();
|
|
74
|
+
await rmDirWithRetry(env.dir);
|
|
61
75
|
}
|
|
62
76
|
});
|
|
63
77
|
|
package/src/store/types.ts
CHANGED
|
@@ -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()
|