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.
package/lib/sdk-bridge.js CHANGED
@@ -44,7 +44,7 @@ function callMcpToolHandler(mcpServers, serverName, toolName, args) {
44
44
 
45
45
  // Merge in-process MCP servers with remote (extension-bridged) MCP servers.
46
46
  // Returns the merged object, or null if no servers exist.
47
- function mergeMcpServers(localServers, getRemoteFn) {
47
+ function mergeMcpServers(localServers, getRemoteFn, session) {
48
48
  var merged = {};
49
49
  var hasAny = false;
50
50
  if (localServers) {
@@ -58,7 +58,7 @@ function mergeMcpServers(localServers, getRemoteFn) {
58
58
  console.log("[mergeMcpServers] local servers: null");
59
59
  }
60
60
  if (typeof getRemoteFn === "function") {
61
- var remote = getRemoteFn();
61
+ var remote = getRemoteFn(session);
62
62
  if (remote) {
63
63
  var rk = Object.keys(remote);
64
64
  console.log("[mergeMcpServers] remote servers:", rk.join(", ") || "(none)");
@@ -537,7 +537,7 @@ function createSDKBridge(opts) {
537
537
  // Auto-approve read-only tools for ALL sessions.
538
538
  // These tools only inspect files and fetch data — no side effects.
539
539
  var readOnlyTools = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
540
- if (readOnlyTools[toolName]) {
540
+ if (readOnlyTools[toolName] === true) {
541
541
  return { behavior: "allow", updatedInput: input };
542
542
  }
543
543
 
@@ -569,7 +569,7 @@ function createSDKBridge(opts) {
569
569
  var safeBrowserTools = { browser_watch_tab: true, browser_unwatch_tab: true };
570
570
  if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
571
571
  var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
572
- if (safeBrowserTools[mcpToolName]) {
572
+ if (safeBrowserTools[mcpToolName] === true) {
573
573
  return { behavior: "allow", updatedInput: input };
574
574
  }
575
575
  }
@@ -610,7 +610,7 @@ function createSDKBridge(opts) {
610
610
  clay_tool_source: true,
611
611
  };
612
612
  if (toolName.indexOf("mcp__clay-tools__") === 0
613
- && safeCapsuleTools[toolName.substring(toolName.lastIndexOf("__") + 2)]) {
613
+ && safeCapsuleTools[toolName.substring(toolName.lastIndexOf("__") + 2)] === true) {
614
614
  return { behavior: "allow", updatedInput: input };
615
615
  }
616
616
 
@@ -678,10 +678,10 @@ function createSDKBridge(opts) {
678
678
  };
679
679
  if (toolName.indexOf("mcp__") === 0) {
680
680
  if (toolName.indexOf(PAIR_MCP_PREFIX) === 0
681
- && pairMcpTools[toolName.slice(PAIR_MCP_PREFIX.length)]) {
681
+ && pairMcpTools[toolName.slice(PAIR_MCP_PREFIX.length)] === true) {
682
682
  return { behavior: "allow", updatedInput: input };
683
683
  }
684
- } else if (pairDynamicTools[toolName]) {
684
+ } else if (pairDynamicTools[toolName] === true) {
685
685
  return { behavior: "allow", updatedInput: input };
686
686
  }
687
687
 
@@ -698,7 +698,7 @@ function createSDKBridge(opts) {
698
698
  remove_note: true,
699
699
  };
700
700
  if (toolName.indexOf(NOTES_MCP_PREFIX) === 0
701
- && notesTools[toolName.slice(NOTES_MCP_PREFIX.length)]) {
701
+ && notesTools[toolName.slice(NOTES_MCP_PREFIX.length)] === true) {
702
702
  return { behavior: "allow", updatedInput: input };
703
703
  }
704
704
 
@@ -710,12 +710,19 @@ function createSDKBridge(opts) {
710
710
  return { behavior: "allow", updatedInput: input };
711
711
  }
712
712
 
713
- // This tool only prepares a read-only UI projection for the document the
714
- // agent is about to edit. The subsequent Edit/Write keeps its own normal
715
- // permission behavior.
716
- if (toolName === "mcp__clay-documents__present_wireframe" || toolName === "clay-documents__present_wireframe" ||
717
- toolName.indexOf("mcp__clay-documents__present_markdown_edit") === 0 ||
718
- toolName.indexOf("clay-documents__present_markdown_edit") !== -1) {
713
+ // These exact session-bound tools only prepare visible UI projections.
714
+ // The Markdown Edit/Write and the wireframe Save/Download flows retain
715
+ // their own normal authorization. Include Codex's resumed-session wrapper
716
+ // without trusting lookalike tools or another MCP server.
717
+ var safeDocumentPresentationTools = {
718
+ "mcp__clay-documents__present_markdown_edit": true,
719
+ "clay-documents__present_markdown_edit": true,
720
+ "mcp__clay-session-tools__clay-documents__present_markdown_edit": true,
721
+ "mcp__clay-documents__present_wireframe": true,
722
+ "clay-documents__present_wireframe": true,
723
+ "mcp__clay-session-tools__clay-documents__present_wireframe": true,
724
+ };
725
+ if (safeDocumentPresentationTools[toolName] === true) {
719
726
  return { behavior: "allow", updatedInput: input };
720
727
  }
721
728
 
@@ -730,7 +737,7 @@ function createSDKBridge(opts) {
730
737
  };
731
738
  if (toolName.indexOf("mcp__clay-email__") === 0) {
732
739
  var emailToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
733
- if (safeEmailTools[emailToolName]) {
740
+ if (safeEmailTools[emailToolName] === true) {
734
741
  return { behavior: "allow", updatedInput: input };
735
742
  }
736
743
  }
@@ -748,23 +755,10 @@ function createSDKBridge(opts) {
748
755
  "mcp__clay-workspace__propose_project_follow_up": true,
749
756
  "mcp__clay-workspace__get_assignment_status": true,
750
757
  };
751
- if (safeWorkspaceTools[toolName] || toolName.indexOf("mcp__clay-history__") === 0) {
758
+ if (safeWorkspaceTools[toolName] === true || toolName.indexOf("mcp__clay-history__") === 0) {
752
759
  return { behavior: "allow", updatedInput: input };
753
760
  }
754
761
 
755
- // Auto-approve remote MCP tools that the user explicitly enabled in project settings.
756
- // These are user-owned local MCP servers, so no additional permission prompt needed.
757
- if (toolName.indexOf("mcp__") === 0 && getRemoteMcpServers) {
758
- var _rmcp = getRemoteMcpServers();
759
- if (_rmcp) {
760
- var _mcpParts = toolName.split("__");
761
- var _mcpServerName = _mcpParts.length >= 2 ? _mcpParts[1] : "";
762
- if (_rmcp[_mcpServerName]) {
763
- return { behavior: "allow", updatedInput: input };
764
- }
765
- }
766
- }
767
-
768
762
  // Auto-approve safe Bash commands (read-only, non-destructive)
769
763
  // Applies to ALL sessions (mates and regular projects alike).
770
764
  // These are purely read-only commands that cannot modify files, install
@@ -1897,7 +1891,7 @@ function createSDKBridge(opts) {
1897
1891
  localMcpServers[userInputServer.name || "yoke-user-input"] = userInputServer;
1898
1892
  }
1899
1893
  }
1900
- var mergedMcpServers = mergeMcpServers(localMcpServers, getRemoteMcpServers) || undefined;
1894
+ var mergedMcpServers = mergeMcpServers(localMcpServers, getRemoteMcpServers, session) || undefined;
1901
1895
  if (selectedUserInputMode === "fallback") {
1902
1896
  sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler, { maxQuestions: scheduleQuestionLimit }));
1903
1897
  }
@@ -18,7 +18,8 @@ function extractMcpDescriptors(mcpServers) {
18
18
  var toolDef = server.instance._registeredTools[toolName];
19
19
  var inputSchema = { type: "object", properties: {} };
20
20
  try {
21
- if (toolDef.inputSchema) inputSchema = toJSONSchema(toolDef.inputSchema);
21
+ if (toolDef._clayInputSchema) inputSchema = toolDef._clayInputSchema;
22
+ else if (toolDef.inputSchema) inputSchema = toJSONSchema(toolDef.inputSchema);
22
23
  } catch (e) {
23
24
  throw new Error("Cannot serialize MCP tool schema " + serverName + "/" + toolName + ": " + e.message);
24
25
  }
@@ -3,6 +3,7 @@ var readline = require("readline");
3
3
  var spawn = require("child_process").spawn;
4
4
  var chromium = require("playwright").chromium;
5
5
  var browser = null;
6
+ var loginProxy = null;
6
7
  var page = null;
7
8
  var installing = null;
8
9
  var viewing = true;
@@ -22,7 +23,7 @@ async function launch() {
22
23
  var channels = ["chrome", "msedge", null];
23
24
  for (var i = 0; i < channels.length; i++) {
24
25
  try {
25
- return await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 20000 });
26
+ return await chromium.launch({ channel: channels[i] || undefined, headless: true, chromiumSandbox: true, timeout: 20000, proxy: loginProxy ? { server: loginProxy.server } : undefined, args: loginProxy ? ["--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] : [] });
26
27
  } catch (e) {
27
28
  if (closing) throw e;
28
29
  }
@@ -39,7 +40,7 @@ async function launch() {
39
40
  });
40
41
  });
41
42
  if (closing) throw new Error("Closed");
42
- return chromium.launch({ headless: true, chromiumSandbox: true, timeout: 20000 });
43
+ return chromium.launch({ headless: true, chromiumSandbox: true, timeout: 20000, proxy: loginProxy ? { server: loginProxy.server } : undefined, args: loginProxy ? ["--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] : [] });
43
44
  }
44
45
 
45
46
  async function capture() {
@@ -55,7 +56,8 @@ async function capture() {
55
56
  if (!closing) captureTimer = setTimeout(capture, 250);
56
57
  }
57
58
 
58
- async function start(url) {
59
+ async function start(url, restrictedNetwork) {
60
+ if (restrictedNetwork) loginProxy = await require("./mcp-browser-proxy").createBrowserProxy();
59
61
  browser = await launch();
60
62
  if (closing) { await browser.close(); return; }
61
63
  var session = await browser.newBrowserCDPSession();
@@ -64,12 +66,19 @@ async function start(url) {
64
66
  finally { await session.detach(); }
65
67
  // Preserve the native version/platform and replace only the headless product token.
66
68
  var userAgent = version.userAgent.replace("HeadlessChrome/", "Chrome/");
67
- var context = await browser.newContext({ viewport: viewport, acceptDownloads: false, userAgent: userAgent });
69
+ var context = await browser.newContext({ viewport: viewport, acceptDownloads: false, serviceWorkers: "block", userAgent: userAgent });
70
+ if (restrictedNetwork) await context.routeWebSocket("**/*", function (socket) { socket.close(); });
68
71
  await context.addInitScript(require("./vendor-login-browser-passkeys").disableLoginPasskeys);
69
72
  var callback = new URL(new URL(url).searchParams.get("redirect_uri"));
70
- await context.route("**/*", function (route) {
73
+ await context.route("**/*", async function (route) {
71
74
  var target = new URL(route.request().url());
72
- if (target.protocol === "https:" || (target.origin === callback.origin && target.pathname === callback.pathname)) return route.continue();
75
+ if (target.origin === callback.origin && target.pathname === callback.pathname) return route.continue();
76
+ if (target.protocol === "https:") {
77
+ if (restrictedNetwork) {
78
+ try { await require("./mcp-network").assertPublicUrl(target.href); } catch (error) { return route.abort(); }
79
+ }
80
+ return route.continue();
81
+ }
73
82
  return route.abort();
74
83
  });
75
84
  context.on("page", function (opened) {
@@ -120,6 +129,7 @@ async function close() {
120
129
  clearTimeout(captureTimer);
121
130
  if (installing) installing.kill();
122
131
  if (browser) await browser.close().catch(function () {});
132
+ if (loginProxy) loginProxy.close();
123
133
  process.exit(0);
124
134
  }
125
135
 
@@ -131,8 +141,8 @@ lines.on("line", function (line) {
131
141
  try { message = JSON.parse(line); } catch (e) { return; }
132
142
  if (message.type === "start" && !started) {
133
143
  started = true;
134
- start(message.url).catch(function () {
135
- status("Unable to open the sign-in browser. The host may need browser libraries or sandbox support. Use terminal sign-in to continue.", "error");
144
+ start(message.url, message.restrictedNetwork).catch(function () {
145
+ status("Unable to open the sign-in browser. The host may need browser libraries or sandbox support." + (message.restrictedNetwork ? " Retry or use an access token." : " Use terminal sign-in to continue."), "error");
136
146
  });
137
147
  } else if (message.type === "viewing") {
138
148
  viewing = !!message.value;
@@ -20,6 +20,12 @@ function loginUrl(value, vendor) {
20
20
  }
21
21
 
22
22
  function createLoginBrowser(options) {
23
+ var initial = null;
24
+ if (options.initialUrl) {
25
+ initial = new URL(options.initialUrl);
26
+ var callback = new URL(initial.searchParams.get("redirect_uri"));
27
+ if (options.vendor !== "mcp" || !options.restrictedNetwork || initial.protocol !== "https:" || initial.username || initial.password || !initial.searchParams.get("state") || !initial.searchParams.get("code_challenge") || callback.protocol !== "http:" || callback.hostname !== "127.0.0.1" || callback.pathname !== "/callback") throw new Error("Invalid MCP sign-in URL");
28
+ }
23
29
  var id = crypto.randomUUID();
24
30
  var dir = fs.mkdtempSync(path.join(os.tmpdir(), "clay-login-"));
25
31
  fs.chmodSync(dir, 0o700);
@@ -98,17 +104,17 @@ function createLoginBrowser(options) {
98
104
  });
99
105
  child.on("error", function () {
100
106
  statusPhase = "error";
101
- status = "The sign-in browser could not start. Use terminal sign-in to continue.";
107
+ status = "The sign-in browser could not start." + (options.vendor === "mcp" ? " Retry or use an access token." : " Use terminal sign-in to continue.");
102
108
  send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
103
109
  });
104
110
  child.on("exit", function () {
105
111
  if (closed) return;
106
112
  statusPhase = "error";
107
- status = "The sign-in browser closed. You can retry or use terminal sign-in.";
113
+ status = "The sign-in browser closed. You can retry.";
108
114
  send({ type: "vendor_login_browser_status", text: status, phase: statusPhase });
109
115
  });
110
116
  child.stdin.on("error", function () {});
111
- write({ type: "start", url: url });
117
+ write({ type: "start", url: url, restrictedNetwork: !!options.restrictedNetwork });
112
118
  }
113
119
  function acceptUrl(value) {
114
120
  var url = loginUrl(value, options.vendor);
@@ -123,6 +129,7 @@ function createLoginBrowser(options) {
123
129
  });
124
130
  }, 250);
125
131
  timer.unref();
132
+ if (initial) start(initial.href);
126
133
  return {
127
134
  id: id,
128
135
  env: { BROWSER: wrapper, CLAY_LOGIN_URL_FILE: urlFile },
@@ -43,9 +43,9 @@ var CODEX_NOTE_TOOLS = {
43
43
  };
44
44
 
45
45
  function canonicalDynamicPermissionToolName(toolName) {
46
- if (CODEX_NOTE_TOOLS[toolName]) return "mcp__clay-notes__" + toolName;
46
+ if (CODEX_NOTE_TOOLS[toolName] === true) return "mcp__clay-notes__" + toolName;
47
47
  if (toolName === "send_to_partner" || toolName === "read_partner") return "mcp__clay-sessions__" + toolName;
48
- if (toolName === "present_markdown_edit") return "mcp__clay-documents__" + toolName;
48
+ if (toolName === "present_markdown_edit" || toolName === "present_wireframe") return "mcp__clay-documents__" + toolName;
49
49
  return toolName;
50
50
  }
51
51
 
@@ -1853,25 +1853,8 @@ function createCodexAdapter(opts) {
1853
1853
  if (co.config) serverOpts.config = Object.assign(serverOpts.config, co.config);
1854
1854
  }
1855
1855
 
1856
- // Track 1: Read local MCP server definitions from ~/.clay/mcp.json
1857
- // and inject into Codex config so Codex manages them natively.
1856
+ // Clay-managed external MCP runs through the owner-bound bridge.
1858
1857
  var mcpServerConfig = Object.assign({}, serverOpts.config.mcp_servers || {});
1859
- try {
1860
- var mcpLocal = require("../../mcp-local");
1861
- var localMcpServers = mcpLocal.readMergedServers();
1862
- var mcpNames = Object.keys(localMcpServers);
1863
- for (var mi = 0; mi < mcpNames.length; mi++) {
1864
- var ms = localMcpServers[mcpNames[mi]];
1865
- if (ms.command) {
1866
- mcpServerConfig[mcpNames[mi]] = { command: ms.command, args: ms.args || [] };
1867
- if (ms.env && Object.keys(ms.env).length > 0) {
1868
- mcpServerConfig[mcpNames[mi]].env = ms.env;
1869
- }
1870
- }
1871
- }
1872
- } catch (e) {
1873
- console.error("[codex] Failed to read local MCP config:", e.message);
1874
- }
1875
1858
 
1876
1859
  // Track 2: Add clay-tools bridge server for in-app + remote MCP tools.
1877
1860
  var bridgePath = require("path").join(__dirname, "..", "mcp-bridge-server.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.9.0-beta.1",
3
+ "version": "5.0.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"
@@ -49,14 +49,18 @@
49
49
  "dependencies": {
50
50
  "@anthropic-ai/claude-agent-sdk": "^0.3.287",
51
51
  "@lydell/node-pty": "^1.2.0-beta.3",
52
+ "@modelcontextprotocol/sdk": "^1.32.1",
52
53
  "@openai/codex": "^0.160.0",
53
54
  "@seald-io/nedb": "^4.1.2",
54
55
  "imapflow": "^1.7.8",
56
+ "ipaddr.js": "^2.5.0",
55
57
  "nodemailer": "^9.1.1",
56
58
  "playwright": "1.63.0",
57
59
  "qrcode-terminal": "^0.12.0",
60
+ "undici": "^6.13.0",
58
61
  "web-push": "^3.6.7",
59
- "ws": "^8.18.0"
62
+ "ws": "^8.18.0",
63
+ "zod": "^4.6.5"
60
64
  },
61
65
  "license": "AGPL-3.0-only",
62
66
  "engines": {