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.
- package/lib/mcp-browser-proxy.js +37 -0
- package/lib/mcp-connection-store.js +39 -0
- package/lib/mcp-network.js +46 -0
- package/lib/mcp-oauth.js +65 -0
- package/lib/mcp-proxy.js +32 -0
- package/lib/mcp-remote-connections.js +215 -0
- package/lib/mcp-session-access.js +15 -0
- package/lib/project-mcp.js +130 -357
- package/lib/project-user-message.js +2 -0
- package/lib/project.js +28 -45
- package/lib/public/app.js +1 -0
- package/lib/public/css/mcp-connections.css +86 -0
- package/lib/public/css/skills-workbench.css +74 -0
- package/lib/public/index.html +16 -15
- package/lib/public/modules/app-messages.js +2 -1
- package/lib/public/modules/mcp-connection-view.js +136 -0
- package/lib/public/modules/mcp-permissions.js +122 -0
- package/lib/public/modules/mcp-ui.js +85 -461
- package/lib/public/modules/skills-actions.js +47 -79
- package/lib/public/modules/skills-detail.js +32 -145
- package/lib/public/modules/skills-list-view.js +29 -0
- package/lib/public/modules/skills-state.js +4 -2
- package/lib/public/modules/skills.js +104 -399
- package/lib/public/modules/vendor-login-browser.js +16 -5
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +3 -16
- package/lib/sdk-mcp-descriptors.js +2 -1
- package/lib/vendor-login-browser-worker.js +18 -8
- package/lib/vendor-login-browser.js +10 -3
- package/lib/yoke/adapters/codex.js +1 -18
- package/package.json +6 -2
package/lib/project-mcp.js
CHANGED
|
@@ -1,375 +1,148 @@
|
|
|
1
|
-
var crypto = require(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// Follows the attachXxx(ctx) pattern per MODULE_MAP.md.
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var createProxy = require('./mcp-proxy').createProxy;
|
|
3
|
+
var createStore = require('./mcp-connection-store').createConnectionStore;
|
|
4
|
+
var createRemoteConnections = require('./mcp-remote-connections').createRemoteConnections;
|
|
6
5
|
|
|
7
6
|
function attachMcp(ctx) {
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
7
|
+
var devices = new Map(), pending = new Map();
|
|
8
|
+
function ownerFor(ws) {
|
|
9
|
+
if (!ctx.canAccess(ws)) throw new Error('MCP access is not permitted.');
|
|
10
|
+
return ctx.isMultiUser() ? String(ws._clayUser.id) : 'default';
|
|
11
|
+
}
|
|
12
|
+
function authorizeOwner(owner) { if (!ctx.canUseOwner(owner)) throw new Error('MCP account is unavailable.'); }
|
|
13
|
+
function sendOwner(owner, msg) {
|
|
14
|
+
ctx.clients.forEach(function (ws) {
|
|
15
|
+
try { if (ws.readyState === 1 && ownerFor(ws) === owner) ctx.sendTo(ws, Object.assign({ slug: ctx.slug }, msg)); } catch (error) {}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
var remote = createRemoteConnections({
|
|
19
|
+
remoteFetch: ctx.remoteFetch, createLoginBrowser: ctx.createLoginBrowser,
|
|
20
|
+
storage: ctx.storage || createStore(ctx.root, ctx.slug), authorize: authorizeOwner,
|
|
21
|
+
changed: function (owner) { broadcast(owner); }, sendOwner: sendOwner, sendTo: function (ws, msg) { ctx.sendTo(ws, Object.assign({ slug: ctx.slug }, msg)); },
|
|
22
|
+
identity: ctx.identity,
|
|
23
|
+
canAccess: function (ws, owner) { try { return ownerFor(ws) === owner; } catch (error) { return false; } },
|
|
24
|
+
});
|
|
25
|
+
function deviceFor(owner) {
|
|
26
|
+
var device = devices.get(owner);
|
|
27
|
+
if (!device || device.ws.readyState !== 1) return null;
|
|
28
|
+
try { if (ownerFor(device.ws) !== owner) return null; } catch (error) { return null; }
|
|
29
|
+
return device;
|
|
30
|
+
}
|
|
31
|
+
function enabled(owner, name) {
|
|
32
|
+
var legacy = !ctx.isMultiUser() && (ctx.getEnabledMcpServers() || []).indexOf(name) >= 0;
|
|
33
|
+
return remote.computerEnabled(owner, name, legacy);
|
|
34
|
+
}
|
|
35
|
+
function buildState(owner) {
|
|
36
|
+
var active = deviceFor(owner), device = devices.get(owner), servers = remote.view(owner);
|
|
37
|
+
if (device) device.servers.forEach(function (server) {
|
|
38
|
+
servers.push({ id: server.name, name: server.name, label: server.name, source: 'computer', transport: 'stdio',
|
|
39
|
+
toolCount: server.tools.length, tools: server.tools, projectEnabled: enabled(owner, server.name),
|
|
40
|
+
extensionEnabled: server.enabled, status: active && server.enabled && device.hostConnected ? 'ready' : 'unavailable',
|
|
41
|
+
error: active && device.hostConnected ? '' : 'Reconnect the local bridge on your computer.' });
|
|
42
|
+
});
|
|
43
|
+
return { type: 'mcp_servers_state', slug: ctx.slug, servers: servers,
|
|
44
|
+
hostConnected: !!(active && device.hostConnected), extensionConnected: !!active,
|
|
45
|
+
extensionId: device ? device.extensionId : null };
|
|
46
|
+
}
|
|
47
|
+
function broadcast(owner) {
|
|
48
|
+
try { sendOwner(owner, buildState(owner)); } catch (error) {}
|
|
49
|
+
}
|
|
50
|
+
function sendConnectionState(ws) {
|
|
51
|
+
try { ctx.sendTo(ws, buildState(ownerFor(ws))); } catch (error) {}
|
|
52
|
+
}
|
|
53
|
+
function disconnectDevice(ws) {
|
|
54
|
+
devices.forEach(function (device, owner) {
|
|
55
|
+
if (ws && device.ws !== ws) return;
|
|
56
|
+
if (!ws && device.ws.readyState === 1) return;
|
|
57
|
+
device.hostConnected = false;
|
|
58
|
+
pending.forEach(function (call, id) {
|
|
59
|
+
if (call.ws !== device.ws) return;
|
|
60
|
+
clearTimeout(call.timer); pending.delete(id); call.reject(new Error('Your computer disconnected.'));
|
|
61
|
+
});
|
|
62
|
+
broadcast(owner);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
31
65
|
function handleMcpMessage(ws, msg) {
|
|
32
|
-
if (msg.type
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
if (!msg || typeof msg.type !== 'string' || (msg.type.indexOf('mcp_') !== 0 && msg.type !== 'mcp_toggle_server')) return false;
|
|
67
|
+
var owner;
|
|
68
|
+
try { owner = ownerFor(ws); } catch (error) { return true; }
|
|
69
|
+
if (msg.type === 'mcp_state_request') { sendConnectionState(ws); return true; }
|
|
70
|
+
if (msg.type === 'mcp_servers_available') {
|
|
71
|
+
var servers = Array.isArray(msg.servers) ? msg.servers : [];
|
|
72
|
+
if (servers.length > 100 || JSON.stringify(servers).length > 2 * 1024 * 1024) return true;
|
|
73
|
+
servers = servers.filter(function (server) { return server && /^[a-zA-Z0-9_-]{1,80}$/.test(server.name) && !/^(clay-|remote-)/.test(server.name) && !Object.prototype.hasOwnProperty.call(Object.prototype, server.name) && !server.url && server.transport !== 'http'; }).map(function (server) {
|
|
74
|
+
return { name: server.name, enabled: server.enabled !== false, tools: Array.isArray(server.tools) ? server.tools.slice(0, 1000) : [] };
|
|
75
|
+
});
|
|
76
|
+
var old = devices.get(owner);
|
|
77
|
+
if (old && old.ws !== ws) disconnectDevice(old.ws);
|
|
78
|
+
devices.set(owner, { ws: ws, servers: servers, hostConnected: !!msg.hostConnected, extensionId: ctx.extensionId(ws) });
|
|
79
|
+
broadcast(owner); return true;
|
|
35
80
|
}
|
|
36
|
-
if (msg.type ===
|
|
37
|
-
|
|
81
|
+
if (msg.type === 'mcp_tool_result' || msg.type === 'mcp_tool_error') {
|
|
82
|
+
var call = pending.get(msg.callId);
|
|
83
|
+
if (!call || call.ws !== ws || call.owner !== owner) return true;
|
|
84
|
+
clearTimeout(call.timer); pending.delete(msg.callId);
|
|
85
|
+
try { call.authorize(); } catch (error) { call.reject(error); return true; }
|
|
86
|
+
if (msg.type === 'mcp_tool_error') call.reject(new Error('Computer MCP tool failed.'));
|
|
87
|
+
else call.resolve(msg.result || { content: [] });
|
|
38
88
|
return true;
|
|
39
89
|
}
|
|
40
|
-
if (msg.type ===
|
|
41
|
-
|
|
90
|
+
if (msg.type.indexOf('mcp_login_') === 0) { remote.handleBrowser(owner, ws, msg); return true; }
|
|
91
|
+
if (msg.type === 'mcp_toggle_server') {
|
|
92
|
+
var device = deviceFor(owner);
|
|
93
|
+
if (device && device.servers.some(function (server) { return server.name === msg.name; })) {
|
|
94
|
+
try { remote.toggleComputer(owner, msg.name, msg.enabled); } catch (error) { ctx.sendTo(ws, { type: 'mcp_action_result', ok: false, error: error.message }); }
|
|
95
|
+
}
|
|
42
96
|
return true;
|
|
43
97
|
}
|
|
44
|
-
if (msg.type ===
|
|
45
|
-
|
|
98
|
+
if (msg.type === 'mcp_connection_action') {
|
|
99
|
+
remote.command(owner, msg, ws).then(function (id) {
|
|
100
|
+
if (ownerFor(ws) === owner) ctx.sendTo(ws, { type: 'mcp_action_result', slug: ctx.slug, requestId: msg.requestId, ok: true, id: id, action: msg.action });
|
|
101
|
+
}).catch(function (error) {
|
|
102
|
+
try { if (ownerFor(ws) === owner) ctx.sendTo(ws, { type: 'mcp_action_result', slug: ctx.slug, requestId: msg.requestId, ok: false, error: error.message }); } catch (ignored) {}
|
|
103
|
+
});
|
|
46
104
|
return true;
|
|
47
105
|
}
|
|
48
106
|
return false;
|
|
49
107
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
var
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
broadcastMcpState();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function handleToolResult(msg) {
|
|
75
|
-
var callId = msg.callId;
|
|
76
|
-
console.log("[mcp-bridge] Tool result received: " + callId);
|
|
77
|
-
var pending = _pendingCalls[callId];
|
|
78
|
-
if (!pending) return;
|
|
79
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
80
|
-
delete _pendingCalls[callId];
|
|
81
|
-
pending.resolve(msg.result || { content: [{ type: "text", text: "(empty result)" }] });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function handleToolError(msg) {
|
|
85
|
-
var callId = msg.callId;
|
|
86
|
-
console.log("[mcp-bridge] Tool error received: " + callId + " error=" + (msg.error || "unknown"));
|
|
87
|
-
var pending = _pendingCalls[callId];
|
|
88
|
-
if (!pending) return;
|
|
89
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
90
|
-
delete _pendingCalls[callId];
|
|
91
|
-
pending.reject(new Error(msg.error || "MCP tool call failed"));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function handleToggleServer(ws, msg) {
|
|
95
|
-
var name = msg.name;
|
|
96
|
-
var enabled = !!msg.enabled;
|
|
97
|
-
|
|
98
|
-
var list = getEnabledMcpServers() || [];
|
|
99
|
-
var idx = list.indexOf(name);
|
|
100
|
-
|
|
101
|
-
if (enabled && idx === -1) {
|
|
102
|
-
list.push(name);
|
|
103
|
-
} else if (!enabled && idx !== -1) {
|
|
104
|
-
list.splice(idx, 1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
setEnabledMcpServers(list);
|
|
108
|
-
rebuildProxyServers();
|
|
109
|
-
broadcastMcpState();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------- Proxy Server Builder ----------
|
|
113
|
-
|
|
114
|
-
function rebuildProxyServers() {
|
|
115
|
-
_proxyServers = {};
|
|
116
|
-
|
|
117
|
-
var sdk;
|
|
118
|
-
try {
|
|
119
|
-
sdk = require("@anthropic-ai/claude-agent-sdk");
|
|
120
|
-
} catch (e) {
|
|
121
|
-
console.error("[mcp-bridge] Failed to load SDK:", e.message);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
var createSdkMcpServer = sdk.createSdkMcpServer;
|
|
126
|
-
var tool = sdk.tool;
|
|
127
|
-
if (!createSdkMcpServer || !tool) {
|
|
128
|
-
console.error("[mcp-bridge] SDK missing createSdkMcpServer or tool helper");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
var z;
|
|
133
|
-
try { z = require("zod").z; } catch (e) {
|
|
134
|
-
try { z = require("zod"); } catch (e2) {
|
|
135
|
-
console.error("[mcp-bridge] Failed to load zod:", e2.message);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
var enabledList = getEnabledMcpServers() || [];
|
|
141
|
-
|
|
142
|
-
// --- Remote servers (via Extension) ---
|
|
143
|
-
var serverNames = Object.keys(_availableServers);
|
|
144
|
-
for (var si = 0; si < serverNames.length; si++) {
|
|
145
|
-
var serverName = serverNames[si];
|
|
146
|
-
var serverInfo = _availableServers[serverName];
|
|
147
|
-
|
|
148
|
-
if (!serverInfo.enabled) continue;
|
|
149
|
-
if (enabledList.indexOf(serverName) === -1) continue;
|
|
150
|
-
|
|
151
|
-
var tools = [];
|
|
152
|
-
var serverTools = serverInfo.tools || [];
|
|
153
|
-
|
|
154
|
-
for (var ti = 0; ti < serverTools.length; ti++) {
|
|
155
|
-
var mcpTool = serverTools[ti];
|
|
156
|
-
var toolName = mcpTool.name;
|
|
157
|
-
var toolDesc = mcpTool.description || toolName;
|
|
158
|
-
var shape = buildZodShape(z, mcpTool.inputSchema);
|
|
159
|
-
|
|
160
|
-
tools.push(tool(
|
|
161
|
-
toolName,
|
|
162
|
-
toolDesc,
|
|
163
|
-
shape,
|
|
164
|
-
createToolHandler(serverName, toolName)
|
|
165
|
-
));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (tools.length > 0) {
|
|
169
|
-
var mcpServer = createSdkMcpServer({
|
|
170
|
-
name: serverName,
|
|
171
|
-
version: "1.0.0",
|
|
172
|
-
tools: tools,
|
|
173
|
-
});
|
|
174
|
-
_proxyServers[serverName] = mcpServer;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// --- Local servers (direct process, localhost only) ---
|
|
179
|
-
if (localMcp && localMcp.isReady()) {
|
|
180
|
-
var localServers = localMcp.getAvailableServers();
|
|
181
|
-
for (var li = 0; li < localServers.length; li++) {
|
|
182
|
-
var ls = localServers[li];
|
|
183
|
-
if (!ls.ready) continue;
|
|
184
|
-
if (_proxyServers[ls.name]) continue; // remote takes precedence if same name
|
|
185
|
-
if (enabledList.indexOf(ls.name) === -1) continue;
|
|
186
|
-
|
|
187
|
-
var localTools = [];
|
|
188
|
-
for (var lti = 0; lti < ls.tools.length; lti++) {
|
|
189
|
-
var lt = ls.tools[lti];
|
|
190
|
-
var ltShape = buildZodShape(z, lt.inputSchema);
|
|
191
|
-
localTools.push(tool(
|
|
192
|
-
lt.name,
|
|
193
|
-
lt.description || lt.name,
|
|
194
|
-
ltShape,
|
|
195
|
-
createLocalToolHandler(ls.name, lt.name)
|
|
196
|
-
));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (localTools.length > 0) {
|
|
200
|
-
_proxyServers[ls.name] = createSdkMcpServer({
|
|
201
|
-
name: ls.name,
|
|
202
|
-
version: "1.0.0",
|
|
203
|
-
tools: localTools,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function createLocalToolHandler(serverName, toolName) {
|
|
211
|
-
return function (args) {
|
|
212
|
-
return localMcp.callTool(serverName, toolName, args);
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function createToolHandler(serverName, toolName) {
|
|
217
|
-
return function (args) {
|
|
218
|
-
return new Promise(function (resolve, reject) {
|
|
219
|
-
var extWs = getExtensionWs();
|
|
220
|
-
if (!extWs || extWs.readyState !== 1) {
|
|
221
|
-
reject(new Error("Browser extension not connected. Cannot reach MCP server: " + serverName));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
var callId = "mc_" + Date.now() + "_" + crypto.randomUUID().slice(0, 8);
|
|
226
|
-
|
|
227
|
-
var timer = setTimeout(function () {
|
|
228
|
-
console.log("[mcp-bridge] Tool call TIMEOUT: " + callId + " server=" + serverName + " tool=" + toolName);
|
|
229
|
-
delete _pendingCalls[callId];
|
|
230
|
-
reject(new Error("MCP tool call timed out after " + (TOOL_TIMEOUT_MS / 1000) + "s"));
|
|
231
|
-
}, TOOL_TIMEOUT_MS);
|
|
232
|
-
|
|
233
|
-
_pendingCalls[callId] = { resolve: resolve, reject: reject, timer: timer };
|
|
234
|
-
|
|
235
|
-
console.log("[mcp-bridge] Sending tool call: " + callId + " server=" + serverName + " tool=" + toolName);
|
|
236
|
-
sendTo(extWs, {
|
|
237
|
-
type: "mcp_tool_call",
|
|
238
|
-
callId: callId,
|
|
239
|
-
server: serverName,
|
|
240
|
-
method: "tools/call",
|
|
241
|
-
params: { name: toolName, arguments: args },
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Build a Zod shape from MCP JSON Schema inputSchema
|
|
248
|
-
function buildZodShape(z, inputSchema) {
|
|
249
|
-
if (!inputSchema || !inputSchema.properties) return {};
|
|
250
|
-
var shape = {};
|
|
251
|
-
var props = inputSchema.properties;
|
|
252
|
-
var required = inputSchema.required || [];
|
|
253
|
-
var keys = Object.keys(props);
|
|
254
|
-
|
|
255
|
-
for (var i = 0; i < keys.length; i++) {
|
|
256
|
-
var k = keys[i];
|
|
257
|
-
var p = props[k];
|
|
258
|
-
var field;
|
|
259
|
-
|
|
260
|
-
if (p.type === "number" || p.type === "integer") {
|
|
261
|
-
field = z.number();
|
|
262
|
-
} else if (p.type === "boolean") {
|
|
263
|
-
field = z.boolean();
|
|
264
|
-
} else if (p.type === "array") {
|
|
265
|
-
field = z.array(z.any());
|
|
266
|
-
} else if (p.type === "object") {
|
|
267
|
-
field = z.record(z.any());
|
|
268
|
-
} else if (p.enum) {
|
|
269
|
-
field = z.enum(p.enum);
|
|
270
|
-
} else {
|
|
271
|
-
field = z.string();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (p.description) field = field.describe(p.description);
|
|
275
|
-
if (required.indexOf(k) === -1) field = field.optional();
|
|
276
|
-
shape[k] = field;
|
|
108
|
+
function callComputer(owner, server, tool, args, authorize) {
|
|
109
|
+
return new Promise(function (resolve, reject) {
|
|
110
|
+
authorize();
|
|
111
|
+
var device = deviceFor(owner);
|
|
112
|
+
if (!device || !device.hostConnected || !enabled(owner, server)) throw new Error('Computer connection is unavailable or disabled.');
|
|
113
|
+
var row = device.servers.find(function (item) { return item.name === server && item.enabled; });
|
|
114
|
+
if (!row || !row.tools.some(function (item) { return item.name === tool; })) throw new Error('Computer tool is unavailable.');
|
|
115
|
+
var id = crypto.randomUUID();
|
|
116
|
+
var timer = setTimeout(function () { pending.delete(id); reject(new Error('Computer tool timed out.')); }, 60000);
|
|
117
|
+
pending.set(id, { owner: owner, ws: device.ws, resolve: resolve, reject: reject, timer: timer, authorize: function () {
|
|
118
|
+
authorize(); if (!enabled(owner, server) || deviceFor(owner) !== device) throw new Error('Computer access changed.');
|
|
119
|
+
} });
|
|
120
|
+
ctx.sendTo(device.ws, { type: 'mcp_tool_call', callId: id, server: server, method: 'tools/call', params: { name: tool, arguments: args } });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function getMcpServers(session) {
|
|
124
|
+
if (!session || !ctx.canUseSession(session)) return {};
|
|
125
|
+
var owner = ctx.isMultiUser() ? String(session.ownerId) : 'default';
|
|
126
|
+
function authorize() {
|
|
127
|
+
authorizeOwner(owner);
|
|
128
|
+
if (!ctx.canUseSession(session) || (ctx.isMultiUser() && String(session.ownerId) !== owner)) throw new Error('Session MCP access changed.');
|
|
277
129
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
var state = buildMcpState();
|
|
285
|
-
send(state);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function sendConnectionState(ws) {
|
|
289
|
-
sendTo(ws, buildMcpState());
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function buildMcpState() {
|
|
293
|
-
var enabledList = getEnabledMcpServers() || [];
|
|
294
|
-
var servers = [];
|
|
295
|
-
var seen = {};
|
|
296
|
-
|
|
297
|
-
// Remote servers (from Extension)
|
|
298
|
-
var names = Object.keys(_availableServers);
|
|
299
|
-
for (var i = 0; i < names.length; i++) {
|
|
300
|
-
var name = names[i];
|
|
301
|
-
var info = _availableServers[name];
|
|
302
|
-
seen[name] = true;
|
|
303
|
-
servers.push({
|
|
304
|
-
name: name,
|
|
305
|
-
transport: info.transport,
|
|
306
|
-
toolCount: (info.tools || []).length,
|
|
307
|
-
extensionEnabled: info.enabled,
|
|
308
|
-
projectEnabled: enabledList.indexOf(name) !== -1,
|
|
309
|
-
source: "remote",
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Local servers
|
|
314
|
-
if (localMcp && localMcp.isReady()) {
|
|
315
|
-
var localServers = localMcp.getAvailableServers();
|
|
316
|
-
for (var j = 0; j < localServers.length; j++) {
|
|
317
|
-
var ls = localServers[j];
|
|
318
|
-
if (seen[ls.name]) continue;
|
|
319
|
-
servers.push({
|
|
320
|
-
name: ls.name,
|
|
321
|
-
transport: ls.transport || "stdio",
|
|
322
|
-
toolCount: ls.toolCount || 0,
|
|
323
|
-
extensionEnabled: true,
|
|
324
|
-
projectEnabled: enabledList.indexOf(ls.name) !== -1,
|
|
325
|
-
source: "local",
|
|
130
|
+
var result = {};
|
|
131
|
+
buildState(owner).servers.forEach(function (server) {
|
|
132
|
+
if (!server.projectEnabled || !server.extensionEnabled || !server.tools.length) return;
|
|
133
|
+
try {
|
|
134
|
+
result[server.name] = createProxy(server.name, server.tools, function (tool, args) {
|
|
135
|
+
return server.source === 'url' ? remote.call(owner, server.id, tool, args, authorize) : callComputer(owner, server.name, tool, args, authorize);
|
|
326
136
|
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
type: "mcp_servers_state",
|
|
332
|
-
servers: servers,
|
|
333
|
-
hostConnected: _remoteHostConnected || !!(localMcp && localMcp.isReady()),
|
|
334
|
-
extensionId: getExtensionId() || null,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ---------- Public API ----------
|
|
339
|
-
|
|
340
|
-
function getMcpServers() {
|
|
341
|
-
return _proxyServers;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function cancelAllPending() {
|
|
345
|
-
var ids = Object.keys(_pendingCalls);
|
|
346
|
-
for (var i = 0; i < ids.length; i++) {
|
|
347
|
-
var pending = _pendingCalls[ids[i]];
|
|
348
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
349
|
-
pending.reject(new Error("MCP bridge disconnected"));
|
|
350
|
-
}
|
|
351
|
-
_pendingCalls = {};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function handleExtensionDisconnect() {
|
|
355
|
-
cancelAllPending();
|
|
356
|
-
_availableServers = {};
|
|
357
|
-
_proxyServers = {};
|
|
358
|
-
broadcastMcpState();
|
|
137
|
+
} catch (error) { /* Unsupported schemas are not advertised as callable tools. */ }
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
359
140
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
broadcastMcpState();
|
|
141
|
+
function destroy() {
|
|
142
|
+
pending.forEach(function (call) { clearTimeout(call.timer); call.reject(new Error('MCP closed.')); }); pending.clear();
|
|
143
|
+
devices.clear(); remote.destroy();
|
|
364
144
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
handleMcpMessage: handleMcpMessage,
|
|
368
|
-
getMcpServers: getMcpServers,
|
|
369
|
-
sendConnectionState: sendConnectionState,
|
|
370
|
-
handleExtensionDisconnect: handleExtensionDisconnect,
|
|
371
|
-
rebuildAndBroadcast: rebuildAndBroadcast,
|
|
372
|
-
};
|
|
145
|
+
return { handleMcpMessage: handleMcpMessage, getMcpServers: getMcpServers, sendConnectionState: sendConnectionState,
|
|
146
|
+
handleExtensionDisconnect: disconnectDevice, rebuildAndBroadcast: function () { devices.forEach(function (device, owner) { broadcast(owner); }); }, destroy: destroy };
|
|
373
147
|
}
|
|
374
|
-
|
|
375
148
|
module.exports = { attachMcp: attachMcp };
|
|
@@ -308,6 +308,7 @@ function attachUserMessage(ctx) {
|
|
|
308
308
|
|
|
309
309
|
// --- Browser Extension ---
|
|
310
310
|
if (msg.type === "browser_tab_list") {
|
|
311
|
+
ws._clayMcpExtensionId = typeof msg.extensionId === "string" ? msg.extensionId : null;
|
|
311
312
|
browserState._extensionWs = ws; // Track which client has the extension
|
|
312
313
|
if (msg.extensionId) browserState._extensionId = msg.extensionId;
|
|
313
314
|
var tabs = msg.tabs || [];
|
|
@@ -394,6 +395,7 @@ function attachUserMessage(ctx) {
|
|
|
394
395
|
|
|
395
396
|
var session = forcedSession || getSessionForWs(ws);
|
|
396
397
|
if (!session) return true;
|
|
398
|
+
if (ws._clayUser && session.ownerId && ws._clayUser.id !== session.ownerId) session._mcpForeignInput = true;
|
|
397
399
|
var pendingInternal = internal && internal.pendingConsumed ? internal : null;
|
|
398
400
|
function finishPendingEarly(outcome, reason) {
|
|
399
401
|
if (pendingInternal && typeof pendingInternal.complete === "function") pendingInternal.complete(outcome, reason);
|
package/lib/project.js
CHANGED
|
@@ -47,7 +47,6 @@ var { attachConnection } = require("./project-connection");
|
|
|
47
47
|
var { attachProjectScheduledMessages } = require("./project-scheduled-messages");
|
|
48
48
|
var { attachMcp } = require("./project-mcp");
|
|
49
49
|
var { createSessionSkillCatalog } = require("./yoke/session-skill-catalog");
|
|
50
|
-
var { createLocalMcp } = require("./mcp-local");
|
|
51
50
|
var { attachEmail: attachEmailModule } = require("./project-email");
|
|
52
51
|
var { attachSessionSpawn } = require("./project-session-spawn");
|
|
53
52
|
var { attachDelegatedSession } = require("./project-delegated-session");
|
|
@@ -696,27 +695,30 @@ function createProjectContext(opts) {
|
|
|
696
695
|
sm.defaultModelByVendor = sm.defaultModelByVendor || {};
|
|
697
696
|
if (savedDefaultModel) sm.defaultModelByVendor[sm.defaultVendor || "claude"] = savedDefaultModel;
|
|
698
697
|
|
|
699
|
-
//
|
|
700
|
-
var _localMcp = createLocalMcp();
|
|
701
|
-
|
|
702
|
-
// --- MCP bridge (remote MCP servers via Chrome Extension) ---
|
|
698
|
+
// Personal computer tools use the extension; remote URLs are owner-bound.
|
|
703
699
|
var _mcp = attachMcp({
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return
|
|
712
|
-
? opts.onGetProjectMcpServers(slug) : [];
|
|
700
|
+
sendTo: sendTo, slug: slug, clients: clients,
|
|
701
|
+
root: require("./config").CONFIG_DIR,
|
|
702
|
+
isMultiUser: function () { return usersModule.isMultiUser(); },
|
|
703
|
+
canAccess: function (ws) { return _requestAccess.canAccessProject(ws, slug); },
|
|
704
|
+
canUseOwner: function (owner) {
|
|
705
|
+
if (!usersModule.isMultiUser()) return owner === "default";
|
|
706
|
+
var user = usersModule.findUserById(owner);
|
|
707
|
+
return !!user && _requestAccess.canAccessProject({ _clayUser: user }, slug);
|
|
713
708
|
},
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
709
|
+
canUseSession: function (session) {
|
|
710
|
+
if (!session || sm.sessions.get(session.localId) !== session) return false;
|
|
711
|
+
if (!usersModule.isMultiUser()) return true;
|
|
712
|
+
if (!require("./mcp-session-access").canUsePersonalConnections(session, sm.sessions)) return false;
|
|
713
|
+
var user = session.ownerId && usersModule.findUserById(session.ownerId);
|
|
714
|
+
return !!user && _requestAccess.canAccessProject({ _clayUser: user }, slug) &&
|
|
715
|
+
usersModule.canAccessSession(user.id, session, opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public" });
|
|
716
|
+
},
|
|
717
|
+
identity: function (ws) { return _requestAccess.osIdentity(ws); },
|
|
718
|
+
extensionId: function (ws) { return ws._clayMcpExtensionId || null; },
|
|
719
|
+
getEnabledMcpServers: function () {
|
|
720
|
+
return typeof opts.onGetProjectMcpServers === "function" ? opts.onGetProjectMcpServers(slug) : [];
|
|
718
721
|
},
|
|
719
|
-
localMcp: _localMcp,
|
|
720
722
|
});
|
|
721
723
|
|
|
722
724
|
// --- Email module (delegated to project-email.js) ---
|
|
@@ -1372,7 +1374,7 @@ function createProjectContext(opts) {
|
|
|
1372
1374
|
osUsers: osUsers,
|
|
1373
1375
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1374
1376
|
mcpServers: getLocalMcpServers,
|
|
1375
|
-
getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
|
|
1377
|
+
getRemoteMcpServers: function (session) { return _mcp.getMcpServers(session); },
|
|
1376
1378
|
clayPort: serverPort,
|
|
1377
1379
|
clayTls: serverTls,
|
|
1378
1380
|
clayAuthToken: serverAuthToken,
|
|
@@ -1706,14 +1708,6 @@ function createProjectContext(opts) {
|
|
|
1706
1708
|
// --- WS connection handler (delegated to project-connection.js) ---
|
|
1707
1709
|
function handleConnection(ws, wsUser) {
|
|
1708
1710
|
_connection.handleConnection(ws, wsUser, handleMessage, handleDisconnection);
|
|
1709
|
-
|
|
1710
|
-
// Initialize local MCP when a localhost client connects
|
|
1711
|
-
if (ws._clayLocal && _localMcp && !_localMcp.isReady()) {
|
|
1712
|
-
_localMcp.initialize(function () {
|
|
1713
|
-
// Rebuild proxy servers and broadcast state when local servers are ready
|
|
1714
|
-
_mcp.rebuildAndBroadcast();
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
1711
|
}
|
|
1718
1712
|
|
|
1719
1713
|
// --- WS message handler ---
|
|
@@ -2028,6 +2022,7 @@ function createProjectContext(opts) {
|
|
|
2028
2022
|
|
|
2029
2023
|
// --- WS disconnection handler (delegated to project-connection.js) ---
|
|
2030
2024
|
function handleDisconnection(ws) {
|
|
2025
|
+
_mcp.handleExtensionDisconnect(ws);
|
|
2031
2026
|
// Clean up extension WS reference if this was the extension client
|
|
2032
2027
|
if (browserState._extensionWs === ws) {
|
|
2033
2028
|
browserState._extensionWs = null;
|
|
@@ -2316,17 +2311,6 @@ function createProjectContext(opts) {
|
|
|
2316
2311
|
&& sm.sessions.get(boundSession.localId) === boundSession
|
|
2317
2312
|
&& Number(boundSession._sdkQueryGeneration || 0) === queryGeneration;
|
|
2318
2313
|
}
|
|
2319
|
-
// Build set of local MCP server names to exclude (Codex handles these natively)
|
|
2320
|
-
var localMcpNames = {};
|
|
2321
|
-
try {
|
|
2322
|
-
var mcpLocalModule = require("./mcp-local");
|
|
2323
|
-
var localConfig = mcpLocalModule.readMergedServers();
|
|
2324
|
-
var lcNames = Object.keys(localConfig);
|
|
2325
|
-
for (var li = 0; li < lcNames.length; li++) {
|
|
2326
|
-
localMcpNames[lcNames[li]] = true;
|
|
2327
|
-
}
|
|
2328
|
-
} catch (e) { /* no local MCP config */ }
|
|
2329
|
-
|
|
2330
2314
|
return {
|
|
2331
2315
|
listTools: function () {
|
|
2332
2316
|
var tools = [];
|
|
@@ -2352,7 +2336,7 @@ function createProjectContext(opts) {
|
|
|
2352
2336
|
var toolNames = Object.keys(server.instance._registeredTools);
|
|
2353
2337
|
for (var j = 0; j < toolNames.length; j++) {
|
|
2354
2338
|
var toolDef = server.instance._registeredTools[toolNames[j]];
|
|
2355
|
-
var inputSchema = normalizeToolSchema(toolDef.inputSchema);
|
|
2339
|
+
var inputSchema = toolDef._clayInputSchema || normalizeToolSchema(toolDef.inputSchema);
|
|
2356
2340
|
tools.push({
|
|
2357
2341
|
server: serverName,
|
|
2358
2342
|
name: toolNames[j],
|
|
@@ -2459,13 +2443,11 @@ function createProjectContext(opts) {
|
|
|
2459
2443
|
}
|
|
2460
2444
|
}
|
|
2461
2445
|
|
|
2462
|
-
//
|
|
2463
|
-
var remoteServers = _mcp.getMcpServers();
|
|
2446
|
+
// Personal computer and remote URL connections.
|
|
2447
|
+
var remoteServers = _mcp.getMcpServers(boundSession);
|
|
2464
2448
|
if (remoteServers) {
|
|
2465
2449
|
var remoteNames = Object.keys(remoteServers);
|
|
2466
2450
|
for (var ri = 0; ri < remoteNames.length; ri++) {
|
|
2467
|
-
// Skip servers that Codex manages natively via Track 1
|
|
2468
|
-
if (localMcpNames[remoteNames[ri]]) continue;
|
|
2469
2451
|
extractServerTools(remoteNames[ri], remoteServers[remoteNames[ri]]);
|
|
2470
2452
|
}
|
|
2471
2453
|
}
|
|
@@ -2571,7 +2553,7 @@ function createProjectContext(opts) {
|
|
|
2571
2553
|
}
|
|
2572
2554
|
}
|
|
2573
2555
|
// Try remote/local proxy servers
|
|
2574
|
-
var remoteServers = _mcp.getMcpServers();
|
|
2556
|
+
var remoteServers = _mcp.getMcpServers(boundSession);
|
|
2575
2557
|
if (remoteServers && remoteServers[serverName]) {
|
|
2576
2558
|
var rServer = remoteServers[serverName];
|
|
2577
2559
|
if (rServer.instance && rServer.instance._registeredTools && rServer.instance._registeredTools[toolName]) {
|
|
@@ -2673,6 +2655,7 @@ function createProjectContext(opts) {
|
|
|
2673
2655
|
|
|
2674
2656
|
// --- Destroy ---
|
|
2675
2657
|
function destroy() {
|
|
2658
|
+
_mcp.destroy();
|
|
2676
2659
|
_sharedBrowser.destroy();
|
|
2677
2660
|
_vendorLogin.destroy();
|
|
2678
2661
|
if (_scheduledMessages) _scheduledMessages.shutdown();
|