clay-server 4.9.0-beta.1 → 5.0.0-beta.2

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.
@@ -1,375 +1,148 @@
1
- var crypto = require("crypto");
2
-
3
- // MCP Bridge module: manages remote MCP servers reported by the Chrome Extension.
4
- // Creates proxy MCP server objects that forward tool calls over WebSocket.
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 send = ctx.send;
9
- var sendTo = ctx.sendTo;
10
- var slug = ctx.slug;
11
- var isMate = ctx.isMate;
12
- var getEnabledMcpServers = ctx.getEnabledMcpServers;
13
- var setEnabledMcpServers = ctx.setEnabledMcpServers;
14
- var getExtensionWs = ctx.getExtensionWs;
15
- var getExtensionId = ctx.getExtensionId || function () { return null; };
16
- var localMcp = ctx.localMcp || null; // mcp-local instance for localhost clients
17
-
18
- // Available servers reported by extension: { name -> { name, transport, tools, enabled } }
19
- var _availableServers = {};
20
-
21
- // Proxy MCP server objects for the SDK: { name -> sdkMcpServerConfig }
22
- var _proxyServers = {};
23
-
24
- // Pending tool calls: { callId -> { resolve, reject, timer } }
25
- var _pendingCalls = {};
26
-
27
- var TOOL_TIMEOUT_MS = 30000;
28
-
29
- // ---------- Message Handler ----------
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 === "mcp_servers_available") {
33
- handleServersAvailable(ws, msg);
34
- return true;
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 === "mcp_tool_result") {
37
- handleToolResult(msg);
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 === "mcp_tool_error") {
41
- handleToolError(msg);
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 === "mcp_toggle_server") {
45
- handleToggleServer(ws, msg);
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
- var _remoteHostConnected = false;
52
-
53
- function handleServersAvailable(ws, msg) {
54
- var servers = msg.servers || [];
55
- _remoteHostConnected = !!msg.hostConnected;
56
- _availableServers = {};
57
- for (var i = 0; i < servers.length; i++) {
58
- var s = servers[i];
59
- _availableServers[s.name] = {
60
- name: s.name,
61
- transport: s.transport || "stdio",
62
- tools: s.tools || [],
63
- enabled: s.enabled !== false,
64
- };
65
- }
66
-
67
- // Rebuild proxy servers based on project-level enabled list
68
- rebuildProxyServers();
69
-
70
- // Broadcast updated state to all clients
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
- return shape;
279
- }
280
-
281
- // ---------- State Broadcasting ----------
282
-
283
- function broadcastMcpState() {
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
- function rebuildAndBroadcast() {
362
- rebuildProxyServers();
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
- return {
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 };
@@ -84,13 +84,19 @@ function attachSessionDocument(ctx) {
84
84
  function presentWireframe(args, caller) {
85
85
  if (!caller) return toolError("present_wireframe requires a session-bound tool server");
86
86
  if (typeof args.source !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(args.id || "")) return toolError("Provide source and a short stable id using letters, digits, hyphens or underscores.");
87
+ var source;
87
88
  try {
88
- var source = wireframeRenderer.prepareSource(args.source, "diagram.puml");
89
- require("./salt-parser").parseSalt(source);
89
+ source = wireframeRenderer.prepareSource(args.source, "diagram.puml");
90
90
  if (!/^\s*@end(?:salt|uml)\b/im.test(source)) return toolError("Provide a complete @startsalt/@endsalt or @startuml/@enduml diagram.");
91
+ } catch (e) {
92
+ return toolError(e.message || String(e));
93
+ }
94
+ return wireframeRenderer.renderPlantUml(source).then(function () {
91
95
  sendToSession(caller.localId, { type: "wireframe_present", source: source, id: args.id, title: String(args.title || "clay-sketch").slice(0, 100), sessionId: caller.localId });
92
96
  return toolResult({ ready: true, id: args.id, saved: false });
93
- } catch (e) { return toolError(e.message); }
97
+ }).catch(function (e) {
98
+ return toolError(e && e.message ? e.message : String(e));
99
+ });
94
100
  }
95
101
 
96
102
  function getToolDefs(boundSession) {
@@ -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);