clay-server 2.28.0-beta.1 → 2.28.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.
@@ -0,0 +1,371 @@
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.
6
+
7
+ 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
+
31
+ function handleMcpMessage(ws, msg) {
32
+ if (msg.type === "mcp_servers_available") {
33
+ handleServersAvailable(ws, msg);
34
+ return true;
35
+ }
36
+ if (msg.type === "mcp_tool_result") {
37
+ handleToolResult(msg);
38
+ return true;
39
+ }
40
+ if (msg.type === "mcp_tool_error") {
41
+ handleToolError(msg);
42
+ return true;
43
+ }
44
+ if (msg.type === "mcp_toggle_server") {
45
+ handleToggleServer(ws, msg);
46
+ return true;
47
+ }
48
+ return false;
49
+ }
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
+ var pending = _pendingCalls[callId];
77
+ if (!pending) return;
78
+ if (pending.timer) clearTimeout(pending.timer);
79
+ delete _pendingCalls[callId];
80
+ pending.resolve(msg.result || { content: [{ type: "text", text: "(empty result)" }] });
81
+ }
82
+
83
+ function handleToolError(msg) {
84
+ var callId = msg.callId;
85
+ var pending = _pendingCalls[callId];
86
+ if (!pending) return;
87
+ if (pending.timer) clearTimeout(pending.timer);
88
+ delete _pendingCalls[callId];
89
+ pending.reject(new Error(msg.error || "MCP tool call failed"));
90
+ }
91
+
92
+ function handleToggleServer(ws, msg) {
93
+ var name = msg.name;
94
+ var enabled = !!msg.enabled;
95
+
96
+ var list = getEnabledMcpServers() || [];
97
+ var idx = list.indexOf(name);
98
+
99
+ if (enabled && idx === -1) {
100
+ list.push(name);
101
+ } else if (!enabled && idx !== -1) {
102
+ list.splice(idx, 1);
103
+ }
104
+
105
+ setEnabledMcpServers(list);
106
+ rebuildProxyServers();
107
+ broadcastMcpState();
108
+ }
109
+
110
+ // ---------- Proxy Server Builder ----------
111
+
112
+ function rebuildProxyServers() {
113
+ _proxyServers = {};
114
+
115
+ var sdk;
116
+ try {
117
+ sdk = require("@anthropic-ai/claude-agent-sdk");
118
+ } catch (e) {
119
+ console.error("[mcp-bridge] Failed to load SDK:", e.message);
120
+ return;
121
+ }
122
+
123
+ var createSdkMcpServer = sdk.createSdkMcpServer;
124
+ var tool = sdk.tool;
125
+ if (!createSdkMcpServer || !tool) {
126
+ console.error("[mcp-bridge] SDK missing createSdkMcpServer or tool helper");
127
+ return;
128
+ }
129
+
130
+ var z;
131
+ try { z = require("zod").z; } catch (e) {
132
+ try { z = require("zod"); } catch (e2) {
133
+ console.error("[mcp-bridge] Failed to load zod:", e2.message);
134
+ return;
135
+ }
136
+ }
137
+
138
+ var enabledList = getEnabledMcpServers() || [];
139
+
140
+ // --- Remote servers (via Extension) ---
141
+ var serverNames = Object.keys(_availableServers);
142
+ for (var si = 0; si < serverNames.length; si++) {
143
+ var serverName = serverNames[si];
144
+ var serverInfo = _availableServers[serverName];
145
+
146
+ if (!serverInfo.enabled) continue;
147
+ if (enabledList.indexOf(serverName) === -1) continue;
148
+
149
+ var tools = [];
150
+ var serverTools = serverInfo.tools || [];
151
+
152
+ for (var ti = 0; ti < serverTools.length; ti++) {
153
+ var mcpTool = serverTools[ti];
154
+ var toolName = mcpTool.name;
155
+ var toolDesc = mcpTool.description || toolName;
156
+ var shape = buildZodShape(z, mcpTool.inputSchema);
157
+
158
+ tools.push(tool(
159
+ toolName,
160
+ toolDesc,
161
+ shape,
162
+ createToolHandler(serverName, toolName)
163
+ ));
164
+ }
165
+
166
+ if (tools.length > 0) {
167
+ var mcpServer = createSdkMcpServer({
168
+ name: serverName,
169
+ version: "1.0.0",
170
+ tools: tools,
171
+ });
172
+ _proxyServers[serverName] = mcpServer;
173
+ }
174
+ }
175
+
176
+ // --- Local servers (direct process, localhost only) ---
177
+ if (localMcp && localMcp.isReady()) {
178
+ var localServers = localMcp.getAvailableServers();
179
+ for (var li = 0; li < localServers.length; li++) {
180
+ var ls = localServers[li];
181
+ if (!ls.ready) continue;
182
+ if (_proxyServers[ls.name]) continue; // remote takes precedence if same name
183
+ if (enabledList.indexOf(ls.name) === -1) continue;
184
+
185
+ var localTools = [];
186
+ for (var lti = 0; lti < ls.tools.length; lti++) {
187
+ var lt = ls.tools[lti];
188
+ var ltShape = buildZodShape(z, lt.inputSchema);
189
+ localTools.push(tool(
190
+ lt.name,
191
+ lt.description || lt.name,
192
+ ltShape,
193
+ createLocalToolHandler(ls.name, lt.name)
194
+ ));
195
+ }
196
+
197
+ if (localTools.length > 0) {
198
+ _proxyServers[ls.name] = createSdkMcpServer({
199
+ name: ls.name,
200
+ version: "1.0.0",
201
+ tools: localTools,
202
+ });
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ function createLocalToolHandler(serverName, toolName) {
209
+ return function (args) {
210
+ return localMcp.callTool(serverName, toolName, args);
211
+ };
212
+ }
213
+
214
+ function createToolHandler(serverName, toolName) {
215
+ return function (args) {
216
+ return new Promise(function (resolve, reject) {
217
+ var extWs = getExtensionWs();
218
+ if (!extWs || extWs.readyState !== 1) {
219
+ reject(new Error("Browser extension not connected. Cannot reach MCP server: " + serverName));
220
+ return;
221
+ }
222
+
223
+ var callId = "mc_" + Date.now() + "_" + crypto.randomUUID().slice(0, 8);
224
+
225
+ var timer = setTimeout(function () {
226
+ delete _pendingCalls[callId];
227
+ reject(new Error("MCP tool call timed out after " + (TOOL_TIMEOUT_MS / 1000) + "s"));
228
+ }, TOOL_TIMEOUT_MS);
229
+
230
+ _pendingCalls[callId] = { resolve: resolve, reject: reject, timer: timer };
231
+
232
+ sendTo(extWs, {
233
+ type: "mcp_tool_call",
234
+ callId: callId,
235
+ server: serverName,
236
+ method: "tools/call",
237
+ params: { name: toolName, arguments: args },
238
+ });
239
+ });
240
+ };
241
+ }
242
+
243
+ // Build a Zod shape from MCP JSON Schema inputSchema
244
+ function buildZodShape(z, inputSchema) {
245
+ if (!inputSchema || !inputSchema.properties) return {};
246
+ var shape = {};
247
+ var props = inputSchema.properties;
248
+ var required = inputSchema.required || [];
249
+ var keys = Object.keys(props);
250
+
251
+ for (var i = 0; i < keys.length; i++) {
252
+ var k = keys[i];
253
+ var p = props[k];
254
+ var field;
255
+
256
+ if (p.type === "number" || p.type === "integer") {
257
+ field = z.number();
258
+ } else if (p.type === "boolean") {
259
+ field = z.boolean();
260
+ } else if (p.type === "array") {
261
+ field = z.array(z.any());
262
+ } else if (p.type === "object") {
263
+ field = z.record(z.any());
264
+ } else if (p.enum) {
265
+ field = z.enum(p.enum);
266
+ } else {
267
+ field = z.string();
268
+ }
269
+
270
+ if (p.description) field = field.describe(p.description);
271
+ if (required.indexOf(k) === -1) field = field.optional();
272
+ shape[k] = field;
273
+ }
274
+ return shape;
275
+ }
276
+
277
+ // ---------- State Broadcasting ----------
278
+
279
+ function broadcastMcpState() {
280
+ var state = buildMcpState();
281
+ send(state);
282
+ }
283
+
284
+ function sendConnectionState(ws) {
285
+ sendTo(ws, buildMcpState());
286
+ }
287
+
288
+ function buildMcpState() {
289
+ var enabledList = getEnabledMcpServers() || [];
290
+ var servers = [];
291
+ var seen = {};
292
+
293
+ // Remote servers (from Extension)
294
+ var names = Object.keys(_availableServers);
295
+ for (var i = 0; i < names.length; i++) {
296
+ var name = names[i];
297
+ var info = _availableServers[name];
298
+ seen[name] = true;
299
+ servers.push({
300
+ name: name,
301
+ transport: info.transport,
302
+ toolCount: (info.tools || []).length,
303
+ extensionEnabled: info.enabled,
304
+ projectEnabled: enabledList.indexOf(name) !== -1,
305
+ source: "remote",
306
+ });
307
+ }
308
+
309
+ // Local servers
310
+ if (localMcp && localMcp.isReady()) {
311
+ var localServers = localMcp.getAvailableServers();
312
+ for (var j = 0; j < localServers.length; j++) {
313
+ var ls = localServers[j];
314
+ if (seen[ls.name]) continue;
315
+ servers.push({
316
+ name: ls.name,
317
+ transport: ls.transport || "stdio",
318
+ toolCount: ls.toolCount || 0,
319
+ extensionEnabled: true,
320
+ projectEnabled: enabledList.indexOf(ls.name) !== -1,
321
+ source: "local",
322
+ });
323
+ }
324
+ }
325
+
326
+ return {
327
+ type: "mcp_servers_state",
328
+ servers: servers,
329
+ hostConnected: _remoteHostConnected || !!(localMcp && localMcp.isReady()),
330
+ extensionId: getExtensionId() || null,
331
+ };
332
+ }
333
+
334
+ // ---------- Public API ----------
335
+
336
+ function getMcpServers() {
337
+ return _proxyServers;
338
+ }
339
+
340
+ function cancelAllPending() {
341
+ var ids = Object.keys(_pendingCalls);
342
+ for (var i = 0; i < ids.length; i++) {
343
+ var pending = _pendingCalls[ids[i]];
344
+ if (pending.timer) clearTimeout(pending.timer);
345
+ pending.reject(new Error("MCP bridge disconnected"));
346
+ }
347
+ _pendingCalls = {};
348
+ }
349
+
350
+ function handleExtensionDisconnect() {
351
+ cancelAllPending();
352
+ _availableServers = {};
353
+ _proxyServers = {};
354
+ broadcastMcpState();
355
+ }
356
+
357
+ function rebuildAndBroadcast() {
358
+ rebuildProxyServers();
359
+ broadcastMcpState();
360
+ }
361
+
362
+ return {
363
+ handleMcpMessage: handleMcpMessage,
364
+ getMcpServers: getMcpServers,
365
+ sendConnectionState: sendConnectionState,
366
+ handleExtensionDisconnect: handleExtensionDisconnect,
367
+ rebuildAndBroadcast: rebuildAndBroadcast,
368
+ };
369
+ }
370
+
371
+ module.exports = { attachMcp: attachMcp };
@@ -210,6 +210,7 @@ function attachUserMessage(ctx) {
210
210
  // --- Browser Extension ---
211
211
  if (msg.type === "browser_tab_list") {
212
212
  browserState._extensionWs = ws; // Track which client has the extension
213
+ if (msg.extensionId) browserState._extensionId = msg.extensionId;
213
214
  var tabs = msg.tabs || [];
214
215
  browserState._browserTabList = {};
215
216
  for (var bti = 0; bti < tabs.length; bti++) {
package/lib/project.js CHANGED
@@ -26,6 +26,8 @@ var { attachFilesystem } = require("./project-filesystem");
26
26
  var { attachSessions } = require("./project-sessions");
27
27
  var { attachUserMessage } = require("./project-user-message");
28
28
  var { attachConnection } = require("./project-connection");
29
+ var { attachMcp } = require("./project-mcp");
30
+ var { createLocalMcp } = require("./mcp-local");
29
31
  // project-notifications is attached globally in server.js, passed via opts.notificationsModule
30
32
 
31
33
  // --- Context Sources persistence ---
@@ -218,22 +220,24 @@ function createProjectContext(opts) {
218
220
  // --- Per-project clients ---
219
221
  var clients = new Set();
220
222
 
221
- // --- Browser extension state ---
222
- var _browserTabList = {}; // tabId -> { id, url, title, favIconUrl }
223
+ // --- Browser extension state (shared mutable object) ---
223
224
  var _pendingDebateProposals = {}; // proposalId -> { resolve, briefData }
224
- var _extensionWs = null; // WebSocket of the client with the Chrome extension
225
225
  var _extToken = crypto.randomUUID(); // Auth token for MCP server bridge
226
- var pendingExtensionRequests = {}; // requestId -> { resolve, timer }
226
+ var browserState = {
227
+ _browserTabList: {},
228
+ _extensionWs: null,
229
+ pendingExtensionRequests: {}
230
+ };
227
231
 
228
232
  function sendExtensionCommand(ws, command, args, timeout) {
229
233
  return new Promise(function(resolve) {
230
234
  var requestId = crypto.randomUUID();
231
235
  var ms = timeout || 3000;
232
236
  var timer = setTimeout(function() {
233
- delete pendingExtensionRequests[requestId];
237
+ delete browserState.pendingExtensionRequests[requestId];
234
238
  resolve(null);
235
239
  }, ms);
236
- pendingExtensionRequests[requestId] = { resolve: resolve, timer: timer };
240
+ browserState.pendingExtensionRequests[requestId] = { resolve: resolve, timer: timer };
237
241
  sendTo(ws, {
238
242
  type: "extension_command",
239
243
  command: command,
@@ -245,10 +249,10 @@ function createProjectContext(opts) {
245
249
 
246
250
  // Send extension command via the tracked extension client (for MCP bridge)
247
251
  function sendExtensionCommandAny(command, args, timeout) {
248
- if (!_extensionWs || _extensionWs.readyState !== 1) {
252
+ if (!browserState._extensionWs || browserState._extensionWs.readyState !== 1) {
249
253
  return Promise.reject(new Error("Browser extension not connected"));
250
254
  }
251
- return sendExtensionCommand(_extensionWs, command, args, timeout);
255
+ return sendExtensionCommand(browserState._extensionWs, command, args, timeout);
252
256
  }
253
257
 
254
258
  function requestTabContext(ws, tabId) {
@@ -405,6 +409,29 @@ function createProjectContext(opts) {
405
409
  // before the SDK has warmed up and fired system/init.
406
410
  if (sm._savedDefaultModel) sm.currentModel = sm._savedDefaultModel;
407
411
 
412
+ // --- Local MCP (direct process management for localhost clients) ---
413
+ var _localMcp = createLocalMcp();
414
+
415
+ // --- MCP bridge (remote MCP servers via Chrome Extension) ---
416
+ var _mcp = attachMcp({
417
+ send: send,
418
+ sendTo: sendTo,
419
+ slug: slug,
420
+ isMate: isMate,
421
+ getExtensionWs: function () { return browserState._extensionWs; },
422
+ getExtensionId: function () { return browserState._extensionId || null; },
423
+ getEnabledMcpServers: function () {
424
+ return typeof opts.onGetProjectMcpServers === "function"
425
+ ? opts.onGetProjectMcpServers(slug) : [];
426
+ },
427
+ setEnabledMcpServers: function (servers) {
428
+ if (typeof opts.onSetProjectMcpServers === "function") {
429
+ opts.onSetProjectMcpServers(slug, servers);
430
+ }
431
+ },
432
+ localMcp: _localMcp,
433
+ });
434
+
408
435
  // --- SDK bridge ---
409
436
  var sdk = createSDKBridge({
410
437
  cwd: cwd,
@@ -445,7 +472,7 @@ function createProjectContext(opts) {
445
472
  try {
446
473
  var browserMcp = require("./browser-mcp-server");
447
474
  var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
448
- return Object.values(_browserTabList || {});
475
+ return Object.values(browserState._browserTabList || {});
449
476
  }, {
450
477
  watchTab: function (tabId) {
451
478
  var key = "tab:" + tabId;
@@ -479,6 +506,7 @@ function createProjectContext(opts) {
479
506
 
480
507
  return Object.keys(servers).length > 0 ? servers : undefined;
481
508
  })(),
509
+ getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
482
510
  onProcessingChanged: onProcessingChanged,
483
511
  onTurnDone: isMate ? function (session, preview) {
484
512
  digestDmTurn(session, preview);
@@ -549,6 +577,14 @@ function createProjectContext(opts) {
549
577
  // --- WS connection handler (delegated to project-connection.js) ---
550
578
  function handleConnection(ws, wsUser) {
551
579
  _connection.handleConnection(ws, wsUser, handleMessage, handleDisconnection);
580
+
581
+ // Initialize local MCP when a localhost client connects
582
+ if (ws._clayLocal && _localMcp && !_localMcp.isReady()) {
583
+ _localMcp.initialize(function () {
584
+ // Rebuild proxy servers and broadcast state when local servers are ready
585
+ _mcp.rebuildAndBroadcast();
586
+ });
587
+ }
552
588
  }
553
589
 
554
590
  // --- WS message handler ---
@@ -697,6 +733,9 @@ function createProjectContext(opts) {
697
733
  return;
698
734
  }
699
735
 
736
+ // --- MCP bridge (remote MCP servers via extension) ---
737
+ if (_mcp.handleMcpMessage(ws, msg)) return;
738
+
700
739
  // --- Knowledge file management (delegated to project-knowledge.js) ---
701
740
  if (_knowledge.handleKnowledgeMessage(ws, msg)) return;
702
741
 
@@ -910,7 +949,7 @@ function createProjectContext(opts) {
910
949
  imagesDir: imagesDir,
911
950
  onProcessingChanged: onProcessingChanged,
912
951
  _loop: _loop,
913
- browserState: { _browserTabList: _browserTabList, _extensionWs: _extensionWs, pendingExtensionRequests: pendingExtensionRequests },
952
+ browserState: browserState,
914
953
  sendExtensionCommandAny: sendExtensionCommandAny,
915
954
  requestTabContext: requestTabContext,
916
955
  scheduleMessage: scheduleMessage,
@@ -964,7 +1003,7 @@ function createProjectContext(opts) {
964
1003
  getOsUserInfoForReq: getOsUserInfoForReq,
965
1004
  sendExtensionCommandAny: sendExtensionCommandAny,
966
1005
  _extToken: _extToken,
967
- _browserTabList: _browserTabList,
1006
+ _browserTabList: browserState._browserTabList,
968
1007
  });
969
1008
  var handleHTTP = _http.handleHTTP;
970
1009
 
@@ -986,6 +1025,7 @@ function createProjectContext(opts) {
986
1025
  sendTo: sendTo,
987
1026
  opts: opts,
988
1027
  _loop: _loop,
1028
+ _mcp: _mcp,
989
1029
  _notifications: _notifications,
990
1030
  hydrateImageRefs: hydrateImageRefs,
991
1031
  broadcastClientCount: broadcastClientCount,
package/lib/public/app.js CHANGED
@@ -33,6 +33,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
33
33
  import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
34
34
  import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
35
35
  import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
36
+ import { initMcp } from './modules/mcp-ui.js';
36
37
  import { initScheduler, resetScheduler, handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, openSchedulerToTab, isSchedulerOpen, closeScheduler, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles, getUpcomingSchedules } from './modules/scheduler.js';
37
38
  import { initAsciiLogo, startLogoAnimation, stopLogoAnimation } from './modules/ascii-logo.js';
38
39
  import { initPlaybook, openPlaybook, getPlaybooks, getPlaybookForTip, isCompleted as isPlaybookCompleted } from './modules/playbook.js';
@@ -1080,6 +1081,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1080
1081
 
1081
1082
  // --- Ralph Preview Modal (delegated to app-loop-ui.js) ---
1082
1083
 
1084
+ // --- MCP Servers ---
1085
+ initMcp();
1086
+
1083
1087
  // --- Skills ---
1084
1088
  initSkills({
1085
1089
  get ws() { return ws; },