@chrysb/alphaclaw 0.9.0-beta.7 → 0.9.0
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/bin/alphaclaw.js +25 -25
- package/lib/cli/git-runtime.js +97 -0
- package/lib/public/css/chat.css +0 -12
- package/lib/public/css/explorer.css +48 -0
- package/lib/public/css/shell.css +149 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/css/theme.css +265 -0
- package/lib/public/dist/app.bundle.js +2770 -2762
- package/lib/public/js/app.js +26 -14
- package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
- package/lib/public/js/components/gateway.js +0 -286
- package/lib/public/js/components/general/index.js +0 -7
- package/lib/public/js/components/icons.js +26 -25
- package/lib/public/js/components/modal-shell.js +1 -1
- package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
- package/lib/public/js/components/models-tab/use-models.js +74 -9
- package/lib/public/js/components/models.js +52 -37
- package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
- package/lib/public/js/components/onboarding/welcome-config.js +76 -10
- package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
- package/lib/public/js/components/onboarding/welcome-header.js +12 -14
- package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
- package/lib/public/js/components/providers.js +53 -42
- package/lib/public/js/components/routes/chat-route.js +2 -9
- package/lib/public/js/components/routes/general-route.js +0 -6
- package/lib/public/js/components/routes/index.js +0 -1
- package/lib/public/js/components/routes/watchdog-route.js +0 -6
- package/lib/public/js/components/sidebar.js +21 -7
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/update-modal.js +174 -51
- package/lib/public/js/components/watchdog-tab/index.js +0 -6
- package/lib/public/js/components/welcome/index.js +0 -2
- package/lib/public/js/components/welcome/use-welcome.js +101 -36
- package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
- package/lib/public/js/lib/api.js +0 -28
- package/lib/public/js/lib/app-navigation.js +0 -2
- package/lib/public/js/lib/channel-provider-availability.js +1 -2
- package/lib/public/js/lib/codex-oauth-window.js +22 -0
- package/lib/public/js/lib/model-catalog.js +20 -0
- package/lib/public/js/lib/storage-keys.js +1 -1
- package/lib/public/login.html +8 -4
- package/lib/public/setup.html +9 -0
- package/lib/scripts/git +47 -1
- package/lib/server/agents/channels.js +1 -4
- package/lib/server/alphaclaw-version.js +590 -132
- package/lib/server/constants.js +5 -0
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/exec-defaults-config.js +163 -0
- package/lib/server/init/register-server-routes.js +0 -8
- package/lib/server/init/server-lifecycle.js +2 -0
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/onboarding/index.js +5 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/nodes.js +9 -23
- package/lib/server/routes/system.js +3 -16
- package/lib/server/routes/webhooks.js +12 -1
- package/lib/server/startup.js +8 -0
- package/lib/server/watchdog-notify.js +172 -55
- package/lib/server.js +17 -2
- package/package.json +2 -2
- package/patches/openclaw+2026.4.9.patch +13 -0
- package/lib/public/js/components/mcp-tab/index.js +0 -237
- package/lib/public/js/components/routes/mcp-route.js +0 -7
- package/lib/server/mcp-bridge.js +0 -158
- package/lib/server/routes/mcp.js +0 -292
- package/patches/openclaw+2026.3.28.patch +0 -13
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { h } from "preact";
|
|
2
|
-
import { useState, useCallback } from "preact/hooks";
|
|
3
|
-
import htm from "htm";
|
|
4
|
-
import {
|
|
5
|
-
fetchMcpInfo,
|
|
6
|
-
startMcpBridge,
|
|
7
|
-
stopMcpBridge,
|
|
8
|
-
} from "../../lib/api.js";
|
|
9
|
-
import { usePolling } from "../../hooks/usePolling.js";
|
|
10
|
-
import { showToast } from "../toast.js";
|
|
11
|
-
import { PageHeader } from "../page-header.js";
|
|
12
|
-
import { ActionButton } from "../action-button.js";
|
|
13
|
-
import { PaneShell } from "../pane-shell.js";
|
|
14
|
-
|
|
15
|
-
const html = htm.bind(h);
|
|
16
|
-
|
|
17
|
-
const kMcpTools = [
|
|
18
|
-
{ name: "conversations_list", desc: "List recent routed conversations with filters" },
|
|
19
|
-
{ name: "conversation_get", desc: "Return a single conversation by session key" },
|
|
20
|
-
{ name: "messages_read", desc: "Retrieve transcript history for a conversation" },
|
|
21
|
-
{ name: "attachments_fetch", desc: "Extract non-text content metadata from messages" },
|
|
22
|
-
{ name: "events_poll", desc: "Read queued live events since a cursor position" },
|
|
23
|
-
{ name: "events_wait", desc: "Long-poll for next matching event with timeout" },
|
|
24
|
-
{ name: "messages_send", desc: "Send text replies through existing routes" },
|
|
25
|
-
{ name: "permissions_list_open", desc: "List pending exec/plugin approval requests" },
|
|
26
|
-
{ name: "permissions_respond", desc: "Resolve approvals (allow-once, allow-always, deny)" },
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const StatusDot = ({ active }) => html`
|
|
30
|
-
<span
|
|
31
|
-
class="inline-block w-2 h-2 rounded-full shrink-0 ${active
|
|
32
|
-
? "bg-green-500"
|
|
33
|
-
: "bg-gray-600"}"
|
|
34
|
-
/>
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
const buildConfigSnippet = ({ origin, token }) => {
|
|
38
|
-
const encodedToken = encodeURIComponent(String(token || ""));
|
|
39
|
-
const sseUrl = `${origin}/mcp/sse?token=${encodedToken}`;
|
|
40
|
-
return JSON.stringify(
|
|
41
|
-
{
|
|
42
|
-
mcpServers: {
|
|
43
|
-
openclaw: {
|
|
44
|
-
url: sseUrl,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
null,
|
|
49
|
-
2,
|
|
50
|
-
);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const McpTab = () => {
|
|
54
|
-
const [acting, setActing] = useState(false);
|
|
55
|
-
|
|
56
|
-
const {
|
|
57
|
-
data: info,
|
|
58
|
-
refresh,
|
|
59
|
-
} = usePolling(fetchMcpInfo, 8000, {
|
|
60
|
-
cacheKey: "/api/mcp/info",
|
|
61
|
-
});
|
|
62
|
-
const loading = !info;
|
|
63
|
-
|
|
64
|
-
const running = !!info?.running;
|
|
65
|
-
const tokenAvailable = !!info?.tokenAvailable;
|
|
66
|
-
const gatewayToken = info?.gatewayToken || "";
|
|
67
|
-
|
|
68
|
-
const handleStart = useCallback(async () => {
|
|
69
|
-
if (acting) return;
|
|
70
|
-
setActing(true);
|
|
71
|
-
try {
|
|
72
|
-
const result = await startMcpBridge();
|
|
73
|
-
if (result?.ok) {
|
|
74
|
-
showToast(
|
|
75
|
-
result.alreadyRunning
|
|
76
|
-
? "MCP bridge already running"
|
|
77
|
-
: "MCP bridge started",
|
|
78
|
-
"success",
|
|
79
|
-
);
|
|
80
|
-
} else {
|
|
81
|
-
showToast("Failed to start MCP bridge", "error");
|
|
82
|
-
}
|
|
83
|
-
await refresh({ force: true });
|
|
84
|
-
} catch (err) {
|
|
85
|
-
showToast("Failed to start: " + err.message, "error");
|
|
86
|
-
} finally {
|
|
87
|
-
setActing(false);
|
|
88
|
-
}
|
|
89
|
-
}, [acting, refresh]);
|
|
90
|
-
|
|
91
|
-
const handleStop = useCallback(async () => {
|
|
92
|
-
if (acting) return;
|
|
93
|
-
setActing(true);
|
|
94
|
-
try {
|
|
95
|
-
const result = await stopMcpBridge();
|
|
96
|
-
if (result?.ok) {
|
|
97
|
-
showToast("MCP bridge stopped", "success");
|
|
98
|
-
} else {
|
|
99
|
-
showToast("Failed to stop MCP bridge", "error");
|
|
100
|
-
}
|
|
101
|
-
await refresh({ force: true });
|
|
102
|
-
} catch (err) {
|
|
103
|
-
showToast("Failed to stop: " + err.message, "error");
|
|
104
|
-
} finally {
|
|
105
|
-
setActing(false);
|
|
106
|
-
}
|
|
107
|
-
}, [acting, refresh]);
|
|
108
|
-
|
|
109
|
-
const handleCopy = useCallback(() => {
|
|
110
|
-
const origin = window.location.origin;
|
|
111
|
-
const snippet = buildConfigSnippet({ origin, token: gatewayToken });
|
|
112
|
-
navigator.clipboard
|
|
113
|
-
.writeText(snippet)
|
|
114
|
-
.then(() => showToast("Copied to clipboard", "success"))
|
|
115
|
-
.catch(() => showToast("Failed to copy", "error"));
|
|
116
|
-
}, [gatewayToken]);
|
|
117
|
-
|
|
118
|
-
const configSnippet = buildConfigSnippet({
|
|
119
|
-
origin: typeof window !== "undefined" ? window.location.origin : "https://your-host",
|
|
120
|
-
token: gatewayToken || "<gateway-token>",
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
if (loading && !info) {
|
|
124
|
-
return html`
|
|
125
|
-
<${PaneShell} header=${html`<${PageHeader} title="MCP" />`}>
|
|
126
|
-
<div class="bg-surface border border-border rounded-xl p-4 text-sm text-fg-muted">
|
|
127
|
-
Loading...
|
|
128
|
-
</div>
|
|
129
|
-
</${PaneShell}>
|
|
130
|
-
`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return html`
|
|
134
|
-
<${PaneShell}
|
|
135
|
-
header=${html`
|
|
136
|
-
<${PageHeader}
|
|
137
|
-
title="MCP"
|
|
138
|
-
actions=${html`
|
|
139
|
-
${running
|
|
140
|
-
? html`<${ActionButton}
|
|
141
|
-
onClick=${handleStop}
|
|
142
|
-
disabled=${acting}
|
|
143
|
-
loading=${acting}
|
|
144
|
-
loadingMode="inline"
|
|
145
|
-
tone="secondary"
|
|
146
|
-
size="sm"
|
|
147
|
-
idleLabel="Stop bridge"
|
|
148
|
-
loadingLabel="Stopping…"
|
|
149
|
-
className="text-xs"
|
|
150
|
-
/>`
|
|
151
|
-
: html`<${ActionButton}
|
|
152
|
-
onClick=${handleStart}
|
|
153
|
-
disabled=${acting}
|
|
154
|
-
loading=${acting}
|
|
155
|
-
loadingMode="inline"
|
|
156
|
-
tone="primary"
|
|
157
|
-
size="sm"
|
|
158
|
-
idleLabel="Start bridge"
|
|
159
|
-
loadingLabel="Starting…"
|
|
160
|
-
className="text-xs"
|
|
161
|
-
/>`}
|
|
162
|
-
`}
|
|
163
|
-
/>
|
|
164
|
-
`}
|
|
165
|
-
>
|
|
166
|
-
<!-- Status -->
|
|
167
|
-
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
168
|
-
<h3 class="card-label text-xs px-4 pt-3 pb-2">Status</h3>
|
|
169
|
-
<div class="px-4 pb-3 space-y-2">
|
|
170
|
-
<div class="flex items-center gap-2 text-sm">
|
|
171
|
-
<${StatusDot} active=${running} />
|
|
172
|
-
<span class="text-body">
|
|
173
|
-
MCP Bridge: ${running ? "Running" : "Stopped"}
|
|
174
|
-
</span>
|
|
175
|
-
${running && info?.pid
|
|
176
|
-
? html`<span class="text-fg-dim text-xs">(PID ${info.pid})</span>`
|
|
177
|
-
: null}
|
|
178
|
-
</div>
|
|
179
|
-
<div class="flex items-center gap-2 text-sm">
|
|
180
|
-
<${StatusDot} active=${tokenAvailable} />
|
|
181
|
-
<span class="text-body">
|
|
182
|
-
Gateway token: ${tokenAvailable ? "Configured" : "Not set"}
|
|
183
|
-
</span>
|
|
184
|
-
</div>
|
|
185
|
-
${info?.gatewayWsUrl
|
|
186
|
-
? html`<div class="text-xs text-fg-dim">
|
|
187
|
-
Gateway: <code class="bg-field px-1 rounded">${info.gatewayWsUrl}</code>
|
|
188
|
-
</div>`
|
|
189
|
-
: null}
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
<!-- Config Snippet -->
|
|
194
|
-
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
195
|
-
<div class="flex items-center justify-between px-4 pt-3 pb-2">
|
|
196
|
-
<h3 class="card-label text-xs">Client Config</h3>
|
|
197
|
-
<button
|
|
198
|
-
onclick=${handleCopy}
|
|
199
|
-
class="text-xs px-2 py-0.5 rounded border border-border text-fg-muted hover:text-body hover:border-fg-muted"
|
|
200
|
-
>
|
|
201
|
-
Copy
|
|
202
|
-
</button>
|
|
203
|
-
</div>
|
|
204
|
-
<div class="px-4 pb-3">
|
|
205
|
-
<p class="text-xs text-fg-dim mb-2">
|
|
206
|
-
Add this to your MCP client config (Cursor, Claude Desktop, etc.):
|
|
207
|
-
</p>
|
|
208
|
-
<pre
|
|
209
|
-
class="bg-field border border-border rounded-lg p-3 text-xs text-body font-mono overflow-x-auto whitespace-pre"
|
|
210
|
-
>${configSnippet}</pre>
|
|
211
|
-
${!running
|
|
212
|
-
? html`<p class="text-xs text-status-warning-muted mt-2">
|
|
213
|
-
Start the MCP bridge above before connecting a client.
|
|
214
|
-
</p>`
|
|
215
|
-
: null}
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
|
-
<!-- Available Tools -->
|
|
220
|
-
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
221
|
-
<h3 class="card-label text-xs px-4 pt-3 pb-2">Available Tools</h3>
|
|
222
|
-
<div class="divide-y divide-border">
|
|
223
|
-
${kMcpTools.map(
|
|
224
|
-
(tool) => html`
|
|
225
|
-
<div class="flex items-start gap-3 px-4 py-2">
|
|
226
|
-
<code class="text-xs shrink-0 pt-0.5" style="min-width: 170px"
|
|
227
|
-
>${tool.name}</code
|
|
228
|
-
>
|
|
229
|
-
<span class="text-xs text-fg-dim">${tool.desc}</span>
|
|
230
|
-
</div>
|
|
231
|
-
`,
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</${PaneShell}>
|
|
236
|
-
`;
|
|
237
|
-
};
|
package/lib/server/mcp-bridge.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
const { spawn } = require("child_process");
|
|
2
|
-
|
|
3
|
-
const kStderrTailLines = 50;
|
|
4
|
-
|
|
5
|
-
let mcpChild = null;
|
|
6
|
-
let mcpStartedAt = null;
|
|
7
|
-
let mcpStderrTail = [];
|
|
8
|
-
let stdoutBuffer = Buffer.alloc(0);
|
|
9
|
-
let onMcpMessage = null;
|
|
10
|
-
|
|
11
|
-
const appendStderrTail = (chunk) => {
|
|
12
|
-
const text = Buffer.isBuffer(chunk)
|
|
13
|
-
? chunk.toString("utf8")
|
|
14
|
-
: String(chunk ?? "");
|
|
15
|
-
for (const line of text.split("\n")) {
|
|
16
|
-
const trimmed = line.trimEnd();
|
|
17
|
-
if (!trimmed) continue;
|
|
18
|
-
mcpStderrTail.push(trimmed);
|
|
19
|
-
}
|
|
20
|
-
if (mcpStderrTail.length > kStderrTailLines) {
|
|
21
|
-
mcpStderrTail = mcpStderrTail.slice(-kStderrTailLines);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const isMcpBridgeRunning = () =>
|
|
26
|
-
mcpChild !== null && mcpChild.exitCode === null && !mcpChild.killed;
|
|
27
|
-
|
|
28
|
-
const getMcpBridgeStatus = () => ({
|
|
29
|
-
running: isMcpBridgeRunning(),
|
|
30
|
-
pid: isMcpBridgeRunning() ? mcpChild.pid : null,
|
|
31
|
-
startedAt: isMcpBridgeRunning() ? mcpStartedAt : null,
|
|
32
|
-
stderrTail: mcpStderrTail.slice(-10),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const setOnMcpMessage = (callback) => {
|
|
36
|
-
onMcpMessage = typeof callback === "function" ? callback : null;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const drainStdoutLines = () => {
|
|
40
|
-
while (stdoutBuffer.length > 0) {
|
|
41
|
-
const newlineIndex = stdoutBuffer.indexOf("\n");
|
|
42
|
-
if (newlineIndex === -1) break;
|
|
43
|
-
|
|
44
|
-
const line = stdoutBuffer.toString("utf8", 0, newlineIndex).replace(/\r$/, "");
|
|
45
|
-
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
46
|
-
if (!line.trim()) continue;
|
|
47
|
-
|
|
48
|
-
let parsed = null;
|
|
49
|
-
try {
|
|
50
|
-
parsed = JSON.parse(line);
|
|
51
|
-
} catch {
|
|
52
|
-
appendStderrTail(`Unparseable MCP stdout line: ${line.slice(0, 120)}`);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
console.log(`[mcp-bridge] ← child stdout: method=${parsed?.method || "(response)"} id=${parsed?.id ?? "none"}`);
|
|
56
|
-
if (onMcpMessage) {
|
|
57
|
-
try {
|
|
58
|
-
onMcpMessage(parsed);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
appendStderrTail(`onMcpMessage error: ${err?.message || err}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const startMcpBridge = ({ gatewayEnv, gatewayWsUrl, gatewayToken }) => {
|
|
67
|
-
if (isMcpBridgeRunning()) {
|
|
68
|
-
return { ok: true, alreadyRunning: true, ...getMcpBridgeStatus() };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const args = ["mcp", "serve"];
|
|
72
|
-
if (gatewayWsUrl) {
|
|
73
|
-
args.push("--url", gatewayWsUrl);
|
|
74
|
-
}
|
|
75
|
-
const childEnv = gatewayEnv();
|
|
76
|
-
if (gatewayToken) {
|
|
77
|
-
childEnv.OPENCLAW_GATEWAY_TOKEN = String(gatewayToken);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
mcpStderrTail = [];
|
|
81
|
-
stdoutBuffer = Buffer.alloc(0);
|
|
82
|
-
|
|
83
|
-
const child = spawn("openclaw", args, {
|
|
84
|
-
env: childEnv,
|
|
85
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
mcpChild = child;
|
|
89
|
-
mcpStartedAt = Date.now();
|
|
90
|
-
|
|
91
|
-
child.stdout.on("data", (data) => {
|
|
92
|
-
const chunk = Buffer.isBuffer(data)
|
|
93
|
-
? data
|
|
94
|
-
: Buffer.from(String(data ?? ""), "utf8");
|
|
95
|
-
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
|
|
96
|
-
drainStdoutLines();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
child.stderr.on("data", (data) => {
|
|
100
|
-
appendStderrTail(data);
|
|
101
|
-
process.stderr.write(`[mcp-bridge] ${data}`);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
child.on("exit", (code, signal) => {
|
|
105
|
-
console.log(
|
|
106
|
-
`[mcp-bridge] Process exited with code ${code}${signal ? ` signal ${signal}` : ""}`,
|
|
107
|
-
);
|
|
108
|
-
if (mcpChild === child) mcpChild = null;
|
|
109
|
-
mcpStartedAt = null;
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
child.on("error", (error) => {
|
|
113
|
-
appendStderrTail(error?.message || String(error || "unknown process error"));
|
|
114
|
-
console.error(
|
|
115
|
-
`[mcp-bridge] Process failed to start: ${error?.message || error}`,
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log(`[mcp-bridge] Started MCP bridge (pid ${child.pid})`);
|
|
120
|
-
return { ok: true, alreadyRunning: false, ...getMcpBridgeStatus() };
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const stopMcpBridge = () => {
|
|
124
|
-
if (!isMcpBridgeRunning()) {
|
|
125
|
-
return { ok: true, wasStopped: true };
|
|
126
|
-
}
|
|
127
|
-
const pid = mcpChild.pid;
|
|
128
|
-
mcpChild.kill("SIGTERM");
|
|
129
|
-
mcpChild = null;
|
|
130
|
-
mcpStartedAt = null;
|
|
131
|
-
console.log(`[mcp-bridge] Stopped MCP bridge (pid ${pid})`);
|
|
132
|
-
return { ok: true, wasStopped: false };
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const writeToMcpBridge = (jsonRpcMessage) => {
|
|
136
|
-
if (!isMcpBridgeRunning()) return false;
|
|
137
|
-
if (jsonRpcMessage == null) return false;
|
|
138
|
-
const payload =
|
|
139
|
-
typeof jsonRpcMessage === "string"
|
|
140
|
-
? jsonRpcMessage
|
|
141
|
-
: JSON.stringify(jsonRpcMessage);
|
|
142
|
-
if (!payload) return false;
|
|
143
|
-
const method = jsonRpcMessage?.method;
|
|
144
|
-
const id = jsonRpcMessage?.id;
|
|
145
|
-
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
|
146
|
-
console.log(`[mcp-bridge] → child stdin: method=${method || "(response)"} id=${id ?? "none"} bytes=${payloadBytes}`);
|
|
147
|
-
mcpChild.stdin.write(`${payload}\n`);
|
|
148
|
-
return true;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
module.exports = {
|
|
152
|
-
isMcpBridgeRunning,
|
|
153
|
-
getMcpBridgeStatus,
|
|
154
|
-
startMcpBridge,
|
|
155
|
-
stopMcpBridge,
|
|
156
|
-
writeToMcpBridge,
|
|
157
|
-
setOnMcpMessage,
|
|
158
|
-
};
|
package/lib/server/routes/mcp.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
const { randomUUID } = require("crypto");
|
|
2
|
-
const { createRequire } = require("module");
|
|
3
|
-
|
|
4
|
-
// Load the SDK through openclaw's dependency tree so its express@5 peer
|
|
5
|
-
// stays nested and never hoists over AlphaClaw's express@4 at the app root.
|
|
6
|
-
const openclawRequire = createRequire(require.resolve("openclaw"));
|
|
7
|
-
const {
|
|
8
|
-
StreamableHTTPServerTransport,
|
|
9
|
-
} = openclawRequire("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
isMcpBridgeRunning,
|
|
13
|
-
getMcpBridgeStatus,
|
|
14
|
-
startMcpBridge,
|
|
15
|
-
stopMcpBridge,
|
|
16
|
-
writeToMcpBridge,
|
|
17
|
-
setOnMcpMessage,
|
|
18
|
-
} = require("../mcp-bridge");
|
|
19
|
-
const { getGatewayPort } = require("../gateway");
|
|
20
|
-
const { readOpenclawConfig } = require("../openclaw-config");
|
|
21
|
-
|
|
22
|
-
const resolveGatewayWsUrl = ({ openclawDir, gatewayPort }) => {
|
|
23
|
-
const cfg = readOpenclawConfig({ openclawDir, fallback: {} });
|
|
24
|
-
const gatewayTlsEnabled = cfg?.gateway?.tls?.enabled === true;
|
|
25
|
-
const scheme = gatewayTlsEnabled ? "wss" : "ws";
|
|
26
|
-
return `${scheme}://127.0.0.1:${gatewayPort}`;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const sessions = new Map();
|
|
30
|
-
let activeTransport = null;
|
|
31
|
-
const kSseKeepAliveMs = 15_000;
|
|
32
|
-
const kMaxSessions = 8;
|
|
33
|
-
|
|
34
|
-
let nextBridgeId = 1;
|
|
35
|
-
const pendingRequests = new Map();
|
|
36
|
-
|
|
37
|
-
const adoptSession = (sessionId, transport) => {
|
|
38
|
-
sessions.set(sessionId, transport);
|
|
39
|
-
activeTransport = transport;
|
|
40
|
-
|
|
41
|
-
if (sessions.size > kMaxSessions) {
|
|
42
|
-
const oldestId = sessions.keys().next().value;
|
|
43
|
-
if (oldestId !== sessionId) {
|
|
44
|
-
const old = sessions.get(oldestId);
|
|
45
|
-
sessions.delete(oldestId);
|
|
46
|
-
old.close().catch(() => {});
|
|
47
|
-
console.log(`[mcp] Evicted oldest session: ${oldestId}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const forwardToBridge = (message, transport) => {
|
|
53
|
-
if (message.id != null) {
|
|
54
|
-
const bridgeId = nextBridgeId++;
|
|
55
|
-
pendingRequests.set(bridgeId, { originalId: message.id, transport });
|
|
56
|
-
writeToMcpBridge({ ...message, id: bridgeId });
|
|
57
|
-
} else {
|
|
58
|
-
writeToMcpBridge(message);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const cleanupTransport = (transport) => {
|
|
63
|
-
for (const [id, t] of sessions) {
|
|
64
|
-
if (t === transport) {
|
|
65
|
-
sessions.delete(id);
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
for (const [bridgeId, pending] of pendingRequests) {
|
|
70
|
-
if (pending.transport === transport) pendingRequests.delete(bridgeId);
|
|
71
|
-
}
|
|
72
|
-
if (activeTransport === transport) activeTransport = null;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const closeAllSessions = () => {
|
|
76
|
-
for (const [, t] of sessions) t.close().catch(() => {});
|
|
77
|
-
sessions.clear();
|
|
78
|
-
pendingRequests.clear();
|
|
79
|
-
activeTransport = null;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const registerMcpRoutes = ({
|
|
83
|
-
app,
|
|
84
|
-
requireAuth,
|
|
85
|
-
constants,
|
|
86
|
-
gatewayEnv,
|
|
87
|
-
openclawDir,
|
|
88
|
-
}) => {
|
|
89
|
-
setOnMcpMessage((message) => {
|
|
90
|
-
if (message.id != null) {
|
|
91
|
-
const pending = pendingRequests.get(message.id);
|
|
92
|
-
if (pending) {
|
|
93
|
-
pendingRequests.delete(message.id);
|
|
94
|
-
pending.transport
|
|
95
|
-
.send({ ...message, id: pending.originalId })
|
|
96
|
-
.catch((err) => {
|
|
97
|
-
console.error("[mcp] Failed to forward response:", err?.message);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (activeTransport) {
|
|
103
|
-
activeTransport.send(message).catch(() => {});
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ── Internal API (session auth) ────────────────────────────────
|
|
108
|
-
|
|
109
|
-
app.get("/api/mcp/info", requireAuth, (_req, res) => {
|
|
110
|
-
const port = getGatewayPort();
|
|
111
|
-
const gatewayWsUrl = resolveGatewayWsUrl({
|
|
112
|
-
openclawDir,
|
|
113
|
-
gatewayPort: port,
|
|
114
|
-
});
|
|
115
|
-
res.json({
|
|
116
|
-
ok: true,
|
|
117
|
-
...getMcpBridgeStatus(),
|
|
118
|
-
gatewayPort: port,
|
|
119
|
-
gatewayWsUrl,
|
|
120
|
-
tokenAvailable: !!constants.GATEWAY_TOKEN,
|
|
121
|
-
gatewayToken: constants.GATEWAY_TOKEN || "",
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
app.post("/api/mcp/start", requireAuth, (_req, res) => {
|
|
126
|
-
const port = getGatewayPort();
|
|
127
|
-
const result = startMcpBridge({
|
|
128
|
-
gatewayEnv,
|
|
129
|
-
gatewayWsUrl: resolveGatewayWsUrl({
|
|
130
|
-
openclawDir,
|
|
131
|
-
gatewayPort: port,
|
|
132
|
-
}),
|
|
133
|
-
gatewayToken: constants.GATEWAY_TOKEN,
|
|
134
|
-
});
|
|
135
|
-
res.json(result);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
app.post("/api/mcp/stop", requireAuth, async (_req, res) => {
|
|
139
|
-
closeAllSessions();
|
|
140
|
-
const result = stopMcpBridge();
|
|
141
|
-
res.json(result);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// ── MCP transport endpoint (token auth) ────────────────────────
|
|
145
|
-
|
|
146
|
-
const validateMcpToken = (req, res) => {
|
|
147
|
-
const bearerToken = String(req.get("authorization") || "")
|
|
148
|
-
.replace(/^Bearer\s+/i, "")
|
|
149
|
-
.trim();
|
|
150
|
-
const queryToken = String(req.query?.token || "");
|
|
151
|
-
const rawToken = bearerToken || queryToken;
|
|
152
|
-
const normalizedToken = rawToken.replace(/ /g, "+");
|
|
153
|
-
if (!constants.GATEWAY_TOKEN) {
|
|
154
|
-
res
|
|
155
|
-
.status(503)
|
|
156
|
-
.json({ error: "Gateway token is not configured for MCP transport" });
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
if (!normalizedToken || normalizedToken !== constants.GATEWAY_TOKEN) {
|
|
160
|
-
res.status(401).json({ error: "Invalid or missing token" });
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
return true;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// Primary MCP endpoint – Streamable HTTP (GET / POST / DELETE)
|
|
167
|
-
app.all("/mcp/sse", async (req, res) => {
|
|
168
|
-
if (!validateMcpToken(req, res)) return;
|
|
169
|
-
|
|
170
|
-
if (!isMcpBridgeRunning()) {
|
|
171
|
-
res.status(503).json({ error: "MCP bridge is not running" });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (req.method === "GET") {
|
|
176
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
177
|
-
const keepAliveId = setInterval(() => {
|
|
178
|
-
if (res.headersSent && !res.writableEnded) {
|
|
179
|
-
res.write(": keepalive\n\n");
|
|
180
|
-
}
|
|
181
|
-
}, kSseKeepAliveMs);
|
|
182
|
-
res.on("close", () => clearInterval(keepAliveId));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
186
|
-
|
|
187
|
-
// ── Existing session ───────────────────────────────────────
|
|
188
|
-
if (sessionId) {
|
|
189
|
-
const transport = sessions.get(sessionId);
|
|
190
|
-
if (transport) {
|
|
191
|
-
console.log(
|
|
192
|
-
`[mcp] ${req.method} sessionId=${sessionId} → routed (sessions=${sessions.size})`,
|
|
193
|
-
);
|
|
194
|
-
try {
|
|
195
|
-
await transport.handleRequest(req, res, req.body);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
console.error(
|
|
198
|
-
"[mcp] handleRequest error (existing session):",
|
|
199
|
-
err?.message,
|
|
200
|
-
);
|
|
201
|
-
if (!res.headersSent) {
|
|
202
|
-
res.status(500).json({ error: "Internal transport error" });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
console.log(
|
|
207
|
-
`[mcp] ${req.method} sessionId=${sessionId} → NOT FOUND (sessions=${sessions.size})`,
|
|
208
|
-
);
|
|
209
|
-
res.status(404).json({
|
|
210
|
-
jsonrpc: "2.0",
|
|
211
|
-
error: {
|
|
212
|
-
code: -32001,
|
|
213
|
-
message: "Session not found. The server may have been restarted.",
|
|
214
|
-
},
|
|
215
|
-
id: null,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── New session (POST without session ID) ────────────────
|
|
222
|
-
if (req.method === "POST") {
|
|
223
|
-
const transport = new StreamableHTTPServerTransport({
|
|
224
|
-
sessionIdGenerator: () => randomUUID(),
|
|
225
|
-
enableJsonResponse: true,
|
|
226
|
-
onsessioninitialized: (newSessionId) => {
|
|
227
|
-
adoptSession(newSessionId, transport);
|
|
228
|
-
console.log(
|
|
229
|
-
`[mcp] Session adopted: ${newSessionId} (sessions=${sessions.size})`,
|
|
230
|
-
);
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
transport.onmessage = (message) => {
|
|
235
|
-
forwardToBridge(message, transport);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
transport.onclose = () => {
|
|
239
|
-
cleanupTransport(transport);
|
|
240
|
-
console.log(`[mcp] Transport closed (sessions=${sessions.size})`);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
transport.onerror = (err) => {
|
|
244
|
-
console.error("[mcp] Transport error:", err?.message);
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
await transport.start();
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
await transport.handleRequest(req, res, req.body);
|
|
251
|
-
} catch (err) {
|
|
252
|
-
console.error(
|
|
253
|
-
"[mcp] handleRequest error (new session):",
|
|
254
|
-
err?.message,
|
|
255
|
-
);
|
|
256
|
-
if (!res.headersSent) {
|
|
257
|
-
res.status(500).json({ error: "Failed to initialize MCP session" });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
res.status(400).json({
|
|
264
|
-
jsonrpc: "2.0",
|
|
265
|
-
error: { code: -32600, message: "Bad Request" },
|
|
266
|
-
id: null,
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// Legacy endpoint for SSE-transport clients that POST to /mcp/message
|
|
271
|
-
app.post("/mcp/message", async (req, res) => {
|
|
272
|
-
if (!validateMcpToken(req, res)) return;
|
|
273
|
-
if (!isMcpBridgeRunning()) {
|
|
274
|
-
res.status(503).json({ error: "MCP bridge is not running" });
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (!activeTransport) {
|
|
278
|
-
res.status(503).json({ error: "No active MCP session" });
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
await activeTransport.handleRequest(req, res, req.body);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error("[mcp] handleRequest error (/mcp/message):", err?.message);
|
|
285
|
-
if (!res.headersSent) {
|
|
286
|
-
res.status(500).json({ error: "Internal transport error" });
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
module.exports = { registerMcpRoutes };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
diff --git a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
|
|
2
|
-
index ca48b932..c12478c4 100644
|
|
3
|
-
--- a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
|
|
4
|
-
+++ b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
|
|
5
|
-
@@ -25935,7 +25935,7 @@ function attachGatewayWsMessageHandler(params) {
|
|
6
|
-
close(1008, truncateCloseReason(authMessage));
|
|
7
|
-
};
|
|
8
|
-
const clearUnboundScopes = () => {
|
|
9
|
-
- if (scopes.length > 0) {
|
|
10
|
-
+ if (scopes.length > 0 && !sharedAuthOk) {
|
|
11
|
-
scopes = [];
|
|
12
|
-
connectParams.scopes = scopes;
|
|
13
|
-
}
|