@cat-factory/server 0.217.0 → 0.218.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.
Files changed (46) hide show
  1. package/dist/agents/capabilityCredentialResolver.d.ts +23 -0
  2. package/dist/agents/capabilityCredentialResolver.d.ts.map +1 -1
  3. package/dist/agents/capabilityCredentialResolver.js +18 -0
  4. package/dist/agents/capabilityCredentialResolver.js.map +1 -1
  5. package/dist/app.d.ts.map +1 -1
  6. package/dist/app.js +2 -0
  7. package/dist/app.js.map +1 -1
  8. package/dist/http/env.d.ts +16 -1
  9. package/dist/http/env.d.ts.map +1 -1
  10. package/dist/http/workspaceAccess.d.ts +42 -0
  11. package/dist/http/workspaceAccess.d.ts.map +1 -1
  12. package/dist/http/workspaceAccess.js +48 -0
  13. package/dist/http/workspaceAccess.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.d.ts +5 -0
  19. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.d.ts.map +1 -1
  20. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.js +14 -2
  21. package/dist/modules/capabilityCredentials/CapabilityCredentialsController.js.map +1 -1
  22. package/dist/modules/capabilityCredentials/declaredCredentials.d.ts.map +1 -1
  23. package/dist/modules/capabilityCredentials/declaredCredentials.js +6 -0
  24. package/dist/modules/capabilityCredentials/declaredCredentials.js.map +1 -1
  25. package/dist/modules/publicApi/PublicApiController.js +13 -8
  26. package/dist/modules/publicApi/PublicApiController.js.map +1 -1
  27. package/dist/modules/toolServers/ToolServerController.d.ts +22 -0
  28. package/dist/modules/toolServers/ToolServerController.d.ts.map +1 -0
  29. package/dist/modules/toolServers/ToolServerController.js +57 -0
  30. package/dist/modules/toolServers/ToolServerController.js.map +1 -0
  31. package/dist/modules/toolServers/declaredToolServers.d.ts +45 -0
  32. package/dist/modules/toolServers/declaredToolServers.d.ts.map +1 -0
  33. package/dist/modules/toolServers/declaredToolServers.js +108 -0
  34. package/dist/modules/toolServers/declaredToolServers.js.map +1 -0
  35. package/dist/modules/toolServers/mcpProbe.d.ts +50 -0
  36. package/dist/modules/toolServers/mcpProbe.d.ts.map +1 -0
  37. package/dist/modules/toolServers/mcpProbe.js +611 -0
  38. package/dist/modules/toolServers/mcpProbe.js.map +1 -0
  39. package/dist/modules/toolServers/probeToolServer.d.ts +30 -0
  40. package/dist/modules/toolServers/probeToolServer.d.ts.map +1 -0
  41. package/dist/modules/toolServers/probeToolServer.js +170 -0
  42. package/dist/modules/toolServers/probeToolServer.js.map +1 -0
  43. package/dist/persistence/rpc-allowlist.d.ts.map +1 -1
  44. package/dist/persistence/rpc-allowlist.js +5 -0
  45. package/dist/persistence/rpc-allowlist.js.map +1 -1
  46. package/package.json +8 -8
@@ -0,0 +1,611 @@
1
+ import { isAllowedMcpHttpUrl, redactSecrets } from '@cat-factory/kernel';
2
+ import { MCP_PROBE_MAX_PAGES, MCP_PROBE_TIMEOUT_MS } from '@cat-factory/contracts';
3
+ // ---------------------------------------------------------------------------
4
+ // A minimal Streamable-HTTP MCP CLIENT, for one purpose: ask a declared `http` tool server whether
5
+ // it is there. `initialize`, `notifications/initialized`, then `tools/list` until the pages run out.
6
+ //
7
+ // Hand-rolled over `fetch` rather than built on `@modelcontextprotocol/sdk`'s client, and the reason
8
+ // is the one slice 3 already recorded about the SERVING side: the backend's HTTP layer is typed
9
+ // against the Web platform alone so it cannot break on workerd, and the SDK's client transport
10
+ // carries an OAuth provider, a reconnecting SSE stream, resumption tokens and a zod-validated
11
+ // message layer — every one of which this needs to NOT do. Three POSTs and a JSON reader is smaller
12
+ // than the adapter that would hold that machinery back, and it keeps the SDK out of a module every
13
+ // facade bundles.
14
+ //
15
+ // What it deliberately keeps from the real client, because a probe that behaves differently from the
16
+ // agent's own CLI answers about the wrong thing:
17
+ //
18
+ // - `accept: application/json, text/event-stream` and a reader for BOTH. A stateless server
19
+ // answers with JSON; a stream-first one answers the same frame as one SSE event. A client that
20
+ // accepted only JSON would report a working server as a protocol error.
21
+ // - the `mcp-session-id` a server may mint on `initialize`, echoed on every later request, and the
22
+ // negotiated `mcp-protocol-version` beside it. Without them a session-keeping server answers
23
+ // the handshake and then 400s the `tools/list` that follows.
24
+ // - redirects, followed by hand up to {@link MAX_REDIRECTS} hops with every hop re-validated
25
+ // against `isAllowedMcpHttpUrl` (the same rule the declared url is held to, re-checked per hop
26
+ // exactly as the local-runner fetch does) — and, when the request carries a credential, held to
27
+ // the SAME ORIGIN as well. That is not extra caution beyond what a real client does, it is what
28
+ // a real client does: the Web platform REMOVES `Authorization` when a redirect crosses origins
29
+ // (fetch's CORS non-wildcard request-header rule), so an agent's own MCP client reaches such a
30
+ // hop unauthenticated. Forwarding a resolved credential there would make the probe the one path
31
+ // that hands a workspace's token to whatever a hijacked or expired vendor host redirects to,
32
+ // and it would answer about a request no run will ever make. Refused BY NAME instead, because
33
+ // "your declared url redirects off its origin" is the fix, where a silently credential-less
34
+ // 401 is a wrong cause.
35
+ // - a POST on every hop, deliberately NOT the spec's 301/302/303 method rewrite. A GET to an MCP
36
+ // endpoint means "open the SSE stream", so degrading to one asks a different question.
37
+ //
38
+ // It never throws. Every failure is a discriminated outcome, because the whole point is to hand the
39
+ // operator a CAUSE, and "the probe blew up" is not one.
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * The protocol version the probe asks for.
43
+ *
44
+ * A literal rather than a read of the SDK's `LATEST_PROTOCOL_VERSION`, and drift is benign by the
45
+ * protocol's own design: negotiation belongs to the SERVER, which answers with the requested version
46
+ * when it speaks it and with one of its own when it does not. So a probe advertising something older
47
+ * than the spec's latest still completes a handshake with a newer server, and the version this
48
+ * reports is always the one the server chose rather than the one we asked for.
49
+ */
50
+ const PROBE_PROTOCOL_VERSION = '2025-11-25';
51
+ /** Redirect hops one probe follows. Bounded because a redirect loop is the server's to author. */
52
+ const MAX_REDIRECTS = 3;
53
+ /**
54
+ * Bytes read from one response before the reader gives up.
55
+ *
56
+ * An SSE body has no length and a server decides when it ends, so a reader with no ceiling hands a
57
+ * remote server the ability to hold this request open until the timeout with a stream of padding.
58
+ * Sized well past any real `tools/list` page (the biggest tool tables in the wild are tens of KB).
59
+ */
60
+ const MAX_RESPONSE_BYTES = 4_000_000;
61
+ /**
62
+ * Speak MCP to `target` and report what happened.
63
+ *
64
+ * The caller owns the DECISION to probe at all (a stdio server has no url; a loopback one is not
65
+ * ours to reach) and the credential resolution that fills `headers`. This owns only the protocol.
66
+ */
67
+ export async function probeMcpHttpServer(target, deps = {}) {
68
+ // ONE deadline for the whole exchange rather than a timeout per request. Three round trips each
69
+ // allowed the full budget is three times the wait an operator was told to expect, and the thing
70
+ // being bounded is how long this endpoint holds a request open.
71
+ const timeoutMs = deps.timeoutMs ?? MCP_PROBE_TIMEOUT_MS;
72
+ const ctx = {
73
+ doFetch: deps.fetch ?? fetch,
74
+ target,
75
+ session: new SessionHeaders(),
76
+ signal: AbortSignal.timeout(timeoutMs),
77
+ timeoutMs,
78
+ };
79
+ const outcome = await handshakeAndList(ctx, deps.maxPages ?? MCP_PROBE_MAX_PAGES);
80
+ // A session the server minted is ENDED before answering, whatever the outcome was. The spec's own
81
+ // termination path, and without it every press of the Test button leaves a session on the server
82
+ // to expire on its own clock. Best effort by construction: it cannot change the verdict, so a
83
+ // server that refuses or ignores the DELETE costs nothing.
84
+ await endSession(ctx);
85
+ return outcome;
86
+ }
87
+ /** The exchange itself: handshake, the required notification, then every page of `tools/list`. */
88
+ async function handshakeAndList(ctx, maxPages) {
89
+ const handshake = await exchange(ctx, {
90
+ id: 1,
91
+ method: 'initialize',
92
+ params: initializeParams(),
93
+ });
94
+ if (!handshake.ok)
95
+ return handshake.failure;
96
+ const negotiated = readHandshake(handshake.result);
97
+ if (!negotiated) {
98
+ return {
99
+ status: 'protocol_error',
100
+ error: 'the endpoint answered the handshake without a protocolVersion or serverInfo, so it is ' +
101
+ 'answering JSON-RPC but is not an MCP server',
102
+ };
103
+ }
104
+ ctx.session.protocolVersion = negotiated.protocolVersion;
105
+ // The spec REQUIRES this notification before any other request, and a strict server rejects
106
+ // `tools/list` without it. It carries no id and expects no body (a `202` is the normal answer), so
107
+ // its outcome is only interesting when the transport itself failed.
108
+ const notified = await notify(ctx, 'notifications/initialized');
109
+ if (notified)
110
+ return notified;
111
+ const listed = await listTools(ctx, maxPages);
112
+ if (!listed.ok)
113
+ return listed.failure;
114
+ return { status: 'ok', ...negotiated, tools: listed.tools, toolsComplete: listed.complete };
115
+ }
116
+ /**
117
+ * What the probe says it is.
118
+ *
119
+ * `capabilities: {}` is honest and load-bearing: this client implements no roots, no sampling and no
120
+ * elicitation, and a server that saw a capability declared here could legitimately try to use it
121
+ * mid-handshake. `tools/list` needs none of them.
122
+ */
123
+ function initializeParams() {
124
+ return {
125
+ protocolVersion: PROBE_PROTOCOL_VERSION,
126
+ capabilities: {},
127
+ clientInfo: { name: 'cat-factory-tool-server-probe', version: '1' },
128
+ };
129
+ }
130
+ /** The negotiated handshake facts, or undefined when the answer was not an MCP one. */
131
+ function readHandshake(result) {
132
+ const protocolVersion = typeof result.protocolVersion === 'string' ? result.protocolVersion : '';
133
+ const info = isRecord(result.serverInfo) ? result.serverInfo : undefined;
134
+ if (!protocolVersion || !info)
135
+ return undefined;
136
+ return {
137
+ protocolVersion,
138
+ serverName: typeof info.name === 'string' ? info.name : '',
139
+ // A server that omits its version is odd, not broken: the field is what the surface renders
140
+ // beside the name, so an empty string there beats refusing an otherwise complete handshake.
141
+ serverVersion: typeof info.version === 'string' ? info.version : '',
142
+ };
143
+ }
144
+ /**
145
+ * The per-session headers a Streamable-HTTP client must echo once it has them.
146
+ *
147
+ * A class rather than two locals because both are minted by the FIRST response and must reach every
148
+ * later request; threading them by hand is exactly how a session-keeping server ends up 400ing the
149
+ * request after the handshake with nothing naming why.
150
+ */
151
+ class SessionHeaders {
152
+ sessionId;
153
+ protocolVersion;
154
+ /**
155
+ * The url the session was minted at, which a redirect may have moved off the declared one. The
156
+ * DELETE that ends the session has to reach the endpoint that owns it, not the url we asked for.
157
+ */
158
+ endpoint;
159
+ apply(headers) {
160
+ if (this.sessionId)
161
+ headers['mcp-session-id'] = this.sessionId;
162
+ if (this.protocolVersion)
163
+ headers['mcp-protocol-version'] = this.protocolVersion;
164
+ }
165
+ adopt(response, url) {
166
+ const minted = response.headers.get('mcp-session-id');
167
+ if (!minted)
168
+ return;
169
+ this.sessionId = minted;
170
+ this.endpoint = url;
171
+ }
172
+ }
173
+ /** Read every page of `tools/list`, up to `maxPages`. */
174
+ async function listTools(exchangeCtx, maxPages) {
175
+ const tools = [];
176
+ let cursor;
177
+ for (let page = 0; page < maxPages; page++) {
178
+ const answer = await exchange(exchangeCtx, {
179
+ id: page + 2,
180
+ method: 'tools/list',
181
+ ...(cursor ? { params: { cursor } } : {}),
182
+ });
183
+ if (!answer.ok)
184
+ return { ok: false, failure: answer.failure };
185
+ const listed = answer.result.tools;
186
+ if (!Array.isArray(listed)) {
187
+ return {
188
+ ok: false,
189
+ failure: {
190
+ status: 'protocol_error',
191
+ error: 'the server answered tools/list without a tools array',
192
+ },
193
+ };
194
+ }
195
+ for (const tool of listed) {
196
+ if (isRecord(tool) && typeof tool.name === 'string' && tool.name)
197
+ tools.push(tool.name);
198
+ }
199
+ const next = answer.result.nextCursor;
200
+ cursor = typeof next === 'string' && next ? next : undefined;
201
+ if (!cursor)
202
+ return { ok: true, tools, complete: true };
203
+ }
204
+ // A cursor survived the page bound: the list is a PREFIX, and saying so is what stops the caller
205
+ // reporting a narrowed `allowedTools` entry as missing on the strength of a partial read.
206
+ return { ok: true, tools, complete: false };
207
+ }
208
+ /** One JSON-RPC REQUEST and its response. */
209
+ async function exchange(ctx, message) {
210
+ const sent = await post(ctx, { jsonrpc: '2.0', ...message });
211
+ if (!sent.ok)
212
+ return { ok: false, failure: sent.failure };
213
+ const body = await readFrame(ctx, sent.response);
214
+ if (!body.ok)
215
+ return { ok: false, failure: body.failure };
216
+ const frame = body.frame;
217
+ if (isRecord(frame.error)) {
218
+ const detail = typeof frame.error.message === 'string' ? frame.error.message : 'no message';
219
+ return {
220
+ ok: false,
221
+ failure: {
222
+ status: 'protocol_error',
223
+ error: `${message.method} was refused by the server: ${redact(detail)}`,
224
+ },
225
+ };
226
+ }
227
+ if (!isRecord(frame.result)) {
228
+ return {
229
+ ok: false,
230
+ failure: {
231
+ status: 'protocol_error',
232
+ error: `${message.method} answered without a JSON-RPC result object`,
233
+ },
234
+ };
235
+ }
236
+ return { ok: true, result: frame.result };
237
+ }
238
+ /**
239
+ * One JSON-RPC NOTIFICATION. Returns a failure only when the transport itself did — a notification
240
+ * has no response to inspect, and a server that answers it with a body is within its rights.
241
+ */
242
+ async function notify(ctx, method) {
243
+ const sent = await post(ctx, { jsonrpc: '2.0', method });
244
+ if (!sent.ok)
245
+ return sent.failure;
246
+ // Drain rather than parse: an unread body on a keep-alive connection is a leaked stream on Node,
247
+ // and there is nothing in it this cares about.
248
+ // silent-catch-ok: cancelling a body we never read has no failure worth a line, and this module
249
+ // holds no logger on purpose (it is a pure protocol client the caller reports for).
250
+ await sent.response.body?.cancel().catch(() => { });
251
+ return undefined;
252
+ }
253
+ /**
254
+ * End the session the server minted, if it minted one. The spec's own termination path.
255
+ *
256
+ * Best effort in the strict sense: it runs AFTER the verdict is decided and cannot change it, so
257
+ * every failure is dropped here rather than reported. A server may answer `405` (termination not
258
+ * allowed) and be entirely correct, the deadline may already have passed, and neither is news. What
259
+ * it buys is that pressing Test repeatedly does not leave a session per press on the server to age
260
+ * out on its own clock.
261
+ */
262
+ async function endSession(ctx) {
263
+ const { sessionId, endpoint } = ctx.session;
264
+ if (!sessionId || !endpoint)
265
+ return;
266
+ try {
267
+ const response = await ctx.doFetch(endpoint, {
268
+ method: 'DELETE',
269
+ headers: requestHeaders(ctx),
270
+ redirect: 'manual',
271
+ signal: ctx.signal,
272
+ });
273
+ // silent-catch-ok: as above — a body we never read, on a request whose outcome is already
274
+ // irrelevant to the caller.
275
+ await response.body?.cancel().catch(() => { });
276
+ }
277
+ catch {
278
+ // Nothing to report and nowhere to report it: the verdict is already computed, and a failed
279
+ // courtesy DELETE says nothing about the server an operator asked about.
280
+ }
281
+ }
282
+ /** POST one frame, following redirects by hand and turning transport faults into outcomes. */
283
+ async function post(ctx, frame) {
284
+ let url = ctx.target.url;
285
+ const carriesCredential = Object.keys(ctx.target.credentialHeaders).length > 0;
286
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
287
+ const sent = await send(ctx, url, frame);
288
+ if (!sent.ok)
289
+ return sent;
290
+ ctx.session.adopt(sent.response, url);
291
+ if (!isRedirect(sent.response))
292
+ return sent;
293
+ const hopTo = nextHop(ctx, url, sent.response, carriesCredential);
294
+ if (!hopTo.ok)
295
+ return hopTo;
296
+ url = hopTo.url;
297
+ }
298
+ return {
299
+ ok: false,
300
+ failure: {
301
+ status: 'protocol_error',
302
+ error: `the endpoint redirected more than ${MAX_REDIRECTS} times`,
303
+ },
304
+ };
305
+ }
306
+ /** One request, with every transport fault turned into an outcome rather than a throw. */
307
+ async function send(ctx, url, frame) {
308
+ try {
309
+ return {
310
+ ok: true,
311
+ response: await ctx.doFetch(url, {
312
+ method: 'POST',
313
+ headers: requestHeaders(ctx),
314
+ body: JSON.stringify(frame),
315
+ redirect: 'manual',
316
+ signal: ctx.signal,
317
+ }),
318
+ };
319
+ }
320
+ catch (error) {
321
+ // Everything that never produced a status: DNS, TLS, connection refused, and the deadline. One
322
+ // outcome for all of them because they share a fix (the endpoint, or the network) and the prose
323
+ // carries which — while a 4xx/5xx is a DIFFERENT fix and gets its own member.
324
+ return {
325
+ ok: false,
326
+ failure: { status: 'unreachable', error: redact(errorText(error, ctx.timeoutMs)) },
327
+ };
328
+ }
329
+ }
330
+ /**
331
+ * The headers one request carries: the declaration's own, the resolved credential, the session pair,
332
+ * and this client's content negotiation.
333
+ *
334
+ * Lower-cased before the probe's own values are written, which is load-bearing rather than tidy:
335
+ * `Headers` APPENDS as it fills from a record, so a declaration spelling `Accept` plus this
336
+ * function's `accept` arrives at the server as ONE combined value, and a stream-first server then
337
+ * negotiates against something neither side offered.
338
+ */
339
+ function requestHeaders(ctx) {
340
+ const headers = lowerCasedKeys({ ...ctx.target.headers, ...ctx.target.credentialHeaders });
341
+ headers['content-type'] = 'application/json';
342
+ headers.accept = 'application/json, text/event-stream';
343
+ ctx.session.apply(headers);
344
+ return headers;
345
+ }
346
+ function lowerCasedKeys(headers) {
347
+ const out = {};
348
+ for (const [name, value] of Object.entries(headers))
349
+ out[name.toLowerCase()] = value;
350
+ return out;
351
+ }
352
+ /** Where a redirect points, or the reason the probe will not follow it. */
353
+ function nextHop(ctx, from, response, carriesCredential) {
354
+ const location = response.headers.get('location');
355
+ const next = location ? resolveHop(from, location) : undefined;
356
+ if (!next) {
357
+ return {
358
+ ok: false,
359
+ failure: {
360
+ status: 'protocol_error',
361
+ error: `the endpoint answered ${response.status} with no usable Location header`,
362
+ },
363
+ };
364
+ }
365
+ // The hop is held to the SAME rule the declared url is: an https endpoint that redirects a
366
+ // credential-bearing request onto cleartext is the one thing that rule exists to stop, and a
367
+ // redirect is the way a declaration that passed boot validation still gets there.
368
+ if (!isAllowedMcpHttpUrl(next)) {
369
+ return {
370
+ ok: false,
371
+ failure: {
372
+ status: 'protocol_error',
373
+ error: `the endpoint redirected to ${redact(next)}, which an MCP tool server may not be ` +
374
+ `reached at (https, or plain http on loopback)`,
375
+ },
376
+ };
377
+ }
378
+ // A credential-bearing request stops at its own ORIGIN, matching the Web platform rather than
379
+ // exceeding it: fetch removes `Authorization` when a redirect crosses origins, so the agent's own
380
+ // MCP client reaches this hop unauthenticated and a probe that forwarded the token would both
381
+ // answer about a request no run makes and hand a workspace's credential to whatever the redirect
382
+ // names. Refused rather than followed credential-less, because the FIX is the declaration naming
383
+ // the final url, and a 401 from a stripped hop would name the token instead.
384
+ if (carriesCredential && !sameOrigin(ctx.target.url, next)) {
385
+ return {
386
+ ok: false,
387
+ failure: {
388
+ status: 'protocol_error',
389
+ error: `the endpoint redirected to ${redact(next)}, a different origin from the declared url, ` +
390
+ `and the request carries a credential — declare the final url, because an agent's own ` +
391
+ `MCP client reaches a cross-origin hop with the credential removed`,
392
+ },
393
+ };
394
+ }
395
+ return { ok: true, url: next };
396
+ }
397
+ function isRedirect(response) {
398
+ return response.status >= 300 && response.status < 400;
399
+ }
400
+ /**
401
+ * Whether two urls share an origin. An UNPARSEABLE url is never same-origin with anything, so the
402
+ * caller's refusal is what an unreadable hop reaches rather than a comparison of two undefineds.
403
+ */
404
+ function sameOrigin(a, b) {
405
+ const left = originOf(a);
406
+ return left !== undefined && left === originOf(b);
407
+ }
408
+ function originOf(raw) {
409
+ try {
410
+ return new URL(raw).origin;
411
+ }
412
+ catch {
413
+ return undefined;
414
+ }
415
+ }
416
+ /** Absolutise a `Location` against the url it came from, or undefined when it is unparseable. */
417
+ function resolveHop(from, location) {
418
+ try {
419
+ return new URL(location, from).toString();
420
+ }
421
+ catch {
422
+ return undefined;
423
+ }
424
+ }
425
+ /**
426
+ * Read one JSON-RPC response frame out of whichever body shape arrived.
427
+ *
428
+ * A non-2xx is settled FIRST and as its own outcome. An MCP server refusing a call answers 200 with
429
+ * a JSON-RPC error, so a status outside 2xx means something other than the MCP layer answered — a
430
+ * gateway, an auth proxy, a wrong path — and reporting it as a protocol error would send the
431
+ * operator to look at the server's tool table instead of at the status.
432
+ */
433
+ async function readFrame(ctx, response) {
434
+ if (!response.ok) {
435
+ const body = await bodyText(response);
436
+ return {
437
+ ok: false,
438
+ failure: {
439
+ status: 'http_error',
440
+ httpStatus: response.status,
441
+ // PREVIEWED, like every other quoted body: an auth proxy or a load balancer answers a 4xx
442
+ // with an HTML page, and this string is both rendered in a browser and logged. The status is
443
+ // the diagnosis; the body is a hint about which proxy answered.
444
+ error: redact(body ? `HTTP ${response.status}: ${preview(body)}` : `HTTP ${response.status}`),
445
+ },
446
+ };
447
+ }
448
+ const contentType = response.headers.get('content-type') ?? '';
449
+ const raw = contentType.includes('text/event-stream')
450
+ ? await firstSseData(response)
451
+ : await bodyText(response);
452
+ // An EMPTY body counts as unreadable rather than as JSON that fails to parse, so the prose names
453
+ // the missing body instead of quoting nothing.
454
+ if (raw === undefined || !raw.trim()) {
455
+ return {
456
+ ok: false,
457
+ failure: bodyFailure(ctx, 'the endpoint answered 200 with no readable body'),
458
+ };
459
+ }
460
+ let parsed;
461
+ try {
462
+ parsed = JSON.parse(raw);
463
+ }
464
+ catch {
465
+ return {
466
+ ok: false,
467
+ failure: bodyFailure(ctx, `the endpoint answered 200 with a body that is not JSON: ${redact(preview(raw))}`),
468
+ };
469
+ }
470
+ // A BATCH response is a legal JSON-RPC shape and every request here is single, so a server that
471
+ // wraps its answer in an array is answering the one call we made. Unwrapped rather than refused.
472
+ const frame = Array.isArray(parsed) ? parsed[0] : parsed;
473
+ if (!isRecord(frame)) {
474
+ return {
475
+ ok: false,
476
+ failure: {
477
+ status: 'protocol_error',
478
+ error: 'the endpoint answered JSON that is not a frame',
479
+ },
480
+ };
481
+ }
482
+ return { ok: true, frame };
483
+ }
484
+ /**
485
+ * The failure to report for a body that yielded no frame.
486
+ *
487
+ * The DEADLINE is checked FIRST, because the one signal aborts the response stream as well as the
488
+ * request: a server that answers 200 and then stalls (an SSE stream that never completes an event, a
489
+ * body that stops mid-way) leaves a partial buffer behind, and calling that a `protocol_error` tells
490
+ * the operator "the url names something else" for what is a slow endpoint — the exact cause
491
+ * `unreachable` is documented to cover. A cause is the whole product here, so the two are kept
492
+ * apart even though the code reaches them through the same branch.
493
+ */
494
+ function bodyFailure(ctx, protocolError) {
495
+ if (ctx.signal.aborted)
496
+ return { status: 'unreachable', error: timeoutText(ctx.timeoutMs) };
497
+ return { status: 'protocol_error', error: protocolError };
498
+ }
499
+ /** A whole body as text, bounded, or undefined when there was no body to read at all. */
500
+ async function bodyText(response) {
501
+ const stream = response.body;
502
+ if (!stream) {
503
+ // A bodyless response is legitimate for a notification and a protocol fault for a request; the
504
+ // caller distinguishes, so this stays apart from an EMPTY body, which read fine and said nothing.
505
+ return undefined;
506
+ }
507
+ return readBounded(stream, () => false);
508
+ }
509
+ /**
510
+ * The first SSE `data:` payload in the stream.
511
+ *
512
+ * Read INCREMENTALLY and stopped at the first complete event rather than draining to the end,
513
+ * because a stream-first server keeps the stream open after answering: `.text()` on it waits for the
514
+ * deadline to close a connection that has already delivered what was asked for.
515
+ */
516
+ async function firstSseData(response) {
517
+ const stream = response.body;
518
+ if (!stream)
519
+ return undefined;
520
+ return extractSseData(await readBounded(stream, (buffered) => extractSseData(buffered) !== undefined));
521
+ }
522
+ /**
523
+ * Read `stream` until `done(buffered)` says enough arrived, the stream ends, or the character ceiling
524
+ * is hit. Cancels what it did not read, so a stream held open costs no connection past this point.
525
+ *
526
+ * Always a string, never undefined: a read that failed part-way still returns what arrived, and
527
+ * whether that is usable is the caller's question (`bodyFailure` turns an empty one into the right
528
+ * cause, deadline included).
529
+ */
530
+ async function readBounded(stream, done) {
531
+ const reader = stream.getReader();
532
+ const decoder = new TextDecoder();
533
+ let buffered = '';
534
+ try {
535
+ for (;;) {
536
+ const chunk = await reader.read();
537
+ if (chunk.done)
538
+ break;
539
+ buffered += decoder.decode(chunk.value, { stream: true });
540
+ if (buffered.length > MAX_RESPONSE_BYTES || done(buffered))
541
+ break;
542
+ }
543
+ }
544
+ catch {
545
+ // A body that failed mid-read: whatever arrived is what the caller gets to parse, and an empty
546
+ // buffer becomes its own "no readable body" outcome above.
547
+ }
548
+ finally {
549
+ // silent-catch-ok: cancelling a reader that has already ended or errored is the normal path
550
+ // here, and a throw out of `finally` would replace the outcome the caller needs with noise.
551
+ await reader.cancel().catch(() => { });
552
+ }
553
+ // Flush the decoder's pending bytes, so a multi-byte character split across the LAST chunk lands
554
+ // in the buffer (as itself, or as the replacement character a truncated sequence deserves) rather
555
+ // than being dropped silently.
556
+ return buffered + decoder.decode();
557
+ }
558
+ /**
559
+ * The `data:` payload of the first COMPLETE event in an SSE buffer, or undefined while none has
560
+ * arrived yet. An event ends at a blank line, which is what makes "complete" decidable mid-stream;
561
+ * a `data:` line with nothing after it may still be half a payload.
562
+ */
563
+ function extractSseData(buffered) {
564
+ const normalised = buffered.replaceAll('\r\n', '\n');
565
+ const terminator = /\n\n/.exec(normalised);
566
+ if (!terminator)
567
+ return undefined;
568
+ const event = normalised.slice(0, terminator.index);
569
+ // Multi-line `data:` fields concatenate with newlines, per the SSE spec.
570
+ const data = event
571
+ .split('\n')
572
+ .filter((line) => line.startsWith('data:'))
573
+ .map((line) => line.slice('data:'.length).replace(/^ /, ''));
574
+ return data.length ? data.join('\n') : undefined;
575
+ }
576
+ function isRecord(value) {
577
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
578
+ }
579
+ /** A thrown value as prose. `AbortSignal.timeout` rejects with a bare `TimeoutError`, so name it. */
580
+ function errorText(error, timeoutMs) {
581
+ if (error instanceof Error) {
582
+ return error.name === 'TimeoutError'
583
+ ? timeoutText(timeoutMs)
584
+ : `${error.name}: ${error.message}`;
585
+ }
586
+ return String(error);
587
+ }
588
+ /**
589
+ * The deadline in prose, authored ONCE: the same expiry is reachable as a rejected request and as an
590
+ * aborted body read, and two spellings of one cause read like two causes.
591
+ */
592
+ function timeoutText(timeoutMs) {
593
+ return `the endpoint did not answer within ${timeoutMs / 1000}s`;
594
+ }
595
+ /** First line and a bounded prefix, for prose that quotes an unexpected body. */
596
+ function preview(raw) {
597
+ const oneLine = raw.replaceAll(/\s+/g, ' ').trim();
598
+ return oneLine.length > 200 ? `${oneLine.slice(0, 200)}…` : oneLine;
599
+ }
600
+ /**
601
+ * Every string this module returns goes through here.
602
+ *
603
+ * A fetch failure routinely echoes the request url, and this request's url may carry userinfo while
604
+ * its headers carry a resolved credential; a 4xx body from an auth proxy echoes tokens as a matter
605
+ * of routine. The result is rendered in a browser and logged, so the scrub happens at the emit site
606
+ * rather than being left to whoever consumes it.
607
+ */
608
+ function redact(text) {
609
+ return redactSecrets(text) ?? '';
610
+ }
611
+ //# sourceMappingURL=mcpProbe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpProbe.js","sourceRoot":"","sources":["../../../src/modules/toolServers/mcpProbe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { Logger, ToolSecretResolver } from '@cat-factory/kernel';
2
+ import type { ToolServerProbeResult } from '@cat-factory/contracts';
3
+ import type { AgentKindRegistry } from '@cat-factory/agents';
4
+ import { type McpProbeDeps } from './mcpProbe.js';
5
+ export interface ProbeToolServerInput {
6
+ agentKindRegistry: AgentKindRegistry;
7
+ workspaceId: string;
8
+ /** The server id, from the request path. */
9
+ serverId: string;
10
+ /**
11
+ * The facade's composed capability-credential chain — the SAME resolver the container executor
12
+ * dispatches with. Absent ⇒ the facade wired none, so a server declaring a required credential is
13
+ * reported as `credentials_missing` rather than probed blind, which is the disposition the dispatch
14
+ * path already applies to the same state.
15
+ */
16
+ resolveToolSecrets?: ToolSecretResolver;
17
+ logger?: Logger;
18
+ /** Injected for tests; forwarded to the protocol client. */
19
+ probe?: McpProbeDeps;
20
+ }
21
+ /**
22
+ * Probe one server. Never throws for a PROBE outcome — a failure is data, because the operator is
23
+ * asking a diagnostic question and an exception answers it with nothing.
24
+ *
25
+ * It does throw for a REFUSAL: an id no kind declares and nothing registered is a 404, not a status.
26
+ * A verdict would be a lie there ("unreachable" for a server that does not exist), and the SPA's
27
+ * list came from the same registry, so a 404 means the deployment changed under the operator.
28
+ */
29
+ export declare function probeToolServer(input: ProbeToolServerInput): Promise<ToolServerProbeResult>;
30
+ //# sourceMappingURL=probeToolServer.d.ts.map