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/project.js CHANGED
@@ -47,7 +47,6 @@ var { attachConnection } = require("./project-connection");
47
47
  var { attachProjectScheduledMessages } = require("./project-scheduled-messages");
48
48
  var { attachMcp } = require("./project-mcp");
49
49
  var { createSessionSkillCatalog } = require("./yoke/session-skill-catalog");
50
- var { createLocalMcp } = require("./mcp-local");
51
50
  var { attachEmail: attachEmailModule } = require("./project-email");
52
51
  var { attachSessionSpawn } = require("./project-session-spawn");
53
52
  var { attachDelegatedSession } = require("./project-delegated-session");
@@ -696,27 +695,30 @@ function createProjectContext(opts) {
696
695
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
697
696
  if (savedDefaultModel) sm.defaultModelByVendor[sm.defaultVendor || "claude"] = savedDefaultModel;
698
697
 
699
- // --- Local MCP (direct process management for localhost clients) ---
700
- var _localMcp = createLocalMcp();
701
-
702
- // --- MCP bridge (remote MCP servers via Chrome Extension) ---
698
+ // Personal computer tools use the extension; remote URLs are owner-bound.
703
699
  var _mcp = attachMcp({
704
- send: send,
705
- sendTo: sendTo,
706
- slug: slug,
707
- isMate: isMate,
708
- getExtensionWs: function () { return browserState._extensionWs; },
709
- getExtensionId: function () { return browserState._extensionId || null; },
710
- getEnabledMcpServers: function () {
711
- return typeof opts.onGetProjectMcpServers === "function"
712
- ? opts.onGetProjectMcpServers(slug) : [];
700
+ sendTo: sendTo, slug: slug, clients: clients,
701
+ root: require("./config").CONFIG_DIR,
702
+ isMultiUser: function () { return usersModule.isMultiUser(); },
703
+ canAccess: function (ws) { return _requestAccess.canAccessProject(ws, slug); },
704
+ canUseOwner: function (owner) {
705
+ if (!usersModule.isMultiUser()) return owner === "default";
706
+ var user = usersModule.findUserById(owner);
707
+ return !!user && _requestAccess.canAccessProject({ _clayUser: user }, slug);
713
708
  },
714
- setEnabledMcpServers: function (servers) {
715
- if (typeof opts.onSetProjectMcpServers === "function") {
716
- opts.onSetProjectMcpServers(slug, servers);
717
- }
709
+ canUseSession: function (session) {
710
+ if (!session || sm.sessions.get(session.localId) !== session) return false;
711
+ if (!usersModule.isMultiUser()) return true;
712
+ if (!require("./mcp-session-access").canUsePersonalConnections(session, sm.sessions)) return false;
713
+ var user = session.ownerId && usersModule.findUserById(session.ownerId);
714
+ return !!user && _requestAccess.canAccessProject({ _clayUser: user }, slug) &&
715
+ usersModule.canAccessSession(user.id, session, opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public" });
716
+ },
717
+ identity: function (ws) { return _requestAccess.osIdentity(ws); },
718
+ extensionId: function (ws) { return ws._clayMcpExtensionId || null; },
719
+ getEnabledMcpServers: function () {
720
+ return typeof opts.onGetProjectMcpServers === "function" ? opts.onGetProjectMcpServers(slug) : [];
718
721
  },
719
- localMcp: _localMcp,
720
722
  });
721
723
 
722
724
  // --- Email module (delegated to project-email.js) ---
@@ -1372,7 +1374,7 @@ function createProjectContext(opts) {
1372
1374
  osUsers: osUsers,
1373
1375
  dangerouslySkipPermissions: dangerouslySkipPermissions,
1374
1376
  mcpServers: getLocalMcpServers,
1375
- getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
1377
+ getRemoteMcpServers: function (session) { return _mcp.getMcpServers(session); },
1376
1378
  clayPort: serverPort,
1377
1379
  clayTls: serverTls,
1378
1380
  clayAuthToken: serverAuthToken,
@@ -1706,14 +1708,6 @@ function createProjectContext(opts) {
1706
1708
  // --- WS connection handler (delegated to project-connection.js) ---
1707
1709
  function handleConnection(ws, wsUser) {
1708
1710
  _connection.handleConnection(ws, wsUser, handleMessage, handleDisconnection);
1709
-
1710
- // Initialize local MCP when a localhost client connects
1711
- if (ws._clayLocal && _localMcp && !_localMcp.isReady()) {
1712
- _localMcp.initialize(function () {
1713
- // Rebuild proxy servers and broadcast state when local servers are ready
1714
- _mcp.rebuildAndBroadcast();
1715
- });
1716
- }
1717
1711
  }
1718
1712
 
1719
1713
  // --- WS message handler ---
@@ -2028,6 +2022,7 @@ function createProjectContext(opts) {
2028
2022
 
2029
2023
  // --- WS disconnection handler (delegated to project-connection.js) ---
2030
2024
  function handleDisconnection(ws) {
2025
+ _mcp.handleExtensionDisconnect(ws);
2031
2026
  // Clean up extension WS reference if this was the extension client
2032
2027
  if (browserState._extensionWs === ws) {
2033
2028
  browserState._extensionWs = null;
@@ -2316,17 +2311,6 @@ function createProjectContext(opts) {
2316
2311
  && sm.sessions.get(boundSession.localId) === boundSession
2317
2312
  && Number(boundSession._sdkQueryGeneration || 0) === queryGeneration;
2318
2313
  }
2319
- // Build set of local MCP server names to exclude (Codex handles these natively)
2320
- var localMcpNames = {};
2321
- try {
2322
- var mcpLocalModule = require("./mcp-local");
2323
- var localConfig = mcpLocalModule.readMergedServers();
2324
- var lcNames = Object.keys(localConfig);
2325
- for (var li = 0; li < lcNames.length; li++) {
2326
- localMcpNames[lcNames[li]] = true;
2327
- }
2328
- } catch (e) { /* no local MCP config */ }
2329
-
2330
2314
  return {
2331
2315
  listTools: function () {
2332
2316
  var tools = [];
@@ -2352,7 +2336,7 @@ function createProjectContext(opts) {
2352
2336
  var toolNames = Object.keys(server.instance._registeredTools);
2353
2337
  for (var j = 0; j < toolNames.length; j++) {
2354
2338
  var toolDef = server.instance._registeredTools[toolNames[j]];
2355
- var inputSchema = normalizeToolSchema(toolDef.inputSchema);
2339
+ var inputSchema = toolDef._clayInputSchema || normalizeToolSchema(toolDef.inputSchema);
2356
2340
  tools.push({
2357
2341
  server: serverName,
2358
2342
  name: toolNames[j],
@@ -2459,13 +2443,11 @@ function createProjectContext(opts) {
2459
2443
  }
2460
2444
  }
2461
2445
 
2462
- // Remote MCP servers (extension-proxied only, skip local proxy servers)
2463
- var remoteServers = _mcp.getMcpServers();
2446
+ // Personal computer and remote URL connections.
2447
+ var remoteServers = _mcp.getMcpServers(boundSession);
2464
2448
  if (remoteServers) {
2465
2449
  var remoteNames = Object.keys(remoteServers);
2466
2450
  for (var ri = 0; ri < remoteNames.length; ri++) {
2467
- // Skip servers that Codex manages natively via Track 1
2468
- if (localMcpNames[remoteNames[ri]]) continue;
2469
2451
  extractServerTools(remoteNames[ri], remoteServers[remoteNames[ri]]);
2470
2452
  }
2471
2453
  }
@@ -2571,7 +2553,7 @@ function createProjectContext(opts) {
2571
2553
  }
2572
2554
  }
2573
2555
  // Try remote/local proxy servers
2574
- var remoteServers = _mcp.getMcpServers();
2556
+ var remoteServers = _mcp.getMcpServers(boundSession);
2575
2557
  if (remoteServers && remoteServers[serverName]) {
2576
2558
  var rServer = remoteServers[serverName];
2577
2559
  if (rServer.instance && rServer.instance._registeredTools && rServer.instance._registeredTools[toolName]) {
@@ -2673,6 +2655,7 @@ function createProjectContext(opts) {
2673
2655
 
2674
2656
  // --- Destroy ---
2675
2657
  function destroy() {
2658
+ _mcp.destroy();
2676
2659
  _sharedBrowser.destroy();
2677
2660
  _vendorLogin.destroy();
2678
2661
  if (_scheduledMessages) _scheduledMessages.shutdown();
package/lib/public/app.js CHANGED
@@ -273,6 +273,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
273
273
  sharedBrowser: null,
274
274
  sharedBrowsers: [],
275
275
  mcpSkillsWorkbench: {},
276
+ mcpConnections: { screen: "list", servers: [], search: "", loaded: false },
276
277
  sharedBrowserOpen: false,
277
278
  sharedBrowserUi: {},
278
279
  // session / routing
@@ -0,0 +1,86 @@
1
+ /* Personal connections share the existing workbench's typography and theme. */
2
+ #mcp-skills-workbench .ext-pill-wrap[hidden] { display:none; }
3
+ .mc-screen { max-width:640px; margin:0 auto; padding:4px 4px 24px; font-size:13px; color:var(--text); }
4
+ .mc-screen button,.mc-screen input,.mc-screen select { font:inherit; }
5
+ .mc-screen button { cursor:pointer; }
6
+ .mc-screen button:disabled { opacity:.5; cursor:wait; }
7
+ .mc-screen svg { width:16px; height:16px; flex-shrink:0; }
8
+ .mc-heading { margin:16px 0 28px; }
9
+ .mc-heading h2 { font-size:22px; font-weight:600; letter-spacing:-.5px; margin:12px 0 8px; overflow-wrap:anywhere; }
10
+ .mc-heading p,.mc-muted,.mc-footnote { color:var(--text-muted); line-height:1.7; margin:0; }
11
+ .mc-emblem { display:inline-flex; padding:12px; background:var(--bg-alt); border:1px solid var(--border); border-radius:12px; }
12
+ .mc-emblem svg { width:24px; height:24px; }
13
+ .mc-button { display:inline-flex; align-items:center; justify-content:center; gap:7px; padding:9px 13px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text); line-height:1.4; }
14
+ .mc-button:hover { background:var(--bg-alt); }
15
+ .mc-primary { background:var(--accent); border-color:var(--accent); color:var(--bg); font-weight:600; }
16
+ .mc-primary:hover { background:var(--accent); filter:brightness(.95); }
17
+ .mc-back { display:inline-flex; align-items:center; gap:7px; padding:4px 0; background:none; border:0; color:var(--text-muted); }
18
+ .mc-back:hover { color:var(--text); }
19
+ .mc-empty { display:flex; flex-direction:column; align-items:center; text-align:center; padding:64px 18px; }
20
+ .mc-empty .mc-heading { margin-bottom:24px; }
21
+ .mc-empty>p { max-width:260px; color:var(--text-muted); line-height:1.7; margin-top:18px; font-size:12px; }
22
+ .mc-search { display:flex; align-items:center; gap:8px; border:1px solid var(--border); background:var(--bg-alt); border-radius:8px; padding:0 10px; color:var(--text-muted); }
23
+ .mc-search input { width:100%; min-width:0; background:transparent; border:0; padding:10px 0; color:var(--text); outline:none; }
24
+ .mc-search:focus-within { outline:2px solid var(--accent); outline-offset:2px; }
25
+ .mc-section-heading { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:24px 0 8px; }
26
+ .mc-section-heading h2 { font-size:13px; font-weight:600; margin:0; }
27
+ .mc-row { display:flex; align-items:center; gap:14px; border-bottom:1px solid var(--border); padding:16px 0; }
28
+ .mc-row-open { display:flex; align-items:flex-start; gap:12px; flex:1; min-width:0; text-align:left; color:var(--text); border:0; background:none; padding:0; border-radius:6px; }
29
+ .mc-server-icon { display:flex; border:1px solid var(--border); border-radius:9px; padding:9px; color:var(--text-muted); }
30
+ .mc-row-info { display:flex; flex-direction:column; gap:5px; min-width:0; }
31
+ .mc-row-info strong { overflow-wrap:anywhere; font-size:14px; font-weight:550; }
32
+ .mc-row-info>span { font-size:11px; color:var(--text-muted); }
33
+ .mc-status { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--text-muted); }
34
+ .mc-status>span { width:6px; height:6px; border-radius:50%; background:var(--text-muted); }
35
+ .mc-status[data-status="ready"]>span { background:var(--success,#55a881); }
36
+ .mc-status[data-status="auth_required"]>span,.mc-status[data-status="unavailable"]>span { background:#c28c42; }
37
+ .mc-switch { flex-shrink:0; width:32px; height:19px; padding:2px; border:1px solid var(--border); border-radius:12px; background:var(--bg-alt); }
38
+ .mc-switch>span { display:block; height:13px; width:13px; border-radius:50%; background:var(--text-muted); transition:transform .15s; }
39
+ .mc-switch[aria-checked="true"] { background:var(--accent); border-color:var(--accent); }
40
+ .mc-switch[aria-checked="true"]>span { background:var(--bg); transform:translateX(13px); }
41
+ .mc-footnote { margin-top:22px; font-size:11px; }
42
+ .mc-choices { display:flex; flex-direction:column; gap:12px; }
43
+ .mc-choice { display:flex; align-items:center; gap:14px; padding:20px 16px; text-align:left; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:12px; }
44
+ .mc-choice:hover { background:var(--bg-alt); border-color:var(--text-muted); }
45
+ .mc-choice>span:nth-child(2) { flex:1; }
46
+ .mc-choice strong { display:block; margin-bottom:6px; font-size:14px; }
47
+ .mc-choice small { display:block; color:var(--text-muted); line-height:1.6; font-size:12px; }
48
+ .mc-choice-icon { display:flex; padding:10px; background:var(--bg-alt); border-radius:9px; }
49
+ .mc-form { display:flex; flex-direction:column; gap:16px; }
50
+ .mc-form label,.mc-permissions label { display:flex; flex-direction:column; gap:8px; font-weight:500; }
51
+ .mc-form input,.mc-permissions select { width:100%; min-width:0; box-sizing:border-box; padding:11px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text); }
52
+ .mc-form>.mc-button { align-self:flex-end; margin-top:4px; }
53
+ .mc-enable { display:flex; justify-content:space-between; align-items:center; gap:12px; padding:20px 0; margin-top:8px; border-bottom:1px solid var(--border); }
54
+ .mc-metadata { display:grid; grid-template-columns:70px minmax(0,1fr); gap:12px; padding:8px 0; font-size:12px; }
55
+ .mc-metadata dt { color:var(--text-muted); }
56
+ .mc-metadata dd { margin:0; overflow-wrap:anywhere; }
57
+ .mc-alert { display:flex; align-items:flex-start; gap:10px; border:1px solid var(--border); border-left:3px solid #c28c42; background:var(--bg-alt); border-radius:8px; padding:12px; line-height:1.6; margin:12px 0; }
58
+ .mc-alert svg { margin-top:3px; }
59
+ .mc-notice { color:var(--text-muted); line-height:1.6; padding:12px; background:var(--bg-alt); border-radius:8px; }
60
+ .mc-auth { margin:20px 0; }
61
+ .mc-auth p { font-size:11px; margin-top:8px; }
62
+ .mc-token,.mc-manage { margin:20px 0; }
63
+ .mc-token summary,.mc-manage summary { color:var(--text-muted); cursor:pointer; font-size:12px; }
64
+ .mc-token form,.mc-manage form { margin:16px 0; }
65
+ .mc-detail-tabs { display:flex; gap:20px; border-bottom:1px solid var(--border); margin:26px 0 18px; }
66
+ .mc-detail-tabs button { color:var(--text-muted); background:none; border:0; border-bottom:2px solid transparent; padding:10px 0; }
67
+ .mc-detail-tabs button[aria-selected="true"] { color:var(--text); border-bottom-color:var(--accent); }
68
+ .mc-tools details { border-bottom:1px solid var(--border); padding:14px 0; }
69
+ .mc-tools summary { cursor:pointer; overflow-wrap:anywhere; }
70
+ .mc-tools p,.mc-permissions p { line-height:1.7; color:var(--text-muted); white-space:pre-wrap; overflow-wrap:anywhere; }
71
+ .mc-tools pre { padding:12px; border-radius:8px; background:var(--bg-alt); overflow:auto; max-height:240px; font-size:11px; }
72
+ .mc-tools h4 { font-size:11px; color:var(--text-muted); }
73
+ .mc-detail-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:24px; }
74
+ .mc-steps { list-style:none; padding:0; margin:0 0 24px; }
75
+ .mc-steps li { display:flex; gap:12px; padding:0 0 26px; }
76
+ .mc-step-marker { width:24px; height:24px; display:flex; align-items:center; justify-content:center; flex-shrink:0; border:1px solid var(--border); border-radius:50%; font-size:11px; }
77
+ .mc-steps li>div { min-width:0; flex:1; }
78
+ .mc-steps strong { font-weight:550; line-height:24px; }
79
+ .mc-steps p { color:var(--text-muted); line-height:1.7; margin:6px 0 12px; }
80
+ .mc-command { border:1px solid var(--border); border-radius:8px; padding:10px; background:var(--bg-alt); }
81
+ .mc-command code { display:block; white-space:pre-wrap; overflow-wrap:anywhere; margin-bottom:8px; font-size:11px; }
82
+ .mc-working { display:flex; gap:8px; align-items:center; color:var(--text-muted); font-size:12px; margin-top:16px; }
83
+ .mc-working svg { animation:mc-spin 1s linear infinite; }
84
+ @keyframes mc-spin { to { transform:rotate(360deg); } }
85
+ @media (prefers-reduced-motion:reduce) { .mc-working svg { animation:none; } .mc-switch>span { transition:none; } }
86
+ @media (max-width:480px) { .mc-screen { padding:0 0 20px; } .mc-empty { padding:40px 12px; } .mc-heading h2 { font-size:21px; } }
@@ -0,0 +1,74 @@
1
+ /* A compact library inside the existing workbench. */
2
+ #mcp-skills-workbench #skills-modal [hidden] { display:none; }
3
+ #mcp-skills-workbench #skills-modal .skills-header { padding:24px 20px 16px; }
4
+ .skills-library-heading h2 { margin:0 0 7px; font-size:22px; letter-spacing:-.5px; font-weight:600; color:var(--text); }
5
+ .skills-library-heading p { margin:0; font-size:12px; color:var(--text-muted); line-height:1.6; }
6
+ #mcp-skills-workbench #skills-modal .skills-back-btn { gap:7px; font-size:12px; padding:0; color:var(--text-muted); background:none; border:0; }
7
+ #mcp-skills-workbench #skills-modal .skills-tabs { margin:0 20px; padding:0; gap:24px; border-bottom:1px solid var(--border); }
8
+ #mcp-skills-workbench #skills-modal .skills-tab { padding:10px 0; font-size:12px; }
9
+ #mcp-skills-workbench #skills-modal .skills-tab.active { color:var(--text); }
10
+ #skills-installed-count { margin-left:5px; color:var(--text-muted); font-size:10px; }
11
+ .skills-toolbar { display:flex; gap:10px; align-items:center; padding:16px 20px 0; }
12
+ #mcp-skills-workbench #skills-modal .skills-search { flex:1; min-width:0; margin:0; padding:9px 10px; border-radius:8px; background:var(--bg-alt); }
13
+ #mcp-skills-workbench #skills-modal .skills-search input { min-width:0; width:100%; font-size:12px; }
14
+ .skills-discovery-views { display:flex; gap:6px; flex-wrap:wrap; padding:12px 20px 0; }
15
+ .skills-discovery-views button { padding:7px 10px; border:1px solid transparent; border-radius:6px; background:none; color:var(--text-muted); font:inherit; font-size:11px; cursor:pointer; }
16
+ .skills-discovery-views button:hover { background:var(--bg-alt); color:var(--text); }
17
+ .skills-discovery-views button[aria-pressed="true"] { background:var(--bg-alt); border-color:var(--border); color:var(--text); }
18
+ .skills-discovery-views button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
19
+ #mcp-skills-workbench #skills-modal .skills-content { padding:20px; }
20
+ .skills-list-intro { display:flex; justify-content:space-between; gap:12px; color:var(--text-muted); font-size:11px; margin-bottom:6px; }
21
+ .skills-library-row { display:flex; align-items:flex-start; width:100%; gap:12px; padding:17px 0; text-align:left; background:none; border:0; border-bottom:1px solid var(--border); color:var(--text); font:inherit; cursor:pointer; border-radius:4px; }
22
+ .skills-library-row:hover { background:var(--bg-alt); }
23
+ .skills-library-icon,.skills-detail-icon { display:inline-flex; padding:9px; border:1px solid var(--border); border-radius:9px; color:var(--text-muted); flex-shrink:0; }
24
+ .skills-library-icon svg { width:16px; height:16px; }
25
+ .skills-library-info { display:flex; flex-direction:column; gap:6px; flex:1; min-width:0; }
26
+ .skills-library-name { font-size:13px; font-weight:550; overflow-wrap:anywhere; }
27
+ .skills-library-desc { font-size:12px; line-height:1.65; color:var(--text-muted); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
28
+ .skills-library-meta { font-size:10px; color:var(--text-dimmer); overflow-wrap:anywhere; }
29
+ .skills-library-end { display:flex; align-items:center; gap:8px; flex-shrink:0; padding-top:3px; color:var(--text-muted); }
30
+ .skills-library-end>svg { width:14px; height:14px; }
31
+ .skills-library-badge { font-size:10px; color:var(--text-muted); }
32
+ .skills-library-installs { display:flex; gap:4px; align-items:center; font-size:10px; }
33
+ .skills-library-installs svg { width:12px; height:12px; }
34
+ .skills-library-empty { padding:48px 16px; text-align:center; color:var(--text-muted); }
35
+ .skills-library-empty>svg { width:28px; height:28px; margin-bottom:12px; }
36
+ .skills-library-empty h3 { color:var(--text); font-size:17px; font-weight:550; }
37
+ .skills-library-empty p { font-size:12px; line-height:1.7; max-width:300px; margin:12px auto; }
38
+ .skills-browse-btn { display:inline-flex; align-items:center; justify-content:center; gap:8px; margin-top:10px; padding:10px 14px; border:1px solid var(--border); border-radius:8px; background:var(--bg-alt); color:var(--text); font:inherit; font-size:12px; cursor:pointer; }
39
+ .skills-browse-btn svg { width:14px; height:14px; }
40
+ .skills-catalog-credit { margin:22px 0 0; font-size:11px; color:var(--text-dimmer); }
41
+ .skills-catalog-credit a,.skills-metadata a { color:var(--text-muted); }
42
+ #mcp-skills-workbench #skills-modal .skills-detail { display:flex; flex-direction:column; gap:24px; max-width:780px; margin:auto; }
43
+ .skills-detail-icon svg { width:22px; height:22px; }
44
+ #mcp-skills-workbench #skills-modal .skills-detail-name { margin:14px 0 10px; font-size:23px; letter-spacing:-.5px; font-weight:600; }
45
+ #mcp-skills-workbench #skills-modal .skills-detail-desc { margin:0; font-size:13px; line-height:1.8; color:var(--text-muted); }
46
+ .skills-detail-byline { display:flex; flex-wrap:wrap; gap:12px; margin-top:14px; font-size:11px; color:var(--text-dimmer); overflow-wrap:anywhere; }
47
+ .skills-placement { border:1px solid var(--border); border-radius:10px; padding:16px; background:var(--bg-alt); }
48
+ .skills-placement h3 { margin:0 0 7px; font-size:13px; font-weight:550; color:var(--text); }
49
+ .skills-placement-heading>p { font-size:11px; line-height:1.7; color:var(--text-muted); margin:0 0 16px; }
50
+ .skills-placement .skills-install-actions { display:flex; flex-wrap:wrap; flex-direction:row; gap:8px; }
51
+ .skills-placement .skills-install-btn { width:auto; flex:1; min-width:150px; border-radius:7px; padding:10px 12px; font-size:12px; }
52
+ .skills-placement .skills-installed-row { width:100%; gap:10px; margin-bottom:8px; flex-wrap:wrap; }
53
+ .skills-placement .skills-uninstall-btn,.skills-cancel-remove { width:auto; height:auto; padding:6px 9px; font:inherit; font-size:11px; color:var(--text-muted); border:1px solid var(--border); border-radius:6px; background:none; cursor:pointer; }
54
+ .skills-placement .skills-installed-status { flex:1; width:auto; font-size:12px; font-weight:500; padding:0; background:none; border:0; justify-content:flex-start; }
55
+ .skills-placement .skills-installed-status svg { width:15px; height:15px; }
56
+ .skills-removed-status { font-size:12px; color:var(--text-muted); }
57
+ #mcp-skills-workbench #skills-modal .skills-detail-md-wrap { flex:none; overflow:visible; padding:0; }
58
+ .skills-detail-section-title { font-size:11px; font-weight:550; letter-spacing:.04em; color:var(--text-muted); margin:0 0 16px; }
59
+ .skills-detail-md { overflow-wrap:anywhere; min-width:0; }
60
+ .skills-detail-md pre { max-width:100%; overflow:auto; }
61
+ .skills-detail-md table { display:block; overflow:auto; }
62
+ .skills-metadata,.skills-manual { border-top:1px solid var(--border); padding-top:16px; }
63
+ .skills-metadata summary,.skills-manual summary { cursor:pointer; font-size:12px; color:var(--text-muted); }
64
+ .skills-metadata dl { display:grid; grid-template-columns:90px minmax(0,1fr); gap:12px; font-size:11px; line-height:1.7; }
65
+ .skills-metadata dt { color:var(--text-muted); }
66
+ .skills-metadata dd { margin:0; overflow-wrap:anywhere; }
67
+ .skills-manual .skills-detail-cmd { margin:12px 0 0; }
68
+ .skills-local-note { display:flex; gap:10px; align-items:flex-start; color:var(--text-muted); }
69
+ .skills-local-note svg { width:16px; height:16px; flex-shrink:0; margin-top:3px; }
70
+ .skills-local-note p { margin:0; font-size:12px; line-height:1.7; }
71
+ @media(max-width:480px) { .skills-toolbar { flex-wrap:wrap; }.skills-library-icon { padding:7px; }.skills-library-row { gap:9px; }.skills-library-badge { max-width:48px; }.skills-library-end { gap:3px; } }
72
+
73
+ .skills-library-row:focus-visible,.skills-browse-btn:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
74
+ .skills-search:focus-within { box-shadow:0 0 0 1px var(--accent); }
@@ -1733,23 +1733,24 @@
1733
1733
  <div class="confirm-backdrop"></div>
1734
1734
  <div class="confirm-dialog skills-dialog">
1735
1735
  <div class="skills-header">
1736
- <span class="skills-title"><i data-lucide="puzzle"></i> Skills <span class="skills-powered-by">powered by <a href="https://skills.sh" target="_blank" rel="noopener noreferrer">skills.sh</a></span></span>
1737
- <div class="skills-header-actions">
1738
- <button class="skills-back-btn hidden" id="skills-back-btn"><i data-lucide="arrow-left"></i></button>
1739
- <button class="skills-close" id="skills-modal-close"><i data-lucide="x"></i></button>
1736
+ <div class="skills-library-heading"><h2>Your skills</h2><p>Reusable instructions for the way you work.</p></div>
1737
+ <button class="skills-back-btn hidden" id="skills-back-btn" aria-label="Back to skills"><i data-lucide="arrow-left"></i><span>Skills</span></button>
1738
+ </div>
1739
+ <div class="skills-tabs" role="tablist" aria-label="Skill library">
1740
+ <button class="skills-tab active" role="tab" aria-selected="true" data-tab="installed">Installed <span id="skills-installed-count"></span></button>
1741
+ <button class="skills-tab" role="tab" aria-selected="false" tabindex="-1" data-tab="discover">Discover</button>
1742
+ </div>
1743
+ <div class="skills-toolbar">
1744
+ <div class="skills-search" id="skills-search">
1745
+ <i data-lucide="search"></i>
1746
+ <input type="search" id="skills-search-input" placeholder="Search installed skills" aria-label="Search installed skills" spellcheck="false" />
1747
+ <button class="skills-search-clear hidden" id="skills-search-clear" aria-label="Clear skill search"><i data-lucide="x"></i></button>
1740
1748
  </div>
1741
1749
  </div>
1742
- <div class="skills-search" id="skills-search">
1743
- <i data-lucide="search"></i>
1744
- <input type="text" id="skills-search-input" placeholder="Search skills ..." spellcheck="false" />
1745
- <kbd class="skills-search-hint" id="skills-search-hint">/</kbd>
1746
- <button class="skills-search-clear hidden" id="skills-search-clear"><i data-lucide="x"></i></button>
1747
- </div>
1748
- <div class="skills-tabs">
1749
- <button class="skills-tab active" data-tab="all">All Time</button>
1750
- <button class="skills-tab" data-tab="trending">Trending</button>
1751
- <button class="skills-tab" data-tab="hot">Hot</button>
1752
- <button class="skills-tab" data-tab="installed">Installed</button>
1750
+ <div class="skills-discovery-views" role="group" aria-label="Discover skills by" hidden>
1751
+ <button type="button" data-discovery="all" aria-pressed="true">Most installed</button>
1752
+ <button type="button" data-discovery="trending" aria-pressed="false">Trending</button>
1753
+ <button type="button" data-discovery="hot" aria-pressed="false">Growing fast</button>
1753
1754
  </div>
1754
1755
  <div class="skills-content" id="skills-content">
1755
1756
  <div class="skills-loading"><div class="skills-spinner"></div> Loading skills...</div>
@@ -61,7 +61,7 @@ import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearP
61
61
  import { checkAdminAccess } from './admin.js';
62
62
  import { mateAvatarUrl } from './avatar.js';
63
63
  import { showImageModal, sendExtensionCommand, handleMcpToolCallMessage } from './app-misc.js';
64
- import { handleMcpServersState, handleMcpPermissionModeResult } from './mcp-ui.js';
64
+ import { handleMcpServersState, handleMcpPermissionModeResult, handleMcpConnectionMessage } from './mcp-ui.js';
65
65
  import { handlePermissionModeResult } from './permission-control.js';
66
66
  import { handleShellCommandResult } from './shell-command.js';
67
67
  import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
@@ -107,6 +107,7 @@ var connectOverlay = document.getElementById("connect-overlay");
107
107
  initMessageDeliveryUi();
108
108
 
109
109
  export function processMessage(msg) {
110
+ if (handleMcpConnectionMessage(msg)) return;
110
111
  if (handleSharedBrowserMessage(msg)) return;
111
112
  if (msg.type === "vendor_login_browser_frame" || msg.type === "vendor_login_browser_status") {
112
113
  handleLoginBrowserMessage(msg);
@@ -0,0 +1,136 @@
1
+ import { escapeHtml } from './utils.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { store } from './store.js';
4
+ import { mcpPermissionModeApplicability, sendMcpPermissionMode } from './mcp-permissions.js';
5
+
6
+ var labels = { ready: 'Ready', idle: 'Saved · connects when used', connecting: 'Connecting…', auth_required: 'Sign-in required', unavailable: 'Unavailable' };
7
+ function text(value) { return escapeHtml(String(value || '')); }
8
+ function button(label, action, primary, icon) {
9
+ return '<button type="button" class="mc-button' + (primary ? ' mc-primary' : '') + '" data-action="' + action + '">' + (icon ? iconHtml(icon) : '') + text(label) + '</button>';
10
+ }
11
+ function heading(title, subtitle, icon) {
12
+ return '<div class="mc-heading">' + (icon ? '<span class="mc-emblem">' + iconHtml(icon) + '</span>' : '') + '<h2 tabindex="-1">' + text(title) + '</h2><p>' + text(subtitle) + '</p></div>';
13
+ }
14
+ function status(row) { return '<span class="mc-status" data-status="' + text(row.status) + '"><span aria-hidden="true"></span>' + text(labels[row.status] || 'Unavailable') + '</span>'; }
15
+ function switchControl(row, busy) {
16
+ return '<button type="button" class="mc-switch" role="switch" aria-checked="' + !!row.projectEnabled + '" aria-label="Enable ' + text(row.label || row.name) + ' in this project" data-toggle="' + text(row.id) + '"' + (busy ? ' disabled' : '') + '><span></span></button>';
17
+ }
18
+ function rows(servers, busy) {
19
+ return '<div class="mc-list">' + servers.map(function (row) {
20
+ return '<div class="mc-row"><button type="button" class="mc-row-open" data-open="' + text(row.id) + '"><span class="mc-server-icon">' + iconHtml(row.source === 'url' ? 'globe' : 'monitor') + '</span><span class="mc-row-info"><strong>' + text(row.label || row.name) + '</strong><span>' + (row.source === 'url' ? 'Remote' : 'Your computer') + ' · ' + row.toolCount + ' tools</span>' + status(row) + '</span></button>' + switchControl(row, busy) + '</div>';
21
+ }).join('') + '</div>';
22
+ }
23
+ function search(value, placeholder) { return '<label class="mc-search">' + iconHtml('search') + '<input type="search" data-search value="' + text(value) + '" placeholder="' + placeholder + '" aria-label="' + placeholder + '"></label>'; }
24
+ function listView(state) {
25
+ if (!state.loaded) return heading('Your connections', 'Loading your MCP servers…', 'cable');
26
+ if (!state.servers.length) return '<div class="mc-empty">' + heading('Connect tools to Clay', 'Let your agent work with the services and tools you choose.', 'cable') + button('Add connection', 'add', true, 'plus') + '<p>Connect a remote service or tools on your computer.</p></div>';
27
+ var servers = state.servers.filter(function (row) { return (row.label || row.name).toLowerCase().includes((state.search || '').toLowerCase()); });
28
+ return search(state.search, 'Search servers…') + '<div class="mc-section-heading"><h2>Your connections</h2>' + button('Add', 'add', false, 'plus') + '</div>' + rows(servers, !!state.pending) + (!servers.length ? '<p class="mc-muted">No matching connections.</p>' : '') + '<p class="mc-footnote">Enabled connections are available to your private sessions in this project. In multi-user Clay, shared conversations cannot use personal connections.</p>';
29
+ }
30
+ function addView() {
31
+ return heading('Add connection', 'Choose where your tools are available.') + '<div class="mc-choices">' +
32
+ '<button class="mc-choice" data-action="remote"><span class="mc-choice-icon">' + iconHtml('globe') + '</span><span><strong>Remote MCP URL</strong><small>Connect a hosted service using its MCP server address.</small></span>' + iconHtml('arrow-right') + '</button>' +
33
+ '<button class="mc-choice" data-action="computer"><span class="mc-choice-icon">' + iconHtml('monitor') + '</span><span><strong>On your computer</strong><small>Connect local tools through the Clay browser extension.</small></span>' + iconHtml('arrow-right') + '</button></div>';
34
+ }
35
+ function remoteView(state) {
36
+ return heading('Connect a remote server', 'Use the MCP address supplied by your service provider.', 'globe') +
37
+ '<form data-form="remote" class="mc-form"><label>Server URL<input name="url" type="url" required maxlength="4096" placeholder="https://mcp.example.com/mcp" value="' + text(store.get('mcpConnectionDraft')) + '" autocomplete="off" spellcheck="false"></label>' +
38
+ '<p class="mc-muted">Public HTTPS servers are supported. You can sign in with Clay’s private browser if the service requires it.</p><button class="mc-button mc-primary" type="submit"' + (state.pending ? ' disabled' : '') + '>' + (state.pending ? 'Checking server…' : 'Continue') + '</button></form>';
39
+ }
40
+ function computerView(state) {
41
+ var servers = state.servers.filter(function (row) { return row.source === 'computer'; });
42
+ var command = 'npx clay-mcp-bridge install ' + (state.extensionId || '<extension-id>');
43
+ return heading('Tools on your computer', 'Your local tools connect through the Clay extension.', 'monitor') +
44
+ '<ol class="mc-steps"><li><span class="mc-step-marker">' + (state.extensionConnected ? iconHtml('check') : '1') + '</span><div><strong>Browser extension</strong><p>' + (state.extensionConnected ? 'Connected to this browser.' : 'Install Clay for Chrome, then refresh this page.') + '</p>' + (!state.extensionConnected ? button('Set up extension', 'setup', false, 'puzzle') : '') + '</div></li>' +
45
+ '<li><span class="mc-step-marker">' + (state.hostConnected ? iconHtml('check') : '2') + '</span><div><strong>Local bridge</strong><p>' + (state.hostConnected ? 'Connected to your computer.' : 'Run this command on your computer, then restart your browser.') + '</p>' + (!state.hostConnected ? '<div class="mc-command"><code>' + text(command) + '</code>' + button('Copy', 'copy', false) + '</div>' : '') + '</div></li>' +
46
+ '<li><span class="mc-step-marker">3</span><div><strong>Choose your tools</strong><p>Add servers from the extension popup. They appear here when connected.</p></div></li></ol>' + rows(servers, !!state.pending) + button('Refresh connections', 'refresh', false, 'refresh-cw');
47
+ }
48
+ function permissionsView(row) {
49
+ var current = store.snap(), availability = mcpPermissionModeApplicability(current);
50
+ var selected = (current.mcpPermissionModeOverrides || {})[row.name] || '';
51
+ var pending = Object.values(current.mcpPermissionModePendingByTarget || {}).some(function (item) { return item.serverName === row.name && item.sessionId === current.activeSessionId; });
52
+ return '<div class="mc-permissions"><h3>Tool approvals</h3><p>Calls follow the active session’s permission policy. Enabling a connection does not automatically approve every action.</p><label>AI review<select data-permission' + (!availability.applicable || pending ? ' disabled' : '') + '>' +
53
+ [['', 'Use session default'], ['auto', 'AI review'], ['default', 'Ask every time']].map(function (item) { return '<option value="' + item[0] + '"' + (selected === item[0] ? ' selected' : '') + '>' + item[1] + '</option>'; }).join('') + '</select></label>' + (!availability.applicable ? '<p class="mc-muted">' + text(availability.reason) + '</p>' : '') + '</div>';
54
+ }
55
+ function detailView(state, row) {
56
+ var remote = row.source === 'url';
57
+ var html = heading(row.label || row.name, remote ? 'Remote MCP connection' : 'Connected through your browser extension', remote ? 'globe' : 'monitor');
58
+ html += status(row) + '<div class="mc-enable"><span>Enabled in this project</span>' + switchControl(row, !!state.pending) + '</div>';
59
+ html += '<dl class="mc-metadata"><dt>Access</dt><dd>Your account only</dd><dt>' + (remote ? 'Server' : 'Location') + '</dt><dd>' + (remote ? text(row.url) : 'Your computer') + '</dd></dl>';
60
+ if (row.error) html += '<div class="mc-alert" role="status">' + iconHtml('circle-alert') + '<span>' + text(row.error) + '</span></div>';
61
+ if (remote && row.status === 'auth_required') html += '<div class="mc-auth">' + button('Sign in with Clay browser', 'signin', true, 'log-in') + '<p class="mc-muted">Only you can view and control this sign-in browser.</p></div>';
62
+ if (remote) html += '<details class="mc-token"><summary>Use an access token</summary><form data-form="token" class="mc-form"><label>Access token<input name="token" type="password" required maxlength="8192" autocomplete="new-password" placeholder="Paste your access token"></label><button type="submit" class="mc-button">Save and connect</button></form></details>';
63
+ html += '<div class="mc-detail-tabs" role="tablist" aria-label="Connection details"><button role="tab" data-tab="tools" aria-selected="' + (state.detailTab !== 'permissions') + '">Tools (' + row.toolCount + ')</button><button role="tab" data-tab="permissions" aria-selected="' + (state.detailTab === 'permissions') + '">Permissions</button></div>';
64
+ if (state.detailTab === 'permissions') html += permissionsView(row);
65
+ else {
66
+ html += search(state.search, 'Search tools…');
67
+ var tools = (row.tools || []).filter(function (tool) { return (tool.name + ' ' + (tool.description || '')).toLowerCase().includes((state.search || '').toLowerCase()); });
68
+ html += '<div class="mc-tools">' + tools.map(function (tool) { return '<details><summary>' + text(tool.name) + '</summary><p>' + text(tool.description || 'No description provided.') + '</p><h4>Inputs</h4><pre>' + text(JSON.stringify(tool.inputSchema || {}, null, 2)) + '</pre></details>'; }).join('') + (!tools.length ? '<p class="mc-muted">' + (row.toolCount ? 'No matching tools.' : 'No tools available. Test the connection to refresh its catalog.') + '</p>' : '') + '</div>';
69
+ }
70
+ html += '<div class="mc-detail-actions">' + (remote ? button('Test connection', 'test', false, 'refresh-cw') + (row.status === 'auth_required' ? '' : button('Sign in again', 'signin', false, 'log-in')) : button('Computer setup', 'computer', false, 'monitor')) + '</div>';
71
+ if (remote) html += '<details class="mc-manage"><summary>Manage connection</summary><form data-form="rename" class="mc-form"><label>Name<input name="label" required maxlength="100" value="' + text(row.label) + '"></label><button type="submit" class="mc-button">Save name</button></form>' + button('Remove connection', 'remove', false, 'trash-2') + '</details>';
72
+ return html;
73
+ }
74
+ export function renderConnections(root, state, events) {
75
+ var sameScreen = root.dataset.screenKey === (state.screen || 'list') + ':' + (state.selected || '');
76
+ var disclosures = sameScreen ? Array.from(root.querySelectorAll('details[open]')).map(function (el) { return el.className || el.querySelector('summary').textContent; }) : [];
77
+ var drafts = {};
78
+ if (sameScreen) root.querySelectorAll('input[name]').forEach(function (el) { drafts[el.name] = el.value; });
79
+ var focused = root.contains(document.activeElement) ? document.activeElement : null;
80
+ var searchFocused = focused && focused.hasAttribute('data-search');
81
+ var start = focused && focused.tagName === 'INPUT' ? focused.selectionStart : null;
82
+ var focusedName = focused && focused.name;
83
+ var row = state.servers.find(function (item) { return item.id === state.selected; });
84
+ var screen = state.screen || 'list';
85
+ var html = screen !== 'list' ? '<button type="button" class="mc-back" data-action="back">' + iconHtml('arrow-left') + 'MCP Servers</button>' : '';
86
+ if (state.error) html += '<div class="mc-alert" role="alert">' + iconHtml('circle-alert') + '<span>' + text(state.error) + '</span></div>';
87
+ if (state.notice) html += '<p class="mc-notice" role="status">' + text(state.notice) + '</p>';
88
+ if (screen === 'add') html += addView();
89
+ else if (screen === 'remote') html += remoteView(state);
90
+ else if (screen === 'computer') html += computerView(state);
91
+ else if (screen === 'remove' && row) html += heading('Remove ' + row.label + '?', 'This removes saved credentials and disables its tools in this project.') + '<div role="group" aria-label="Confirm removal">' + button('Cancel', 'cancel-remove', false) + button('Remove connection', 'confirm-remove', true, 'trash-2') + '</div>';
92
+ else if (screen === 'detail' && row) html += detailView(state, row);
93
+ else html += listView(state);
94
+ if (state.pending && screen !== 'remote') html += '<p class="mc-working" role="status">' + iconHtml('loader-circle') + 'Updating connection…</p>';
95
+ root.innerHTML = '<div class="mc-screen">' + html + '</div>';
96
+ root.dataset.screenKey = screen + ':' + (state.selected || '');
97
+ root.querySelectorAll('details').forEach(function (el) { el.open = disclosures.indexOf(el.className || el.querySelector('summary').textContent) >= 0; });
98
+ root.querySelectorAll('input[name]').forEach(function (el) {
99
+ if (Object.prototype.hasOwnProperty.call(drafts, el.name)) el.value = drafts[el.name];
100
+ if (focusedName === el.name) { el.focus(); try { el.setSelectionRange(start, start); } catch (error) {} }
101
+ });
102
+ if (!sameScreen) { var title = root.querySelector('h2'); if (title) title.focus({ preventScroll: true }); }
103
+ root.querySelectorAll('[data-action]').forEach(function (el) {
104
+ var action = el.dataset.action;
105
+ if (action === 'copy' && !state.extensionId) el.disabled = true;
106
+ if (state.pending && ['signin', 'test', 'remove', 'confirm-remove'].includes(action)) el.disabled = true;
107
+ el.onclick = function () {
108
+ if (action === 'back') events.navigate('list');
109
+ else if (['add', 'remote', 'computer'].includes(action)) events.navigate(action);
110
+ else if (action === 'remove' && row) events.remove(row);
111
+ else if (action === 'cancel-remove') events.navigate('detail', row.id);
112
+ else if (action === 'confirm-remove') events.action('remove', { id: row.id });
113
+ else if (action === 'refresh') events.refresh();
114
+ else if (action === 'setup') events.setup();
115
+ else if (action === 'copy') navigator.clipboard.writeText('npx clay-mcp-bridge install ' + (state.extensionId || '<extension-id>')).then(function () { el.textContent = 'Copied'; }).catch(function () { el.textContent = 'Select command to copy'; });
116
+ else events.action(action);
117
+ };
118
+ });
119
+ root.querySelectorAll('[data-open]').forEach(function (el) { el.onclick = function () { events.navigate('detail', el.dataset.open); }; });
120
+ root.querySelectorAll('[data-toggle]').forEach(function (el) { el.onclick = function () { var item = state.servers.find(function (server) { return server.id === el.dataset.toggle; }); if (item) events.toggle(item, !item.projectEnabled); }; });
121
+ root.querySelectorAll('[data-tab]').forEach(function (el) { el.onclick = function () { store.set({ mcpConnections: Object.assign({}, store.get('mcpConnections'), { detailTab: el.dataset.tab, search: '' }) }); }; });
122
+ var input = root.querySelector('[data-search]');
123
+ if (input) { input.oninput = function () { events.search(input.value); }; if (searchFocused) { input.focus(); try { input.setSelectionRange(start, start); } catch (error) {} } }
124
+ var urlInput = root.querySelector('[name="url"]'); if (urlInput) urlInput.oninput = function () { events.draft(urlInput.value); };
125
+ root.querySelectorAll('form').forEach(function (form) {
126
+ form.onsubmit = function (event) {
127
+ event.preventDefault(); if (state.pending) return;
128
+ if (form.dataset.form === 'remote') events.action('add', { url: form.elements.url.value });
129
+ if (form.dataset.form === 'token') { var token = form.elements.token.value; form.reset(); events.action('token', { token: token }); }
130
+ if (form.dataset.form === 'rename') events.action('rename', { label: form.elements.label.value });
131
+ };
132
+ });
133
+ var permission = root.querySelector('[data-permission]');
134
+ if (permission) permission.onchange = function () { sendMcpPermissionMode(store.get('currentSlug'), store.get('activeSessionId'), row.name, permission.value); };
135
+ refreshIcons(root);
136
+ }