openmausbot 0.1.80 → 0.1.82
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/README.md +6 -5
- package/dist/assets/{index-D6RZKWks.js → index-5HVwp5m2.js} +1 -1
- package/dist/assets/index-BLLsDK2F.js +309 -0
- package/dist/assets/index-CKysBq-V.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +41 -10
- package/dist-server/drivers/agents-proxy.js +50 -11
- package/dist-server/index.js +3525 -1296
- package/dist-server/openmausbot.js +412 -84
- package/dist-server/pair-cli.js +412 -84
- package/dist-server/server/box-delete-journal.js +471 -0
- package/dist-server/server/box.js +345 -75
- package/dist-server/server/cli-phone-setup.js +2 -2
- package/dist-server/server/cli.js +84 -12
- package/dist-server/server/config.js +48 -18
- package/dist-server/server/container-computer.js +7 -0
- package/dist-server/server/contracts.js +5 -0
- package/dist-server/server/default-model-selection.js +1 -0
- package/dist-server/server/drivers/acp/core.js +113 -19
- package/dist-server/server/drivers/acp/opencode-go.js +19 -0
- package/dist-server/server/drivers/agents-proxy.js +49 -11
- package/dist-server/server/drivers/chat-mcp-tools.js +6 -1
- package/dist-server/server/drivers/claude.js +33 -2
- package/dist-server/server/drivers/codex.js +93 -8
- package/dist-server/server/drivers/native.js +6 -1
- package/dist-server/server/harness/bus.js +4 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/index.js +869 -226
- package/dist-server/server/local-vm-lease.js +4 -0
- package/dist-server/server/mcp-http.js +337 -0
- package/dist-server/server/mcp-probe.js +67 -18
- package/dist-server/server/mcp-registry.js +153 -58
- package/dist-server/server/member-turn.js +13 -0
- package/dist-server/server/message-db.js +74 -8
- package/dist-server/server/notify.js +8 -3
- package/dist-server/server/recall-disclosure.js +4 -0
- package/dist-server/server/recent-work.js +142 -0
- package/dist-server/server/request-auth.js +3 -1
- package/dist-server/server/routines.js +36 -1
- package/dist-server/server/screen-frame-gate.js +4 -94
- package/dist-server/server/sessions.js +22 -3
- package/dist-server/server/store.js +16 -1
- package/dist-server/server/surface.js +41 -19
- package/dist-server/server/team-setup-requests.js +1 -1
- package/dist-server/server/thread-log-rotation.js +104 -0
- package/dist-server/server/tts/fish.js +155 -0
- package/dist-server/server/tts/index.js +25 -3
- package/dist-server/server/turn-resources.js +7 -2
- package/dist-server/server/vps-computer.js +26 -4
- package/dist-server/server/vps-ssh.js +88 -0
- package/dist-server/server/workspace-backup.js +11 -0
- package/dist-server/server/workspace.js +2 -1
- package/dist-server/shared/credential-request.js +10 -0
- package/dist-server/shared/tool-surface.js +62 -0
- package/dist-server/vps-container-mcp.js +41 -10
- package/package.json +1 -1
- package/dist/assets/index-Cu8BkIfo.js +0 -305
- package/dist/assets/index-tfpeJAIG.css +0 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
export class McpHttpError extends Error {
|
|
2
|
+
kind;
|
|
3
|
+
status;
|
|
4
|
+
constructor(kind, message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "McpHttpError";
|
|
7
|
+
this.kind = kind;
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Bytes of one response (or one SSE stream) a probe is willing to read. */
|
|
12
|
+
const MAX_BODY_BYTES = 1_048_576;
|
|
13
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function unwrap(message) {
|
|
18
|
+
if (message.error !== undefined) {
|
|
19
|
+
const detail = isRecord(message.error) && typeof message.error.message === "string" ? message.error.message : "request failed";
|
|
20
|
+
throw new McpHttpError("protocol", `MCP error: ${detail}`);
|
|
21
|
+
}
|
|
22
|
+
return message.result;
|
|
23
|
+
}
|
|
24
|
+
function parseMessage(text) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(text);
|
|
27
|
+
if (Array.isArray(parsed))
|
|
28
|
+
return parsed.filter(isRecord);
|
|
29
|
+
return isRecord(parsed) ? parsed : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseSseBlock(block) {
|
|
36
|
+
let event = "message";
|
|
37
|
+
const data = [];
|
|
38
|
+
for (const line of block.split("\n")) {
|
|
39
|
+
if (!line || line.startsWith(":"))
|
|
40
|
+
continue;
|
|
41
|
+
const colon = line.indexOf(":");
|
|
42
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
43
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
44
|
+
if (value.startsWith(" "))
|
|
45
|
+
value = value.slice(1);
|
|
46
|
+
if (field === "event")
|
|
47
|
+
event = value;
|
|
48
|
+
else if (field === "data")
|
|
49
|
+
data.push(value);
|
|
50
|
+
}
|
|
51
|
+
return data.length ? { event, data: data.join("\n") } : null;
|
|
52
|
+
}
|
|
53
|
+
/** Read an event stream until the handler says stop, the server ends it,
|
|
54
|
+
* or the signal fires. Bounded, so a chatty server cannot fill memory. */
|
|
55
|
+
async function readSse(response, signal, onEvent) {
|
|
56
|
+
const reader = response.body?.getReader();
|
|
57
|
+
if (!reader)
|
|
58
|
+
throw new McpHttpError("protocol", "empty event stream");
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let buffer = "";
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
const onAbort = () => void reader.cancel().catch(() => { });
|
|
63
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
try {
|
|
65
|
+
for (;;) {
|
|
66
|
+
const { value, done } = await reader.read();
|
|
67
|
+
if (done)
|
|
68
|
+
return;
|
|
69
|
+
bytes += value.byteLength;
|
|
70
|
+
if (bytes > MAX_BODY_BYTES)
|
|
71
|
+
throw new McpHttpError("protocol", "event stream too large");
|
|
72
|
+
buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
73
|
+
let separator;
|
|
74
|
+
while ((separator = buffer.indexOf("\n\n")) !== -1) {
|
|
75
|
+
const block = buffer.slice(0, separator);
|
|
76
|
+
buffer = buffer.slice(separator + 2);
|
|
77
|
+
const event = parseSseBlock(block);
|
|
78
|
+
if (event && onEvent(event) === "stop") {
|
|
79
|
+
await reader.cancel().catch(() => { });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
signal.removeEventListener("abort", onAbort);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function readBounded(response) {
|
|
90
|
+
const reader = response.body?.getReader();
|
|
91
|
+
if (!reader)
|
|
92
|
+
return "";
|
|
93
|
+
const decoder = new TextDecoder();
|
|
94
|
+
let text = "";
|
|
95
|
+
let bytes = 0;
|
|
96
|
+
for (;;) {
|
|
97
|
+
const { value, done } = await reader.read();
|
|
98
|
+
if (done)
|
|
99
|
+
return text + decoder.decode();
|
|
100
|
+
bytes += value.byteLength;
|
|
101
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
102
|
+
await reader.cancel().catch(() => { });
|
|
103
|
+
throw new McpHttpError("protocol", "response too large");
|
|
104
|
+
}
|
|
105
|
+
text += decoder.decode(value, { stream: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function drain(response) {
|
|
109
|
+
void response.body?.cancel().catch(() => { });
|
|
110
|
+
}
|
|
111
|
+
export class RemoteMcpClient {
|
|
112
|
+
target;
|
|
113
|
+
fetchImpl;
|
|
114
|
+
sessionId = null;
|
|
115
|
+
protocolVersion = null;
|
|
116
|
+
nextId = 1;
|
|
117
|
+
closed = false;
|
|
118
|
+
/** Legacy SSE: one long GET stream carries every response. */
|
|
119
|
+
pending = new Map();
|
|
120
|
+
sseEndpoint = null;
|
|
121
|
+
sseAbort = new AbortController();
|
|
122
|
+
constructor(target, options = {}) {
|
|
123
|
+
this.target = target;
|
|
124
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
125
|
+
}
|
|
126
|
+
/** The MCP handshake: initialize, then the initialized notification. */
|
|
127
|
+
async initialize(clientName, signal) {
|
|
128
|
+
const result = await this.request("initialize", {
|
|
129
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
130
|
+
capabilities: {},
|
|
131
|
+
clientInfo: { name: clientName, version: "1" },
|
|
132
|
+
}, signal);
|
|
133
|
+
if (isRecord(result) && typeof result.protocolVersion === "string")
|
|
134
|
+
this.protocolVersion = result.protocolVersion;
|
|
135
|
+
await this.notify("notifications/initialized", undefined, signal);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
request(method, params, signal) {
|
|
139
|
+
if (this.closed)
|
|
140
|
+
return Promise.reject(new McpHttpError("protocol", "client closed"));
|
|
141
|
+
const id = this.nextId++;
|
|
142
|
+
const frame = { jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) };
|
|
143
|
+
return this.target.type === "sse" ? this.sseRequest(id, frame, signal) : this.streamableRequest(id, frame, signal);
|
|
144
|
+
}
|
|
145
|
+
async notify(method, params, signal) {
|
|
146
|
+
if (this.closed)
|
|
147
|
+
throw new McpHttpError("protocol", "client closed");
|
|
148
|
+
const frame = { jsonrpc: "2.0", method, ...(params === undefined ? {} : { params }) };
|
|
149
|
+
if (this.target.type === "sse") {
|
|
150
|
+
await this.ssePost(frame, signal);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const response = await this.post(this.target.url, frame, signal);
|
|
154
|
+
drain(response);
|
|
155
|
+
}
|
|
156
|
+
async close() {
|
|
157
|
+
if (this.closed)
|
|
158
|
+
return;
|
|
159
|
+
this.closed = true;
|
|
160
|
+
this.sseAbort.abort();
|
|
161
|
+
for (const entry of this.pending.values())
|
|
162
|
+
entry.reject(new McpHttpError("network", "client closed"));
|
|
163
|
+
this.pending.clear();
|
|
164
|
+
// Streamable HTTP: tell the server its session is over. Best effort —
|
|
165
|
+
// many servers answer 405 here, and a probe has nothing left to lose.
|
|
166
|
+
if (this.sessionId && this.target.type !== "sse") {
|
|
167
|
+
try {
|
|
168
|
+
const headers = this.headers();
|
|
169
|
+
headers.set("mcp-session-id", this.sessionId);
|
|
170
|
+
const response = await this.fetchImpl(this.target.url, { method: "DELETE", headers, signal: AbortSignal.timeout(2_000) });
|
|
171
|
+
drain(response);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// the session lapses on its own
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
headers() {
|
|
179
|
+
const headers = new Headers();
|
|
180
|
+
for (const [name, value] of Object.entries(this.target.headers))
|
|
181
|
+
headers.set(name, value);
|
|
182
|
+
headers.set("accept", "application/json, text/event-stream");
|
|
183
|
+
if (this.sessionId)
|
|
184
|
+
headers.set("mcp-session-id", this.sessionId);
|
|
185
|
+
if (this.protocolVersion)
|
|
186
|
+
headers.set("mcp-protocol-version", this.protocolVersion);
|
|
187
|
+
return headers;
|
|
188
|
+
}
|
|
189
|
+
async post(url, frame, signal) {
|
|
190
|
+
const headers = this.headers();
|
|
191
|
+
headers.set("content-type", "application/json");
|
|
192
|
+
let response;
|
|
193
|
+
try {
|
|
194
|
+
response = await this.fetchImpl(url, { method: "POST", headers, body: JSON.stringify(frame), signal });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (signal.aborted)
|
|
198
|
+
throw error;
|
|
199
|
+
throw new McpHttpError("network", "request failed");
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
drain(response);
|
|
203
|
+
throw new McpHttpError("status", `HTTP ${response.status}`, response.status);
|
|
204
|
+
}
|
|
205
|
+
const session = response.headers.get("mcp-session-id");
|
|
206
|
+
if (session)
|
|
207
|
+
this.sessionId = session;
|
|
208
|
+
return response;
|
|
209
|
+
}
|
|
210
|
+
// ── streamable HTTP: each request is one POST; the answer is JSON or a
|
|
211
|
+
// short event stream that carries the response among server messages ──
|
|
212
|
+
async streamableRequest(id, frame, signal) {
|
|
213
|
+
const response = await this.post(this.target.url, frame, signal);
|
|
214
|
+
const type = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
215
|
+
if (type.startsWith("application/json")) {
|
|
216
|
+
const parsed = parseMessage(await readBounded(response));
|
|
217
|
+
const message = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []).find((entry) => entry.id === id);
|
|
218
|
+
if (!message)
|
|
219
|
+
throw new McpHttpError("protocol", "response did not answer the request");
|
|
220
|
+
return unwrap(message);
|
|
221
|
+
}
|
|
222
|
+
if (type.startsWith("text/event-stream")) {
|
|
223
|
+
let found;
|
|
224
|
+
await readSse(response, signal, (event) => {
|
|
225
|
+
if (event.event !== "message")
|
|
226
|
+
return undefined;
|
|
227
|
+
const parsed = parseMessage(event.data);
|
|
228
|
+
const message = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []).find((entry) => entry.id === id);
|
|
229
|
+
if (!message)
|
|
230
|
+
return undefined;
|
|
231
|
+
found = message;
|
|
232
|
+
return "stop";
|
|
233
|
+
});
|
|
234
|
+
if (signal.aborted)
|
|
235
|
+
throw new McpHttpError("network", "aborted");
|
|
236
|
+
if (!found)
|
|
237
|
+
throw new McpHttpError("protocol", "event stream ended without the response");
|
|
238
|
+
return unwrap(found);
|
|
239
|
+
}
|
|
240
|
+
drain(response);
|
|
241
|
+
throw new McpHttpError("protocol", "unexpected content type");
|
|
242
|
+
}
|
|
243
|
+
// ── legacy SSE: a GET opens the stream, its first event names where to
|
|
244
|
+
// POST, and every response comes back over the stream ──
|
|
245
|
+
openSseStream(signal) {
|
|
246
|
+
if (this.sseEndpoint)
|
|
247
|
+
return this.sseEndpoint;
|
|
248
|
+
this.sseEndpoint = new Promise((resolveEndpoint, rejectEndpoint) => {
|
|
249
|
+
let settled = false;
|
|
250
|
+
const fail = (error) => {
|
|
251
|
+
if (!settled) {
|
|
252
|
+
settled = true;
|
|
253
|
+
rejectEndpoint(error);
|
|
254
|
+
}
|
|
255
|
+
for (const entry of this.pending.values())
|
|
256
|
+
entry.reject(error);
|
|
257
|
+
this.pending.clear();
|
|
258
|
+
};
|
|
259
|
+
const headers = this.headers();
|
|
260
|
+
headers.set("accept", "text/event-stream");
|
|
261
|
+
// the caller's signal covers the opening; once open, the stream lives
|
|
262
|
+
// until close(), because later requests are answered on it
|
|
263
|
+
const opening = AbortSignal.any([signal, this.sseAbort.signal]);
|
|
264
|
+
this.fetchImpl(this.target.url, { method: "GET", headers, signal: opening })
|
|
265
|
+
.then(async (response) => {
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
drain(response);
|
|
268
|
+
throw new McpHttpError("status", `HTTP ${response.status}`, response.status);
|
|
269
|
+
}
|
|
270
|
+
await readSse(response, this.sseAbort.signal, (event) => {
|
|
271
|
+
if (event.event === "endpoint") {
|
|
272
|
+
if (!settled) {
|
|
273
|
+
settled = true;
|
|
274
|
+
try {
|
|
275
|
+
resolveEndpoint(new URL(event.data, this.target.url).toString());
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
rejectEndpoint(new McpHttpError("protocol", "invalid endpoint event"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
if (event.event !== "message")
|
|
284
|
+
return undefined;
|
|
285
|
+
const parsed = parseMessage(event.data);
|
|
286
|
+
for (const message of Array.isArray(parsed) ? parsed : parsed ? [parsed] : []) {
|
|
287
|
+
const entry = typeof message.id === "number" ? this.pending.get(message.id) : undefined;
|
|
288
|
+
if (!entry)
|
|
289
|
+
continue;
|
|
290
|
+
this.pending.delete(message.id);
|
|
291
|
+
try {
|
|
292
|
+
entry.resolve(unwrap(message));
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
entry.reject(error instanceof Error ? error : new McpHttpError("protocol", String(error)));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return undefined;
|
|
299
|
+
});
|
|
300
|
+
fail(new McpHttpError("network", "event stream ended"));
|
|
301
|
+
})
|
|
302
|
+
.catch((error) => {
|
|
303
|
+
fail(error instanceof McpHttpError ? error : new McpHttpError("network", "event stream failed"));
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
return this.sseEndpoint;
|
|
307
|
+
}
|
|
308
|
+
async ssePost(frame, signal) {
|
|
309
|
+
const endpoint = await this.openSseStream(signal);
|
|
310
|
+
const response = await this.post(endpoint, frame, signal);
|
|
311
|
+
drain(response);
|
|
312
|
+
}
|
|
313
|
+
sseRequest(id, frame, signal) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const onAbort = () => {
|
|
316
|
+
this.pending.delete(id);
|
|
317
|
+
reject(new McpHttpError("network", "aborted"));
|
|
318
|
+
};
|
|
319
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
320
|
+
this.pending.set(id, {
|
|
321
|
+
resolve: (value) => {
|
|
322
|
+
signal.removeEventListener("abort", onAbort);
|
|
323
|
+
resolve(value);
|
|
324
|
+
},
|
|
325
|
+
reject: (error) => {
|
|
326
|
+
signal.removeEventListener("abort", onAbort);
|
|
327
|
+
reject(error);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
this.ssePost(frame, signal).catch((error) => {
|
|
331
|
+
this.pending.delete(id);
|
|
332
|
+
signal.removeEventListener("abort", onAbort);
|
|
333
|
+
reject(error instanceof Error ? error : new McpHttpError("network", String(error)));
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { augmentedPath } from "./env-path.js";
|
|
2
2
|
import { PROVIDER_CREDENTIAL_ENV, stripWorkspaceCredentialEnv, } from "./config.js";
|
|
3
3
|
import { createLineSplitter } from "./mcp-bridge.js";
|
|
4
|
+
import { McpHttpError, RemoteMcpClient } from "./mcp-http.js";
|
|
5
|
+
import { isRemoteMcpServer } from "./mcp-registry.js";
|
|
4
6
|
import { killCliTree, spawnCli } from "./procs.js";
|
|
5
7
|
const MAX_STDOUT_BYTES = 1_048_576;
|
|
6
8
|
const MAX_TOOLS = 100;
|
|
@@ -24,18 +26,79 @@ function publicProbeError(kind) {
|
|
|
24
26
|
return "Connection test was cancelled.";
|
|
25
27
|
return "The command did not return a valid MCP tools list.";
|
|
26
28
|
}
|
|
27
|
-
function redactConfiguredValues(value,
|
|
29
|
+
function redactConfiguredValues(value, secrets) {
|
|
28
30
|
let redacted = value;
|
|
29
|
-
for (const secret of Object.values(
|
|
31
|
+
for (const secret of Object.values(secrets)) {
|
|
30
32
|
if (secret)
|
|
31
33
|
redacted = redacted.split(secret).join("[redacted]");
|
|
32
34
|
}
|
|
33
35
|
return redacted;
|
|
34
36
|
}
|
|
37
|
+
/** The bounded, redacted tool list the renderer may see. `secrets` are the
|
|
38
|
+
* configured values (env or header values) a careless server might echo. */
|
|
39
|
+
function publicTools(raw, secrets) {
|
|
40
|
+
const tools = [];
|
|
41
|
+
for (const entry of raw.slice(0, MAX_TOOLS)) {
|
|
42
|
+
if (!entry || typeof entry !== "object")
|
|
43
|
+
continue;
|
|
44
|
+
const candidate = entry;
|
|
45
|
+
if (typeof candidate.name !== "string" || !candidate.name.trim())
|
|
46
|
+
continue;
|
|
47
|
+
tools.push({
|
|
48
|
+
name: redactConfiguredValues(candidate.name, secrets).slice(0, 200),
|
|
49
|
+
...(typeof candidate.description === "string"
|
|
50
|
+
? { description: redactConfiguredValues(candidate.description, secrets).slice(0, 500) }
|
|
51
|
+
: {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return tools;
|
|
55
|
+
}
|
|
56
|
+
/** Prove the MCP handshake and list the tools of one configured server,
|
|
57
|
+
* whichever way it is reached. Neither path returns anything the renderer
|
|
58
|
+
* must not see: child stderr, environment values, header values. */
|
|
59
|
+
export function probeMcpServer(server, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
|
|
60
|
+
return isRemoteMcpServer(server)
|
|
61
|
+
? probeRemoteMcpServer(server, timeoutMs, signal)
|
|
62
|
+
: probeStdioMcpServer(server, timeoutMs, signal);
|
|
63
|
+
}
|
|
64
|
+
/** Connect to a remote server over its transport, bounded by the same
|
|
65
|
+
* timeout as a command. HTTP status codes are safe to show and are the one
|
|
66
|
+
* detail that tells a wrong token from a wrong address. */
|
|
67
|
+
async function probeRemoteMcpServer(server, timeoutMs, signal) {
|
|
68
|
+
if (signal?.aborted)
|
|
69
|
+
return { ok: false, error: publicProbeError("cancelled") };
|
|
70
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
71
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
72
|
+
const client = new RemoteMcpClient(server);
|
|
73
|
+
try {
|
|
74
|
+
await client.initialize("OpenMausBot", combined);
|
|
75
|
+
const result = await client.request("tools/list", {}, combined);
|
|
76
|
+
const tools = result && typeof result === "object" ? result.tools : undefined;
|
|
77
|
+
if (!Array.isArray(tools))
|
|
78
|
+
return { ok: false, error: "The server did not return a valid MCP tools list." };
|
|
79
|
+
return { ok: true, tools: publicTools(tools, server.headers) };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (signal?.aborted)
|
|
83
|
+
return { ok: false, error: publicProbeError("cancelled") };
|
|
84
|
+
if (timeout.aborted)
|
|
85
|
+
return { ok: false, error: publicProbeError("timeout") };
|
|
86
|
+
if (error instanceof McpHttpError && error.kind === "status") {
|
|
87
|
+
return { ok: false, error: `The server answered HTTP ${error.status}. Check the address and headers.` };
|
|
88
|
+
}
|
|
89
|
+
if (error instanceof McpHttpError && error.kind === "protocol") {
|
|
90
|
+
return { ok: false, error: "The server did not return a valid MCP tools list." };
|
|
91
|
+
}
|
|
92
|
+
return { ok: false, error: "Could not reach this address. Check the URL and your network." };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await client.close().catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
35
98
|
/** Start one stdio server long enough to prove the MCP handshake and list its
|
|
36
99
|
* tools. It is always reaped, never inherits OpenMaus credentials, and never
|
|
37
100
|
* returns child stderr or environment values to the renderer. */
|
|
38
|
-
|
|
101
|
+
function probeStdioMcpServer(server, timeoutMs, signal) {
|
|
39
102
|
return new Promise((resolve) => {
|
|
40
103
|
if (signal?.aborted) {
|
|
41
104
|
resolve({ ok: false, error: publicProbeError("cancelled") });
|
|
@@ -102,21 +165,7 @@ export function probeMcpServer(server, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
|
|
|
102
165
|
finish({ ok: false, error: publicProbeError("protocol") });
|
|
103
166
|
return;
|
|
104
167
|
}
|
|
105
|
-
|
|
106
|
-
for (const raw of result.tools.slice(0, MAX_TOOLS)) {
|
|
107
|
-
if (!raw || typeof raw !== "object")
|
|
108
|
-
continue;
|
|
109
|
-
const candidate = raw;
|
|
110
|
-
if (typeof candidate.name !== "string" || !candidate.name.trim())
|
|
111
|
-
continue;
|
|
112
|
-
tools.push({
|
|
113
|
-
name: redactConfiguredValues(candidate.name, server.env).slice(0, 200),
|
|
114
|
-
...(typeof candidate.description === "string"
|
|
115
|
-
? { description: redactConfiguredValues(candidate.description, server.env).slice(0, 500) }
|
|
116
|
-
: {}),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
finish({ ok: true, tools });
|
|
168
|
+
finish({ ok: true, tools: publicTools(result.tools, server.env) });
|
|
120
169
|
});
|
|
121
170
|
timer = setTimeout(() => {
|
|
122
171
|
finish({ ok: false, error: publicProbeError("timeout") });
|