@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.
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +15 -0
- package/dist/daemon.d.ts +3 -3
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +49 -15
- package/dist/mcpHttp.d.ts +2 -2
- package/dist/mcpHttp.js +8 -2
- package/package.json +5 -7
- package/src/bin.ts +19 -0
- package/src/daemon.ts +3 -3
- package/src/experimental.test.ts +2 -2
- package/src/index.ts +4 -4
- package/src/mcp.ts +51 -19
- package/src/mcpHttp.test.ts +3 -3
- package/src/mcpHttp.ts +10 -4
- package/src/mcpLayer.e2e.test.ts +2 -2
- package/src/newCapabilities.e2e.test.ts +3 -3
- package/dist/auth.d.ts +0 -53
- package/dist/auth.js +0 -212
- package/dist/bridge.d.ts +0 -323
- package/dist/bridge.js +0 -1618
- package/dist/cli.d.ts +0 -18
- package/dist/cli.js +0 -293
- package/dist/dashboardApi.d.ts +0 -40
- package/dist/dashboardApi.js +0 -142
- package/dist/dashboardSpa.d.ts +0 -18
- package/dist/dashboardSpa.js +0 -180
- package/dist/dashboardUrl.d.ts +0 -13
- package/dist/dashboardUrl.js +0 -18
- package/dist/eventsHandler.d.ts +0 -24
- package/dist/eventsHandler.js +0 -114
- package/dist/identity.d.ts +0 -74
- package/dist/identity.js +0 -101
- package/dist/openBrowser.d.ts +0 -33
- package/dist/openBrowser.js +0 -63
- package/dist/remoteBridge.d.ts +0 -61
- package/dist/remoteBridge.js +0 -307
- package/dist/replayCreate.d.ts +0 -36
- package/dist/replayCreate.js +0 -156
- package/dist/replayViewer.d.ts +0 -20
- package/dist/replayViewer.js +0 -168
- package/dist/sessionRouter.d.ts +0 -45
- package/dist/sessionRouter.js +0 -88
- package/dist/store/JsonMemoryStore.d.ts +0 -52
- package/dist/store/JsonMemoryStore.js +0 -119
- package/dist/store/JsonTaskStore.d.ts +0 -21
- package/dist/store/JsonTaskStore.js +0 -53
- package/dist/store/JsonlStore.d.ts +0 -128
- package/dist/store/JsonlStore.js +0 -1172
- package/dist/store/MemoryEventStore.d.ts +0 -47
- package/dist/store/MemoryEventStore.js +0 -111
- package/dist/store/WriteQueue.d.ts +0 -51
- package/dist/store/WriteQueue.js +0 -142
- package/dist/store/index.d.ts +0 -6
- package/dist/store/index.js +0 -5
- package/dist/store/types.d.ts +0 -427
- package/dist/store/types.js +0 -19
- package/dist/visitorTimeline.d.ts +0 -24
- package/dist/visitorTimeline.js +0 -68
- package/src/auth.test.ts +0 -90
- package/src/auth.ts +0 -248
- package/src/bridge-auth.test.ts +0 -196
- package/src/bridge.test.ts +0 -1708
- package/src/bridge.ts +0 -1854
- package/src/cli.ts +0 -338
- package/src/dashboardApi.test.ts +0 -235
- package/src/dashboardApi.ts +0 -184
- package/src/dashboardSpa.test.ts +0 -239
- package/src/dashboardSpa.ts +0 -195
- package/src/dashboardUrl.test.ts +0 -46
- package/src/dashboardUrl.ts +0 -28
- package/src/eventsHandler.test.ts +0 -247
- package/src/eventsHandler.ts +0 -136
- package/src/identity.test.ts +0 -86
- package/src/identity.ts +0 -116
- package/src/openBrowser.test.ts +0 -103
- package/src/openBrowser.ts +0 -81
- package/src/remoteBridge.test.ts +0 -119
- package/src/remoteBridge.ts +0 -404
- package/src/replay.test.ts +0 -271
- package/src/replayCreate.ts +0 -194
- package/src/replayViewer.ts +0 -173
- package/src/sessionRouter.ts +0 -119
- package/src/store/JsonMemoryStore.test.ts +0 -175
- package/src/store/JsonMemoryStore.ts +0 -128
- package/src/store/JsonTaskStore.test.ts +0 -212
- package/src/store/JsonTaskStore.ts +0 -59
- package/src/store/JsonlStore.test.ts +0 -1538
- package/src/store/JsonlStore.ts +0 -1325
- package/src/store/MemoryEventStore.test.ts +0 -119
- package/src/store/MemoryEventStore.ts +0 -151
- package/src/store/WriteQueue.ts +0 -165
- package/src/store/identityTagging.test.ts +0 -67
- package/src/store/index.ts +0 -29
- package/src/store/types.ts +0 -532
- package/src/visitorTimeline.test.ts +0 -197
- package/src/visitorTimeline.ts +0 -89
package/src/openBrowser.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/openBrowser.ts
DELETED
|
@@ -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
|
-
}
|
package/src/remoteBridge.test.ts
DELETED
|
@@ -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
|
-
});
|