clay-server 4.9.0-beta.1 → 5.0.0-beta.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.
@@ -0,0 +1,37 @@
1
+ // Pin each sign-in browser tunnel to a checked public address, preventing DNS rebinding.
2
+ var http = require('http');
3
+ var net = require('net');
4
+ var network = require('./mcp-network');
5
+ async function createBrowserProxy() {
6
+ var sockets = new Set();
7
+ var server = http.createServer(function (req, res) { res.writeHead(403); res.end(); });
8
+ server.on('connection', function (socket) {
9
+ sockets.add(socket); socket.once('close', function () { sockets.delete(socket); });
10
+ socket.on('error', function () {});
11
+ });
12
+ server.on('connect', function (req, socket, head) {
13
+ Promise.resolve().then(async function () {
14
+ var url = network.remoteUrl('https://' + req.url);
15
+ var host = url.hostname.replace(/^\[|\]$/g, '');
16
+ var addresses = await network.publicLookup(host);
17
+ if (socket.destroyed) return;
18
+ var upstream = net.connect({ host: addresses[0].address, family: addresses[0].family, port: Number(url.port || 443), timeout: 15000 });
19
+ sockets.add(upstream);
20
+ upstream.once('close', function () { sockets.delete(upstream); socket.destroy(); });
21
+ upstream.on('error', function () { socket.destroy(); });
22
+ upstream.once('timeout', function () { upstream.destroy(); });
23
+ socket.once('close', function () { upstream.destroy(); });
24
+ upstream.once('connect', function () {
25
+ upstream.setTimeout(0);
26
+ socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
27
+ if (head.length) upstream.write(head);
28
+ upstream.pipe(socket); socket.pipe(upstream);
29
+ });
30
+ }).catch(function () { socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); });
31
+ });
32
+ await new Promise(function (resolve, reject) { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
33
+ return { server: 'http://127.0.0.1:' + server.address().port, close: function () {
34
+ server.close(); sockets.forEach(function (socket) { socket.destroy(); });
35
+ } };
36
+ }
37
+ module.exports = { createBrowserProxy: createBrowserProxy };
@@ -0,0 +1,39 @@
1
+ // Owner/project-scoped records with encrypted credentials; never sent to the UI.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var crypto = require('crypto');
5
+ function createConnectionStore(root, project) {
6
+ var directory = path.join(root, 'mcp-connections');
7
+ function scope(owner) {
8
+ if (typeof owner !== 'string' || !owner || owner.length > 160) throw new Error('MCP account owner is required.');
9
+ return JSON.stringify([project, owner]);
10
+ }
11
+ function filename(owner) { return path.join(directory, crypto.createHash('sha256').update(scope(owner)).digest('hex') + '.json'); }
12
+ function key() {
13
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
14
+ var file = path.join(directory, '.key');
15
+ try { fs.writeFileSync(file, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 }); }
16
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
17
+ return fs.readFileSync(file);
18
+ }
19
+ function load(owner) {
20
+ var data;
21
+ try { data = JSON.parse(fs.readFileSync(filename(owner), 'utf8')); }
22
+ catch (error) { if (error.code === 'ENOENT') return []; throw new Error('Could not read MCP connections.'); }
23
+ var parts = data.encrypted.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
24
+ var cipher = crypto.createDecipheriv('aes-256-gcm', key(), parts[0]);
25
+ cipher.setAAD(Buffer.from(scope(owner))); cipher.setAuthTag(parts[1]);
26
+ return JSON.parse(Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8'));
27
+ }
28
+ function save(owner, records) {
29
+ var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', key(), iv);
30
+ cipher.setAAD(Buffer.from(scope(owner)));
31
+ var bytes = Buffer.concat([cipher.update(JSON.stringify(records), 'utf8'), cipher.final()]);
32
+ var value = { encrypted: [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.') };
33
+ var file = filename(owner), temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
34
+ try { fs.writeFileSync(temp, JSON.stringify(value), { mode: 0o600 }); fs.renameSync(temp, file); }
35
+ finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
36
+ }
37
+ return { load: load, save: save };
38
+ }
39
+ module.exports = { createConnectionStore: createConnectionStore };
@@ -0,0 +1,46 @@
1
+ // Remote MCP must not turn a shared Clay host into a private-network proxy.
2
+ var dns = require('dns').promises;
3
+ var ipaddr = require('ipaddr.js');
4
+ var undici = require('undici');
5
+
6
+ function publicAddress(address) {
7
+ try { return ipaddr.process(address).range() === 'unicast'; } catch (error) { return false; }
8
+ }
9
+ function remoteUrl(value) {
10
+ if (typeof value !== 'string' || value.length > 4096) throw new Error('Enter a valid HTTPS MCP address.');
11
+ var url;
12
+ try { url = new URL(value.trim()); } catch (error) { throw new Error('Enter a valid HTTPS MCP address.'); }
13
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) throw new Error('Use an HTTPS address without embedded credentials or a fragment.');
14
+ var hostname = url.hostname.replace(/^\[|\]$/g, '');
15
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || (ipaddr.isValid(hostname) && !publicAddress(hostname))) {
16
+ throw new Error('Remote connections require a public server. Use the extension for tools on your computer.');
17
+ }
18
+ return url;
19
+ }
20
+ async function publicLookup(hostname) {
21
+ var records = await dns.lookup(hostname, { all: true, verbatim: true });
22
+ if (!records.length || records.some(function (item) { return !publicAddress(item.address); })) throw new Error('The server address resolves to a private or reserved network.');
23
+ return records;
24
+ }
25
+ async function assertPublicUrl(value) {
26
+ var url = remoteUrl(String(value));
27
+ await publicLookup(url.hostname.replace(/^\[|\]$/g, ''));
28
+ return url;
29
+ }
30
+ function createRemoteFetch() {
31
+ var agent = new undici.Agent({ connect: { timeout: 15000, lookup: function (host, options, callback) {
32
+ publicLookup(host).then(function (records) {
33
+ var matching = records.filter(function (item) { return !options.family || item.family === options.family; });
34
+ if (!matching.length) return callback(new Error('No public address is available.'));
35
+ if (options.all) callback(null, matching); else callback(null, matching[0].address, matching[0].family);
36
+ }, callback);
37
+ } } });
38
+ async function request(input, init) {
39
+ var url = await assertPublicUrl(typeof input === 'string' || input instanceof URL ? input : input.url);
40
+ // No redirect may forward credentials to a different endpoint.
41
+ return undici.fetch(url, Object.assign({}, init, { dispatcher: agent, redirect: 'error',
42
+ signal: init && init.signal && typeof AbortSignal.any !== 'function' ? init.signal : (init && init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(60000)]) : AbortSignal.timeout(60000)) }));
43
+ }
44
+ return { fetch: request, close: function () { return agent.destroy(); } };
45
+ }
46
+ module.exports = { remoteUrl: remoteUrl, publicAddress: publicAddress, publicLookup: publicLookup, assertPublicUrl: assertPublicUrl, createRemoteFetch: createRemoteFetch };
@@ -0,0 +1,65 @@
1
+ var crypto = require('crypto');
2
+ var http = require('http');
3
+ var assertPublicUrl = require('./mcp-network').assertPublicUrl;
4
+
5
+ function createOAuthProvider(record, persist, flow) {
6
+ record.auth = record.auth || {};
7
+ var auth = record.auth;
8
+ function guard() { if (flow && flow.cancelled) throw new Error('Sign-in expired.'); }
9
+ function save() { persist(); }
10
+ return {
11
+ get redirectUrl() { return flow ? flow.redirectUrl : auth.redirectUrl; },
12
+ get clientMetadata() { return { client_name: 'Clay', redirect_uris: [flow ? flow.redirectUrl : auth.redirectUrl],
13
+ grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none' }; },
14
+ state: function () { return flow ? flow.state : crypto.randomBytes(32).toString('hex'); },
15
+ clientInformation: function () { return auth.client; },
16
+ saveClientInformation: function (value) { guard(); auth.client = value; save(); },
17
+ tokens: function () { return auth.tokens; },
18
+ saveTokens: function (value) { guard(); auth.tokens = value; save(); },
19
+ saveCodeVerifier: function (value) { if (flow) flow.verifier = value; },
20
+ codeVerifier: function () { if (!flow || !flow.verifier) throw new Error('Sign in again.'); return flow.verifier; },
21
+ redirectToAuthorization: async function (url) {
22
+ if (!flow) { var required = new Error('Sign in to connect this server.'); required.name = 'UnauthorizedError'; throw required; }
23
+ await (flow.validateUrl || assertPublicUrl)(url.href);
24
+ guard();
25
+ if (url.searchParams.get('state') !== flow.state || url.searchParams.get('redirect_uri') !== flow.redirectUrl || !url.searchParams.get('code_challenge')) throw new Error('Invalid sign-in request.');
26
+ flow.authorizationUrl = url.href;
27
+ },
28
+ discoveryState: function () { return auth.discovery; },
29
+ saveDiscoveryState: function (value) {
30
+ // Bind client credentials to the discovered issuer.
31
+ if (auth.discovery && auth.discovery.authorizationServerUrl !== value.authorizationServerUrl) { delete auth.client; delete auth.tokens; }
32
+ guard(); auth.discovery = value; save();
33
+ },
34
+ invalidateCredentials: function (which) {
35
+ if (which === 'all' || which === 'client') delete auth.client;
36
+ if (which === 'all' || which === 'tokens') delete auth.tokens;
37
+ if (which === 'all' || which === 'discovery') delete auth.discovery;
38
+ if (flow && (which === 'all' || which === 'verifier')) flow.verifier = null;
39
+ save();
40
+ },
41
+ };
42
+ }
43
+ async function createCallback(onCode) {
44
+ var flow = { state: crypto.randomBytes(32).toString('hex'), used: false };
45
+ var server = http.createServer(function (req, res) {
46
+ var url = new URL(req.url, flow.redirectUrl);
47
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
48
+ res.setHeader('Cache-Control', 'no-store');
49
+ res.setHeader('Referrer-Policy', 'no-referrer');
50
+ if (req.method !== 'GET' || url.pathname !== '/callback' || flow.used || url.searchParams.getAll('state').length !== 1 || url.searchParams.getAll('code').length !== 1 || url.searchParams.getAll('iss').length > 1 || url.searchParams.get('state') !== flow.state) {
51
+ res.writeHead(400); res.end('Invalid or expired sign-in.'); return;
52
+ }
53
+ var code = url.searchParams.get('code');
54
+ if (!code || code.length > 8192 || url.searchParams.has('error')) { res.writeHead(400); res.end('Sign-in was not completed. Return to Clay and retry.'); return; }
55
+ flow.used = true;
56
+ Promise.resolve().then(function () { return onCode(code, url.searchParams.get('iss')); }).then(function () {
57
+ res.end('Connected. You can return to Clay.');
58
+ }, function () { res.writeHead(400); res.end('Sign-in failed. Return to Clay and retry.'); });
59
+ });
60
+ await new Promise(function (resolve, reject) { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
61
+ flow.redirectUrl = 'http://127.0.0.1:' + server.address().port + '/callback';
62
+ flow.close = function () { server.close(); server.closeAllConnections(); flow.verifier = null; };
63
+ return flow;
64
+ }
65
+ module.exports = { createOAuthProvider: createOAuthProvider, createCallback: createCallback };
@@ -0,0 +1,32 @@
1
+ // Preserve the server's JSON Schema across SDK and stdio projections.
2
+ function validateDefinitions(definitions) {
3
+ var z = require('zod');
4
+ var names = new Set();
5
+ return definitions.map(function (definition) {
6
+ if (!definition || !/^[a-zA-Z0-9_.-]{1,128}$/.test(definition.name) || names.has(definition.name) || Object.prototype.hasOwnProperty.call(Object.prototype, definition.name)) throw new Error('Unsupported MCP tool catalog.');
7
+ names.add(definition.name);
8
+ var parsed = z.fromJSONSchema(definition.inputSchema || { type: 'object', properties: {} });
9
+ if (!parsed.shape) throw new Error('MCP tools must accept an object.');
10
+ return parsed;
11
+ });
12
+ }
13
+ function createProxy(name, definitions, invoke) {
14
+ var sdk = require('@anthropic-ai/claude-agent-sdk');
15
+ var schemas = validateDefinitions(definitions);
16
+ var tools = definitions.map(function (definition, index) {
17
+ return sdk.tool(definition.name, definition.description || definition.name, schemas[index].shape, function (args) { return invoke(definition.name, args); });
18
+ });
19
+ var server = sdk.createSdkMcpServer({ name: name, version: '1.0.0', tools: tools });
20
+ definitions.forEach(function (definition, index) {
21
+ var tool = server.instance._registeredTools[definition.name];
22
+ tool.inputSchema = schemas[index];
23
+ tool._clayInputSchema = definition.inputSchema || { type: 'object', properties: {} };
24
+ });
25
+ server.instance.server.setRequestHandler(require('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema, function () {
26
+ return { tools: definitions.map(function (definition) {
27
+ return { name: definition.name, description: definition.description, inputSchema: definition.inputSchema || { type: 'object', properties: {} } };
28
+ }) };
29
+ });
30
+ return server;
31
+ }
32
+ module.exports = { createProxy: createProxy, validateDefinitions: validateDefinitions };
@@ -0,0 +1,215 @@
1
+ var crypto = require('crypto');
2
+ var Client = require('@modelcontextprotocol/sdk/client/index.js').Client;
3
+ var StreamableHTTPClientTransport = require('@modelcontextprotocol/sdk/client/streamableHttp.js').StreamableHTTPClientTransport;
4
+ var network = require('./mcp-network');
5
+ var oauth = require('./mcp-oauth');
6
+ var createLoginBrowser = require('./vendor-login-browser').createLoginBrowser;
7
+
8
+ function createRemoteConnections(ctx) {
9
+ var accounts = new Map(), live = new Map(), flows = new Map(), queues = new Map();
10
+ var remoteFetch = ctx.remoteFetch || network.createRemoteFetch();
11
+ var closed = false;
12
+ function records(owner) {
13
+ ctx.authorize(owner);
14
+ if (!accounts.has(owner)) accounts.set(owner, ctx.storage.load(owner));
15
+ return accounts.get(owner);
16
+ }
17
+ function persist(owner) { ctx.authorize(owner); if (closed) throw new Error('MCP connections closed.'); ctx.storage.save(owner, records(owner)); }
18
+ function find(owner, id) {
19
+ var row = records(owner).find(function (item) { return item.id === id; });
20
+ if (!row) throw new Error('Connection is unavailable.');
21
+ return row;
22
+ }
23
+ function key(owner, id) { return JSON.stringify([owner, id]); }
24
+ function check(owner, row) {
25
+ if (closed || find(owner, row.id) !== row) throw new Error('Connection changed.');
26
+ }
27
+ function changed(owner) { if (!closed) ctx.changed(owner); }
28
+ function view(owner) {
29
+ return records(owner).filter(function (row) { return row.source === 'url'; }).map(function (row) {
30
+ var entry = live.get(key(owner, row.id));
31
+ return { id: row.id, name: row.id, label: row.label, source: 'url', transport: 'http', url: row.url,
32
+ projectEnabled: !!row.enabled, extensionEnabled: true, tools: row.tools || [], toolCount: (row.tools || []).length,
33
+ status: entry ? entry.status : (row.tools ? 'idle' : 'unavailable'), error: entry ? entry.error : '', authMode: row.authMode || 'auto' };
34
+ });
35
+ }
36
+ async function disconnect(owner, id) {
37
+ var entry = live.get(key(owner, id));
38
+ live.delete(key(owner, id));
39
+ if (entry && entry.client) await entry.client.close().catch(function () {});
40
+ }
41
+ function provider(owner, row, flow) {
42
+ return oauth.createOAuthProvider(row, function () { check(owner, row); persist(owner); }, flow);
43
+ }
44
+ async function connect(owner, row, flow) {
45
+ check(owner, row);
46
+ await disconnect(owner, row.id);
47
+ check(owner, row);
48
+ if (flow && flow.cancelled) throw new Error('Sign-in expired.');
49
+ var client = new Client({ name: 'clay', version: '1.0.0' }, { capabilities: {} });
50
+ var options = { fetch: remoteFetch.fetch };
51
+ if (row.authMode === 'token' && row.token) options.requestInit = { headers: { Authorization: 'Bearer ' + row.token } };
52
+ else if (flow || (row.auth && row.auth.tokens)) options.authProvider = provider(owner, row, flow);
53
+ var transport = new StreamableHTTPClientTransport(new URL(row.url), options);
54
+ var entry = { client: client, transport: transport, status: 'connecting', error: '' };
55
+ live.set(key(owner, row.id), entry); changed(owner);
56
+ try {
57
+ await client.connect(transport, { timeout: 30000 });
58
+ var tools = [], cursor, seen = new Set();
59
+ do {
60
+ var page = await client.listTools(cursor ? { cursor: cursor } : {}, { timeout: 30000 });
61
+ tools = tools.concat(page.tools);
62
+ if (tools.length > 1000 || seen.size >= 100 || (page.nextCursor && seen.has(page.nextCursor))) throw new Error('Tool catalog exceeds the supported limit.');
63
+ cursor = page.nextCursor; seen.add(cursor);
64
+ } while (cursor);
65
+ if (JSON.stringify(tools).length > 2 * 1024 * 1024) throw new Error('Tool catalog exceeds the supported limit.');
66
+ check(owner, row);
67
+ if (live.get(key(owner, row.id)) !== entry || (flow && flow.cancelled)) throw new Error('Connection changed.');
68
+ require('./mcp-proxy').validateDefinitions(tools);
69
+ row.tools = tools; entry.status = 'ready'; persist(owner);
70
+ return entry;
71
+ } catch (error) {
72
+ var requiresAuth = error.code === 401 || error.code === 403 || error.name === 'UnauthorizedError' || (flow && flow.authorizationUrl);
73
+ entry.status = requiresAuth ? 'auth_required' : 'unavailable';
74
+ entry.error = requiresAuth ? 'Sign in or provide an access token to connect.' : 'Could not connect. Check the address and try again.';
75
+ if (!flow) await client.close().catch(function () {});
76
+ return entry;
77
+ } finally { changed(owner); }
78
+ }
79
+ function cancelLogin(owner, id) {
80
+ var idKey = key(owner, id), flow = flows.get(idKey);
81
+ if (!flow) return;
82
+ flow.cancelled = true; flows.delete(idKey); clearTimeout(flow.timer);
83
+ if (flow.browser) { flow.browser.close(); ctx.sendOwner(owner, { type: 'mcp_login_closed', browserId: flow.browser.id }); }
84
+ flow.close();
85
+ }
86
+ async function signIn(owner, row, ws) {
87
+ cancelLogin(owner, row.id);
88
+ var identity = ctx.identity(ws);
89
+ var flow = await oauth.createCallback(async function (code, issuer) {
90
+ check(owner, row);
91
+ if (flows.get(key(owner, row.id)) !== flow) throw new Error('Sign-in expired.');
92
+ var metadata = row.auth && row.auth.discovery;
93
+ var expected = metadata && metadata.authorizationServerMetadata && metadata.authorizationServerMetadata.issuer;
94
+ if ((metadata && metadata.authorizationServerMetadata && metadata.authorizationServerMetadata.authorization_response_iss_parameter_supported && !issuer) || (issuer && (!expected || issuer !== expected))) throw new Error('Sign-in issuer mismatch.');
95
+ var entry = live.get(key(owner, row.id));
96
+ if (!entry || !entry.transport) throw new Error('Sign in again.');
97
+ await entry.transport.finishAuth(code);
98
+ check(owner, row);
99
+ if (flows.get(key(owner, row.id)) !== flow) throw new Error('Sign-in expired.');
100
+ var ready = await connect(owner, row);
101
+ if (ready.status !== 'ready') throw new Error('Connection failed after sign-in.');
102
+ setTimeout(function () { if (flows.get(key(owner, row.id)) === flow) { cancelLogin(owner, row.id); changed(owner); } }, 500);
103
+ });
104
+ flows.set(key(owner, row.id), flow);
105
+ flow.timer = setTimeout(function () { cancelLogin(owner, row.id); }, 10 * 60 * 1000); flow.timer.unref();
106
+ row.authMode = 'auto'; delete row.token;
107
+ // A fresh loopback redirect requires a matching client registration.
108
+ row.auth = { redirectUrl: flow.redirectUrl }; persist(owner);
109
+ var entry = await connect(owner, row, flow);
110
+ if (!flow.authorizationUrl) { cancelLogin(owner, row.id); if (entry.status !== 'ready') throw new Error('This server could not start browser sign-in. Use an access token if supported.'); return; }
111
+ check(owner, row);
112
+ if (flow.cancelled) throw new Error('Sign-in expired.');
113
+ try { flow.browser = (ctx.createLoginBrowser || createLoginBrowser)({ ws: ws, vendor: 'mcp', initialUrl: flow.authorizationUrl,
114
+ osUserInfo: identity, restrictedNetwork: true,
115
+ canAccess: function (peer) { return flows.get(key(owner, row.id)) === flow && ctx.canAccess(peer, owner); },
116
+ sendTo: function (peer, message) { ctx.sendTo(peer, Object.assign({}, message, { type: message.type.replace('vendor_login_', 'mcp_login_'), connectionId: row.id })); } });
117
+ } catch (error) { cancelLogin(owner, row.id); throw new Error('Could not open the sign-in browser. Retry or use an access token.'); }
118
+ ctx.sendTo(ws, { type: 'mcp_login_ready', browserId: flow.browser.id, connectionId: row.id, label: row.label });
119
+ }
120
+ function serial(owner, task) {
121
+ var pending = (queues.get(owner) || Promise.resolve()).catch(function () {}).then(task);
122
+ queues.set(owner, pending);
123
+ pending.finally(function () { if (queues.get(owner) === pending) queues.delete(owner); }).catch(function () {});
124
+ return pending;
125
+ }
126
+ function command(owner, msg, ws) {
127
+ // Revocation takes effect before any queued network work completes.
128
+ if (msg.action === 'remove' || (msg.action === 'enable' && msg.enabled !== true)) {
129
+ try {
130
+ var revoked = find(owner, msg.id);
131
+ if (revoked.source !== 'url') throw new Error('Not a remote connection.');
132
+ revoked.enabled = false; cancelLogin(owner, revoked.id); persist(owner);
133
+ disconnect(owner, revoked.id); changed(owner);
134
+ } catch (error) { return Promise.reject(error); }
135
+ }
136
+ return serial(owner, async function () {
137
+ var row;
138
+ if (msg.action === 'add') {
139
+ var url = network.remoteUrl(msg.url).href;
140
+ if (records(owner).filter(function (item) { return item.source === 'url'; }).length >= 30) throw new Error('You can add up to 30 remote connections per project.');
141
+ if (records(owner).some(function (item) { return item.url === url; })) throw new Error('This server is already connected. Open it from your connections.');
142
+ row = { id: 'remote-' + crypto.randomBytes(8).toString('hex'), source: 'url', url: url, label: new URL(url).hostname, enabled: false };
143
+ records(owner).push(row); persist(owner);
144
+ await connect(owner, row);
145
+ } else {
146
+ row = find(owner, msg.id);
147
+ if (row.source !== 'url') throw new Error('Not a remote connection.');
148
+ if (msg.action === 'enable') { row.enabled = msg.enabled === true; persist(owner); }
149
+ else if (msg.action === 'rename') {
150
+ if (typeof msg.label !== 'string' || !msg.label.trim() || msg.label.length > 100) throw new Error('Use a name between 1 and 100 characters.');
151
+ row.label = msg.label.trim(); persist(owner);
152
+ } else if (msg.action === 'remove') {
153
+ cancelLogin(owner, row.id); await disconnect(owner, row.id);
154
+ accounts.set(owner, records(owner).filter(function (item) { return item !== row; })); persist(owner);
155
+ } else if (msg.action === 'test') { cancelLogin(owner, row.id); await connect(owner, row); }
156
+ else if (msg.action === 'token') {
157
+ if (typeof msg.token !== 'string' || !msg.token.trim() || msg.token.length > 8192 || /[\r\n]/.test(msg.token)) throw new Error('Enter a valid access token.');
158
+ cancelLogin(owner, row.id); row.token = msg.token.trim(); row.authMode = 'token'; delete row.auth; persist(owner); await connect(owner, row);
159
+ } else if (msg.action === 'signin') await signIn(owner, row, ws);
160
+ else throw new Error('Unsupported connection action.');
161
+ }
162
+ changed(owner);
163
+ return row.id;
164
+ });
165
+ }
166
+ function handleBrowser(owner, ws, msg) {
167
+ var flow = flows.get(key(owner, msg.connectionId));
168
+ if (!flow || !flow.browser || flow.browser.id !== msg.browserId || !ctx.canAccess(ws, owner)) return;
169
+ if (msg.type === 'mcp_login_cancel') cancelLogin(owner, msg.connectionId);
170
+ else if (msg.type === 'mcp_login_browser_attach') flow.browser.attach(ws);
171
+ else if (msg.type === 'mcp_login_browser_input') flow.browser.input(ws, msg.event);
172
+ }
173
+ async function call(owner, id, name, args, authorize) {
174
+ return serial(owner, async function () {
175
+ authorize(); var row = find(owner, id);
176
+ if (!row.enabled) throw new Error('Connection is disabled.');
177
+ var entry = live.get(key(owner, id));
178
+ if (!entry || entry.status !== 'ready') entry = await connect(owner, row);
179
+ authorize(); check(owner, row);
180
+ if (!row.enabled || entry.status !== 'ready') throw new Error('MCP connection is not ready. Open MCP Servers to reconnect.');
181
+ if (!(row.tools || []).some(function (tool) { return tool.name === name; })) throw new Error('Tool is unavailable.');
182
+ // Never replay a tools/call after an ambiguous transport failure.
183
+ var result;
184
+ try { result = await entry.client.callTool({ name: name, arguments: args || {} }, undefined, { timeout: 60000 }); }
185
+ catch (error) {
186
+ if (live.get(key(owner, id)) === entry) {
187
+ entry.status = error.code === 401 || error.code === 403 || error.name === 'UnauthorizedError' ? 'auth_required' : 'unavailable';
188
+ entry.error = entry.status === 'auth_required' ? 'Sign in again to reconnect.' : 'The tool call could not complete. Check the service before retrying.';
189
+ changed(owner);
190
+ }
191
+ throw new Error('MCP tool call failed. Open MCP Servers to check the connection.');
192
+ }
193
+ authorize(); check(owner, row);
194
+ if (!row.enabled || live.get(key(owner, id)) !== entry) throw new Error('Connection was disabled or changed.');
195
+ return result;
196
+ });
197
+ }
198
+ function computerEnabled(owner, name, fallback) {
199
+ var row = records(owner).find(function (item) { return item.source === 'computer' && item.name === name; });
200
+ return row ? row.enabled : !!fallback;
201
+ }
202
+ function toggleComputer(owner, name, enabled) {
203
+ var list = records(owner), row = list.find(function (item) { return item.source === 'computer' && item.name === name; });
204
+ if (!row) { row = { id: crypto.randomUUID(), source: 'computer', name: name }; list.push(row); }
205
+ row.enabled = !!enabled; persist(owner); changed(owner);
206
+ }
207
+ function destroy() {
208
+ flows.forEach(function (flow, flowKey) { var pair = JSON.parse(flowKey); cancelLogin(pair[0], pair[1]); });
209
+ closed = true; live.forEach(function (entry) { entry.client.close().catch(function () {}); }); live.clear();
210
+ remoteFetch.close();
211
+ }
212
+ return { view: view, command: command, call: call, handleBrowser: handleBrowser, destroy: destroy,
213
+ computerEnabled: computerEnabled, toggleComputer: toggleComputer };
214
+ }
215
+ module.exports = { createRemoteConnections: createRemoteConnections };
@@ -0,0 +1,15 @@
1
+ // A shared Driver must not reach personal connections indirectly through its Worker.
2
+ function canUsePersonalConnections(session, sessions) {
3
+ if (!session || sessions.get(session.localId) !== session || !session.ownerId) return false;
4
+ if (session.sessionVisibility === 'shared' || session._mcpForeignInput) return false;
5
+ if ((session.history || []).some(function (entry) { return entry.type === 'user_message' && entry.from && entry.from !== session.ownerId; })) return false;
6
+ var provenance = session.sessionProvenance;
7
+ if (provenance && provenance.kind === 'worker') {
8
+ var parents = Array.from(sessions.values()).filter(function (candidate) { return candidate.sessionOriginId && candidate.sessionOriginId === provenance.parentSessionOriginId; });
9
+ var parent = parents.length === 1 && parents[0];
10
+ if (!parent || parent.ownerId !== session.ownerId || (parent.sessionProvenance && parent.sessionProvenance.kind === 'worker')) return false;
11
+ return canUsePersonalConnections(parent, sessions);
12
+ }
13
+ return !session.delegated && !session._delegatedBy && !session._pairDelegation;
14
+ }
15
+ module.exports = { canUsePersonalConnections: canUsePersonalConnections };