mandala-computer-mcp 0.5.0 → 0.6.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/README.md +119 -11
- package/dist/api.d.ts +47 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +248 -22
- package/dist/api.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -0
- package/dist/cli.js.map +1 -1
- package/dist/errors.d.ts +93 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +166 -7
- package/dist/errors.js.map +1 -1
- package/dist/format.d.ts +25 -0
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +31 -2
- package/dist/format.js.map +1 -1
- package/dist/http.d.ts +42 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +379 -3
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +10 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +15 -5
- package/dist/paths.js.map +1 -1
- package/dist/secret-errors.d.ts +59 -0
- package/dist/secret-errors.d.ts.map +1 -0
- package/dist/secret-errors.js +199 -0
- package/dist/secret-errors.js.map +1 -0
- package/dist/secret-store.d.ts +115 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/secret-store.js +105 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/session.d.ts +6 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +1 -1
- package/dist/session.js.map +1 -1
- package/dist/tool-filters.d.ts +1 -1
- package/dist/tool-filters.d.ts.map +1 -1
- package/dist/tool-filters.js +9 -1
- package/dist/tool-filters.js.map +1 -1
- package/dist/tools/computers.d.ts.map +1 -1
- package/dist/tools/computers.js +11 -10
- package/dist/tools/computers.js.map +1 -1
- package/dist/tools/guest.d.ts.map +1 -1
- package/dist/tools/guest.js +134 -20
- package/dist/tools/guest.js.map +1 -1
- package/dist/tools/input.d.ts +2 -0
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +31 -5
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/secrets.d.ts +33 -13
- package/dist/tools/secrets.d.ts.map +1 -1
- package/dist/tools/secrets.js +259 -15
- package/dist/tools/secrets.js.map +1 -1
- package/dist/tools/snapshots.d.ts.map +1 -1
- package/dist/tools/snapshots.js +9 -3
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/webhooks.js +1 -1
- package/dist/tools/webhooks.js.map +1 -1
- package/package.json +1 -1
package/dist/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
|
package/dist/http.d.ts
CHANGED
|
@@ -12,7 +12,49 @@ export type HttpConfig = Omit<ServerConfig, 'apiKey' | 'activity'> & {
|
|
|
12
12
|
maxSessions?: number;
|
|
13
13
|
/** How many requests may retain a parsed body above 256 KiB at once. */
|
|
14
14
|
maxLargeBodyParses?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The OAuth protected-resource metadata URL (RFC 9728) of this server, for
|
|
17
|
+
* the hosted install (OPL-4982). Setting it turns on the OAuth answers: a
|
|
18
|
+
* request with no bearer, or whose bearer the platform refused, is a 401
|
|
19
|
+
* carrying {@link bearerChallenge}, which is how an MCP client discovers
|
|
20
|
+
* where to authorize and knows to refresh. Unset, nothing changes.
|
|
21
|
+
*/
|
|
22
|
+
resourceMetadataUrl?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The platform's shared secret for this service, sent as
|
|
25
|
+
* `X-Mandala-MCP-Service` on every platform request a session makes. Needed
|
|
26
|
+
* for the platform to take an OAuth access token at all. Never logged.
|
|
27
|
+
*/
|
|
28
|
+
serviceSecret?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Hosted mode: how long a bearer the platform accepted is taken on trust
|
|
31
|
+
* before it is checked again, in ms. Default 60 s.
|
|
32
|
+
*/
|
|
33
|
+
bearerCheckTtlMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Hosted mode: refused initializes one source may make per minute before it
|
|
36
|
+
* is answered 429 without asking the platform. Default 20.
|
|
37
|
+
*/
|
|
38
|
+
maxFailedInitializes?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Hosted mode: once a source has spent that budget, how often it may still
|
|
41
|
+
* have one new bearer checked, in ms. Default 5 s.
|
|
42
|
+
*/
|
|
43
|
+
exhaustedProbeIntervalMs?: number;
|
|
44
|
+
/** The SDK's SSE keep-alive interval, in ms. For tests; default 15 s. */
|
|
45
|
+
sseKeepAliveMs?: number;
|
|
15
46
|
};
|
|
47
|
+
/** The OAuth scope the hosted resource asks for. */
|
|
48
|
+
export declare const MCP_SCOPE = "mcp:tools";
|
|
49
|
+
/**
|
|
50
|
+
* The `WWW-Authenticate` value of every OAuth refusal this server makes.
|
|
51
|
+
*
|
|
52
|
+
* The same header whether no bearer was sent or the platform refused one: RFC
|
|
53
|
+
* 9728 §5.1 has a client find the authorization server through
|
|
54
|
+
* `resource_metadata`, and the MCP authorization spec has it refresh or
|
|
55
|
+
* re-authorize on any 401 that carries it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function bearerChallenge(resourceMetadataUrl: string): string;
|
|
16
58
|
export declare function isLoopbackHost(host: string): boolean;
|
|
17
59
|
/**
|
|
18
60
|
* One operator-supplied allowlist entry, as the Host headers it should match.
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,EAA6C,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3F,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,UAAU,CAAC,GAAG;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAyBF,oDAAoD;AACpD,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAEnE;AA+FD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUpD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAWlE;AA+BD;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAs+B9D"}
|
package/dist/http.js
CHANGED
|
@@ -5,9 +5,57 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
5
5
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import express, {} from 'express';
|
|
7
7
|
import { Api, MODEL_KEY_HEADER } from './api.js';
|
|
8
|
+
import { APIError } from './errors.js';
|
|
8
9
|
import { MeteredBody } from './http-body.js';
|
|
10
|
+
import { SSH_KEYS } from './paths.js';
|
|
9
11
|
import { createServer, SERVER_NAME, SERVER_VERSION } from './server.js';
|
|
10
12
|
import { toolFilter } from './tool-filters.js';
|
|
13
|
+
/** The OAuth scope the hosted resource asks for. */
|
|
14
|
+
export const MCP_SCOPE = 'mcp:tools';
|
|
15
|
+
/**
|
|
16
|
+
* The `WWW-Authenticate` value of every OAuth refusal this server makes.
|
|
17
|
+
*
|
|
18
|
+
* The same header whether no bearer was sent or the platform refused one: RFC
|
|
19
|
+
* 9728 §5.1 has a client find the authorization server through
|
|
20
|
+
* `resource_metadata`, and the MCP authorization spec has it refresh or
|
|
21
|
+
* re-authorize on any 401 that carries it.
|
|
22
|
+
*/
|
|
23
|
+
export function bearerChallenge(resourceMetadataUrl) {
|
|
24
|
+
return `Bearer resource_metadata="${resourceMetadataUrl}", scope="${MCP_SCOPE}"`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The metadata URL, or a refusal naming the setting.
|
|
28
|
+
*
|
|
29
|
+
* It goes inside a quoted-string in a response header, so a quote, a backslash
|
|
30
|
+
* or a control character would change what the header says — refused rather
|
|
31
|
+
* than escaped, since no real URL needs one.
|
|
32
|
+
*/
|
|
33
|
+
function checkedMetadataUrl(raw) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error('MANDALA_MCP_RESOURCE_METADATA_URL is not a URL. Set it to the absolute https URL of ' +
|
|
40
|
+
'this resource’s OAuth metadata, e.g. https://app.mandala.computer/.well-known/oauth-protected-resource/mcp');
|
|
41
|
+
}
|
|
42
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
43
|
+
throw new Error('MANDALA_MCP_RESOURCE_METADATA_URL must be an http(s) URL.');
|
|
44
|
+
}
|
|
45
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: refusing them is the point.
|
|
46
|
+
if (/["\\\u0000-\u001f\u007f\s]/.test(raw)) {
|
|
47
|
+
throw new Error('MANDALA_MCP_RESOURCE_METADATA_URL contains a quote, backslash, space or control character.');
|
|
48
|
+
}
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
/** Visible ASCII only: anything else cannot be a header value, and says so here. */
|
|
52
|
+
function checkedServiceSecret(raw) {
|
|
53
|
+
if (!/^[\x21-\x7e]+$/.test(raw)) {
|
|
54
|
+
// The value is deliberately not echoed.
|
|
55
|
+
throw new Error('MANDALA_MCP_SERVICE_SECRET must be printable ASCII with no spaces; it is sent as a header.');
|
|
56
|
+
}
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
11
59
|
const DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
12
60
|
/**
|
|
13
61
|
* A ceiling on live sessions.
|
|
@@ -142,7 +190,138 @@ export async function runHttp(cfg) {
|
|
|
142
190
|
// with a generic 500. Validate it before opening the listening socket, using
|
|
143
191
|
// the same constructor and therefore the same rules as every real session.
|
|
144
192
|
new Api('startup-validation-only', cfg.baseUrl);
|
|
193
|
+
// The hosted install's two settings, checked before the bind for the same
|
|
194
|
+
// reason. `challenge` being set IS hosted mode: every OAuth-specific answer
|
|
195
|
+
// below is gated on it, so a server started without the metadata URL behaves
|
|
196
|
+
// exactly as it did before OPL-4982.
|
|
197
|
+
const metadataUrl = cfg.resourceMetadataUrl !== undefined ? checkedMetadataUrl(cfg.resourceMetadataUrl) : undefined;
|
|
198
|
+
const serviceSecret = cfg.serviceSecret !== undefined ? checkedServiceSecret(cfg.serviceSecret) : undefined;
|
|
199
|
+
const challenge = metadataUrl ? bearerChallenge(metadataUrl) : undefined;
|
|
200
|
+
/** The POST being answered, so a refused bearer can turn it into a 401. */
|
|
201
|
+
const heldAnswer = new AsyncLocalStorage();
|
|
202
|
+
// ---- Checking a bearer with the platform (hosted mode) -----------------
|
|
203
|
+
//
|
|
204
|
+
// Initialize makes no platform request, so without this an invented bearer
|
|
205
|
+
// bought a whole session: 256 of them filled the cap and every real client
|
|
206
|
+
// was 503'd until the sweep. And a POST's event stream is committed by the
|
|
207
|
+
// SDK's keep-alive after 15 s, after which a refused token can only end that
|
|
208
|
+
// call as a tool error. So in hosted mode a bearer is checked before a
|
|
209
|
+
// session is created for it, and before a POST carrying a request is
|
|
210
|
+
// dispatched — the latter cached briefly, keyed by the token's digest and
|
|
211
|
+
// never by the token.
|
|
212
|
+
const checkTtl = cfg.bearerCheckTtlMs ?? DEFAULT_BEARER_CHECK_TTL_MS;
|
|
213
|
+
const accepted = new Map();
|
|
214
|
+
let checksInFlight = 0;
|
|
215
|
+
/** Whether the platform accepted this bearer within the cache window. */
|
|
216
|
+
const isAccepted = (key) => {
|
|
217
|
+
const until = accepted.get(digest(key).toString('hex'));
|
|
218
|
+
return until !== undefined && until > Date.now();
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* `ok`: the probe answered 2xx, and only that — the one answer that says the
|
|
222
|
+
* platform authenticated this credential AND let it act. `refused`: a 401.
|
|
223
|
+
* `unknown`: anything else — a 403 (a suspended account or a lost
|
|
224
|
+
* membership), a 404 (the route moved), a 429, a 5xx, a timeout, or this
|
|
225
|
+
* server already asking about as many bearers as it will at once. Only `ok`
|
|
226
|
+
* is cached.
|
|
227
|
+
*/
|
|
228
|
+
// `onStart` runs only when a probe really starts — never for a cached answer
|
|
229
|
+
// or a full cap — and synchronously, before the first await, so a caller's
|
|
230
|
+
// bookkeeping and its check against it cannot interleave with another's.
|
|
231
|
+
const checkBearer = async (key, onStart) => {
|
|
232
|
+
const id = digest(key).toString('hex');
|
|
233
|
+
if (isAccepted(key))
|
|
234
|
+
return 'ok';
|
|
235
|
+
if (checksInFlight >= MAX_BEARER_CHECKS_IN_FLIGHT)
|
|
236
|
+
return 'unknown';
|
|
237
|
+
checksInFlight++;
|
|
238
|
+
onStart?.();
|
|
239
|
+
try {
|
|
240
|
+
const api = new Api(key, cfg.baseUrl, AbortSignal.timeout(BEARER_CHECK_TIMEOUT_MS), {
|
|
241
|
+
serviceSecret,
|
|
242
|
+
});
|
|
243
|
+
// `GET ssh-keys`, chosen because it is the cheapest read that every
|
|
244
|
+
// valid credential is answered 2xx on. The platform's route table
|
|
245
|
+
// gives it the lowest role there is (viewer), a workspace-scoped key is
|
|
246
|
+
// refused only the key WRITES, and the handler lists the caller's own
|
|
247
|
+
// keys from the control plane without asking any host — so no role a
|
|
248
|
+
// grant can carry, and no host being down, turns it into a refusal.
|
|
249
|
+
// `account` is the alternative and costs two fleet inventories. The one
|
|
250
|
+
// valid credential it does not admit is a suspended account's (403),
|
|
251
|
+
// which is answered as unconfirmed. It takes no parameters, so none are
|
|
252
|
+
// sent.
|
|
253
|
+
await api.json('GET', SSH_KEYS);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
return err instanceof APIError && err.status === 401 ? 'refused' : 'unknown';
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
checksInFlight--;
|
|
260
|
+
}
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
if (accepted.size >= MAX_ACCEPTED_BEARERS) {
|
|
263
|
+
for (const [k, t] of accepted)
|
|
264
|
+
if (t <= now)
|
|
265
|
+
accepted.delete(k);
|
|
266
|
+
// Still full: drop the oldest, which Map iteration yields first.
|
|
267
|
+
if (accepted.size >= MAX_ACCEPTED_BEARERS) {
|
|
268
|
+
accepted.delete(accepted.keys().next().value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
accepted.set(id, now + checkTtl);
|
|
272
|
+
return 'ok';
|
|
273
|
+
};
|
|
274
|
+
// Refused initializes per source, in fixed one-minute windows. Kept apart
|
|
275
|
+
// from the session cap so a flood of invented tokens is turned away before
|
|
276
|
+
// it asks the platform anything, rather than by starving the pool.
|
|
277
|
+
const maxFailed = cfg.maxFailedInitializes ?? DEFAULT_MAX_FAILED_INITIALIZES;
|
|
278
|
+
// `lastProbeAt` lives on the window's entry, so the window's reset restores
|
|
279
|
+
// the normal budget and forgets the spent-address clock in one step.
|
|
280
|
+
const failures = new Map();
|
|
281
|
+
const exhaustedInterval = cfg.exhaustedProbeIntervalMs ?? DEFAULT_EXHAUSTED_PROBE_INTERVAL_MS;
|
|
282
|
+
const sourceOf = (req) => req.ip ?? req.socket.remoteAddress ?? 'unknown';
|
|
283
|
+
const overFailureBudget = (req) => {
|
|
284
|
+
const entry = failures.get(sourceOf(req));
|
|
285
|
+
if (!entry)
|
|
286
|
+
return undefined;
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
if (entry.resetAt <= now) {
|
|
289
|
+
failures.delete(sourceOf(req));
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
return entry.count >= maxFailed ? Math.ceil((entry.resetAt - now) / 1000) : undefined;
|
|
293
|
+
};
|
|
294
|
+
const noteFailure = (req) => {
|
|
295
|
+
const source = sourceOf(req);
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
const entry = failures.get(source);
|
|
298
|
+
if (entry && entry.resetAt > now) {
|
|
299
|
+
entry.count++;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (failures.size >= MAX_FAILURE_SOURCES) {
|
|
303
|
+
for (const [k, e] of failures)
|
|
304
|
+
if (e.resetAt <= now)
|
|
305
|
+
failures.delete(k);
|
|
306
|
+
if (failures.size >= MAX_FAILURE_SOURCES) {
|
|
307
|
+
failures.delete(failures.keys().next().value);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
failures.set(source, { count: 1, resetAt: now + FAILURE_WINDOW_MS });
|
|
311
|
+
};
|
|
312
|
+
/** A 401 an OAuth client acts on: authorize, or refresh and try again. */
|
|
313
|
+
const challenged = (res, message, id = null) => {
|
|
314
|
+
if (challenge)
|
|
315
|
+
res.set('WWW-Authenticate', challenge);
|
|
316
|
+
unauthorized(res, message, id);
|
|
317
|
+
};
|
|
145
318
|
const app = express();
|
|
319
|
+
// Hosted mode sits behind a proxy on this machine, and the refused-
|
|
320
|
+
// initialize budget is per source: without this every caller would be the
|
|
321
|
+
// proxy's one loopback address, sharing one budget. Only a loopback peer's
|
|
322
|
+
// X-Forwarded-For is believed, so a direct caller cannot choose its source.
|
|
323
|
+
if (challenge)
|
|
324
|
+
app.set('trust proxy', 'loopback');
|
|
146
325
|
// parseBody consumes every POST /mcp body. Every other route deliberately
|
|
147
326
|
// ignores request bodies, so put those requests in flowing mode immediately:
|
|
148
327
|
// leaving bytes unread can strand a keep-alive connection behind one wrong
|
|
@@ -548,6 +727,14 @@ export async function runHttp(cfg) {
|
|
|
548
727
|
app.post('/mcp', parseBody, async (req, res) => {
|
|
549
728
|
const sessionId = req.header('mcp-session-id');
|
|
550
729
|
const key = bearer(req);
|
|
730
|
+
// Hosted: EVERY request needs a bearer, initialize and tools/list included.
|
|
731
|
+
// Orgo answers those two anonymously; refusing them is simpler, costs a
|
|
732
|
+
// client nothing (it authorizes on the first 401 whichever request that
|
|
733
|
+
// is), and means no McpServer is ever built for a caller with no credential.
|
|
734
|
+
if (challenge && !key) {
|
|
735
|
+
releaseLargeBody(res);
|
|
736
|
+
return challenged(res, NO_TOKEN, rpcId(req));
|
|
737
|
+
}
|
|
551
738
|
if (sessionId) {
|
|
552
739
|
try {
|
|
553
740
|
const live = sessions.get(sessionId);
|
|
@@ -557,10 +744,36 @@ export async function runHttp(cfg) {
|
|
|
557
744
|
// session id travels in a plain header and is the sort of thing that ends
|
|
558
745
|
// up in a proxy log; on its own it must not be a credential.
|
|
559
746
|
if (!key || !sameKey(live.keyDigest, key)) {
|
|
747
|
+
// Hosted: a different bearer is most often the SAME client after a
|
|
748
|
+
// token refresh, and nothing this server can see proves that — an
|
|
749
|
+
// access token is opaque, and the refresh token never comes here. So
|
|
750
|
+
// the session is not rebound, which would hand its bound computer,
|
|
751
|
+
// buffered events and retained results to whoever holds any valid
|
|
752
|
+
// credential and the session id. It is answered as unknown instead,
|
|
753
|
+
// which is the MCP spec's signal to initialize a new session; the new
|
|
754
|
+
// bearer gets a new session, and this one is left to the idle sweep
|
|
755
|
+
// (closing it here would let anyone with the id end it).
|
|
756
|
+
if (challenge) {
|
|
757
|
+
return notFound(res, 'Unknown session. Initialize a new one.', rpcId(req));
|
|
758
|
+
}
|
|
560
759
|
return unauthorized(res, 'This session belongs to a different API key.', rpcId(req));
|
|
561
760
|
}
|
|
761
|
+
if (challenge && live.refused)
|
|
762
|
+
return challenged(res, TOKEN_REFUSED, rpcId(req));
|
|
763
|
+
// Checked before a request is dispatched, while the answer can still
|
|
764
|
+
// be a 401 whatever the call goes on to do. A platform that cannot say
|
|
765
|
+
// is not a refusal: the call goes ahead, and a real refusal during it
|
|
766
|
+
// still reaches the client through the held answer or the mark.
|
|
767
|
+
if (challenge && carriesRequest(req.body) && (await checkBearer(key)) === 'refused') {
|
|
768
|
+
live.refused = true;
|
|
769
|
+
return challenged(res, TOKEN_REFUSED, rpcId(req));
|
|
770
|
+
}
|
|
562
771
|
const lease = res.locals.largeBodyLease;
|
|
563
|
-
|
|
772
|
+
const id = rpcId(req);
|
|
773
|
+
const held = challenge
|
|
774
|
+
? holdAnswer(res, () => challenged(res, TOKEN_REFUSED, id))
|
|
775
|
+
: undefined;
|
|
776
|
+
return await heldAnswer.run(held, () => requestBodyLease.run(lease, () => serving(live, () => live.transport.handleRequest(req, res, req.body))));
|
|
564
777
|
}
|
|
565
778
|
finally {
|
|
566
779
|
// A verified session is the only request allowed through the large
|
|
@@ -587,6 +800,43 @@ export async function runHttp(cfg) {
|
|
|
587
800
|
const refused = dnsRebindingRefusal(req, hosts, allowedOrigins);
|
|
588
801
|
if (refused)
|
|
589
802
|
return rpcError(res, 403, -32000, refused, rpcId(req));
|
|
803
|
+
// Hosted: no session for a bearer the platform does not accept. Before the
|
|
804
|
+
// cap and the reservation, so a refused bearer never holds a slot.
|
|
805
|
+
if (challenge) {
|
|
806
|
+
// A spent budget does not close the address. Many clients can share one
|
|
807
|
+
// (a NAT, an office), and one bad neighbour must not lock out the rest:
|
|
808
|
+
// a bearer the platform already accepted passes outright, and a new one
|
|
809
|
+
// still gets a probe — but at most one per interval per spent address,
|
|
810
|
+
// taken when the probe starts, so the address costs the platform one probe per
|
|
811
|
+
// interval however hard it pushes, and a valid client there waits at
|
|
812
|
+
// most one interval. The process-wide cap on probes bounds the total.
|
|
813
|
+
const spent = !isAccepted(key) && overFailureBudget(req) !== undefined;
|
|
814
|
+
if (spent) {
|
|
815
|
+
const entry = failures.get(sourceOf(req));
|
|
816
|
+
const wait = entry?.lastProbeAt !== undefined ? entry.lastProbeAt + exhaustedInterval - Date.now() : 0;
|
|
817
|
+
if (wait > 0) {
|
|
818
|
+
res.set('Retry-After', String(Math.max(1, Math.ceil(wait / 1000))));
|
|
819
|
+
return rpcError(res, 429, -32002, 'Too many refused initializes from this address. Retry later with a valid token.', rpcId(req));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// The interval is spent only by a probe that starts: a request turned
|
|
823
|
+
// away because the process-wide cap is full reached nobody, and must not
|
|
824
|
+
// make the next client at this address wait out an interval for it.
|
|
825
|
+
const verdict = await checkBearer(key, spent
|
|
826
|
+
? () => {
|
|
827
|
+
const entry = failures.get(sourceOf(req));
|
|
828
|
+
if (entry)
|
|
829
|
+
entry.lastProbeAt = Date.now();
|
|
830
|
+
}
|
|
831
|
+
: undefined);
|
|
832
|
+
if (verdict === 'refused') {
|
|
833
|
+
noteFailure(req);
|
|
834
|
+
return challenged(res, TOKEN_REFUSED, rpcId(req));
|
|
835
|
+
}
|
|
836
|
+
if (verdict === 'unknown') {
|
|
837
|
+
return unavailable(res, 'The platform could not confirm this token just now. Retry shortly.', rpcId(req));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
590
840
|
// Swept sessions free their slot on the timer; this is the backstop for the
|
|
591
841
|
// case the timer cannot help with, which is arrivals faster than the TTL.
|
|
592
842
|
if (sessions.size + pending >= maxSessions) {
|
|
@@ -618,6 +868,7 @@ export async function runHttp(cfg) {
|
|
|
618
868
|
try {
|
|
619
869
|
const t = new StreamableHTTPServerTransport({
|
|
620
870
|
sessionIdGenerator: () => randomUUID(),
|
|
871
|
+
...(cfg.sseKeepAliveMs !== undefined ? { keepAliveMs: cfg.sseKeepAliveMs } : {}),
|
|
621
872
|
// On whenever there is a list to check against, which for a loopback
|
|
622
873
|
// bind is always — see `allowedHosts`. A browser cannot be stopped from
|
|
623
874
|
// resolving a name it controls to 127.0.0.1, so the Host header is the
|
|
@@ -635,6 +886,7 @@ export async function runHttp(cfg) {
|
|
|
635
886
|
// Initialize is already in flight when the session first becomes
|
|
636
887
|
// visible to the sweeper. Count it until handleRequest settles.
|
|
637
888
|
active: 1,
|
|
889
|
+
refused: false,
|
|
638
890
|
};
|
|
639
891
|
sessions.set(id, live);
|
|
640
892
|
let held = true;
|
|
@@ -680,6 +932,21 @@ export async function runHttp(cfg) {
|
|
|
680
932
|
// every call until they run use_computer 404s, and the id of a machine
|
|
681
933
|
// that is not theirs is named back to them by way of explanation.
|
|
682
934
|
computerId: undefined,
|
|
935
|
+
platform: {
|
|
936
|
+
serviceSecret,
|
|
937
|
+
// Hosted only. Marks the session, so the NEXT request on this bearer
|
|
938
|
+
// is refused before dispatch, and turns the request in flight into a
|
|
939
|
+
// 401 if nothing of its answer has been sent yet.
|
|
940
|
+
onBearerRefused: challenge
|
|
941
|
+
? () => {
|
|
942
|
+
const id = t.sessionId;
|
|
943
|
+
const live = id ? sessions.get(id) : undefined;
|
|
944
|
+
if (live)
|
|
945
|
+
live.refused = true;
|
|
946
|
+
heldAnswer.getStore()?.refuse();
|
|
947
|
+
}
|
|
948
|
+
: undefined,
|
|
949
|
+
},
|
|
683
950
|
});
|
|
684
951
|
mcp = server;
|
|
685
952
|
await server.connect(t);
|
|
@@ -717,13 +984,22 @@ export async function runHttp(cfg) {
|
|
|
717
984
|
// the POST does.
|
|
718
985
|
const bySession = async (req, res) => {
|
|
719
986
|
const sessionId = req.header('mcp-session-id');
|
|
987
|
+
const key = bearer(req);
|
|
988
|
+
if (challenge && !key)
|
|
989
|
+
return challenged(res, NO_TOKEN);
|
|
720
990
|
const live = sessionId ? sessions.get(sessionId) : undefined;
|
|
721
991
|
if (!live)
|
|
722
992
|
return notFound(res, 'Unknown session.');
|
|
723
|
-
const key = bearer(req);
|
|
724
993
|
if (!key || !sameKey(live.keyDigest, key)) {
|
|
994
|
+
// As on POST: hosted answers a different bearer as an unknown session.
|
|
995
|
+
if (challenge)
|
|
996
|
+
return notFound(res, 'Unknown session.');
|
|
725
997
|
return unauthorized(res, 'This session belongs to a different API key.');
|
|
726
998
|
}
|
|
999
|
+
// The stream is refused on a token the platform has already refused; a
|
|
1000
|
+
// DELETE is still honoured, since the digest shows it is the holder.
|
|
1001
|
+
if (challenge && live.refused && req.method === 'GET')
|
|
1002
|
+
return challenged(res, TOKEN_REFUSED);
|
|
727
1003
|
// The DELETE is a request and is held for; the GET is the notification
|
|
728
1004
|
// stream and is only noted. See `serving`.
|
|
729
1005
|
if (req.method === 'GET') {
|
|
@@ -794,7 +1070,13 @@ export async function runHttp(cfg) {
|
|
|
794
1070
|
const addr = http.address();
|
|
795
1071
|
const bound = typeof addr === 'object' && addr ? addr.port : cfg.port;
|
|
796
1072
|
boundPort = bound;
|
|
797
|
-
console.error(
|
|
1073
|
+
console.error(metadataUrl
|
|
1074
|
+
? `mandala-computer-mcp on http://${cfg.host}:${bound}/mcp — OAuth: callers without a bearer are sent to ${metadataUrl}`
|
|
1075
|
+
: `mandala-computer-mcp on http://${cfg.host}:${bound}/mcp — callers authenticate with their own Mandala API key`);
|
|
1076
|
+
// Said, never shown: whether the secret is set is the useful fact.
|
|
1077
|
+
if (metadataUrl && !serviceSecret) {
|
|
1078
|
+
console.error(' MANDALA_MCP_SERVICE_SECRET is not set — the platform accepts OAuth access tokens only with it, so every tool call will be refused');
|
|
1079
|
+
}
|
|
798
1080
|
// Said once, at the only moment anybody is reading. A bind that is not
|
|
799
1081
|
// loopback cannot have its legitimate Host values guessed, so the check
|
|
800
1082
|
// is off and the operator is the only one who can turn it on — and an
|
|
@@ -860,6 +1142,100 @@ export async function runHttp(cfg) {
|
|
|
860
1142
|
http.on('close', teardown);
|
|
861
1143
|
});
|
|
862
1144
|
}
|
|
1145
|
+
const DEFAULT_BEARER_CHECK_TTL_MS = 60_000;
|
|
1146
|
+
const DEFAULT_MAX_FAILED_INITIALIZES = 20;
|
|
1147
|
+
const DEFAULT_EXHAUSTED_PROBE_INTERVAL_MS = 5_000;
|
|
1148
|
+
const FAILURE_WINDOW_MS = 60_000;
|
|
1149
|
+
const BEARER_CHECK_TIMEOUT_MS = 10_000;
|
|
1150
|
+
/** How many bearers this server will be asking the platform about at once. */
|
|
1151
|
+
const MAX_BEARER_CHECKS_IN_FLIGHT = 32;
|
|
1152
|
+
const MAX_ACCEPTED_BEARERS = 4096;
|
|
1153
|
+
const MAX_FAILURE_SOURCES = 10_000;
|
|
1154
|
+
/** Whether a POST body holds a JSON-RPC request, which is what opens a stream. */
|
|
1155
|
+
function carriesRequest(body) {
|
|
1156
|
+
const isRequest = (m) => typeof m === 'object' && m !== null && 'method' in m && 'id' in m;
|
|
1157
|
+
return Array.isArray(body) ? body.some(isRequest) : isRequest(body);
|
|
1158
|
+
}
|
|
1159
|
+
const NO_TOKEN = 'Authorization required. Authorize this client with Mandala (OAuth), or send a Mandala API key: Authorization: Bearer …';
|
|
1160
|
+
const TOKEN_REFUSED = 'The platform no longer accepts this access token. Refresh it, or authorize again.';
|
|
1161
|
+
/**
|
|
1162
|
+
* Hold a response's status line until its first byte of body (hosted mode).
|
|
1163
|
+
*
|
|
1164
|
+
* A Streamable HTTP POST answers 200 and opens its event stream the moment the
|
|
1165
|
+
* request is dispatched, long before a tool reaches the platform — so a token
|
|
1166
|
+
* the platform refuses could only ever come back as a tool error inside a 200,
|
|
1167
|
+
* which no MCP client treats as "refresh your token". Deferring `writeHead`
|
|
1168
|
+
* and `flushHeaders` until the first write lets that refusal become the 401
|
|
1169
|
+
* the client acts on instead: the platform's 401 is reported before the tool
|
|
1170
|
+
* has produced anything, so the first write finds the answer already refused.
|
|
1171
|
+
*
|
|
1172
|
+
* The first byte of anything — a result, a progress notification, the SDK's
|
|
1173
|
+
* keep-alive comment — commits the stream, and a refusal after that stays a
|
|
1174
|
+
* tool error. The session is marked either way, so the next request carrying
|
|
1175
|
+
* the refused bearer gets the 401 before it is dispatched.
|
|
1176
|
+
*/
|
|
1177
|
+
function holdAnswer(res, answer401) {
|
|
1178
|
+
const target = res;
|
|
1179
|
+
const names = ['writeHead', 'flushHeaders', 'write', 'end'];
|
|
1180
|
+
const own = Object.fromEntries(names.map((n) => [n, target[n]]));
|
|
1181
|
+
const deferred = [];
|
|
1182
|
+
let state = 'holding';
|
|
1183
|
+
const restore = () => {
|
|
1184
|
+
for (const n of names)
|
|
1185
|
+
target[n] = own[n];
|
|
1186
|
+
};
|
|
1187
|
+
const release = () => {
|
|
1188
|
+
if (state !== 'holding')
|
|
1189
|
+
return;
|
|
1190
|
+
state = 'released';
|
|
1191
|
+
restore();
|
|
1192
|
+
for (const replay of deferred.splice(0))
|
|
1193
|
+
replay();
|
|
1194
|
+
};
|
|
1195
|
+
const refuse = () => {
|
|
1196
|
+
if (state !== 'holding')
|
|
1197
|
+
return;
|
|
1198
|
+
state = 'refused';
|
|
1199
|
+
restore();
|
|
1200
|
+
if (!res.headersSent && !res.writableEnded && !res.destroyed)
|
|
1201
|
+
answer401();
|
|
1202
|
+
// Whatever the transport still writes belongs to the answer that was
|
|
1203
|
+
// replaced, and writing after end would raise an unhandled 'error'.
|
|
1204
|
+
target.writeHead = () => res;
|
|
1205
|
+
target.flushHeaders = () => undefined;
|
|
1206
|
+
target.write = (...args) => {
|
|
1207
|
+
const cb = args.find((a) => typeof a === 'function');
|
|
1208
|
+
cb?.();
|
|
1209
|
+
return true;
|
|
1210
|
+
};
|
|
1211
|
+
target.end = (...args) => {
|
|
1212
|
+
const cb = args.find((a) => typeof a === 'function');
|
|
1213
|
+
cb?.();
|
|
1214
|
+
return res;
|
|
1215
|
+
};
|
|
1216
|
+
};
|
|
1217
|
+
target.writeHead = (...args) => {
|
|
1218
|
+
if (state !== 'holding')
|
|
1219
|
+
return target.writeHead.apply(res, args);
|
|
1220
|
+
deferred.push(() => own.writeHead.apply(res, args));
|
|
1221
|
+
return res;
|
|
1222
|
+
};
|
|
1223
|
+
target.flushHeaders = (...args) => {
|
|
1224
|
+
if (state !== 'holding')
|
|
1225
|
+
return target.flushHeaders.apply(res, args);
|
|
1226
|
+
deferred.push(() => own.flushHeaders.apply(res, args));
|
|
1227
|
+
return undefined;
|
|
1228
|
+
};
|
|
1229
|
+
target.write = (...args) => {
|
|
1230
|
+
release();
|
|
1231
|
+
return target.write.apply(res, args);
|
|
1232
|
+
};
|
|
1233
|
+
target.end = (...args) => {
|
|
1234
|
+
release();
|
|
1235
|
+
return target.end.apply(res, args);
|
|
1236
|
+
};
|
|
1237
|
+
return { refuse };
|
|
1238
|
+
}
|
|
863
1239
|
/**
|
|
864
1240
|
* Whether this request is allowed through the 96mb parser.
|
|
865
1241
|
*
|