clay-server 4.8.2 → 5.0.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.
- package/LICENSES/README.md +13 -0
- package/LICENSES/open-iconic.txt +21 -0
- package/LICENSES/plantuml-salt.txt +674 -0
- package/lib/assets/salt-assets.json +15 -0
- package/lib/assets/salt-colors.json +156 -0
- package/lib/assets/salt-font-10.bin.gz +0 -0
- package/lib/assets/salt-font-13.bin.gz +0 -0
- package/lib/assets/salt-font-14.bin.gz +0 -0
- package/lib/assets/salt-font-16.bin.gz +0 -0
- package/lib/assets/salt-font-18.bin.gz +0 -0
- package/lib/assets/salt-font-20.bin.gz +0 -0
- package/lib/assets/salt-font-metrics.bin.gz +0 -0
- package/lib/assets/salt-font-metrics.json +101 -0
- package/lib/assets/salt-font-monospace-10.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-13.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-14.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-16.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-18.bin.gz +0 -0
- package/lib/assets/salt-font-monospace-20.bin.gz +0 -0
- package/lib/assets/salt-font-monospace.bin.gz +0 -0
- package/lib/assets/salt-icons.json +671 -0
- package/lib/bundled-skills/clay-sketch/SKILL.md +198 -0
- package/lib/mcp-browser-proxy.js +37 -0
- package/lib/mcp-connection-store.js +39 -0
- package/lib/mcp-network.js +46 -0
- package/lib/mcp-oauth.js +65 -0
- package/lib/mcp-proxy.js +32 -0
- package/lib/mcp-remote-connections.js +215 -0
- package/lib/mcp-session-access.js +15 -0
- package/lib/plantuml-renderer.js +43 -0
- package/lib/project-file-http.js +2 -0
- package/lib/project-mcp.js +130 -357
- package/lib/project-plantuml.js +48 -0
- package/lib/project-session-document.js +17 -1
- package/lib/project-user-message.js +2 -0
- package/lib/project-wireframe-http.js +88 -0
- package/lib/project.js +28 -45
- package/lib/public/app.js +7 -0
- package/lib/public/css/filebrowser.css +5 -0
- package/lib/public/css/mcp-connections.css +86 -0
- package/lib/public/css/skills-workbench.css +74 -0
- package/lib/public/css/wireframe.css +95 -0
- package/lib/public/index.html +16 -15
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/filebrowser.js +29 -3
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/mcp-connection-view.js +136 -0
- package/lib/public/modules/mcp-permissions.js +122 -0
- package/lib/public/modules/mcp-ui.js +85 -461
- package/lib/public/modules/plantuml-preview.js +76 -0
- package/lib/public/modules/skills-actions.js +47 -79
- package/lib/public/modules/skills-detail.js +32 -145
- package/lib/public/modules/skills-list-view.js +29 -0
- package/lib/public/modules/skills-state.js +4 -2
- package/lib/public/modules/skills.js +104 -399
- package/lib/public/modules/split-view.js +14 -0
- package/lib/public/modules/vendor-login-browser.js +16 -5
- package/lib/public/modules/wireframe-chat.js +52 -0
- package/lib/public/modules/wireframe-render.js +69 -0
- package/lib/public/modules/wireframe-style.js +6 -0
- package/lib/public/modules/wireframe-viewport.js +70 -0
- package/lib/public/modules/wireframe-workbench.js +116 -0
- package/lib/public/style.css +3 -0
- package/lib/salt-chrome.js +36 -0
- package/lib/salt-hand.js +37 -0
- package/lib/salt-images.js +87 -0
- package/lib/salt-layout.js +60 -0
- package/lib/salt-options.js +23 -0
- package/lib/salt-overlays.js +45 -0
- package/lib/salt-parser.js +153 -0
- package/lib/salt-screen.js +111 -0
- package/lib/salt-svg-shapes.js +80 -0
- package/lib/salt-svg.js +164 -0
- package/lib/salt-text.js +112 -0
- package/lib/salt-tree.js +45 -0
- package/lib/sdk-bridge.js +5 -17
- package/lib/sdk-mcp-descriptors.js +2 -1
- package/lib/session-document-mcp-server.js +9 -0
- package/lib/vendor-login-browser-worker.js +18 -8
- package/lib/vendor-login-browser.js +10 -3
- package/lib/yoke/adapters/codex.js +1 -18
- package/package.json +6 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
var MAX_SOURCE_BYTES = 100000;
|
|
2
|
+
var MAX_SVG_BYTES = 8 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
function failure(message, status) {
|
|
5
|
+
var error = new Error(message);
|
|
6
|
+
error.status = status || 422;
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isPlantUmlFile(file) {
|
|
11
|
+
return /\.(puml|plantuml|pu|uml|salt)$/i.test(String(file || ""));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function prepareSource(source, file) {
|
|
15
|
+
source = String(source || "").replace(/^\uFEFF/, "");
|
|
16
|
+
if (Buffer.byteLength(source) > MAX_SOURCE_BYTES) throw failure("Diagram is too large to preview (100 KB maximum).", 413);
|
|
17
|
+
if (!source.trim()) throw failure("This diagram is empty.");
|
|
18
|
+
if (/\.salt$/i.test(file) && !/@start\w+/i.test(source)) source = "@startsalt\n" + source + "\n@endsalt";
|
|
19
|
+
if (!/^\s*@start\w+\b/im.test(source)) throw failure("Add @startsalt / @endsalt or @startuml / @enduml around the diagram.");
|
|
20
|
+
if ((source.match(/^\s*@start\w+\b/gim) || []).length > 1) throw failure("Preview supports one diagram per file. Split these diagrams into separate files.");
|
|
21
|
+
return source;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderPlantUml(source, options) {
|
|
25
|
+
options = options || {};
|
|
26
|
+
return new Promise(function (resolve, reject) {
|
|
27
|
+
setImmediate(async function () {
|
|
28
|
+
try {
|
|
29
|
+
if (options.signal && options.signal.aborted) throw failure("Diagram rendering cancelled.", 499);
|
|
30
|
+
source = prepareSource(source, "diagram.puml");
|
|
31
|
+
var tree = require("./salt-parser").parseSalt(source);
|
|
32
|
+
await require('./salt-images').resolveImages(tree, options);
|
|
33
|
+
if (options.signal && options.signal.aborted) throw failure("Diagram rendering cancelled.", 499);
|
|
34
|
+
var svg = require("./salt-svg").renderSaltSvg(tree);
|
|
35
|
+
if (Buffer.byteLength(svg) > MAX_SVG_BYTES) throw failure("Rendered wireframe is too large.", 413);
|
|
36
|
+
resolve(svg);
|
|
37
|
+
} catch (error) { reject(error); }
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { renderPlantUml: renderPlantUml, prepareSource: prepareSource,
|
|
43
|
+
isPlantUmlFile: isPlantUmlFile, MAX_SOURCE_BYTES: MAX_SOURCE_BYTES };
|
package/lib/project-file-http.js
CHANGED
|
@@ -14,8 +14,10 @@ function attachFileHTTP(ctx) {
|
|
|
14
14
|
var access = ctx.requestAccess || attachRequestAccess(Object.assign({}, ctx, {
|
|
15
15
|
getOsUserInfoForWs: ctx.getOsUserInfoForReq,
|
|
16
16
|
}));
|
|
17
|
+
var plantUml = require("./project-plantuml").attachPlantUmlHTTP(Object.assign({}, ctx, { requestAccess: access }));
|
|
17
18
|
|
|
18
19
|
function handleHTTP(req, res, urlPath) {
|
|
20
|
+
if (plantUml.handleHTTP(req, res, urlPath)) return true;
|
|
19
21
|
var download = urlPath.startsWith("/api/file/download?");
|
|
20
22
|
if (req.method !== "GET" || (!download && !urlPath.startsWith("/api/file?"))) return false;
|
|
21
23
|
try {
|
package/lib/project-mcp.js
CHANGED
|
@@ -1,375 +1,148 @@
|
|
|
1
|
-
var crypto = require(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// Follows the attachXxx(ctx) pattern per MODULE_MAP.md.
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var createProxy = require('./mcp-proxy').createProxy;
|
|
3
|
+
var createStore = require('./mcp-connection-store').createConnectionStore;
|
|
4
|
+
var createRemoteConnections = require('./mcp-remote-connections').createRemoteConnections;
|
|
6
5
|
|
|
7
6
|
function attachMcp(ctx) {
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
7
|
+
var devices = new Map(), pending = new Map();
|
|
8
|
+
function ownerFor(ws) {
|
|
9
|
+
if (!ctx.canAccess(ws)) throw new Error('MCP access is not permitted.');
|
|
10
|
+
return ctx.isMultiUser() ? String(ws._clayUser.id) : 'default';
|
|
11
|
+
}
|
|
12
|
+
function authorizeOwner(owner) { if (!ctx.canUseOwner(owner)) throw new Error('MCP account is unavailable.'); }
|
|
13
|
+
function sendOwner(owner, msg) {
|
|
14
|
+
ctx.clients.forEach(function (ws) {
|
|
15
|
+
try { if (ws.readyState === 1 && ownerFor(ws) === owner) ctx.sendTo(ws, Object.assign({ slug: ctx.slug }, msg)); } catch (error) {}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
var remote = createRemoteConnections({
|
|
19
|
+
remoteFetch: ctx.remoteFetch, createLoginBrowser: ctx.createLoginBrowser,
|
|
20
|
+
storage: ctx.storage || createStore(ctx.root, ctx.slug), authorize: authorizeOwner,
|
|
21
|
+
changed: function (owner) { broadcast(owner); }, sendOwner: sendOwner, sendTo: function (ws, msg) { ctx.sendTo(ws, Object.assign({ slug: ctx.slug }, msg)); },
|
|
22
|
+
identity: ctx.identity,
|
|
23
|
+
canAccess: function (ws, owner) { try { return ownerFor(ws) === owner; } catch (error) { return false; } },
|
|
24
|
+
});
|
|
25
|
+
function deviceFor(owner) {
|
|
26
|
+
var device = devices.get(owner);
|
|
27
|
+
if (!device || device.ws.readyState !== 1) return null;
|
|
28
|
+
try { if (ownerFor(device.ws) !== owner) return null; } catch (error) { return null; }
|
|
29
|
+
return device;
|
|
30
|
+
}
|
|
31
|
+
function enabled(owner, name) {
|
|
32
|
+
var legacy = !ctx.isMultiUser() && (ctx.getEnabledMcpServers() || []).indexOf(name) >= 0;
|
|
33
|
+
return remote.computerEnabled(owner, name, legacy);
|
|
34
|
+
}
|
|
35
|
+
function buildState(owner) {
|
|
36
|
+
var active = deviceFor(owner), device = devices.get(owner), servers = remote.view(owner);
|
|
37
|
+
if (device) device.servers.forEach(function (server) {
|
|
38
|
+
servers.push({ id: server.name, name: server.name, label: server.name, source: 'computer', transport: 'stdio',
|
|
39
|
+
toolCount: server.tools.length, tools: server.tools, projectEnabled: enabled(owner, server.name),
|
|
40
|
+
extensionEnabled: server.enabled, status: active && server.enabled && device.hostConnected ? 'ready' : 'unavailable',
|
|
41
|
+
error: active && device.hostConnected ? '' : 'Reconnect the local bridge on your computer.' });
|
|
42
|
+
});
|
|
43
|
+
return { type: 'mcp_servers_state', slug: ctx.slug, servers: servers,
|
|
44
|
+
hostConnected: !!(active && device.hostConnected), extensionConnected: !!active,
|
|
45
|
+
extensionId: device ? device.extensionId : null };
|
|
46
|
+
}
|
|
47
|
+
function broadcast(owner) {
|
|
48
|
+
try { sendOwner(owner, buildState(owner)); } catch (error) {}
|
|
49
|
+
}
|
|
50
|
+
function sendConnectionState(ws) {
|
|
51
|
+
try { ctx.sendTo(ws, buildState(ownerFor(ws))); } catch (error) {}
|
|
52
|
+
}
|
|
53
|
+
function disconnectDevice(ws) {
|
|
54
|
+
devices.forEach(function (device, owner) {
|
|
55
|
+
if (ws && device.ws !== ws) return;
|
|
56
|
+
if (!ws && device.ws.readyState === 1) return;
|
|
57
|
+
device.hostConnected = false;
|
|
58
|
+
pending.forEach(function (call, id) {
|
|
59
|
+
if (call.ws !== device.ws) return;
|
|
60
|
+
clearTimeout(call.timer); pending.delete(id); call.reject(new Error('Your computer disconnected.'));
|
|
61
|
+
});
|
|
62
|
+
broadcast(owner);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
31
65
|
function handleMcpMessage(ws, msg) {
|
|
32
|
-
if (msg.type
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
if (!msg || typeof msg.type !== 'string' || (msg.type.indexOf('mcp_') !== 0 && msg.type !== 'mcp_toggle_server')) return false;
|
|
67
|
+
var owner;
|
|
68
|
+
try { owner = ownerFor(ws); } catch (error) { return true; }
|
|
69
|
+
if (msg.type === 'mcp_state_request') { sendConnectionState(ws); return true; }
|
|
70
|
+
if (msg.type === 'mcp_servers_available') {
|
|
71
|
+
var servers = Array.isArray(msg.servers) ? msg.servers : [];
|
|
72
|
+
if (servers.length > 100 || JSON.stringify(servers).length > 2 * 1024 * 1024) return true;
|
|
73
|
+
servers = servers.filter(function (server) { return server && /^[a-zA-Z0-9_-]{1,80}$/.test(server.name) && !/^(clay-|remote-)/.test(server.name) && !Object.prototype.hasOwnProperty.call(Object.prototype, server.name) && !server.url && server.transport !== 'http'; }).map(function (server) {
|
|
74
|
+
return { name: server.name, enabled: server.enabled !== false, tools: Array.isArray(server.tools) ? server.tools.slice(0, 1000) : [] };
|
|
75
|
+
});
|
|
76
|
+
var old = devices.get(owner);
|
|
77
|
+
if (old && old.ws !== ws) disconnectDevice(old.ws);
|
|
78
|
+
devices.set(owner, { ws: ws, servers: servers, hostConnected: !!msg.hostConnected, extensionId: ctx.extensionId(ws) });
|
|
79
|
+
broadcast(owner); return true;
|
|
35
80
|
}
|
|
36
|
-
if (msg.type ===
|
|
37
|
-
|
|
81
|
+
if (msg.type === 'mcp_tool_result' || msg.type === 'mcp_tool_error') {
|
|
82
|
+
var call = pending.get(msg.callId);
|
|
83
|
+
if (!call || call.ws !== ws || call.owner !== owner) return true;
|
|
84
|
+
clearTimeout(call.timer); pending.delete(msg.callId);
|
|
85
|
+
try { call.authorize(); } catch (error) { call.reject(error); return true; }
|
|
86
|
+
if (msg.type === 'mcp_tool_error') call.reject(new Error('Computer MCP tool failed.'));
|
|
87
|
+
else call.resolve(msg.result || { content: [] });
|
|
38
88
|
return true;
|
|
39
89
|
}
|
|
40
|
-
if (msg.type ===
|
|
41
|
-
|
|
90
|
+
if (msg.type.indexOf('mcp_login_') === 0) { remote.handleBrowser(owner, ws, msg); return true; }
|
|
91
|
+
if (msg.type === 'mcp_toggle_server') {
|
|
92
|
+
var device = deviceFor(owner);
|
|
93
|
+
if (device && device.servers.some(function (server) { return server.name === msg.name; })) {
|
|
94
|
+
try { remote.toggleComputer(owner, msg.name, msg.enabled); } catch (error) { ctx.sendTo(ws, { type: 'mcp_action_result', ok: false, error: error.message }); }
|
|
95
|
+
}
|
|
42
96
|
return true;
|
|
43
97
|
}
|
|
44
|
-
if (msg.type ===
|
|
45
|
-
|
|
98
|
+
if (msg.type === 'mcp_connection_action') {
|
|
99
|
+
remote.command(owner, msg, ws).then(function (id) {
|
|
100
|
+
if (ownerFor(ws) === owner) ctx.sendTo(ws, { type: 'mcp_action_result', slug: ctx.slug, requestId: msg.requestId, ok: true, id: id, action: msg.action });
|
|
101
|
+
}).catch(function (error) {
|
|
102
|
+
try { if (ownerFor(ws) === owner) ctx.sendTo(ws, { type: 'mcp_action_result', slug: ctx.slug, requestId: msg.requestId, ok: false, error: error.message }); } catch (ignored) {}
|
|
103
|
+
});
|
|
46
104
|
return true;
|
|
47
105
|
}
|
|
48
106
|
return false;
|
|
49
107
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
var
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
broadcastMcpState();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function handleToolResult(msg) {
|
|
75
|
-
var callId = msg.callId;
|
|
76
|
-
console.log("[mcp-bridge] Tool result received: " + callId);
|
|
77
|
-
var pending = _pendingCalls[callId];
|
|
78
|
-
if (!pending) return;
|
|
79
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
80
|
-
delete _pendingCalls[callId];
|
|
81
|
-
pending.resolve(msg.result || { content: [{ type: "text", text: "(empty result)" }] });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function handleToolError(msg) {
|
|
85
|
-
var callId = msg.callId;
|
|
86
|
-
console.log("[mcp-bridge] Tool error received: " + callId + " error=" + (msg.error || "unknown"));
|
|
87
|
-
var pending = _pendingCalls[callId];
|
|
88
|
-
if (!pending) return;
|
|
89
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
90
|
-
delete _pendingCalls[callId];
|
|
91
|
-
pending.reject(new Error(msg.error || "MCP tool call failed"));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function handleToggleServer(ws, msg) {
|
|
95
|
-
var name = msg.name;
|
|
96
|
-
var enabled = !!msg.enabled;
|
|
97
|
-
|
|
98
|
-
var list = getEnabledMcpServers() || [];
|
|
99
|
-
var idx = list.indexOf(name);
|
|
100
|
-
|
|
101
|
-
if (enabled && idx === -1) {
|
|
102
|
-
list.push(name);
|
|
103
|
-
} else if (!enabled && idx !== -1) {
|
|
104
|
-
list.splice(idx, 1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
setEnabledMcpServers(list);
|
|
108
|
-
rebuildProxyServers();
|
|
109
|
-
broadcastMcpState();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------- Proxy Server Builder ----------
|
|
113
|
-
|
|
114
|
-
function rebuildProxyServers() {
|
|
115
|
-
_proxyServers = {};
|
|
116
|
-
|
|
117
|
-
var sdk;
|
|
118
|
-
try {
|
|
119
|
-
sdk = require("@anthropic-ai/claude-agent-sdk");
|
|
120
|
-
} catch (e) {
|
|
121
|
-
console.error("[mcp-bridge] Failed to load SDK:", e.message);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
var createSdkMcpServer = sdk.createSdkMcpServer;
|
|
126
|
-
var tool = sdk.tool;
|
|
127
|
-
if (!createSdkMcpServer || !tool) {
|
|
128
|
-
console.error("[mcp-bridge] SDK missing createSdkMcpServer or tool helper");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
var z;
|
|
133
|
-
try { z = require("zod").z; } catch (e) {
|
|
134
|
-
try { z = require("zod"); } catch (e2) {
|
|
135
|
-
console.error("[mcp-bridge] Failed to load zod:", e2.message);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
var enabledList = getEnabledMcpServers() || [];
|
|
141
|
-
|
|
142
|
-
// --- Remote servers (via Extension) ---
|
|
143
|
-
var serverNames = Object.keys(_availableServers);
|
|
144
|
-
for (var si = 0; si < serverNames.length; si++) {
|
|
145
|
-
var serverName = serverNames[si];
|
|
146
|
-
var serverInfo = _availableServers[serverName];
|
|
147
|
-
|
|
148
|
-
if (!serverInfo.enabled) continue;
|
|
149
|
-
if (enabledList.indexOf(serverName) === -1) continue;
|
|
150
|
-
|
|
151
|
-
var tools = [];
|
|
152
|
-
var serverTools = serverInfo.tools || [];
|
|
153
|
-
|
|
154
|
-
for (var ti = 0; ti < serverTools.length; ti++) {
|
|
155
|
-
var mcpTool = serverTools[ti];
|
|
156
|
-
var toolName = mcpTool.name;
|
|
157
|
-
var toolDesc = mcpTool.description || toolName;
|
|
158
|
-
var shape = buildZodShape(z, mcpTool.inputSchema);
|
|
159
|
-
|
|
160
|
-
tools.push(tool(
|
|
161
|
-
toolName,
|
|
162
|
-
toolDesc,
|
|
163
|
-
shape,
|
|
164
|
-
createToolHandler(serverName, toolName)
|
|
165
|
-
));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (tools.length > 0) {
|
|
169
|
-
var mcpServer = createSdkMcpServer({
|
|
170
|
-
name: serverName,
|
|
171
|
-
version: "1.0.0",
|
|
172
|
-
tools: tools,
|
|
173
|
-
});
|
|
174
|
-
_proxyServers[serverName] = mcpServer;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// --- Local servers (direct process, localhost only) ---
|
|
179
|
-
if (localMcp && localMcp.isReady()) {
|
|
180
|
-
var localServers = localMcp.getAvailableServers();
|
|
181
|
-
for (var li = 0; li < localServers.length; li++) {
|
|
182
|
-
var ls = localServers[li];
|
|
183
|
-
if (!ls.ready) continue;
|
|
184
|
-
if (_proxyServers[ls.name]) continue; // remote takes precedence if same name
|
|
185
|
-
if (enabledList.indexOf(ls.name) === -1) continue;
|
|
186
|
-
|
|
187
|
-
var localTools = [];
|
|
188
|
-
for (var lti = 0; lti < ls.tools.length; lti++) {
|
|
189
|
-
var lt = ls.tools[lti];
|
|
190
|
-
var ltShape = buildZodShape(z, lt.inputSchema);
|
|
191
|
-
localTools.push(tool(
|
|
192
|
-
lt.name,
|
|
193
|
-
lt.description || lt.name,
|
|
194
|
-
ltShape,
|
|
195
|
-
createLocalToolHandler(ls.name, lt.name)
|
|
196
|
-
));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (localTools.length > 0) {
|
|
200
|
-
_proxyServers[ls.name] = createSdkMcpServer({
|
|
201
|
-
name: ls.name,
|
|
202
|
-
version: "1.0.0",
|
|
203
|
-
tools: localTools,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function createLocalToolHandler(serverName, toolName) {
|
|
211
|
-
return function (args) {
|
|
212
|
-
return localMcp.callTool(serverName, toolName, args);
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function createToolHandler(serverName, toolName) {
|
|
217
|
-
return function (args) {
|
|
218
|
-
return new Promise(function (resolve, reject) {
|
|
219
|
-
var extWs = getExtensionWs();
|
|
220
|
-
if (!extWs || extWs.readyState !== 1) {
|
|
221
|
-
reject(new Error("Browser extension not connected. Cannot reach MCP server: " + serverName));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
var callId = "mc_" + Date.now() + "_" + crypto.randomUUID().slice(0, 8);
|
|
226
|
-
|
|
227
|
-
var timer = setTimeout(function () {
|
|
228
|
-
console.log("[mcp-bridge] Tool call TIMEOUT: " + callId + " server=" + serverName + " tool=" + toolName);
|
|
229
|
-
delete _pendingCalls[callId];
|
|
230
|
-
reject(new Error("MCP tool call timed out after " + (TOOL_TIMEOUT_MS / 1000) + "s"));
|
|
231
|
-
}, TOOL_TIMEOUT_MS);
|
|
232
|
-
|
|
233
|
-
_pendingCalls[callId] = { resolve: resolve, reject: reject, timer: timer };
|
|
234
|
-
|
|
235
|
-
console.log("[mcp-bridge] Sending tool call: " + callId + " server=" + serverName + " tool=" + toolName);
|
|
236
|
-
sendTo(extWs, {
|
|
237
|
-
type: "mcp_tool_call",
|
|
238
|
-
callId: callId,
|
|
239
|
-
server: serverName,
|
|
240
|
-
method: "tools/call",
|
|
241
|
-
params: { name: toolName, arguments: args },
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Build a Zod shape from MCP JSON Schema inputSchema
|
|
248
|
-
function buildZodShape(z, inputSchema) {
|
|
249
|
-
if (!inputSchema || !inputSchema.properties) return {};
|
|
250
|
-
var shape = {};
|
|
251
|
-
var props = inputSchema.properties;
|
|
252
|
-
var required = inputSchema.required || [];
|
|
253
|
-
var keys = Object.keys(props);
|
|
254
|
-
|
|
255
|
-
for (var i = 0; i < keys.length; i++) {
|
|
256
|
-
var k = keys[i];
|
|
257
|
-
var p = props[k];
|
|
258
|
-
var field;
|
|
259
|
-
|
|
260
|
-
if (p.type === "number" || p.type === "integer") {
|
|
261
|
-
field = z.number();
|
|
262
|
-
} else if (p.type === "boolean") {
|
|
263
|
-
field = z.boolean();
|
|
264
|
-
} else if (p.type === "array") {
|
|
265
|
-
field = z.array(z.any());
|
|
266
|
-
} else if (p.type === "object") {
|
|
267
|
-
field = z.record(z.any());
|
|
268
|
-
} else if (p.enum) {
|
|
269
|
-
field = z.enum(p.enum);
|
|
270
|
-
} else {
|
|
271
|
-
field = z.string();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (p.description) field = field.describe(p.description);
|
|
275
|
-
if (required.indexOf(k) === -1) field = field.optional();
|
|
276
|
-
shape[k] = field;
|
|
108
|
+
function callComputer(owner, server, tool, args, authorize) {
|
|
109
|
+
return new Promise(function (resolve, reject) {
|
|
110
|
+
authorize();
|
|
111
|
+
var device = deviceFor(owner);
|
|
112
|
+
if (!device || !device.hostConnected || !enabled(owner, server)) throw new Error('Computer connection is unavailable or disabled.');
|
|
113
|
+
var row = device.servers.find(function (item) { return item.name === server && item.enabled; });
|
|
114
|
+
if (!row || !row.tools.some(function (item) { return item.name === tool; })) throw new Error('Computer tool is unavailable.');
|
|
115
|
+
var id = crypto.randomUUID();
|
|
116
|
+
var timer = setTimeout(function () { pending.delete(id); reject(new Error('Computer tool timed out.')); }, 60000);
|
|
117
|
+
pending.set(id, { owner: owner, ws: device.ws, resolve: resolve, reject: reject, timer: timer, authorize: function () {
|
|
118
|
+
authorize(); if (!enabled(owner, server) || deviceFor(owner) !== device) throw new Error('Computer access changed.');
|
|
119
|
+
} });
|
|
120
|
+
ctx.sendTo(device.ws, { type: 'mcp_tool_call', callId: id, server: server, method: 'tools/call', params: { name: tool, arguments: args } });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function getMcpServers(session) {
|
|
124
|
+
if (!session || !ctx.canUseSession(session)) return {};
|
|
125
|
+
var owner = ctx.isMultiUser() ? String(session.ownerId) : 'default';
|
|
126
|
+
function authorize() {
|
|
127
|
+
authorizeOwner(owner);
|
|
128
|
+
if (!ctx.canUseSession(session) || (ctx.isMultiUser() && String(session.ownerId) !== owner)) throw new Error('Session MCP access changed.');
|
|
277
129
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
var state = buildMcpState();
|
|
285
|
-
send(state);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function sendConnectionState(ws) {
|
|
289
|
-
sendTo(ws, buildMcpState());
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function buildMcpState() {
|
|
293
|
-
var enabledList = getEnabledMcpServers() || [];
|
|
294
|
-
var servers = [];
|
|
295
|
-
var seen = {};
|
|
296
|
-
|
|
297
|
-
// Remote servers (from Extension)
|
|
298
|
-
var names = Object.keys(_availableServers);
|
|
299
|
-
for (var i = 0; i < names.length; i++) {
|
|
300
|
-
var name = names[i];
|
|
301
|
-
var info = _availableServers[name];
|
|
302
|
-
seen[name] = true;
|
|
303
|
-
servers.push({
|
|
304
|
-
name: name,
|
|
305
|
-
transport: info.transport,
|
|
306
|
-
toolCount: (info.tools || []).length,
|
|
307
|
-
extensionEnabled: info.enabled,
|
|
308
|
-
projectEnabled: enabledList.indexOf(name) !== -1,
|
|
309
|
-
source: "remote",
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Local servers
|
|
314
|
-
if (localMcp && localMcp.isReady()) {
|
|
315
|
-
var localServers = localMcp.getAvailableServers();
|
|
316
|
-
for (var j = 0; j < localServers.length; j++) {
|
|
317
|
-
var ls = localServers[j];
|
|
318
|
-
if (seen[ls.name]) continue;
|
|
319
|
-
servers.push({
|
|
320
|
-
name: ls.name,
|
|
321
|
-
transport: ls.transport || "stdio",
|
|
322
|
-
toolCount: ls.toolCount || 0,
|
|
323
|
-
extensionEnabled: true,
|
|
324
|
-
projectEnabled: enabledList.indexOf(ls.name) !== -1,
|
|
325
|
-
source: "local",
|
|
130
|
+
var result = {};
|
|
131
|
+
buildState(owner).servers.forEach(function (server) {
|
|
132
|
+
if (!server.projectEnabled || !server.extensionEnabled || !server.tools.length) return;
|
|
133
|
+
try {
|
|
134
|
+
result[server.name] = createProxy(server.name, server.tools, function (tool, args) {
|
|
135
|
+
return server.source === 'url' ? remote.call(owner, server.id, tool, args, authorize) : callComputer(owner, server.name, tool, args, authorize);
|
|
326
136
|
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
type: "mcp_servers_state",
|
|
332
|
-
servers: servers,
|
|
333
|
-
hostConnected: _remoteHostConnected || !!(localMcp && localMcp.isReady()),
|
|
334
|
-
extensionId: getExtensionId() || null,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ---------- Public API ----------
|
|
339
|
-
|
|
340
|
-
function getMcpServers() {
|
|
341
|
-
return _proxyServers;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function cancelAllPending() {
|
|
345
|
-
var ids = Object.keys(_pendingCalls);
|
|
346
|
-
for (var i = 0; i < ids.length; i++) {
|
|
347
|
-
var pending = _pendingCalls[ids[i]];
|
|
348
|
-
if (pending.timer) clearTimeout(pending.timer);
|
|
349
|
-
pending.reject(new Error("MCP bridge disconnected"));
|
|
350
|
-
}
|
|
351
|
-
_pendingCalls = {};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function handleExtensionDisconnect() {
|
|
355
|
-
cancelAllPending();
|
|
356
|
-
_availableServers = {};
|
|
357
|
-
_proxyServers = {};
|
|
358
|
-
broadcastMcpState();
|
|
137
|
+
} catch (error) { /* Unsupported schemas are not advertised as callable tools. */ }
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
359
140
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
broadcastMcpState();
|
|
141
|
+
function destroy() {
|
|
142
|
+
pending.forEach(function (call) { clearTimeout(call.timer); call.reject(new Error('MCP closed.')); }); pending.clear();
|
|
143
|
+
devices.clear(); remote.destroy();
|
|
364
144
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
handleMcpMessage: handleMcpMessage,
|
|
368
|
-
getMcpServers: getMcpServers,
|
|
369
|
-
sendConnectionState: sendConnectionState,
|
|
370
|
-
handleExtensionDisconnect: handleExtensionDisconnect,
|
|
371
|
-
rebuildAndBroadcast: rebuildAndBroadcast,
|
|
372
|
-
};
|
|
145
|
+
return { handleMcpMessage: handleMcpMessage, getMcpServers: getMcpServers, sendConnectionState: sendConnectionState,
|
|
146
|
+
handleExtensionDisconnect: disconnectDevice, rebuildAndBroadcast: function () { devices.forEach(function (device, owner) { broadcast(owner); }); }, destroy: destroy };
|
|
373
147
|
}
|
|
374
|
-
|
|
375
148
|
module.exports = { attachMcp: attachMcp };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var filePaths = require("./project-file-path");
|
|
3
|
+
var renderer = require("./plantuml-renderer");
|
|
4
|
+
|
|
5
|
+
function attachPlantUmlHTTP(ctx) {
|
|
6
|
+
var wireframes = require("./project-wireframe-http").attachWireframeHTTP(ctx);
|
|
7
|
+
function handleHTTP(req, res, urlPath) {
|
|
8
|
+
if (wireframes.handleHTTP(req, res, urlPath)) return true;
|
|
9
|
+
if (!urlPath.startsWith("/api/file/plantuml?")) return false;
|
|
10
|
+
function fail(error) {
|
|
11
|
+
if (res.destroyed) return;
|
|
12
|
+
var mapped = filePaths.fileError(error);
|
|
13
|
+
res.writeHead(error.status || mapped.status, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
|
|
14
|
+
res.end(error.status ? error.message : mapped.message);
|
|
15
|
+
}
|
|
16
|
+
if (req.method !== "GET") { fail({ status: 405, message: "Use GET to preview a diagram." }); return true; }
|
|
17
|
+
var scope;
|
|
18
|
+
var requested;
|
|
19
|
+
var source;
|
|
20
|
+
try {
|
|
21
|
+
scope = ctx.requestAccess.fileScope(req);
|
|
22
|
+
requested = new URLSearchParams(urlPath.substring(urlPath.indexOf("?"))).get("path");
|
|
23
|
+
var file = filePaths.resolveFilePath(ctx.cwd, requested, scope);
|
|
24
|
+
if (!renderer.isPlantUmlFile(file)) throw { status: 415, message: "Select a PlantUML or Salt file to preview." };
|
|
25
|
+
var stat = scope.identity ? ctx.fsAsUser("stat", { file: file }, scope.identity) : fs.statSync(file);
|
|
26
|
+
if (stat.size > renderer.MAX_SOURCE_BYTES) throw { status: 413, message: "Diagram is too large to preview (100 KB maximum)." };
|
|
27
|
+
source = scope.identity ? ctx.fsAsUser("read", { file: file, readContent: true }, scope.identity).content : fs.readFileSync(file, "utf8");
|
|
28
|
+
source = renderer.prepareSource(source, requested);
|
|
29
|
+
} catch (error) { fail(error); return true; }
|
|
30
|
+
var controller = new AbortController();
|
|
31
|
+
function cancel() { controller.abort(); }
|
|
32
|
+
if (res.once) res.once("close", cancel);
|
|
33
|
+
(ctx.renderPlantUml || renderer.renderPlantUml)(source, { signal: controller.signal }).then(function (svg) {
|
|
34
|
+
if (res.destroyed || controller.signal.aborted) return;
|
|
35
|
+
try { ctx.requestAccess.fileScope(req); }
|
|
36
|
+
catch (error) { fail(error); return; }
|
|
37
|
+
res.writeHead(200, {
|
|
38
|
+
"Content-Type": "image/svg+xml; charset=utf-8", "Cache-Control": "no-store",
|
|
39
|
+
"X-Content-Type-Options": "nosniff", "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
|
|
40
|
+
});
|
|
41
|
+
res.end(svg);
|
|
42
|
+
}).catch(fail).finally(function () { if (res.removeListener) res.removeListener("close", cancel); });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return { handleHTTP: handleHTTP };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { attachPlantUmlHTTP: attachPlantUmlHTTP };
|
|
@@ -2,6 +2,9 @@ var fs = require("fs");
|
|
|
2
2
|
var path = require("path");
|
|
3
3
|
var sessionDocumentMcp = require("./session-document-mcp-server");
|
|
4
4
|
|
|
5
|
+
var wireframeRenderer = require("./plantuml-renderer");
|
|
6
|
+
var WIREFRAME_PROMPT = "When the user asks to see a wireframe, visualize a UI plan or sketch a page, automatically use the built-in clay-sketch skill even if they do not name it. Requests such as 'show me a wireframe of our work' use the current project and conversation as context. Do not ask them to invoke the skill or choose a format. Use ASCII wireframes only when explicitly requested. Read its SKILL.md from the shared skills catalog for the supported Salt syntax and whole-page examples. For an entire screen use its Clay canvas/region layout directives, not magnification of a compact diagram. Show complete @startsalt/@endsalt source in a fenced clay-sketch block; Clay renders it inline with no installation or external service. For requested revisions, call present_wireframe with complete source, a stable id and title to open/update the temporary Sketch viewer in the right panel, and include the same revised sketch in your response for history. The viewer has Save to project and Download; edits happen through conversation. Do not create a file unless requested. Other UML types and advanced Salt syntax are unsupported; use Mermaid for architecture or sequence diagrams.";
|
|
7
|
+
|
|
5
8
|
var DOCUMENT_PROMPT = "When the user's primary request is to create or revise Markdown, call present_markdown_edit once per targeted document with its resolved .md/.mdx path, immediately before that document's first Edit or Write. Do not call it for incidental Markdown changes during coding, maintenance, tests, or refactoring.";
|
|
6
9
|
|
|
7
10
|
function toolResult(value) {
|
|
@@ -78,9 +81,22 @@ function attachSessionDocument(ctx) {
|
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function presentWireframe(args, caller) {
|
|
85
|
+
if (!caller) return toolError("present_wireframe requires a session-bound tool server");
|
|
86
|
+
if (typeof args.source !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(args.id || "")) return toolError("Provide source and a short stable id using letters, digits, hyphens or underscores.");
|
|
87
|
+
try {
|
|
88
|
+
var source = wireframeRenderer.prepareSource(args.source, "diagram.puml");
|
|
89
|
+
require("./salt-parser").parseSalt(source);
|
|
90
|
+
if (!/^\s*@end(?:salt|uml)\b/im.test(source)) return toolError("Provide a complete @startsalt/@endsalt or @startuml/@enduml diagram.");
|
|
91
|
+
sendToSession(caller.localId, { type: "wireframe_present", source: source, id: args.id, title: String(args.title || "clay-sketch").slice(0, 100), sessionId: caller.localId });
|
|
92
|
+
return toolResult({ ready: true, id: args.id, saved: false });
|
|
93
|
+
} catch (e) { return toolError(e.message); }
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
function getToolDefs(boundSession) {
|
|
82
97
|
if (isMate) return [];
|
|
83
98
|
return sessionDocumentMcp.getToolDefs({
|
|
99
|
+
presentWireframe: function (args) { return presentWireframe(args, boundSession || null); },
|
|
84
100
|
present: function (args) { return present(args, boundSession || null); },
|
|
85
101
|
});
|
|
86
102
|
}
|
|
@@ -96,7 +112,7 @@ function attachSessionDocument(ctx) {
|
|
|
96
112
|
|
|
97
113
|
return {
|
|
98
114
|
createMcpServer: createMcpServer,
|
|
99
|
-
getSystemPrompt: function () { return isMate ? "" : DOCUMENT_PROMPT; },
|
|
115
|
+
getSystemPrompt: function () { return isMate ? "" : DOCUMENT_PROMPT + "\n\n" + WIREFRAME_PROMPT; },
|
|
100
116
|
getToolDefs: getToolDefs,
|
|
101
117
|
};
|
|
102
118
|
}
|
|
@@ -308,6 +308,7 @@ function attachUserMessage(ctx) {
|
|
|
308
308
|
|
|
309
309
|
// --- Browser Extension ---
|
|
310
310
|
if (msg.type === "browser_tab_list") {
|
|
311
|
+
ws._clayMcpExtensionId = typeof msg.extensionId === "string" ? msg.extensionId : null;
|
|
311
312
|
browserState._extensionWs = ws; // Track which client has the extension
|
|
312
313
|
if (msg.extensionId) browserState._extensionId = msg.extensionId;
|
|
313
314
|
var tabs = msg.tabs || [];
|
|
@@ -394,6 +395,7 @@ function attachUserMessage(ctx) {
|
|
|
394
395
|
|
|
395
396
|
var session = forcedSession || getSessionForWs(ws);
|
|
396
397
|
if (!session) return true;
|
|
398
|
+
if (ws._clayUser && session.ownerId && ws._clayUser.id !== session.ownerId) session._mcpForeignInput = true;
|
|
397
399
|
var pendingInternal = internal && internal.pendingConsumed ? internal : null;
|
|
398
400
|
function finishPendingEarly(outcome, reason) {
|
|
399
401
|
if (pendingInternal && typeof pendingInternal.complete === "function") pendingInternal.complete(outcome, reason);
|