@salesforcebob/remote-mcp-bridge-local 1.0.1
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/__tests__/crypto.test.d.ts +2 -0
- package/dist/__tests__/crypto.test.d.ts.map +1 -0
- package/dist/__tests__/crypto.test.js +113 -0
- package/dist/__tests__/crypto.test.js.map +1 -0
- package/dist/crypto.d.ts +24 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +48 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/playwright-bridge.d.ts +36 -0
- package/dist/playwright-bridge.d.ts.map +1 -0
- package/dist/playwright-bridge.js +71 -0
- package/dist/playwright-bridge.js.map +1 -0
- package/dist/tunnel.d.ts +33 -0
- package/dist/tunnel.d.ts.map +1 -0
- package/dist/tunnel.js +92 -0
- package/dist/tunnel.js.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ws-server.d.ts +17 -0
- package/dist/ws-server.d.ts.map +1 -0
- package/dist/ws-server.js +144 -0
- package/dist/ws-server.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/crypto.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { Wallet } from 'ethers';
|
|
3
|
+
import { canonicalize, verifyPayload, isTimestampValid, MAX_MESSAGE_AGE_MS } from '../crypto.js';
|
|
4
|
+
// Reuse remote-server's signPayload logic for round-trip testing
|
|
5
|
+
async function signPayload(message, privateKey) {
|
|
6
|
+
const wallet = new Wallet(privateKey);
|
|
7
|
+
const canonical = canonicalize(message);
|
|
8
|
+
return wallet.signMessage(canonical);
|
|
9
|
+
}
|
|
10
|
+
const TEST_PRIVATE_KEY = '0x5336e184210b834aca58a6f25983490f12ff21a84373703334dd67d543924423';
|
|
11
|
+
const TEST_ADDRESS = '0xAf92293492B0cb01A3A22A91C05C93Fa02a85534';
|
|
12
|
+
describe('canonicalize', () => {
|
|
13
|
+
it('sorts object keys alphabetically', () => {
|
|
14
|
+
const obj = { z: 1, a: 2, m: 3 };
|
|
15
|
+
const result = JSON.parse(canonicalize(obj));
|
|
16
|
+
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
|
|
17
|
+
});
|
|
18
|
+
it('produces identical output regardless of key insertion order', () => {
|
|
19
|
+
const a = { method: 'tools/list', id: '123', timestamp: 1000, type: 'request' };
|
|
20
|
+
const b = { id: '123', type: 'request', timestamp: 1000, method: 'tools/list' };
|
|
21
|
+
expect(canonicalize(a)).toBe(canonicalize(b));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('verifyPayload', () => {
|
|
25
|
+
it('verifies a correctly signed message', async () => {
|
|
26
|
+
const message = {
|
|
27
|
+
id: 'test-verify-1',
|
|
28
|
+
type: 'request',
|
|
29
|
+
method: 'tools/list',
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
const signature = await signPayload(message, TEST_PRIVATE_KEY);
|
|
33
|
+
const signed = { ...message, signature };
|
|
34
|
+
expect(verifyPayload(signed, TEST_ADDRESS)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('rejects a message signed by a different key', async () => {
|
|
37
|
+
const differentKey = Wallet.createRandom().privateKey;
|
|
38
|
+
const message = {
|
|
39
|
+
id: 'test-wrong-key',
|
|
40
|
+
type: 'request',
|
|
41
|
+
method: 'tools/list',
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
};
|
|
44
|
+
const signature = await signPayload(message, differentKey);
|
|
45
|
+
const signed = { ...message, signature };
|
|
46
|
+
expect(verifyPayload(signed, TEST_ADDRESS)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it('rejects a tampered message', async () => {
|
|
49
|
+
const message = {
|
|
50
|
+
id: 'test-tampered',
|
|
51
|
+
type: 'request',
|
|
52
|
+
method: 'tools/list',
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
};
|
|
55
|
+
const signature = await signPayload(message, TEST_PRIVATE_KEY);
|
|
56
|
+
const signed = { ...message, signature };
|
|
57
|
+
// Tamper with the method
|
|
58
|
+
signed.method = 'tools/call';
|
|
59
|
+
expect(verifyPayload(signed, TEST_ADDRESS)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it('is case-insensitive on the expected address', async () => {
|
|
62
|
+
const message = {
|
|
63
|
+
id: 'test-case',
|
|
64
|
+
type: 'request',
|
|
65
|
+
method: 'tools/list',
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
const signature = await signPayload(message, TEST_PRIVATE_KEY);
|
|
69
|
+
const signed = { ...message, signature };
|
|
70
|
+
expect(verifyPayload(signed, TEST_ADDRESS.toLowerCase())).toBe(true);
|
|
71
|
+
expect(verifyPayload(signed, TEST_ADDRESS.toUpperCase())).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('handles messages with params', async () => {
|
|
74
|
+
const message = {
|
|
75
|
+
id: 'test-params',
|
|
76
|
+
type: 'request',
|
|
77
|
+
method: 'tools/call',
|
|
78
|
+
params: { name: 'browser_navigate', arguments: { url: 'https://example.com' } },
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
};
|
|
81
|
+
const signature = await signPayload(message, TEST_PRIVATE_KEY);
|
|
82
|
+
const signed = { ...message, signature };
|
|
83
|
+
expect(verifyPayload(signed, TEST_ADDRESS)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('isTimestampValid', () => {
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
vi.restoreAllMocks();
|
|
89
|
+
});
|
|
90
|
+
it('accepts a current timestamp', () => {
|
|
91
|
+
expect(isTimestampValid(Date.now())).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
it('accepts a timestamp within the 30s window', () => {
|
|
94
|
+
const withinWindow = Date.now() - (MAX_MESSAGE_AGE_MS - 1000);
|
|
95
|
+
expect(isTimestampValid(withinWindow)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('rejects a timestamp older than 30s', () => {
|
|
98
|
+
const tooOld = Date.now() - (MAX_MESSAGE_AGE_MS + 1000);
|
|
99
|
+
expect(isTimestampValid(tooOld)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
it('rejects a timestamp too far in the future', () => {
|
|
102
|
+
const futureTimestamp = Date.now() + MAX_MESSAGE_AGE_MS + 1000;
|
|
103
|
+
expect(isTimestampValid(futureTimestamp)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
it('accepts a slightly future timestamp (clock skew)', () => {
|
|
106
|
+
const slightlyFuture = Date.now() + 5000;
|
|
107
|
+
expect(isTimestampValid(slightlyFuture)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('has a MAX_MESSAGE_AGE_MS of 30 seconds', () => {
|
|
110
|
+
expect(MAX_MESSAGE_AGE_MS).toBe(30_000);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=crypto.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.test.js","sourceRoot":"","sources":["../../src/__tests__/crypto.test.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SignedMessage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic JSON serialization.
|
|
4
|
+
* Sorts object keys recursively to ensure identical strings
|
|
5
|
+
* on both signing and verification sides.
|
|
6
|
+
*/
|
|
7
|
+
export declare function canonicalize(obj: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* Verify that a SignedMessage was signed by the expected Ethereum address.
|
|
10
|
+
*
|
|
11
|
+
* Extracts the signature, canonicalizes the remaining fields,
|
|
12
|
+
* recovers the signer address, and compares against expectedAddress.
|
|
13
|
+
*
|
|
14
|
+
* @returns true if signature is valid and matches expectedAddress
|
|
15
|
+
*/
|
|
16
|
+
export declare function verifyPayload(signedMessage: SignedMessage, expectedAddress: string): boolean;
|
|
17
|
+
/** Maximum age of a message in milliseconds (30 seconds) */
|
|
18
|
+
export declare const MAX_MESSAGE_AGE_MS = 30000;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a message timestamp is within the acceptable window.
|
|
21
|
+
* Rejects messages older than MAX_MESSAGE_AGE_MS to prevent replay attacks.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isTimestampValid(timestamp: number): boolean;
|
|
24
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,YAAY,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAYjD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,MAAM,GACtB,OAAO,CAUT;AAED,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAI3D"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { verifyMessage } from 'ethers';
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic JSON serialization.
|
|
4
|
+
* Sorts object keys recursively to ensure identical strings
|
|
5
|
+
* on both signing and verification sides.
|
|
6
|
+
*/
|
|
7
|
+
export function canonicalize(obj) {
|
|
8
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
9
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
10
|
+
return Object.keys(value)
|
|
11
|
+
.sort()
|
|
12
|
+
.reduce((sorted, k) => {
|
|
13
|
+
sorted[k] = value[k];
|
|
14
|
+
return sorted;
|
|
15
|
+
}, {});
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Verify that a SignedMessage was signed by the expected Ethereum address.
|
|
22
|
+
*
|
|
23
|
+
* Extracts the signature, canonicalizes the remaining fields,
|
|
24
|
+
* recovers the signer address, and compares against expectedAddress.
|
|
25
|
+
*
|
|
26
|
+
* @returns true if signature is valid and matches expectedAddress
|
|
27
|
+
*/
|
|
28
|
+
export function verifyPayload(signedMessage, expectedAddress) {
|
|
29
|
+
// Extract signature and reconstruct the original unsigned message
|
|
30
|
+
const { signature, ...message } = signedMessage;
|
|
31
|
+
const canonical = canonicalize(message);
|
|
32
|
+
// verifyMessage recovers the signer address — it does NOT throw on invalid sigs,
|
|
33
|
+
// it just returns the wrong address. So we always compare explicitly.
|
|
34
|
+
const recoveredAddress = verifyMessage(canonical, signature);
|
|
35
|
+
return recoveredAddress.toLowerCase() === expectedAddress.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
/** Maximum age of a message in milliseconds (30 seconds) */
|
|
38
|
+
export const MAX_MESSAGE_AGE_MS = 30_000;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a message timestamp is within the acceptable window.
|
|
41
|
+
* Rejects messages older than MAX_MESSAGE_AGE_MS to prevent replay attacks.
|
|
42
|
+
*/
|
|
43
|
+
export function isTimestampValid(timestamp) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const age = Math.abs(now - timestamp);
|
|
46
|
+
return age <= MAX_MESSAGE_AGE_MS;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAGvC;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACzC,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;iBACtB,IAAI,EAAE;iBACN,MAAM,CAA0B,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBAC7C,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACrB,OAAO,MAAM,CAAC;YAChB,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,aAA4B,EAC5B,eAAuB;IAEvB,kEAAkE;IAClE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,GAAG,aAAa,CAAC;IAChD,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAExC,iFAAiF;IACjF,sEAAsE;IACtE,MAAM,gBAAgB,GAAG,aAAa,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAE7D,OAAO,gBAAgB,CAAC,WAAW,EAAE,KAAK,eAAe,CAAC,WAAW,EAAE,CAAC;AAC1E,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IACtC,OAAO,GAAG,IAAI,kBAAkB,CAAC;AACnC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { PlaywrightBridge } from './playwright-bridge.js';
|
|
3
|
+
import { BridgeWsServer } from './ws-server.js';
|
|
4
|
+
import { TunnelManager } from './tunnel.js';
|
|
5
|
+
function loadConfig() {
|
|
6
|
+
const remotePublicAddress = process.env.REMOTE_PUBLIC_ADDRESS;
|
|
7
|
+
if (!remotePublicAddress) {
|
|
8
|
+
throw new Error('REMOTE_PUBLIC_ADDRESS environment variable is required');
|
|
9
|
+
}
|
|
10
|
+
const tunnelName = process.env.TUNNEL_NAME;
|
|
11
|
+
if (!tunnelName) {
|
|
12
|
+
throw new Error('TUNNEL_NAME environment variable is required (e.g. "mcp-bridge")');
|
|
13
|
+
}
|
|
14
|
+
const tunnelUrl = process.env.TUNNEL_URL;
|
|
15
|
+
if (!tunnelUrl) {
|
|
16
|
+
throw new Error('TUNNEL_URL environment variable is required (e.g. "https://mcp-bridge.yourdomain.com")');
|
|
17
|
+
}
|
|
18
|
+
const localPort = parseInt(process.env.LOCAL_PORT || '3100', 10);
|
|
19
|
+
const userDataDir = process.env.BROWSER_PROFILE_DIR || undefined;
|
|
20
|
+
return { remotePublicAddress, localPort, tunnelName, tunnelUrl, userDataDir };
|
|
21
|
+
}
|
|
22
|
+
async function main() {
|
|
23
|
+
console.log('=== MCP Bridge — Local Server ===\n');
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
console.log(`Remote public address: ${config.remotePublicAddress}`);
|
|
26
|
+
console.log(`Local WS port: ${config.localPort}`);
|
|
27
|
+
console.log(`Tunnel: ${config.tunnelName} → ${config.tunnelUrl}`);
|
|
28
|
+
console.log('');
|
|
29
|
+
// 1. Start the browser bridge (persistent profile at ~/.mcp-bridge/browser-profile)
|
|
30
|
+
const playwrightBridge = new PlaywrightBridge({
|
|
31
|
+
userDataDir: config.userDataDir,
|
|
32
|
+
});
|
|
33
|
+
await playwrightBridge.start();
|
|
34
|
+
// 2. Start the WebSocket server
|
|
35
|
+
const wsServer = new BridgeWsServer({
|
|
36
|
+
port: config.localPort,
|
|
37
|
+
remotePublicAddress: config.remotePublicAddress,
|
|
38
|
+
playwrightBridge,
|
|
39
|
+
});
|
|
40
|
+
await wsServer.start();
|
|
41
|
+
// 3. Start the Cloudflare named tunnel
|
|
42
|
+
const tunnel = new TunnelManager({
|
|
43
|
+
localPort: config.localPort,
|
|
44
|
+
tunnelName: config.tunnelName,
|
|
45
|
+
tunnelUrl: config.tunnelUrl,
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
const tunnelUrl = await tunnel.start();
|
|
49
|
+
const wssUrl = tunnelUrl.replace(/^https:\/\//, 'wss://');
|
|
50
|
+
console.log(`\n========================================`);
|
|
51
|
+
console.log(` Cloudflare Tunnel is live!`);
|
|
52
|
+
console.log(` HTTPS: ${tunnelUrl}`);
|
|
53
|
+
console.log(` WSS: ${wssUrl}`);
|
|
54
|
+
console.log(`========================================\n`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : 'Unknown tunnel error';
|
|
58
|
+
console.error(`\n[WARNING] Cloudflare tunnel failed: ${msg}`);
|
|
59
|
+
console.log('The WebSocket server is still running on localhost.');
|
|
60
|
+
console.log(`If on the same network, connect directly to ws://localhost:${config.localPort}\n`);
|
|
61
|
+
}
|
|
62
|
+
// Graceful shutdown
|
|
63
|
+
const shutdown = async () => {
|
|
64
|
+
console.log('\nShutting down...');
|
|
65
|
+
tunnel.stop();
|
|
66
|
+
wsServer.stop();
|
|
67
|
+
await playwrightBridge.stop();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
};
|
|
70
|
+
process.on('SIGINT', shutdown);
|
|
71
|
+
process.on('SIGTERM', shutdown);
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
console.error('Fatal error:', err);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export interface PlaywrightBridgeConfig {
|
|
3
|
+
/** Custom user data directory for the browser profile. Defaults to ~/.mcp-bridge/browser-profile */
|
|
4
|
+
userDataDir?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* PlaywrightBridge spawns @playwright/mcp as a child process
|
|
8
|
+
* and proxies tool calls to it via the MCP stdio transport.
|
|
9
|
+
*
|
|
10
|
+
* Uses a persistent user data directory so cookies, extensions,
|
|
11
|
+
* and other browser state are preserved between launches.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PlaywrightBridge {
|
|
14
|
+
private client;
|
|
15
|
+
private transport;
|
|
16
|
+
private cachedTools;
|
|
17
|
+
private config;
|
|
18
|
+
constructor(config?: PlaywrightBridgeConfig);
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Get the list of available browser tools.
|
|
22
|
+
*/
|
|
23
|
+
getTools(): Tool[];
|
|
24
|
+
/**
|
|
25
|
+
* Call a browser tool by name with the given arguments.
|
|
26
|
+
*/
|
|
27
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<{
|
|
28
|
+
content: unknown[];
|
|
29
|
+
isError?: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Shut down the subprocess.
|
|
33
|
+
*/
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=playwright-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright-bridge.d.ts","sourceRoot":"","sources":["../src/playwright-bridge.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAI/D,MAAM,WAAW,sBAAsB;IACrC,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAyB;gBAE3B,MAAM,GAAE,sBAA2B;IAIzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B5B;;OAEG;IACH,QAAQ,IAAI,IAAI,EAAE;IAIlB;;OAEG;IACG,QAAQ,CACZ,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAUrD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAW5B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* PlaywrightBridge spawns @playwright/mcp as a child process
|
|
7
|
+
* and proxies tool calls to it via the MCP stdio transport.
|
|
8
|
+
*
|
|
9
|
+
* Uses a persistent user data directory so cookies, extensions,
|
|
10
|
+
* and other browser state are preserved between launches.
|
|
11
|
+
*/
|
|
12
|
+
export class PlaywrightBridge {
|
|
13
|
+
client = null;
|
|
14
|
+
transport = null;
|
|
15
|
+
cachedTools = [];
|
|
16
|
+
config;
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
async start() {
|
|
21
|
+
const userDataDir = this.config.userDataDir || join(homedir(), '.mcp-bridge', 'browser-profile');
|
|
22
|
+
const args = ['-y', '@playwright/mcp@latest', '--user-data-dir', userDataDir];
|
|
23
|
+
console.log(`[PlaywrightBridge] Launching browser with persistent profile: ${userDataDir}`);
|
|
24
|
+
this.transport = new StdioClientTransport({
|
|
25
|
+
command: 'npx',
|
|
26
|
+
args,
|
|
27
|
+
});
|
|
28
|
+
this.client = new Client({
|
|
29
|
+
name: 'local-mcp-bridge',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
});
|
|
32
|
+
await this.client.connect(this.transport);
|
|
33
|
+
console.log('[PlaywrightBridge] Connected to @playwright/mcp');
|
|
34
|
+
// Cache the available tools
|
|
35
|
+
const toolsResult = await this.client.listTools();
|
|
36
|
+
this.cachedTools = toolsResult.tools;
|
|
37
|
+
console.log(`[PlaywrightBridge] Discovered ${this.cachedTools.length} tools`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the list of available browser tools.
|
|
41
|
+
*/
|
|
42
|
+
getTools() {
|
|
43
|
+
return this.cachedTools;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Call a browser tool by name with the given arguments.
|
|
47
|
+
*/
|
|
48
|
+
async callTool(name, args) {
|
|
49
|
+
if (!this.client) {
|
|
50
|
+
throw new Error('PlaywrightBridge not started');
|
|
51
|
+
}
|
|
52
|
+
console.log(`[PlaywrightBridge] Calling tool: ${name}`);
|
|
53
|
+
const result = await this.client.callTool({ name, arguments: args ?? {} });
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Shut down the subprocess.
|
|
58
|
+
*/
|
|
59
|
+
async stop() {
|
|
60
|
+
if (this.client) {
|
|
61
|
+
await this.client.close();
|
|
62
|
+
this.client = null;
|
|
63
|
+
}
|
|
64
|
+
if (this.transport) {
|
|
65
|
+
await this.transport.close();
|
|
66
|
+
this.transport = null;
|
|
67
|
+
}
|
|
68
|
+
console.log('[PlaywrightBridge] Stopped');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=playwright-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright-bridge.js","sourceRoot":"","sources":["../src/playwright-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAOjC;;;;;;GAMG;AACH,MAAM,OAAO,gBAAgB;IACnB,MAAM,GAAkB,IAAI,CAAC;IAC7B,SAAS,GAAgC,IAAI,CAAC;IAC9C,WAAW,GAAW,EAAE,CAAC;IACzB,MAAM,CAAyB;IAEvC,YAAY,SAAiC,EAAE;QAC7C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,iBAAiB,CAAC,CAAC;QAEjG,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,WAAW,CAAC,CAAC;QAE9E,OAAO,CAAC,GAAG,CAAC,iEAAiE,WAAW,EAAE,CAAC,CAAC;QAE5F,IAAI,CAAC,SAAS,GAAG,IAAI,oBAAoB,CAAC;YACxC,OAAO,EAAE,KAAK;YACd,IAAI;SACL,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC;YACvB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,OAAO;SACjB,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;QAE/D,4BAA4B;QAC5B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAClD,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,iCAAiC,IAAI,CAAC,WAAW,CAAC,MAAM,QAAQ,CAAC,CAAC;IAChF,CAAC;IAED;;OAEG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,QAAQ,CACZ,IAAY,EACZ,IAA8B;QAE9B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,EAAE,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3E,OAAO,MAAmD,CAAC;IAC7D,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAC5C,CAAC;CACF"}
|
package/dist/tunnel.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface TunnelConfig {
|
|
2
|
+
/** Local port to expose (the WebSocket server port) */
|
|
3
|
+
localPort: number;
|
|
4
|
+
/** Named tunnel identifier (e.g. "mcp-bridge") */
|
|
5
|
+
tunnelName: string;
|
|
6
|
+
/** Known tunnel URL (e.g. "https://mcp-bridge.sfse.dev") */
|
|
7
|
+
tunnelUrl: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Manages a cloudflared named tunnel subprocess.
|
|
11
|
+
*
|
|
12
|
+
* Spawns: cloudflared tunnel run --url http://localhost:<port> <tunnelName>
|
|
13
|
+
*
|
|
14
|
+
* Named tunnels require:
|
|
15
|
+
* 1. cloudflared tunnel login
|
|
16
|
+
* 2. cloudflared tunnel create <name>
|
|
17
|
+
* 3. cloudflared tunnel route dns <name> <hostname>
|
|
18
|
+
*
|
|
19
|
+
* Only makes outbound connections — works behind corporate firewalls.
|
|
20
|
+
*/
|
|
21
|
+
export declare class TunnelManager {
|
|
22
|
+
private process;
|
|
23
|
+
private config;
|
|
24
|
+
constructor(config: TunnelConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Start the cloudflared named tunnel.
|
|
27
|
+
* Waits for the connection to register, then returns the known tunnel URL.
|
|
28
|
+
*/
|
|
29
|
+
start(): Promise<string>;
|
|
30
|
+
getUrl(): string;
|
|
31
|
+
stop(): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=tunnel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,EAAE,YAAY;IAIhC;;;OAGG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAqExB,MAAM,IAAI,MAAM;IAIhB,IAAI,IAAI,IAAI;CAOb"}
|
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Manages a cloudflared named tunnel subprocess.
|
|
4
|
+
*
|
|
5
|
+
* Spawns: cloudflared tunnel run --url http://localhost:<port> <tunnelName>
|
|
6
|
+
*
|
|
7
|
+
* Named tunnels require:
|
|
8
|
+
* 1. cloudflared tunnel login
|
|
9
|
+
* 2. cloudflared tunnel create <name>
|
|
10
|
+
* 3. cloudflared tunnel route dns <name> <hostname>
|
|
11
|
+
*
|
|
12
|
+
* Only makes outbound connections — works behind corporate firewalls.
|
|
13
|
+
*/
|
|
14
|
+
export class TunnelManager {
|
|
15
|
+
process = null;
|
|
16
|
+
config;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Start the cloudflared named tunnel.
|
|
22
|
+
* Waits for the connection to register, then returns the known tunnel URL.
|
|
23
|
+
*/
|
|
24
|
+
start() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const args = [
|
|
27
|
+
'tunnel', 'run',
|
|
28
|
+
'--url', `http://localhost:${this.config.localPort}`,
|
|
29
|
+
this.config.tunnelName,
|
|
30
|
+
];
|
|
31
|
+
console.log(`[Tunnel] Spawning: cloudflared ${args.join(' ')}`);
|
|
32
|
+
this.process = spawn('cloudflared', args, {
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
});
|
|
35
|
+
let resolved = false;
|
|
36
|
+
const timeout = setTimeout(() => {
|
|
37
|
+
if (!resolved) {
|
|
38
|
+
resolved = true;
|
|
39
|
+
reject(new Error('Timed out waiting for named tunnel to register (30s)'));
|
|
40
|
+
}
|
|
41
|
+
}, 30_000);
|
|
42
|
+
// Wait for "Registered tunnel connection" in stderr
|
|
43
|
+
this.process.stderr?.on('data', (data) => {
|
|
44
|
+
const output = data.toString();
|
|
45
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
console.log(`[cloudflared] ${line.trim()}`);
|
|
48
|
+
}
|
|
49
|
+
if (output.includes('Registered tunnel connection') && !resolved) {
|
|
50
|
+
resolved = true;
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
resolve(this.config.tunnelUrl);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
this.process.stdout?.on('data', (data) => {
|
|
56
|
+
const output = data.toString();
|
|
57
|
+
if (output.includes('Registered tunnel connection') && !resolved) {
|
|
58
|
+
resolved = true;
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
resolve(this.config.tunnelUrl);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this.process.on('error', (err) => {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
resolved = true;
|
|
67
|
+
reject(new Error(`Failed to start cloudflared: ${err.message}. ` +
|
|
68
|
+
'Is cloudflared installed? See: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
this.process.on('exit', (code) => {
|
|
72
|
+
console.log(`[Tunnel] cloudflared exited with code ${code}`);
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
resolved = true;
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
reject(new Error(`cloudflared exited with code ${code} before registering`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
getUrl() {
|
|
82
|
+
return this.config.tunnelUrl;
|
|
83
|
+
}
|
|
84
|
+
stop() {
|
|
85
|
+
if (this.process) {
|
|
86
|
+
this.process.kill('SIGTERM');
|
|
87
|
+
this.process = null;
|
|
88
|
+
console.log('[Tunnel] cloudflared stopped');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=tunnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket message envelope for the MCP bridge.
|
|
3
|
+
* Every message between remote and local servers uses this format.
|
|
4
|
+
*/
|
|
5
|
+
export interface BridgeMessage {
|
|
6
|
+
/** Unique ID for request/response correlation */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Message direction */
|
|
9
|
+
type: 'request' | 'response';
|
|
10
|
+
/** MCP method (e.g., 'tools/list', 'tools/call') */
|
|
11
|
+
method: string;
|
|
12
|
+
/** JSON-RPC params (for requests) */
|
|
13
|
+
params?: unknown;
|
|
14
|
+
/** JSON-RPC result (for responses) */
|
|
15
|
+
result?: unknown;
|
|
16
|
+
/** JSON-RPC error (for responses) */
|
|
17
|
+
error?: {
|
|
18
|
+
code: number;
|
|
19
|
+
message: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
};
|
|
22
|
+
/** Unix timestamp in ms — for replay protection */
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A BridgeMessage with an attached cryptographic signature.
|
|
27
|
+
* The signature covers all fields except `signature` itself.
|
|
28
|
+
*/
|
|
29
|
+
export interface SignedMessage extends BridgeMessage {
|
|
30
|
+
/** ethers EIP-191 signature of the canonicalized message */
|
|
31
|
+
signature: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convenience type for responses from local -> remote.
|
|
35
|
+
*/
|
|
36
|
+
export interface BridgeResponse extends BridgeMessage {
|
|
37
|
+
type: 'response';
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,wBAAwB;IACxB,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC1D,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAc,SAAQ,aAAa;IAClD,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,IAAI,EAAE,UAAU,CAAC;CAClB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PlaywrightBridge } from './playwright-bridge.js';
|
|
2
|
+
export interface WsServerConfig {
|
|
3
|
+
port: number;
|
|
4
|
+
remotePublicAddress: string;
|
|
5
|
+
playwrightBridge: PlaywrightBridge;
|
|
6
|
+
}
|
|
7
|
+
export declare class BridgeWsServer {
|
|
8
|
+
private wss;
|
|
9
|
+
private config;
|
|
10
|
+
private heartbeatInterval;
|
|
11
|
+
constructor(config: WsServerConfig);
|
|
12
|
+
start(): Promise<void>;
|
|
13
|
+
private handleConnection;
|
|
14
|
+
private sendError;
|
|
15
|
+
stop(): void;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ws-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-server.d.ts","sourceRoot":"","sources":["../src/ws-server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AASD,qBAAa,cAAc;IACzB,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,iBAAiB,CAA+B;gBAE5C,MAAM,EAAE,cAAc;IAIlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCtB,OAAO,CAAC,gBAAgB;IAoFxB,OAAO,CAAC,SAAS;IAWjB,IAAI,IAAI,IAAI;CAUb"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { verifyPayload, isTimestampValid } from './crypto.js';
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket server that accepts connections from the remote MCP server.
|
|
6
|
+
* Verifies every incoming message using ethers signature verification.
|
|
7
|
+
*/
|
|
8
|
+
/** Ping interval to keep Cloudflare tunnel alive (100s idle timeout) */
|
|
9
|
+
const HEARTBEAT_INTERVAL_MS = 45_000;
|
|
10
|
+
export class BridgeWsServer {
|
|
11
|
+
wss = null;
|
|
12
|
+
config;
|
|
13
|
+
heartbeatInterval = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
start() {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
this.wss = new WebSocketServer({ port: this.config.port });
|
|
20
|
+
this.wss.on('listening', () => {
|
|
21
|
+
console.log(`[WsServer] Listening on port ${this.config.port}`);
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
this.wss.on('connection', (ws) => {
|
|
25
|
+
console.log('[WsServer] Remote client connected');
|
|
26
|
+
ws.isAlive = true;
|
|
27
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
28
|
+
this.handleConnection(ws);
|
|
29
|
+
});
|
|
30
|
+
// Heartbeat: ping all clients every 45s to survive Cloudflare's 100s idle timeout
|
|
31
|
+
this.heartbeatInterval = setInterval(() => {
|
|
32
|
+
this.wss?.clients.forEach((ws) => {
|
|
33
|
+
if (ws.isAlive === false) {
|
|
34
|
+
console.log('[WsServer] Terminating unresponsive client');
|
|
35
|
+
return ws.terminate();
|
|
36
|
+
}
|
|
37
|
+
ws.isAlive = false;
|
|
38
|
+
ws.ping();
|
|
39
|
+
});
|
|
40
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
41
|
+
this.wss.on('error', (err) => {
|
|
42
|
+
console.error('[WsServer] Server error:', err.message);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
handleConnection(ws) {
|
|
47
|
+
ws.on('message', async (data) => {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = data.toString('utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.sendError(ws, 'invalid-parse', 'Failed to parse message buffer');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let signedMessage;
|
|
57
|
+
try {
|
|
58
|
+
signedMessage = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
this.sendError(ws, 'invalid-json', 'Invalid JSON');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Security Layer 1: Timestamp validation (replay protection)
|
|
65
|
+
if (!isTimestampValid(signedMessage.timestamp)) {
|
|
66
|
+
console.warn('[WsServer] Rejected: message timestamp out of range');
|
|
67
|
+
this.sendError(ws, signedMessage.id, 'Message timestamp expired or too far in future');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Security Layer 2: Signature verification
|
|
71
|
+
if (!signedMessage.signature) {
|
|
72
|
+
console.warn('[WsServer] Rejected: missing signature');
|
|
73
|
+
this.sendError(ws, signedMessage.id, 'Missing signature');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const isValid = verifyPayload(signedMessage, this.config.remotePublicAddress);
|
|
77
|
+
if (!isValid) {
|
|
78
|
+
console.warn('[WsServer] Rejected: invalid signature');
|
|
79
|
+
this.sendError(ws, signedMessage.id, 'Invalid signature — address mismatch');
|
|
80
|
+
ws.close(4001, 'Unauthorized');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`[WsServer] Verified message: ${signedMessage.method} (id: ${signedMessage.id})`);
|
|
84
|
+
// Route the request to the Playwright bridge
|
|
85
|
+
try {
|
|
86
|
+
let result;
|
|
87
|
+
let error;
|
|
88
|
+
if (signedMessage.method === 'tools/list') {
|
|
89
|
+
const tools = this.config.playwrightBridge.getTools();
|
|
90
|
+
result = { tools };
|
|
91
|
+
}
|
|
92
|
+
else if (signedMessage.method === 'tools/call') {
|
|
93
|
+
const params = signedMessage.params;
|
|
94
|
+
const toolResult = await this.config.playwrightBridge.callTool(params.name, params.arguments);
|
|
95
|
+
result = toolResult;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
error = { code: -32601, message: `Unknown method: ${signedMessage.method}` };
|
|
99
|
+
}
|
|
100
|
+
const response = {
|
|
101
|
+
id: signedMessage.id,
|
|
102
|
+
type: 'response',
|
|
103
|
+
method: signedMessage.method,
|
|
104
|
+
result,
|
|
105
|
+
error,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
ws.send(JSON.stringify(response));
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
112
|
+
console.error(`[WsServer] Tool execution error: ${errorMessage}`);
|
|
113
|
+
this.sendError(ws, signedMessage.id, errorMessage, signedMessage.method);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
ws.on('close', () => {
|
|
117
|
+
console.log('[WsServer] Remote client disconnected');
|
|
118
|
+
});
|
|
119
|
+
ws.on('error', (err) => {
|
|
120
|
+
console.error('[WsServer] WebSocket error:', err.message);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
sendError(ws, id, message, method) {
|
|
124
|
+
const response = {
|
|
125
|
+
id: id || randomUUID(),
|
|
126
|
+
type: 'response',
|
|
127
|
+
method: method || 'error',
|
|
128
|
+
error: { code: -32000, message },
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
};
|
|
131
|
+
ws.send(JSON.stringify(response));
|
|
132
|
+
}
|
|
133
|
+
stop() {
|
|
134
|
+
if (this.heartbeatInterval) {
|
|
135
|
+
clearInterval(this.heartbeatInterval);
|
|
136
|
+
this.heartbeatInterval = null;
|
|
137
|
+
}
|
|
138
|
+
if (this.wss) {
|
|
139
|
+
this.wss.close();
|
|
140
|
+
console.log('[WsServer] Stopped');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=ws-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-server.js","sourceRoot":"","sources":["../src/ws-server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@salesforcebob/remote-mcp-bridge-local",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Local MCP bridge server — browser control via Playwright + Cloudflare tunnel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"remote-mcp-bridge-local": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "node --env-file=.env --import tsx/esm src/index.ts",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
24
|
+
"@playwright/mcp": "^0.0.28",
|
|
25
|
+
"ethers": "^6.13.0",
|
|
26
|
+
"ws": "^8.18.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.3.1",
|
|
30
|
+
"@types/ws": "^8.5.0",
|
|
31
|
+
"tsx": "^4.19.0",
|
|
32
|
+
"typescript": "^5.5.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "ISC"
|
|
35
|
+
}
|