@harness-fe/mcp-server 4.0.0-next.1 → 4.0.0-next.3

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.
Files changed (100) hide show
  1. package/dist/bin.d.ts +2 -0
  2. package/dist/bin.js +15 -0
  3. package/dist/daemon.d.ts +3 -3
  4. package/dist/daemon.js +1 -1
  5. package/dist/index.d.ts +4 -4
  6. package/dist/index.js +3 -3
  7. package/dist/mcp.d.ts +2 -2
  8. package/dist/mcp.js +49 -15
  9. package/dist/mcpHttp.d.ts +2 -2
  10. package/dist/mcpHttp.js +8 -2
  11. package/package.json +5 -7
  12. package/src/bin.ts +19 -0
  13. package/src/daemon.ts +3 -3
  14. package/src/experimental.test.ts +2 -2
  15. package/src/index.ts +4 -4
  16. package/src/mcp.ts +51 -19
  17. package/src/mcpHttp.test.ts +3 -3
  18. package/src/mcpHttp.ts +10 -4
  19. package/src/mcpLayer.e2e.test.ts +2 -2
  20. package/src/newCapabilities.e2e.test.ts +3 -3
  21. package/dist/auth.d.ts +0 -53
  22. package/dist/auth.js +0 -212
  23. package/dist/bridge.d.ts +0 -323
  24. package/dist/bridge.js +0 -1618
  25. package/dist/cli.d.ts +0 -18
  26. package/dist/cli.js +0 -293
  27. package/dist/dashboardApi.d.ts +0 -40
  28. package/dist/dashboardApi.js +0 -142
  29. package/dist/dashboardSpa.d.ts +0 -18
  30. package/dist/dashboardSpa.js +0 -180
  31. package/dist/dashboardUrl.d.ts +0 -13
  32. package/dist/dashboardUrl.js +0 -18
  33. package/dist/eventsHandler.d.ts +0 -24
  34. package/dist/eventsHandler.js +0 -114
  35. package/dist/identity.d.ts +0 -74
  36. package/dist/identity.js +0 -101
  37. package/dist/openBrowser.d.ts +0 -33
  38. package/dist/openBrowser.js +0 -63
  39. package/dist/remoteBridge.d.ts +0 -61
  40. package/dist/remoteBridge.js +0 -307
  41. package/dist/replayCreate.d.ts +0 -36
  42. package/dist/replayCreate.js +0 -156
  43. package/dist/replayViewer.d.ts +0 -20
  44. package/dist/replayViewer.js +0 -168
  45. package/dist/sessionRouter.d.ts +0 -45
  46. package/dist/sessionRouter.js +0 -88
  47. package/dist/store/JsonMemoryStore.d.ts +0 -52
  48. package/dist/store/JsonMemoryStore.js +0 -119
  49. package/dist/store/JsonTaskStore.d.ts +0 -21
  50. package/dist/store/JsonTaskStore.js +0 -53
  51. package/dist/store/JsonlStore.d.ts +0 -128
  52. package/dist/store/JsonlStore.js +0 -1172
  53. package/dist/store/MemoryEventStore.d.ts +0 -47
  54. package/dist/store/MemoryEventStore.js +0 -111
  55. package/dist/store/WriteQueue.d.ts +0 -51
  56. package/dist/store/WriteQueue.js +0 -142
  57. package/dist/store/index.d.ts +0 -6
  58. package/dist/store/index.js +0 -5
  59. package/dist/store/types.d.ts +0 -427
  60. package/dist/store/types.js +0 -19
  61. package/dist/visitorTimeline.d.ts +0 -24
  62. package/dist/visitorTimeline.js +0 -68
  63. package/src/auth.test.ts +0 -90
  64. package/src/auth.ts +0 -248
  65. package/src/bridge-auth.test.ts +0 -196
  66. package/src/bridge.test.ts +0 -1708
  67. package/src/bridge.ts +0 -1854
  68. package/src/cli.ts +0 -338
  69. package/src/dashboardApi.test.ts +0 -235
  70. package/src/dashboardApi.ts +0 -184
  71. package/src/dashboardSpa.test.ts +0 -239
  72. package/src/dashboardSpa.ts +0 -195
  73. package/src/dashboardUrl.test.ts +0 -46
  74. package/src/dashboardUrl.ts +0 -28
  75. package/src/eventsHandler.test.ts +0 -247
  76. package/src/eventsHandler.ts +0 -136
  77. package/src/identity.test.ts +0 -86
  78. package/src/identity.ts +0 -116
  79. package/src/openBrowser.test.ts +0 -103
  80. package/src/openBrowser.ts +0 -81
  81. package/src/remoteBridge.test.ts +0 -119
  82. package/src/remoteBridge.ts +0 -404
  83. package/src/replay.test.ts +0 -271
  84. package/src/replayCreate.ts +0 -194
  85. package/src/replayViewer.ts +0 -173
  86. package/src/sessionRouter.ts +0 -119
  87. package/src/store/JsonMemoryStore.test.ts +0 -175
  88. package/src/store/JsonMemoryStore.ts +0 -128
  89. package/src/store/JsonTaskStore.test.ts +0 -212
  90. package/src/store/JsonTaskStore.ts +0 -59
  91. package/src/store/JsonlStore.test.ts +0 -1538
  92. package/src/store/JsonlStore.ts +0 -1325
  93. package/src/store/MemoryEventStore.test.ts +0 -119
  94. package/src/store/MemoryEventStore.ts +0 -151
  95. package/src/store/WriteQueue.ts +0 -165
  96. package/src/store/identityTagging.test.ts +0 -67
  97. package/src/store/index.ts +0 -29
  98. package/src/store/types.ts +0 -532
  99. package/src/visitorTimeline.test.ts +0 -197
  100. package/src/visitorTimeline.ts +0 -89
@@ -1,103 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { openBrowser } from './openBrowser.js';
3
-
4
- /**
5
- * Unit tests for the cross-platform browser launcher. We mock both
6
- * `spawn` and the platform/env probes so the test runs deterministically
7
- * on any host — no actual browser windows pop open in CI.
8
- */
9
- describe('openBrowser', () => {
10
- function spy() {
11
- return vi.fn(() => ({
12
- unref: vi.fn(),
13
- }) as any);
14
- }
15
-
16
- it('uses `open` on darwin', () => {
17
- const spawnFn = spy();
18
- const out = openBrowser('https://example.test', {
19
- platformOverride: 'darwin',
20
- envOverride: {},
21
- spawnOverride: spawnFn,
22
- });
23
- expect(out.opened).toBe(true);
24
- expect(spawnFn).toHaveBeenCalledWith('open', ['https://example.test'], expect.any(Object));
25
- });
26
-
27
- it('uses `xdg-open` on linux', () => {
28
- const spawnFn = spy();
29
- const out = openBrowser('https://example.test', {
30
- platformOverride: 'linux',
31
- envOverride: {},
32
- spawnOverride: spawnFn,
33
- });
34
- expect(out.opened).toBe(true);
35
- expect(spawnFn).toHaveBeenCalledWith('xdg-open', ['https://example.test'], expect.any(Object));
36
- });
37
-
38
- it('uses `cmd /c start "" <url>` on win32 (empty title is required)', () => {
39
- const spawnFn = spy();
40
- const out = openBrowser('https://example.test', {
41
- platformOverride: 'win32',
42
- envOverride: {},
43
- spawnOverride: spawnFn,
44
- });
45
- expect(out.opened).toBe(true);
46
- expect(spawnFn).toHaveBeenCalledWith(
47
- 'cmd',
48
- ['/c', 'start', '', 'https://example.test'],
49
- expect.any(Object),
50
- );
51
- });
52
-
53
- it('short-circuits when HARNESS_FE_HEADLESS=1 is set', () => {
54
- const spawnFn = spy();
55
- const out = openBrowser('https://example.test', {
56
- platformOverride: 'darwin',
57
- envOverride: { HARNESS_FE_HEADLESS: '1' },
58
- spawnOverride: spawnFn,
59
- });
60
- expect(out.opened).toBe(false);
61
- expect(out.reason).toMatch(/HEADLESS/);
62
- expect(spawnFn).not.toHaveBeenCalled();
63
- });
64
-
65
- it('returns opened=false on unsupported platforms with a reason', () => {
66
- const spawnFn = spy();
67
- const out = openBrowser('https://example.test', {
68
- platformOverride: 'freebsd' as NodeJS.Platform,
69
- envOverride: {},
70
- spawnOverride: spawnFn,
71
- });
72
- expect(out.opened).toBe(false);
73
- expect(out.reason).toMatch(/unsupported platform/);
74
- expect(spawnFn).not.toHaveBeenCalled();
75
- });
76
-
77
- it('catches spawn errors and returns opened=false', () => {
78
- const spawnFn = vi.fn(() => {
79
- throw new Error('ENOENT: no such file');
80
- }) as unknown as typeof openBrowser['arguments'][1]['spawnOverride'];
81
- const out = openBrowser('https://example.test', {
82
- platformOverride: 'darwin',
83
- envOverride: {},
84
- spawnOverride: spawnFn as any,
85
- });
86
- expect(out.opened).toBe(false);
87
- expect(out.reason).toMatch(/ENOENT/);
88
- });
89
-
90
- it('detaches and unrefs the child so it survives the parent exit', () => {
91
- const unref = vi.fn();
92
- const spawnFn = vi.fn(() => ({ unref })) as any;
93
- openBrowser('https://example.test', {
94
- platformOverride: 'darwin',
95
- envOverride: {},
96
- spawnOverride: spawnFn,
97
- });
98
- const opts = (spawnFn.mock.calls[0]?.[2] ?? {}) as { detached?: boolean; stdio?: string };
99
- expect(opts.detached).toBe(true);
100
- expect(opts.stdio).toBe('ignore');
101
- expect(unref).toHaveBeenCalledOnce();
102
- });
103
- });
@@ -1,81 +0,0 @@
1
- /**
2
- * Cross-platform "open this URL in the user's default browser" — a tiny
3
- * wrapper around the OS-native command.
4
- *
5
- * Detection rules:
6
- * - darwin → `open <url>`
7
- * - linux → `xdg-open <url>`
8
- * - win32 → `cmd /c start "" <url>` (the empty title is required, otherwise `start` treats the URL as a title)
9
- *
10
- * Escape hatches:
11
- * - `HARNESS_FE_HEADLESS=1` short-circuits and returns `false` without
12
- * spawning anything — useful when the daemon runs in Docker / CI /
13
- * remote host where there's no GUI to open
14
- * - any other platform returns `false`
15
- *
16
- * The spawned process is detached and stdio'd to ignore so we don't
17
- * accidentally tie its lifetime to ours.
18
- */
19
-
20
- import { spawn } from 'node:child_process';
21
-
22
- export interface OpenBrowserOptions {
23
- /** Inject an alternate `process.platform` value, for tests. */
24
- platformOverride?: NodeJS.Platform;
25
- /** Inject the env lookup, for tests. */
26
- envOverride?: Record<string, string | undefined>;
27
- /** Inject the spawn function, for tests. */
28
- spawnOverride?: typeof spawn;
29
- }
30
-
31
- export interface OpenBrowserResult {
32
- opened: boolean;
33
- /** Set when `opened` is false to explain why. */
34
- reason?: string;
35
- }
36
-
37
- export function openBrowser(url: string, opts: OpenBrowserOptions = {}): OpenBrowserResult {
38
- const env = opts.envOverride ?? process.env;
39
- if (env.HARNESS_FE_HEADLESS === '1') {
40
- return { opened: false, reason: 'HARNESS_FE_HEADLESS=1' };
41
- }
42
- const platform = opts.platformOverride ?? process.platform;
43
- const spawnFn = opts.spawnOverride ?? spawn;
44
-
45
- let cmd: string;
46
- let args: string[];
47
- switch (platform) {
48
- case 'darwin':
49
- cmd = 'open';
50
- args = [url];
51
- break;
52
- case 'linux':
53
- cmd = 'xdg-open';
54
- args = [url];
55
- break;
56
- case 'win32':
57
- // `start` is a cmd builtin, not a standalone exe. The first
58
- // empty-string arg is the window title — required, because
59
- // otherwise `start "https://…"` treats the URL as the title
60
- // and never opens anything.
61
- cmd = 'cmd';
62
- args = ['/c', 'start', '', url];
63
- break;
64
- default:
65
- return { opened: false, reason: `unsupported platform: ${platform}` };
66
- }
67
-
68
- try {
69
- const child = spawnFn(cmd, args, {
70
- detached: true,
71
- stdio: 'ignore',
72
- });
73
- child.unref();
74
- return { opened: true };
75
- } catch (err) {
76
- return {
77
- opened: false,
78
- reason: err instanceof Error ? err.message : String(err),
79
- };
80
- }
81
- }
@@ -1,119 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { WebSocket } from 'ws';
3
- import { Bridge } from './bridge.js';
4
- import { RemoteBridge } from './remoteBridge.js';
5
- import type { Frame, HelloAckFrame, ResponseFrame } from '@harness-fe/protocol';
6
-
7
- async function spawnLeader(): Promise<Bridge> {
8
- const bridge = new Bridge({ port: 0, host: '127.0.0.1', tasksFile: '', store: null });
9
- await bridge.start();
10
- return bridge;
11
- }
12
-
13
- function getPort(bridge: Bridge): number {
14
- const port = bridge.getBoundPort();
15
- if (!port) throw new Error('no address');
16
- return port;
17
- }
18
-
19
- async function fakeRuntimeClient(
20
- port: number,
21
- tabId: string,
22
- ): Promise<WebSocket> {
23
- const ws = new WebSocket(`ws://127.0.0.1:${port}`);
24
- await new Promise<void>((resolve, reject) => {
25
- ws.once('open', () => resolve());
26
- ws.once('error', reject);
27
- });
28
- ws.send(
29
- JSON.stringify({
30
- type: 'hello',
31
- id: 'h-rc',
32
- role: 'runtime-client',
33
- projectId: 'demo',
34
- tabId,
35
- sessionId: 'sess-1',
36
- page: { url: 'http://localhost:5173/', title: 'Demo' },
37
- }),
38
- );
39
- await new Promise<HelloAckFrame>((resolve, reject) => {
40
- const timer = setTimeout(() => reject(new Error('hello.ack timeout')), 1000);
41
- ws.once('message', (raw) => {
42
- clearTimeout(timer);
43
- resolve(JSON.parse(raw.toString()) as HelloAckFrame);
44
- });
45
- });
46
- return ws;
47
- }
48
-
49
- describe('RemoteBridge (follower → leader)', () => {
50
- it('listTabs reflects tabs registered on the leader', async () => {
51
- const leader = await spawnLeader();
52
- try {
53
- const port = getPort(leader);
54
- const rc = await fakeRuntimeClient(port, 't-remote-1');
55
-
56
- const follower = new RemoteBridge({ port, host: '127.0.0.1' });
57
- await follower.connect();
58
- try {
59
- const tabs = await follower.listTabs();
60
- expect(tabs).toHaveLength(1);
61
- expect(tabs[0].tabId).toBe('t-remote-1');
62
- } finally {
63
- await follower.stop();
64
- rc.close();
65
- }
66
- } finally {
67
- await leader.stop();
68
- }
69
- });
70
-
71
- it('sendCommand forwards through leader to runtime-client and returns the result', async () => {
72
- const leader = await spawnLeader();
73
- try {
74
- const port = getPort(leader);
75
- const rc = await fakeRuntimeClient(port, 't-remote-2');
76
- // Echo: respond ok with { echoed: args }
77
- rc.on('message', (raw) => {
78
- const frame = JSON.parse(raw.toString()) as Frame;
79
- if (frame.type !== 'command') return;
80
- const reply: ResponseFrame = {
81
- type: 'response',
82
- id: frame.id,
83
- ok: true,
84
- result: { echoed: frame.args },
85
- };
86
- rc.send(JSON.stringify(reply));
87
- });
88
-
89
- const follower = new RemoteBridge({ port, host: '127.0.0.1' });
90
- await follower.connect();
91
- try {
92
- const out = (await follower.sendCommand(
93
- 'page.evaluate',
94
- { expr: '1+1' },
95
- { tabId: 't-remote-2' },
96
- )) as { echoed: { expr: string } };
97
- expect(out.echoed.expr).toBe('1+1');
98
- } finally {
99
- await follower.stop();
100
- rc.close();
101
- }
102
- } finally {
103
- await leader.stop();
104
- }
105
- });
106
-
107
- it('rejects pending calls when leader disappears', async () => {
108
- const leader = await spawnLeader();
109
- const port = getPort(leader);
110
- const follower = new RemoteBridge({ port, host: '127.0.0.1' });
111
- await follower.connect();
112
- // Issue a call with no runtime-client connected → leader will throw; we just
113
- // need to confirm follower receives the propagated error frame cleanly.
114
- const callPromise = follower.sendCommand('page.click', { selector: { css: '#x' } });
115
- await expect(callPromise).rejects.toThrow(/no runtime-client|connection|closed/i);
116
- await follower.stop();
117
- await leader.stop();
118
- });
119
- });