@zackbart/connecta 0.7.5 → 0.7.6
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/CHANGELOG.md +51 -0
- package/README.md +1 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +11 -0
- package/dist/execute.js.map +1 -1
- package/dist/executor-admission.d.ts +18 -1
- package/dist/executor-admission.d.ts.map +1 -1
- package/dist/executor-admission.js +82 -3
- package/dist/executor-admission.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +1 -0
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-protocol.js +7 -4
- package/dist/executors/quickjs-protocol.js.map +1 -1
- package/dist/executors/quickjs-runtime.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.js +22 -9
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +44 -10
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +175 -16
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/src/execute.ts +11 -0
- package/src/executor-admission.ts +90 -3
- package/src/executors/quickjs-protocol.ts +7 -4
- package/src/executors/quickjs-runtime.ts +31 -9
- package/src/executors/quickjs.ts +61 -12
- package/src/index.ts +87 -1
- package/src/server.ts +217 -29
- package/src/types.ts +27 -0
- package/src/version.ts +1 -1
package/src/server.ts
CHANGED
|
@@ -33,6 +33,12 @@ import type {
|
|
|
33
33
|
ToolkitBinding,
|
|
34
34
|
} from "./types.js";
|
|
35
35
|
import { CONNECTA_FAVICON_ICO } from "./favicon.js";
|
|
36
|
+
import {
|
|
37
|
+
ExecutorAdmissionError,
|
|
38
|
+
isAdmittingExecutor,
|
|
39
|
+
type AdmissionController,
|
|
40
|
+
type AdmissionLease,
|
|
41
|
+
} from "./executor-admission.js";
|
|
36
42
|
import {
|
|
37
43
|
buildUiData,
|
|
38
44
|
CONNECTA_FAVICON_SVG,
|
|
@@ -96,6 +102,8 @@ export interface ServerOptions {
|
|
|
96
102
|
probeTimeoutMs?: number;
|
|
97
103
|
/** When set, the execute_code meta-tool is registered on top of the nine. */
|
|
98
104
|
executor?: Executor;
|
|
105
|
+
/** Global FIFO boundary for all non-preflight `/mcp` requests. */
|
|
106
|
+
requestAdmission: AdmissionController;
|
|
99
107
|
/** Encrypted connector-credential storage backing the Credentials page. */
|
|
100
108
|
credentialVault?: CredentialVault;
|
|
101
109
|
/** Optional browser UI and OAuth result-page labels. */
|
|
@@ -301,7 +309,7 @@ function withMcpCors(response: Response): Response {
|
|
|
301
309
|
}
|
|
302
310
|
headers.set(
|
|
303
311
|
"Access-Control-Expose-Headers",
|
|
304
|
-
"WWW-Authenticate, mcp-session-id, mcp-protocol-version",
|
|
312
|
+
"WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version",
|
|
305
313
|
);
|
|
306
314
|
return new Response(response.body, {
|
|
307
315
|
status: response.status,
|
|
@@ -310,6 +318,102 @@ function withMcpCors(response: Response): Response {
|
|
|
310
318
|
});
|
|
311
319
|
}
|
|
312
320
|
|
|
321
|
+
function requestAdmissionFailure(error: ExecutorAdmissionError): Response {
|
|
322
|
+
const overloaded = error.code === "executor_overloaded";
|
|
323
|
+
const data = {
|
|
324
|
+
code: overloaded ? "server_overloaded" : "server_shutting_down",
|
|
325
|
+
retryable: overloaded,
|
|
326
|
+
...(overloaded && error.retryAfterMs !== undefined
|
|
327
|
+
? { retryAfterMs: error.retryAfterMs }
|
|
328
|
+
: {}),
|
|
329
|
+
};
|
|
330
|
+
const headers = new Headers({
|
|
331
|
+
"Content-Type": "application/json",
|
|
332
|
+
"Cache-Control": "no-store",
|
|
333
|
+
});
|
|
334
|
+
if (overloaded && error.retryAfterMs !== undefined) {
|
|
335
|
+
headers.set(
|
|
336
|
+
"Retry-After",
|
|
337
|
+
String(Math.max(1, Math.ceil(error.retryAfterMs / 1_000))),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return new Response(
|
|
341
|
+
JSON.stringify({
|
|
342
|
+
jsonrpc: "2.0",
|
|
343
|
+
id: null,
|
|
344
|
+
error: {
|
|
345
|
+
code: overloaded ? -32001 : -32002,
|
|
346
|
+
message: overloaded
|
|
347
|
+
? "Server capacity is exhausted. Retry later."
|
|
348
|
+
: "Server is shutting down.",
|
|
349
|
+
data,
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
{ status: 503, headers },
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* A request owns its permit through the response body, not merely until the
|
|
358
|
+
* handler returns. This is what makes slow clients and response-stream failure
|
|
359
|
+
* part of the same bounded lifecycle as success, error, and cancellation.
|
|
360
|
+
*/
|
|
361
|
+
function releaseAdmissionWithResponse(
|
|
362
|
+
response: Response,
|
|
363
|
+
lease: AdmissionLease,
|
|
364
|
+
signal: AbortSignal,
|
|
365
|
+
): Response {
|
|
366
|
+
let released = false;
|
|
367
|
+
let onAbort = () => {};
|
|
368
|
+
const release = () => {
|
|
369
|
+
if (released) return;
|
|
370
|
+
released = true;
|
|
371
|
+
signal.removeEventListener("abort", onAbort);
|
|
372
|
+
lease.release();
|
|
373
|
+
};
|
|
374
|
+
if (!response.body) {
|
|
375
|
+
release();
|
|
376
|
+
return response;
|
|
377
|
+
}
|
|
378
|
+
const reader = response.body.getReader();
|
|
379
|
+
onAbort = () => {
|
|
380
|
+
// `cancel()` belongs to an operator/auth/SDK-provided stream and may
|
|
381
|
+
// reject. Consume both outcomes: `.finally(release)` would release the
|
|
382
|
+
// permit but preserve the rejection as an unhandled promise.
|
|
383
|
+
void reader.cancel(signal.reason).then(release, release);
|
|
384
|
+
};
|
|
385
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
386
|
+
if (signal.aborted) onAbort();
|
|
387
|
+
const body = new ReadableStream<Uint8Array>({
|
|
388
|
+
async pull(controller) {
|
|
389
|
+
try {
|
|
390
|
+
const next = await reader.read();
|
|
391
|
+
if (next.done) {
|
|
392
|
+
release();
|
|
393
|
+
controller.close();
|
|
394
|
+
} else {
|
|
395
|
+
controller.enqueue(next.value);
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
release();
|
|
399
|
+
controller.error(error);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
async cancel(reason) {
|
|
403
|
+
try {
|
|
404
|
+
await reader.cancel(reason);
|
|
405
|
+
} finally {
|
|
406
|
+
release();
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
return new Response(body, {
|
|
411
|
+
status: response.status,
|
|
412
|
+
statusText: response.statusText,
|
|
413
|
+
headers: response.headers,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
313
417
|
function withSecurityHeaders(
|
|
314
418
|
response: Response,
|
|
315
419
|
requestUrl: URL,
|
|
@@ -1042,6 +1146,23 @@ export function createFetchHandler(
|
|
|
1042
1146
|
runtimeContext?: RuntimeExecutionContext,
|
|
1043
1147
|
) => Promise<Response> {
|
|
1044
1148
|
const { registry, auth, publicUrl, serverInfo } = opts;
|
|
1149
|
+
let lastAdmissionWarningAt = 0;
|
|
1150
|
+
let suppressedAdmissionWarnings = 0;
|
|
1151
|
+
const warnAdmissionRejected = (error: ExecutorAdmissionError): void => {
|
|
1152
|
+
const now = Date.now();
|
|
1153
|
+
if (now - lastAdmissionWarningAt < 1_000) {
|
|
1154
|
+
suppressedAdmissionWarnings++;
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
opts.logger.warn("[connecta] MCP request admission rejected", {
|
|
1158
|
+
retryAfterMs: error.retryAfterMs,
|
|
1159
|
+
active: opts.requestAdmission.activeCount,
|
|
1160
|
+
queued: opts.requestAdmission.queuedCount,
|
|
1161
|
+
suppressedSinceLastWarning: suppressedAdmissionWarnings,
|
|
1162
|
+
});
|
|
1163
|
+
lastAdmissionWarningAt = now;
|
|
1164
|
+
suppressedAdmissionWarnings = 0;
|
|
1165
|
+
};
|
|
1045
1166
|
return async function fetch(
|
|
1046
1167
|
request: Request,
|
|
1047
1168
|
runtimeContext?: RuntimeExecutionContext,
|
|
@@ -1154,10 +1275,29 @@ export function createFetchHandler(
|
|
|
1154
1275
|
}
|
|
1155
1276
|
|
|
1156
1277
|
if (path === "/health") {
|
|
1278
|
+
const codeAdmission =
|
|
1279
|
+
opts.executor && isAdmittingExecutor(opts.executor)
|
|
1280
|
+
? opts.executor.admissionSnapshot?.()
|
|
1281
|
+
: undefined;
|
|
1157
1282
|
return Response.json({
|
|
1158
1283
|
status: "ok",
|
|
1159
1284
|
connectors: registry.listConnectors().length,
|
|
1160
1285
|
server: opts.serverInfo,
|
|
1286
|
+
admission: {
|
|
1287
|
+
policy: "global-fifo",
|
|
1288
|
+
requests: opts.requestAdmission.snapshot(),
|
|
1289
|
+
code: opts.executor
|
|
1290
|
+
? (codeAdmission ?? { managedByExecutor: true })
|
|
1291
|
+
: null,
|
|
1292
|
+
reservedRoutes: [
|
|
1293
|
+
"/health",
|
|
1294
|
+
"/",
|
|
1295
|
+
"/credentials",
|
|
1296
|
+
"/activity",
|
|
1297
|
+
"/ui",
|
|
1298
|
+
"/ui/*",
|
|
1299
|
+
],
|
|
1300
|
+
},
|
|
1161
1301
|
...(opts.deploymentInfo ? { deployment: opts.deploymentInfo } : {}),
|
|
1162
1302
|
});
|
|
1163
1303
|
}
|
|
@@ -1312,34 +1452,82 @@ export function createFetchHandler(
|
|
|
1312
1452
|
}
|
|
1313
1453
|
|
|
1314
1454
|
if (path === "/mcp") {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1455
|
+
let admission: AdmissionLease;
|
|
1456
|
+
try {
|
|
1457
|
+
admission = await opts.requestAdmission.acquire({
|
|
1458
|
+
signal: request.signal,
|
|
1459
|
+
});
|
|
1460
|
+
if (admission.waitMs > 0) {
|
|
1461
|
+
opts.logger.debug("[connecta] MCP request admitted after queue wait", {
|
|
1462
|
+
waitMs: admission.waitMs,
|
|
1463
|
+
active: opts.requestAdmission.activeCount,
|
|
1464
|
+
queued: opts.requestAdmission.queuedCount,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
if (
|
|
1469
|
+
error instanceof ExecutorAdmissionError &&
|
|
1470
|
+
error.code === "executor_cancelled"
|
|
1471
|
+
) {
|
|
1472
|
+
throw request.signal.reason ?? error;
|
|
1473
|
+
}
|
|
1474
|
+
if (error instanceof ExecutorAdmissionError) {
|
|
1475
|
+
if (error.code === "executor_overloaded") {
|
|
1476
|
+
warnAdmissionRejected(error);
|
|
1477
|
+
}
|
|
1478
|
+
return withMcpCors(requestAdmissionFailure(error));
|
|
1479
|
+
}
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
// Authenticate BEFORE resolving ?toolkit=: an unauthenticated caller
|
|
1484
|
+
// must not be able to probe which toolkit names exist.
|
|
1485
|
+
const authz = await authorize(request, baseUrl, auth, opts.logger);
|
|
1486
|
+
if (!authz.ok) {
|
|
1487
|
+
return releaseAdmissionWithResponse(
|
|
1488
|
+
withMcpCors(authz.response),
|
|
1489
|
+
admission,
|
|
1490
|
+
request.signal,
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
const selected = resolveToolkitScope(
|
|
1494
|
+
url,
|
|
1495
|
+
registry,
|
|
1496
|
+
opts.toolkits,
|
|
1497
|
+
opts.logger,
|
|
1498
|
+
{
|
|
1499
|
+
actor: authz.actor,
|
|
1500
|
+
...(authz.toolkitBinding
|
|
1501
|
+
? { binding: authz.toolkitBinding }
|
|
1502
|
+
: {}),
|
|
1503
|
+
},
|
|
1504
|
+
);
|
|
1505
|
+
if (!selected.ok) {
|
|
1506
|
+
return releaseAdmissionWithResponse(
|
|
1507
|
+
withMcpCors(selected.response),
|
|
1508
|
+
admission,
|
|
1509
|
+
request.signal,
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
sweepCredentials();
|
|
1513
|
+
return releaseAdmissionWithResponse(
|
|
1514
|
+
withMcpCors(
|
|
1515
|
+
await serveMcp(
|
|
1516
|
+
request,
|
|
1517
|
+
opts,
|
|
1518
|
+
baseUrl,
|
|
1519
|
+
authz.actor,
|
|
1520
|
+
selected.scope,
|
|
1521
|
+
runtimeContext,
|
|
1522
|
+
),
|
|
1523
|
+
),
|
|
1524
|
+
admission,
|
|
1525
|
+
request.signal,
|
|
1526
|
+
);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
admission.release();
|
|
1529
|
+
throw error;
|
|
1530
|
+
}
|
|
1343
1531
|
}
|
|
1344
1532
|
|
|
1345
1533
|
// Connector-owned public routes, dispatched last: a connector can add a
|
package/src/types.ts
CHANGED
|
@@ -268,6 +268,29 @@ export interface Executor {
|
|
|
268
268
|
close?(): void | Promise<void>;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/** Payload-free, monotonically increasing admission observations. */
|
|
272
|
+
export interface AdmissionSnapshot {
|
|
273
|
+
concurrency: number;
|
|
274
|
+
maxQueueSize: number;
|
|
275
|
+
queueTimeoutMs: number;
|
|
276
|
+
retryAfterMs: number;
|
|
277
|
+
active: number;
|
|
278
|
+
queued: number;
|
|
279
|
+
closed: boolean;
|
|
280
|
+
totals: {
|
|
281
|
+
admitted: number;
|
|
282
|
+
queued: number;
|
|
283
|
+
rejected: number;
|
|
284
|
+
cancelled: number;
|
|
285
|
+
closed: number;
|
|
286
|
+
};
|
|
287
|
+
queueWaitMs: {
|
|
288
|
+
count: number;
|
|
289
|
+
total: number;
|
|
290
|
+
max: number;
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
271
294
|
/**
|
|
272
295
|
* Optional admission capability used by bounded executors. The acquired lease
|
|
273
296
|
* carries execution so an already-admitted caller cannot accidentally acquire
|
|
@@ -275,9 +298,13 @@ export interface Executor {
|
|
|
275
298
|
*/
|
|
276
299
|
export interface AdmittingExecutor extends Executor {
|
|
277
300
|
acquire(options?: { signal?: AbortSignal }): Promise<ExecutorLease>;
|
|
301
|
+
/** Payload-free health/metrics view when the executor exposes one. */
|
|
302
|
+
admissionSnapshot?(): AdmissionSnapshot;
|
|
278
303
|
}
|
|
279
304
|
|
|
280
305
|
export interface ExecutorLease {
|
|
306
|
+
/** Time spent waiting before this lease was granted, when observed. */
|
|
307
|
+
readonly waitMs?: number;
|
|
281
308
|
execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
|
|
282
309
|
/** Idempotent. Call from finally even when provider construction fails. */
|
|
283
310
|
release(): void;
|
package/src/version.ts
CHANGED