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.
package/lib/daemon.js CHANGED
@@ -597,6 +597,27 @@ var relay = createServer({
597
597
  }
598
598
  return { ok: false, error: "Project not found" };
599
599
  },
600
+ onGetProjectMcpServers: function (slug) {
601
+ for (var i = 0; i < config.projects.length; i++) {
602
+ if (config.projects[i].slug === slug) {
603
+ return config.projects[i].enabledMcpServers || [];
604
+ }
605
+ }
606
+ return [];
607
+ },
608
+ onSetProjectMcpServers: function (slug, servers) {
609
+ for (var i = 0; i < config.projects.length; i++) {
610
+ if (config.projects[i].slug === slug) {
611
+ if (servers && servers.length > 0) {
612
+ config.projects[i].enabledMcpServers = servers;
613
+ } else {
614
+ delete config.projects[i].enabledMcpServers;
615
+ }
616
+ saveConfig(config);
617
+ return;
618
+ }
619
+ }
620
+ },
600
621
  onGetServerDefaultMode: function () {
601
622
  return { mode: config.defaultMode || null };
602
623
  },
@@ -0,0 +1,355 @@
1
+ // mcp-local.js - Local MCP process manager
2
+ // Spawns and manages MCP stdio processes directly on the Clay server machine.
3
+ // Used when the client connects from localhost (no Native Host needed).
4
+
5
+ var child_process = require("child_process");
6
+ var fs = require("fs");
7
+ var path = require("path");
8
+ var os = require("os");
9
+
10
+ var CLAY_CONFIG_PATH = path.join(os.homedir(), ".clay", "mcp.json");
11
+
12
+ function createLocalMcp() {
13
+ var _configCache = {}; // name -> { command, args, env, url }
14
+ var _processes = {}; // name -> { proc, buffer, ready, tools, pendingInit }
15
+ var _pendingRequests = {}; // rpcId -> { callId, resolve, reject, timer }
16
+ var _initCallbacks = {}; // rpcId -> { name, phase }
17
+ var _jsonRpcId = 1;
18
+ var _initialized = false;
19
+ var _onServersReady = null; // callback when server list changes
20
+
21
+ // ---------- Config ----------
22
+
23
+ function ensureConfig() {
24
+ var dir = path.dirname(CLAY_CONFIG_PATH);
25
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
26
+ if (!fs.existsSync(CLAY_CONFIG_PATH)) {
27
+ fs.writeFileSync(CLAY_CONFIG_PATH, JSON.stringify({ mcpServers: {}, include: [] }, null, 2));
28
+ }
29
+ }
30
+
31
+ function readConfig() {
32
+ ensureConfig();
33
+ try {
34
+ return JSON.parse(fs.readFileSync(CLAY_CONFIG_PATH, "utf8"));
35
+ } catch (e) {
36
+ return { mcpServers: {}, include: [] };
37
+ }
38
+ }
39
+
40
+ function writeConfig(config) {
41
+ ensureConfig();
42
+ fs.writeFileSync(CLAY_CONFIG_PATH, JSON.stringify(config, null, 2));
43
+ }
44
+
45
+ function getMergedServers() {
46
+ var config = readConfig();
47
+ var merged = Object.assign({}, config.mcpServers || {});
48
+
49
+ var includes = config.include || [];
50
+ for (var i = 0; i < includes.length; i++) {
51
+ var resolved = includes[i].replace(/^~/, os.homedir());
52
+ try {
53
+ var ext = JSON.parse(fs.readFileSync(resolved, "utf8"));
54
+ var extServers = ext.mcpServers || {};
55
+ var names = Object.keys(extServers);
56
+ for (var j = 0; j < names.length; j++) {
57
+ if (!merged[names[j]]) merged[names[j]] = extServers[names[j]];
58
+ }
59
+ } catch (e) {
60
+ // Skip unreadable files
61
+ }
62
+ }
63
+
64
+ _configCache = merged;
65
+ return merged;
66
+ }
67
+
68
+ // ---------- Process Spawning ----------
69
+
70
+ function spawnServer(name) {
71
+ if (_processes[name]) return;
72
+ var cfg = _configCache[name];
73
+ if (!cfg || cfg.url || !cfg.command) return;
74
+
75
+ var env = Object.assign({}, process.env, cfg.env || {});
76
+ var proc;
77
+ try {
78
+ proc = child_process.spawn(cfg.command, cfg.args || [], {
79
+ env: env,
80
+ stdio: ["pipe", "pipe", "pipe"],
81
+ windowsHide: true,
82
+ });
83
+ } catch (e) {
84
+ return;
85
+ }
86
+
87
+ var entry = {
88
+ proc: proc,
89
+ buffer: "",
90
+ ready: false,
91
+ tools: [],
92
+ };
93
+ _processes[name] = entry;
94
+
95
+ proc.stdout.on("data", function (chunk) {
96
+ entry.buffer += chunk.toString("utf8");
97
+ drainJsonRpc(name, entry);
98
+ });
99
+
100
+ proc.stderr.on("data", function () {
101
+ // Ignore stderr
102
+ });
103
+
104
+ proc.on("error", function () {
105
+ delete _processes[name];
106
+ });
107
+
108
+ proc.on("exit", function () {
109
+ delete _processes[name];
110
+ });
111
+
112
+ // MCP initialize handshake
113
+ var initId = _jsonRpcId++;
114
+ _initCallbacks[initId] = { name: name, phase: "initialize" };
115
+ proc.stdin.write(JSON.stringify({
116
+ jsonrpc: "2.0",
117
+ id: initId,
118
+ method: "initialize",
119
+ params: {
120
+ protocolVersion: "2024-11-05",
121
+ capabilities: {},
122
+ clientInfo: { name: "clay-mcp-local", version: "1.0.0" },
123
+ },
124
+ }) + "\n");
125
+ }
126
+
127
+ function killServer(name) {
128
+ var entry = _processes[name];
129
+ if (!entry) return;
130
+ entry.proc.kill("SIGTERM");
131
+ setTimeout(function () {
132
+ if (entry.proc && !entry.proc.killed) entry.proc.kill("SIGKILL");
133
+ }, 3000);
134
+ delete _processes[name];
135
+ }
136
+
137
+ // ---------- JSON-RPC Parser ----------
138
+
139
+ function drainJsonRpc(name, entry) {
140
+ var lines = entry.buffer.split("\n");
141
+ entry.buffer = lines.pop();
142
+ for (var i = 0; i < lines.length; i++) {
143
+ var line = lines[i].trim();
144
+ if (!line) continue;
145
+ try {
146
+ var msg = JSON.parse(line);
147
+ handleMcpResponse(name, msg);
148
+ } catch (e) {
149
+ // Skip unparseable
150
+ }
151
+ }
152
+ }
153
+
154
+ function handleMcpResponse(name, msg) {
155
+ // Init handshake responses
156
+ if (msg.id !== undefined && _initCallbacks[msg.id]) {
157
+ var cb = _initCallbacks[msg.id];
158
+ delete _initCallbacks[msg.id];
159
+
160
+ if (cb.phase === "initialize") {
161
+ var entry = _processes[name];
162
+ if (entry) {
163
+ // Send initialized notification
164
+ entry.proc.stdin.write(JSON.stringify({
165
+ jsonrpc: "2.0",
166
+ method: "notifications/initialized",
167
+ }) + "\n");
168
+
169
+ // Request tools/list
170
+ var toolsId = _jsonRpcId++;
171
+ _initCallbacks[toolsId] = { name: name, phase: "tools_list" };
172
+ entry.proc.stdin.write(JSON.stringify({
173
+ jsonrpc: "2.0",
174
+ id: toolsId,
175
+ method: "tools/list",
176
+ params: {},
177
+ }) + "\n");
178
+ }
179
+ return;
180
+ }
181
+
182
+ if (cb.phase === "tools_list") {
183
+ var tools = (msg.result && msg.result.tools) || [];
184
+ var entry2 = _processes[name];
185
+ if (entry2) {
186
+ entry2.tools = tools;
187
+ entry2.ready = true;
188
+ }
189
+ if (_onServersReady) _onServersReady();
190
+ return;
191
+ }
192
+ }
193
+
194
+ // Tool call responses
195
+ if (msg.id !== undefined && _pendingRequests[msg.id]) {
196
+ var req = _pendingRequests[msg.id];
197
+ delete _pendingRequests[msg.id];
198
+ if (req.timer) clearTimeout(req.timer);
199
+
200
+ if (msg.error) {
201
+ req.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
202
+ } else {
203
+ req.resolve(msg.result);
204
+ }
205
+ return;
206
+ }
207
+ }
208
+
209
+ // ---------- Tool Call Relay ----------
210
+
211
+ function callTool(serverName, toolName, args) {
212
+ return new Promise(function (resolve, reject) {
213
+ var entry = _processes[serverName];
214
+ if (!entry || !entry.ready) {
215
+ reject(new Error("MCP server not running: " + serverName));
216
+ return;
217
+ }
218
+
219
+ var rpcId = _jsonRpcId++;
220
+ _pendingRequests[rpcId] = {
221
+ resolve: resolve,
222
+ reject: reject,
223
+ timer: setTimeout(function () {
224
+ delete _pendingRequests[rpcId];
225
+ reject(new Error("Tool call timed out after 30s"));
226
+ }, 30000),
227
+ };
228
+
229
+ entry.proc.stdin.write(JSON.stringify({
230
+ jsonrpc: "2.0",
231
+ id: rpcId,
232
+ method: "tools/call",
233
+ params: { name: toolName, arguments: args || {} },
234
+ }) + "\n");
235
+ });
236
+ }
237
+
238
+ // ---------- Public API ----------
239
+
240
+ function initialize(onReady) {
241
+ if (_initialized) return;
242
+ _initialized = true;
243
+ _onServersReady = onReady || null;
244
+
245
+ getMergedServers();
246
+ var names = Object.keys(_configCache);
247
+ for (var i = 0; i < names.length; i++) {
248
+ spawnServer(names[i]);
249
+ }
250
+ }
251
+
252
+ function shutdown() {
253
+ var names = Object.keys(_processes);
254
+ for (var i = 0; i < names.length; i++) {
255
+ killServer(names[i]);
256
+ }
257
+ _processes = {};
258
+ _initialized = false;
259
+ }
260
+
261
+ function getAvailableServers() {
262
+ var servers = [];
263
+ var names = Object.keys(_configCache);
264
+ for (var i = 0; i < names.length; i++) {
265
+ var name = names[i];
266
+ var cfg = _configCache[name];
267
+ var entry = _processes[name];
268
+ servers.push({
269
+ name: name,
270
+ transport: cfg.url ? "http" : "stdio",
271
+ ready: !!(entry && entry.ready),
272
+ tools: entry ? entry.tools : [],
273
+ toolCount: entry ? entry.tools.length : 0,
274
+ source: "local",
275
+ });
276
+ }
277
+ return servers;
278
+ }
279
+
280
+ function isReady() {
281
+ return _initialized;
282
+ }
283
+
284
+ function addServer(name, command, args, env) {
285
+ var config = readConfig();
286
+ config.mcpServers = config.mcpServers || {};
287
+ config.mcpServers[name] = { command: command, args: args || [], env: env || {} };
288
+ writeConfig(config);
289
+ _configCache[name] = config.mcpServers[name];
290
+ spawnServer(name);
291
+ }
292
+
293
+ function removeServer(name) {
294
+ var config = readConfig();
295
+ if (config.mcpServers && config.mcpServers[name]) {
296
+ delete config.mcpServers[name];
297
+ writeConfig(config);
298
+ }
299
+ killServer(name);
300
+ delete _configCache[name];
301
+ }
302
+
303
+ function addImport(filePath) {
304
+ var resolved = filePath.replace(/^~/, os.homedir());
305
+ try {
306
+ var ext = JSON.parse(fs.readFileSync(resolved, "utf8"));
307
+ var count = Object.keys(ext.mcpServers || {}).length;
308
+ if (count === 0) return { error: "No mcpServers found in " + filePath };
309
+ } catch (e) {
310
+ return { error: "Cannot read file: " + e.message };
311
+ }
312
+
313
+ var config = readConfig();
314
+ config.include = config.include || [];
315
+ if (config.include.indexOf(filePath) === -1) {
316
+ config.include.push(filePath);
317
+ writeConfig(config);
318
+ }
319
+
320
+ // Re-merge and spawn new servers
321
+ getMergedServers();
322
+ var names = Object.keys(_configCache);
323
+ for (var i = 0; i < names.length; i++) {
324
+ if (!_processes[names[i]]) spawnServer(names[i]);
325
+ }
326
+
327
+ return { ok: true, count: count };
328
+ }
329
+
330
+ function removeImport(filePath) {
331
+ var config = readConfig();
332
+ config.include = (config.include || []).filter(function (p) { return p !== filePath; });
333
+ writeConfig(config);
334
+ }
335
+
336
+ function getImports() {
337
+ var config = readConfig();
338
+ return config.include || [];
339
+ }
340
+
341
+ return {
342
+ initialize: initialize,
343
+ shutdown: shutdown,
344
+ callTool: callTool,
345
+ getAvailableServers: getAvailableServers,
346
+ isReady: isReady,
347
+ addServer: addServer,
348
+ removeServer: removeServer,
349
+ addImport: addImport,
350
+ removeImport: removeImport,
351
+ getImports: getImports,
352
+ };
353
+ }
354
+
355
+ module.exports = { createLocalMcp: createLocalMcp };
@@ -34,6 +34,7 @@ function attachConnection(ctx) {
34
34
  var sendTo = ctx.sendTo;
35
35
  var opts = ctx.opts;
36
36
  var _loop = ctx._loop;
37
+ var _mcp = ctx._mcp;
37
38
  var _notifications = ctx._notifications;
38
39
  var hydrateImageRefs = ctx.hydrateImageRefs;
39
40
  var broadcastClientCount = ctx.broadcastClientCount;
@@ -98,6 +99,7 @@ function attachConnection(ctx) {
98
99
  sendTo(ws, { type: "notes_list", notes: nm.list() });
99
100
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
100
101
  _loop.sendConnectionState(ws);
102
+ if (_mcp) _mcp.sendConnectionState(ws);
101
103
  if (_notifications) _notifications.sendConnectionState(ws, sendTo);
102
104
 
103
105
  // Session list (filtered for access control)