clay-server 2.31.0 → 2.32.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.
Files changed (74) hide show
  1. package/lib/browser-mcp-server.js +32 -44
  2. package/lib/debate-mcp-server.js +14 -31
  3. package/lib/mcp-local.js +31 -1
  4. package/lib/project-connection.js +4 -2
  5. package/lib/project-filesystem.js +47 -1
  6. package/lib/project-http.js +75 -8
  7. package/lib/project-mcp.js +4 -0
  8. package/lib/project-sessions.js +88 -51
  9. package/lib/project-user-message.js +12 -7
  10. package/lib/project.js +204 -90
  11. package/lib/public/app.js +123 -448
  12. package/lib/public/codex-avatar.png +0 -0
  13. package/lib/public/css/debate.css +3 -2
  14. package/lib/public/css/filebrowser.css +91 -1
  15. package/lib/public/css/icon-strip.css +21 -5
  16. package/lib/public/css/input.css +181 -100
  17. package/lib/public/css/mates.css +43 -0
  18. package/lib/public/css/mention.css +48 -4
  19. package/lib/public/css/menus.css +1 -1
  20. package/lib/public/css/messages.css +2 -0
  21. package/lib/public/css/notifications-center.css +19 -0
  22. package/lib/public/index.html +46 -24
  23. package/lib/public/modules/app-connection.js +138 -37
  24. package/lib/public/modules/app-cursors.js +18 -17
  25. package/lib/public/modules/app-debate-ui.js +9 -9
  26. package/lib/public/modules/app-dm.js +170 -131
  27. package/lib/public/modules/app-favicon.js +28 -26
  28. package/lib/public/modules/app-header.js +79 -68
  29. package/lib/public/modules/app-home-hub.js +55 -47
  30. package/lib/public/modules/app-loop-ui.js +34 -18
  31. package/lib/public/modules/app-loop-wizard.js +6 -6
  32. package/lib/public/modules/app-messages.js +195 -152
  33. package/lib/public/modules/app-misc.js +23 -12
  34. package/lib/public/modules/app-notifications.js +91 -3
  35. package/lib/public/modules/app-panels.js +203 -49
  36. package/lib/public/modules/app-projects.js +159 -150
  37. package/lib/public/modules/app-rate-limit.js +5 -4
  38. package/lib/public/modules/app-rendering.js +149 -101
  39. package/lib/public/modules/app-skills-install.js +4 -4
  40. package/lib/public/modules/context-sources.js +12 -41
  41. package/lib/public/modules/dom-refs.js +21 -0
  42. package/lib/public/modules/filebrowser.js +173 -2
  43. package/lib/public/modules/input.js +86 -0
  44. package/lib/public/modules/mate-sidebar.js +38 -0
  45. package/lib/public/modules/mention.js +24 -6
  46. package/lib/public/modules/scheduler.js +1 -1
  47. package/lib/public/modules/sidebar-mates.js +66 -34
  48. package/lib/public/modules/sidebar-mobile.js +34 -30
  49. package/lib/public/modules/sidebar-projects.js +60 -57
  50. package/lib/public/modules/sidebar-sessions.js +75 -69
  51. package/lib/public/modules/sidebar.js +12 -20
  52. package/lib/public/modules/skills.js +8 -9
  53. package/lib/public/modules/sticky-notes.js +1 -2
  54. package/lib/public/modules/store.js +9 -2
  55. package/lib/public/modules/stt.js +4 -1
  56. package/lib/public/modules/tools.js +14 -9
  57. package/lib/sdk-bridge.js +511 -1113
  58. package/lib/sdk-message-processor.js +123 -134
  59. package/lib/sdk-worker.js +4 -0
  60. package/lib/server-dm.js +1 -0
  61. package/lib/server.js +86 -1
  62. package/lib/sessions.js +47 -36
  63. package/lib/ws-schema.js +2 -0
  64. package/lib/yoke/adapters/claude-worker.js +559 -0
  65. package/lib/yoke/adapters/claude.js +1418 -0
  66. package/lib/yoke/adapters/codex.js +968 -0
  67. package/lib/yoke/adapters/gemini.js +668 -0
  68. package/lib/yoke/codex-app-server.js +307 -0
  69. package/lib/yoke/index.js +199 -0
  70. package/lib/yoke/instructions.js +62 -0
  71. package/lib/yoke/interface.js +92 -0
  72. package/lib/yoke/mcp-bridge-server.js +294 -0
  73. package/lib/yoke/package.json +7 -0
  74. package/package.json +3 -1
@@ -1,11 +1,11 @@
1
- // Browser MCP Server for Clay (in-process SDK version)
2
- // Provides browser automation tools to Claude via createSdkMcpServer.
3
- // Calls sendExtensionCommand directly instead of HTTP bridge.
1
+ // Browser MCP Server for Clay
2
+ // Provides browser automation tool definitions.
3
+ // SDK-free: returns runtime-agnostic tool definitions for YOKE adapter.
4
4
  //
5
5
  // Usage:
6
6
  // var browserMcp = require("./browser-mcp-server");
7
- // var mcpConfig = browserMcp.create(sendExtensionCommandAny);
8
- // // Pass mcpConfig to sdk-bridge opts.mcpServers
7
+ // var toolDefs = browserMcp.getToolDefs(sendExtensionCommandAny, getTabList, contextOps);
8
+ // var mcpConfig = adapter.createToolServer({ name: "clay-browser", version: "1.0.0", tools: toolDefs });
9
9
 
10
10
  var z;
11
11
  try { z = require("zod"); } catch (e) { z = null; }
@@ -30,21 +30,14 @@ function buildShape(props, required) {
30
30
  return shape;
31
31
  }
32
32
 
33
- function create(sendCommand, getTabList, contextOps) {
33
+ // Helper: convert positional args (name, desc, schema, handler) to tool definition object
34
+ function def(name, description, inputSchema, handler) {
35
+ return { name: name, description: description, inputSchema: inputSchema, handler: handler };
36
+ }
37
+
38
+ function getToolDefs(sendCommand, getTabList, contextOps) {
34
39
  // sendCommand(command, args, timeout) -> Promise<result>
35
40
  // getTabList() -> array of { id, url, title, favIconUrl }
36
- var sdk;
37
- try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
38
- console.error("[browser-mcp] Failed to load SDK:", e.message);
39
- return null;
40
- }
41
-
42
- var createSdkMcpServer = sdk.createSdkMcpServer;
43
- var tool = sdk.tool;
44
- if (!createSdkMcpServer || !tool) {
45
- console.error("[browser-mcp] SDK missing createSdkMcpServer or tool helper");
46
- return null;
47
- }
48
41
 
49
42
  // Helper: ensure inject.js loaded (best-effort)
50
43
  function ensureInjected(tabId) {
@@ -54,7 +47,7 @@ function create(sendCommand, getTabList, contextOps) {
54
47
  var tools = [];
55
48
 
56
49
  // --- browser_list_tabs ---
57
- tools.push(tool(
50
+ tools.push(def(
58
51
  "browser_list_tabs",
59
52
  "List all open browser tabs with their IDs, URLs, and titles",
60
53
  buildShape({}, []),
@@ -65,7 +58,7 @@ function create(sendCommand, getTabList, contextOps) {
65
58
  ));
66
59
 
67
60
  // --- browser_open ---
68
- tools.push(tool(
61
+ tools.push(def(
69
62
  "browser_open",
70
63
  "Open a new browser tab and return its tab ID",
71
64
  buildShape({
@@ -80,7 +73,7 @@ function create(sendCommand, getTabList, contextOps) {
80
73
  ));
81
74
 
82
75
  // --- browser_close ---
83
- tools.push(tool(
76
+ tools.push(def(
84
77
  "browser_close",
85
78
  "Close a browser tab",
86
79
  buildShape({
@@ -94,7 +87,7 @@ function create(sendCommand, getTabList, contextOps) {
94
87
  ));
95
88
 
96
89
  // --- browser_navigate ---
97
- tools.push(tool(
90
+ tools.push(def(
98
91
  "browser_navigate",
99
92
  "Navigate a tab to a new URL",
100
93
  buildShape({
@@ -109,7 +102,7 @@ function create(sendCommand, getTabList, contextOps) {
109
102
  ));
110
103
 
111
104
  // --- browser_screenshot ---
112
- tools.push(tool(
105
+ tools.push(def(
113
106
  "browser_screenshot",
114
107
  "Capture a screenshot of a browser tab. Skip if the tab is already attached as a context source (data is auto-injected).",
115
108
  buildShape({
@@ -132,7 +125,7 @@ function create(sendCommand, getTabList, contextOps) {
132
125
  ));
133
126
 
134
127
  // --- browser_console ---
135
- tools.push(tool(
128
+ tools.push(def(
136
129
  "browser_console",
137
130
  "Read captured console logs from a tab. Skip if the tab is already a context source (data is auto-injected).",
138
131
  buildShape({
@@ -155,7 +148,7 @@ function create(sendCommand, getTabList, contextOps) {
155
148
  ));
156
149
 
157
150
  // --- browser_network ---
158
- tools.push(tool(
151
+ tools.push(def(
159
152
  "browser_network",
160
153
  "Read captured network requests (fetch/XHR) from a tab. Skip if the tab is already a context source.",
161
154
  buildShape({
@@ -179,7 +172,7 @@ function create(sendCommand, getTabList, contextOps) {
179
172
  ));
180
173
 
181
174
  // --- browser_read_page ---
182
- tools.push(tool(
175
+ tools.push(def(
183
176
  "browser_read_page",
184
177
  "Read page text content (innerText). Skip if the tab is already a context source (text is auto-injected). Use for tabs NOT in context sources, or to read a specific element via selector.",
185
178
  buildShape({
@@ -204,7 +197,7 @@ function create(sendCommand, getTabList, contextOps) {
204
197
  ));
205
198
 
206
199
  // --- browser_dom ---
207
- tools.push(tool(
200
+ tools.push(def(
208
201
  "browser_dom",
209
202
  "Get a simplified DOM tree (tag, id, class, children) for structural analysis",
210
203
  buildShape({
@@ -247,7 +240,7 @@ function create(sendCommand, getTabList, contextOps) {
247
240
  ));
248
241
 
249
242
  // --- browser_styles ---
250
- tools.push(tool(
243
+ tools.push(def(
251
244
  "browser_styles",
252
245
  "Get computed styles of an element (display, position, size, colors, etc.)",
253
246
  buildShape({
@@ -276,7 +269,7 @@ function create(sendCommand, getTabList, contextOps) {
276
269
  ));
277
270
 
278
271
  // --- browser_storage ---
279
- tools.push(tool(
272
+ tools.push(def(
280
273
  "browser_storage",
281
274
  "Read browser storage (localStorage, sessionStorage, or cookies)",
282
275
  buildShape({
@@ -300,7 +293,7 @@ function create(sendCommand, getTabList, contextOps) {
300
293
  ));
301
294
 
302
295
  // --- browser_evaluate ---
303
- tools.push(tool(
296
+ tools.push(def(
304
297
  "browser_evaluate",
305
298
  "Execute arbitrary JavaScript in the page context and return the result",
306
299
  buildShape({
@@ -317,7 +310,7 @@ function create(sendCommand, getTabList, contextOps) {
317
310
  ));
318
311
 
319
312
  // --- browser_click ---
320
- tools.push(tool(
313
+ tools.push(def(
321
314
  "browser_click",
322
315
  "Click an element on the page",
323
316
  buildShape({
@@ -339,7 +332,7 @@ function create(sendCommand, getTabList, contextOps) {
339
332
  ));
340
333
 
341
334
  // --- browser_type ---
342
- tools.push(tool(
335
+ tools.push(def(
343
336
  "browser_type",
344
337
  "Type text into an input element (sets value and dispatches input/change events)",
345
338
  buildShape({
@@ -366,7 +359,7 @@ function create(sendCommand, getTabList, contextOps) {
366
359
  ));
367
360
 
368
361
  // --- browser_scroll ---
369
- tools.push(tool(
362
+ tools.push(def(
370
363
  "browser_scroll",
371
364
  "Scroll the page or scroll a specific element into view",
372
365
  buildShape({
@@ -396,7 +389,7 @@ function create(sendCommand, getTabList, contextOps) {
396
389
  ));
397
390
 
398
391
  // --- browser_wait ---
399
- tools.push(tool(
392
+ tools.push(def(
400
393
  "browser_wait",
401
394
  "Wait for an element matching a CSS selector to appear in the DOM",
402
395
  buildShape({
@@ -432,7 +425,7 @@ function create(sendCommand, getTabList, contextOps) {
432
425
  ));
433
426
 
434
427
  // --- browser_wait_navigation ---
435
- tools.push(tool(
428
+ tools.push(def(
436
429
  "browser_wait_navigation",
437
430
  "Wait for page navigation to complete (URL change + load event)",
438
431
  buildShape({
@@ -450,7 +443,7 @@ function create(sendCommand, getTabList, contextOps) {
450
443
 
451
444
  // --- browser_watch_tab ---
452
445
  if (contextOps && contextOps.watchTab) {
453
- tools.push(tool(
446
+ tools.push(def(
454
447
  "browser_watch_tab",
455
448
  "Add a browser tab as a persistent context source. Its screenshot and text will be automatically included in every subsequent message.",
456
449
  buildShape({
@@ -470,7 +463,7 @@ function create(sendCommand, getTabList, contextOps) {
470
463
  }
471
464
  ));
472
465
 
473
- tools.push(tool(
466
+ tools.push(def(
474
467
  "browser_unwatch_tab",
475
468
  "Remove a browser tab from persistent context sources. Stops auto-including its content.",
476
469
  buildShape({
@@ -485,12 +478,7 @@ function create(sendCommand, getTabList, contextOps) {
485
478
  ));
486
479
  }
487
480
 
488
- // Create the in-process MCP server
489
- return createSdkMcpServer({
490
- name: "clay-browser",
491
- version: "1.0.0",
492
- tools: tools,
493
- });
481
+ return tools;
494
482
  }
495
483
 
496
- module.exports = { create: create };
484
+ module.exports = { getToolDefs: getToolDefs };
@@ -1,11 +1,11 @@
1
- // Debate MCP Server for Clay (in-process SDK version)
2
- // Provides the propose_debate tool so mates can propose debates
3
- // via the SDK tool system instead of writing files to disk.
1
+ // Debate MCP Server for Clay
2
+ // Provides the propose_debate tool definition.
3
+ // SDK-free: returns runtime-agnostic tool definitions for YOKE adapter.
4
4
  //
5
5
  // Usage:
6
6
  // var debateMcp = require("./debate-mcp-server");
7
- // var mcpConfig = debateMcp.create(onPropose);
8
- // // Pass mcpConfig to sdk-bridge opts.mcpServers
7
+ // var toolDefs = debateMcp.getToolDefs(onPropose);
8
+ // var mcpConfig = adapter.createToolServer({ name: "clay-debate", version: "1.0.0", tools: toolDefs });
9
9
 
10
10
  var z;
11
11
  try { z = require("zod"); } catch (e) { z = null; }
@@ -31,33 +31,20 @@ function buildShape(props, required) {
31
31
 
32
32
  // onPropose(briefData) -> Promise<{action: "start"|"cancel"}>
33
33
  // The returned Promise blocks the tool until the user approves or cancels.
34
- function create(onPropose) {
35
- var sdk;
36
- try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
37
- console.error("[debate-mcp] Failed to load SDK:", e.message);
38
- return null;
39
- }
40
-
41
- var createSdkMcpServer = sdk.createSdkMcpServer;
42
- var tool = sdk.tool;
43
- if (!createSdkMcpServer || !tool) {
44
- console.error("[debate-mcp] SDK missing createSdkMcpServer or tool helper");
45
- return null;
46
- }
47
-
34
+ function getToolDefs(onPropose) {
48
35
  var tools = [];
49
36
 
50
- tools.push(tool(
51
- "propose_debate",
52
- "Propose a structured debate among Clay Mates. The user will see an inline approval card. The tool blocks until the user approves or cancels.",
53
- buildShape({
37
+ tools.push({
38
+ name: "propose_debate",
39
+ description: "Propose a structured debate among Clay Mates. The user will see an inline approval card. The tool blocks until the user approves or cancels.",
40
+ inputSchema: buildShape({
54
41
  topic: { type: "string", description: "The debate topic" },
55
42
  format: { type: "string", description: "Debate format, e.g. free_discussion (default)" },
56
43
  context: { type: "string", description: "Key context from the conversation that panelists should know" },
57
44
  specialRequests: { type: "string", description: "Special instructions for the debate, or empty" },
58
45
  panelists: { type: "string", description: "JSON array of panelist objects: [{\"mateId\": \"<UUID>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]" },
59
46
  }, ["topic", "panelists"]),
60
- function (args) {
47
+ handler: function (args) {
61
48
  var panelists;
62
49
  try {
63
50
  panelists = JSON.parse(args.panelists);
@@ -82,13 +69,9 @@ function create(onPropose) {
82
69
  return { content: [{ type: "text", text: "Debate proposal was cancelled by the user." }] };
83
70
  });
84
71
  }
85
- ));
86
-
87
- return createSdkMcpServer({
88
- name: "clay-debate",
89
- version: "1.0.0",
90
- tools: tools,
91
72
  });
73
+
74
+ return tools;
92
75
  }
93
76
 
94
- module.exports = { create: create };
77
+ module.exports = { getToolDefs: getToolDefs };
package/lib/mcp-local.js CHANGED
@@ -352,4 +352,34 @@ function createLocalMcp() {
352
352
  };
353
353
  }
354
354
 
355
- module.exports = { createLocalMcp: createLocalMcp };
355
+ // Standalone config reader (no process spawning).
356
+ // Returns merged server definitions from ~/.clay/mcp.json + includes.
357
+ // Used by Codex adapter to pass server configs for native MCP management.
358
+ function readMergedServers() {
359
+ var dir = path.dirname(CLAY_CONFIG_PATH);
360
+ if (!fs.existsSync(dir)) return {};
361
+ var config;
362
+ try {
363
+ config = JSON.parse(fs.readFileSync(CLAY_CONFIG_PATH, "utf8"));
364
+ } catch (e) {
365
+ return {};
366
+ }
367
+ var merged = Object.assign({}, config.mcpServers || {});
368
+ var includes = config.include || [];
369
+ for (var i = 0; i < includes.length; i++) {
370
+ var resolved = includes[i].replace(/^~/, os.homedir());
371
+ try {
372
+ var ext = JSON.parse(fs.readFileSync(resolved, "utf8"));
373
+ var extServers = ext.mcpServers || {};
374
+ var names = Object.keys(extServers);
375
+ for (var j = 0; j < names.length; j++) {
376
+ if (!merged[names[j]]) merged[names[j]] = extServers[names[j]];
377
+ }
378
+ } catch (e) {
379
+ // Skip unreadable files
380
+ }
381
+ }
382
+ return merged;
383
+ }
384
+
385
+ module.exports = { createLocalMcp: createLocalMcp, readMergedServers: readMergedServers };
@@ -92,7 +92,8 @@ function attachConnection(ctx) {
92
92
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
93
93
  }
94
94
  if (sm.currentModel) {
95
- sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
95
+ // Vendor is resolved per-session in session_switched; send default here
96
+ sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [], vendor: sm.defaultVendor || "claude", availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
96
97
  }
97
98
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
98
99
  sendTo(ws, { type: "term_list", terminals: tm.list() });
@@ -180,7 +181,8 @@ function attachConnection(ctx) {
180
181
  sm.saveSessionFile(active);
181
182
  }
182
183
  ws._clayActiveSession = active.localId;
183
- sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
184
+ var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
185
+ sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps });
184
186
  // Send per-session context sources
185
187
  var sessionSources = loadContextSources(slug, active.localId);
186
188
  sendTo(ws, { type: "context_sources_state", active: sessionSources });
@@ -41,7 +41,7 @@ function attachFilesystem(ctx) {
41
41
 
42
42
  function handleFilesystemMessage(ws, msg) {
43
43
  // --- File browser permission gate ---
44
- if (msg.type === "fs_list" || msg.type === "fs_read" || msg.type === "fs_write" || msg.type === "fs_delete" || msg.type === "fs_rename" || msg.type === "fs_mkdir" || msg.type === "fs_upload") {
44
+ if (msg.type === "fs_list" || msg.type === "fs_read" || msg.type === "fs_write" || msg.type === "fs_delete" || msg.type === "fs_rename" || msg.type === "fs_mkdir" || msg.type === "fs_upload" || msg.type === "fs_search") {
45
45
  if (ws._clayUser) {
46
46
  var fbPerms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
47
47
  if (!fbPerms.fileBrowser) {
@@ -98,6 +98,52 @@ function attachFilesystem(ctx) {
98
98
  return true;
99
99
  }
100
100
 
101
+ // --- fs_search ---
102
+ if (msg.type === "fs_search") {
103
+ var query = (msg.query || "").trim().toLowerCase();
104
+ if (!query) {
105
+ sendTo(ws, { type: "fs_search_result", query: msg.query, entries: [] });
106
+ return true;
107
+ }
108
+ try {
109
+ var searchResults = [];
110
+ var MAX_RESULTS = 50;
111
+ var searchUserInfo = getOsUserInfoForWs(ws);
112
+
113
+ function walkDir(dir, relPrefix) {
114
+ if (searchResults.length >= MAX_RESULTS) return;
115
+ var items;
116
+ try {
117
+ if (searchUserInfo) {
118
+ items = fsAsUser("list", { dir: dir }, searchUserInfo);
119
+ } else {
120
+ items = fs.readdirSync(dir, { withFileTypes: true }).map(function (d) {
121
+ return { name: d.name, isDir: d.isDirectory() };
122
+ });
123
+ }
124
+ } catch (e) { return; }
125
+ for (var i = 0; i < items.length; i++) {
126
+ if (searchResults.length >= MAX_RESULTS) return;
127
+ var it = items[i];
128
+ if (it.isDir && IGNORED_DIRS.has(it.name)) continue;
129
+ var rel = relPrefix ? relPrefix + "/" + it.name : it.name;
130
+ if (it.name.toLowerCase().indexOf(query) !== -1) {
131
+ searchResults.push({ name: it.name, type: it.isDir ? "dir" : "file", path: rel });
132
+ }
133
+ if (it.isDir) {
134
+ walkDir(path.join(dir, it.name), rel);
135
+ }
136
+ }
137
+ }
138
+
139
+ walkDir(cwd, "");
140
+ sendTo(ws, { type: "fs_search_result", query: msg.query, entries: searchResults });
141
+ } catch (e) {
142
+ sendTo(ws, { type: "fs_search_result", query: msg.query, entries: [], error: e.message });
143
+ }
144
+ return true;
145
+ }
146
+
101
147
  // --- fs_read ---
102
148
  if (msg.type === "fs_read") {
103
149
  var fsFile = safePath(cwd, msg.path);
@@ -504,6 +504,10 @@ function attachHTTP(ctx) {
504
504
  return true;
505
505
  }
506
506
 
507
+ // Skill update check cache (avoid redundant GitHub fetches)
508
+ if (!ctx._skillCheckCache) ctx._skillCheckCache = {};
509
+ var SKILL_CHECK_TTL = 5 * 60 * 1000; // 5 minutes
510
+
507
511
  // Check skill updates (compare installed vs remote versions)
508
512
  if (req.method === "POST" && urlPath === "/api/check-skill-updates") {
509
513
  parseJsonBody(req).then(function (body) {
@@ -572,7 +576,16 @@ function attachHTTP(ctx) {
572
576
  (function (skill) {
573
577
  var installedVer = getInstalledVersion(skill.name);
574
578
  var installed = !!installedVer;
575
- console.log("[skill-check] " + skill.name + " installed=" + installed + " localVersion=" + (installedVer || "none"));
579
+
580
+ // Return cached result if fresh
581
+ var cacheKey = skill.name + ":" + (installedVer || "");
582
+ var cached = ctx._skillCheckCache[cacheKey];
583
+ if (cached && (Date.now() - cached.ts) < SKILL_CHECK_TTL) {
584
+ results.push(cached.result);
585
+ finishOne();
586
+ return;
587
+ }
588
+
576
589
  // Convert GitHub repo URL to raw SKILL.md URL
577
590
  var rawUrl = "";
578
591
  var ghMatch = skill.url.match(/github\.com\/([^/]+)\/([^/]+)/);
@@ -580,17 +593,16 @@ function attachHTTP(ctx) {
580
593
  rawUrl = "https://raw.githubusercontent.com/" + ghMatch[1] + "/" + ghMatch[2] + "/main/SKILL.md";
581
594
  }
582
595
  if (!rawUrl) {
583
- console.log("[skill-check] " + skill.name + " no valid GitHub URL, skipping remote check");
584
- results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" });
596
+ var r0 = { name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: "", status: installed ? "ok" : "missing" };
597
+ ctx._skillCheckCache[cacheKey] = { ts: Date.now(), result: r0 };
598
+ results.push(r0);
585
599
  finishOne();
586
600
  return;
587
601
  }
588
- console.log("[skill-check] " + skill.name + " fetching remote: " + rawUrl);
589
602
  // Fetch remote SKILL.md
590
603
  var https = require("https");
591
604
  https.get(rawUrl, function (resp) {
592
- console.log("[skill-check] " + skill.name + " remote response status=" + resp.statusCode);
593
- var data = "";
605
+ var data = "";
594
606
  resp.on("data", function (chunk) { data += chunk; });
595
607
  resp.on("end", function () {
596
608
  try {
@@ -601,8 +613,9 @@ function attachHTTP(ctx) {
601
613
  } else if (remoteVer && compareVersions(installedVer, remoteVer) < 0) {
602
614
  status = "outdated";
603
615
  }
604
- console.log("[skill-check] " + skill.name + " remoteVersion=" + remoteVer + " status=" + status);
605
- results.push({ name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: remoteVer, status: status });
616
+ var r1 = { name: skill.name, installed: installed, installedVersion: installedVer, remoteVersion: remoteVer, status: status };
617
+ ctx._skillCheckCache[cacheKey] = { ts: Date.now(), result: r1 };
618
+ results.push(r1);
606
619
  finishOne();
607
620
  } catch (e) {
608
621
  console.error("[skill-check] " + skill.name + " version parse failed:", e.message || e);
@@ -666,6 +679,60 @@ function attachHTTP(ctx) {
666
679
  return true;
667
680
  }
668
681
 
682
+ // MCP bridge endpoint: allows Codex's mcp-bridge-server.js to list/call
683
+ // in-app and remote MCP tools via HTTP (localhost only).
684
+ if (req.method === "POST" && urlPath === "/api/mcp-bridge") {
685
+ parseJsonBody(req).then(function (body) {
686
+ var action = body.action;
687
+ var getMcpBridgeHandler = ctx.getMcpBridgeHandler;
688
+ if (!getMcpBridgeHandler) {
689
+ res.writeHead(500, { "Content-Type": "application/json" });
690
+ res.end('{"error":"MCP bridge not configured"}');
691
+ return;
692
+ }
693
+ var handler = getMcpBridgeHandler();
694
+ if (!handler) {
695
+ res.writeHead(500, { "Content-Type": "application/json" });
696
+ res.end('{"error":"MCP bridge handler unavailable"}');
697
+ return;
698
+ }
699
+
700
+ if (action === "list_tools") {
701
+ handler.listTools().then(function (tools) {
702
+ var serverCounts = {};
703
+ for (var ti = 0; ti < tools.length; ti++) {
704
+ serverCounts[tools[ti].server] = (serverCounts[tools[ti].server] || 0) + 1;
705
+ }
706
+ console.log("[mcp-bridge-http] list_tools:", tools.length, "tools -", Object.keys(serverCounts).map(function(s) { return s + "(" + serverCounts[s] + ")"; }).join(", ") || "(none)");
707
+ res.writeHead(200, { "Content-Type": "application/json" });
708
+ res.end(JSON.stringify({ tools: tools }));
709
+ }).catch(function (err) {
710
+ res.writeHead(500, { "Content-Type": "application/json" });
711
+ res.end(JSON.stringify({ error: err.message || "Failed to list tools" }));
712
+ });
713
+ } else if (action === "call_tool") {
714
+ var server = body.server;
715
+ var tool = body.tool;
716
+ var args = body.args || {};
717
+ console.log("[mcp-bridge-http] call_tool:", server + "/" + tool);
718
+ handler.callTool(server, tool, args).then(function (result) {
719
+ res.writeHead(200, { "Content-Type": "application/json" });
720
+ res.end(JSON.stringify({ result: result }));
721
+ }).catch(function (err) {
722
+ res.writeHead(200, { "Content-Type": "application/json" });
723
+ res.end(JSON.stringify({ error: err.message || "Tool call failed" }));
724
+ });
725
+ } else {
726
+ res.writeHead(400, { "Content-Type": "application/json" });
727
+ res.end('{"error":"Unknown action: ' + (action || '') + '"}');
728
+ }
729
+ }).catch(function () {
730
+ res.writeHead(400, { "Content-Type": "application/json" });
731
+ res.end('{"error":"Invalid JSON body"}');
732
+ });
733
+ return true;
734
+ }
735
+
669
736
  // Info endpoint
670
737
  if (req.method === "GET" && urlPath === "/info") {
671
738
  res.writeHead(200, {
@@ -73,6 +73,7 @@ function attachMcp(ctx) {
73
73
 
74
74
  function handleToolResult(msg) {
75
75
  var callId = msg.callId;
76
+ console.log("[mcp-bridge] Tool result received: " + callId);
76
77
  var pending = _pendingCalls[callId];
77
78
  if (!pending) return;
78
79
  if (pending.timer) clearTimeout(pending.timer);
@@ -82,6 +83,7 @@ function attachMcp(ctx) {
82
83
 
83
84
  function handleToolError(msg) {
84
85
  var callId = msg.callId;
86
+ console.log("[mcp-bridge] Tool error received: " + callId + " error=" + (msg.error || "unknown"));
85
87
  var pending = _pendingCalls[callId];
86
88
  if (!pending) return;
87
89
  if (pending.timer) clearTimeout(pending.timer);
@@ -223,12 +225,14 @@ function attachMcp(ctx) {
223
225
  var callId = "mc_" + Date.now() + "_" + crypto.randomUUID().slice(0, 8);
224
226
 
225
227
  var timer = setTimeout(function () {
228
+ console.log("[mcp-bridge] Tool call TIMEOUT: " + callId + " server=" + serverName + " tool=" + toolName);
226
229
  delete _pendingCalls[callId];
227
230
  reject(new Error("MCP tool call timed out after " + (TOOL_TIMEOUT_MS / 1000) + "s"));
228
231
  }, TOOL_TIMEOUT_MS);
229
232
 
230
233
  _pendingCalls[callId] = { resolve: resolve, reject: reject, timer: timer };
231
234
 
235
+ console.log("[mcp-bridge] Sending tool call: " + callId + " server=" + serverName + " tool=" + toolName);
232
236
  sendTo(extWs, {
233
237
  type: "mcp_tool_call",
234
238
  callId: callId,