@zackbart/connecta 0.7.1 → 0.7.4
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 +122 -0
- package/README.md +5 -2
- package/dist/connector-scope.d.ts +7 -4
- package/dist/connector-scope.d.ts.map +1 -1
- package/dist/connector-scope.js +36 -16
- package/dist/connector-scope.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +164 -95
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +11 -7
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +26 -19
- package/dist/credential-health.js.map +1 -1
- package/dist/execute.d.ts +3 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +70 -29
- package/dist/execute.js.map +1 -1
- package/dist/executor-admission.d.ts +53 -0
- package/dist/executor-admission.d.ts.map +1 -0
- package/dist/executor-admission.js +151 -0
- package/dist/executor-admission.js.map +1 -0
- package/dist/executor-result.d.ts +13 -0
- package/dist/executor-result.d.ts.map +1 -0
- package/dist/executor-result.js +57 -0
- package/dist/executor-result.js.map +1 -0
- package/dist/executors/quickjs-child.d.ts +2 -0
- package/dist/executors/quickjs-child.d.ts.map +1 -0
- package/dist/executors/quickjs-child.js +131 -0
- package/dist/executors/quickjs-child.js.map +1 -0
- package/dist/executors/quickjs-protocol.d.ts +55 -0
- package/dist/executors/quickjs-protocol.d.ts.map +1 -0
- package/dist/executors/quickjs-protocol.js +22 -0
- package/dist/executors/quickjs-protocol.js.map +1 -0
- package/dist/executors/quickjs-runtime.d.ts +26 -0
- package/dist/executors/quickjs-runtime.d.ts.map +1 -0
- package/dist/executors/quickjs-runtime.js +383 -0
- package/dist/executors/quickjs-runtime.js.map +1 -0
- package/dist/executors/quickjs.d.ts +20 -15
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +465 -265
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +49 -12
- package/dist/meta-tools.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +36 -3
- package/dist/node.js.map +1 -1
- package/dist/registry.d.ts +21 -24
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +37 -25
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +19 -9
- package/dist/server.js.map +1 -1
- package/dist/timeout.d.ts +9 -4
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +34 -4
- package/dist/timeout.js.map +1 -1
- package/dist/toolkits.d.ts +8 -0
- package/dist/toolkits.d.ts.map +1 -1
- package/dist/toolkits.js +3 -0
- package/dist/toolkits.js.map +1 -1
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +13 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +188 -7
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/connector-scope.ts +43 -18
- package/src/connectors/remote-mcp.ts +184 -102
- package/src/credential-health.ts +33 -17
- package/src/execute.ts +93 -35
- package/src/executor-admission.ts +229 -0
- package/src/executor-result.ts +63 -0
- package/src/executors/quickjs-child.ts +168 -0
- package/src/executors/quickjs-protocol.ts +77 -0
- package/src/executors/quickjs-runtime.ts +440 -0
- package/src/executors/quickjs.ts +669 -302
- package/src/index.ts +30 -5
- package/src/meta-tools.ts +84 -27
- package/src/node.ts +36 -2
- package/src/registry.ts +53 -22
- package/src/server.ts +21 -10
- package/src/timeout.ts +41 -4
- package/src/toolkits.ts +11 -0
- package/src/types.ts +19 -2
- package/src/ui.ts +218 -12
- package/src/version.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
CredentialCheckResult,
|
|
20
20
|
CredentialHealthConfig,
|
|
21
21
|
} from "./credential-health.js";
|
|
22
|
+
import type { DeferredWork } from "./connector-scope.js";
|
|
22
23
|
import type {
|
|
23
24
|
Connector,
|
|
24
25
|
ConnectaBranding,
|
|
@@ -82,7 +83,8 @@ export interface ConnectaDiscoveryConfig {
|
|
|
82
83
|
* Deadline (ms) for each downstream probe/catalog call fanned out by
|
|
83
84
|
* `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
|
|
84
85
|
* 30_000. A timed-out connector degrades independently; this does not apply
|
|
85
|
-
* to tool calls
|
|
86
|
+
* to tool calls. Catalog walks receive the same cancellation signal, which
|
|
87
|
+
* aborts an in-flight page where supported and prevents another from starting.
|
|
86
88
|
*/
|
|
87
89
|
probeTimeoutMs?: number;
|
|
88
90
|
}
|
|
@@ -191,7 +193,10 @@ export interface Connecta {
|
|
|
191
193
|
*
|
|
192
194
|
* ```ts
|
|
193
195
|
* // Cloudflare Workers (wrangler.jsonc: "triggers": { "crons": ["*\/15 * * * *"] })
|
|
194
|
-
* async scheduled(_c, env, ctx) {
|
|
196
|
+
* async scheduled(_c, env, ctx) {
|
|
197
|
+
* const defer = ctx.waitUntil.bind(ctx);
|
|
198
|
+
* ctx.waitUntil(build(env).checkCredentials({ defer }));
|
|
199
|
+
* }
|
|
195
200
|
* // Node
|
|
196
201
|
* setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
|
|
197
202
|
* ```
|
|
@@ -207,7 +212,14 @@ export interface Connecta {
|
|
|
207
212
|
baseUrl?: string;
|
|
208
213
|
force?: boolean;
|
|
209
214
|
ids?: string[];
|
|
215
|
+
/**
|
|
216
|
+
* Runtime continuation for bounded teardown after the check result is ready.
|
|
217
|
+
* Workers should pass `ctx.waitUntil.bind(ctx)`.
|
|
218
|
+
*/
|
|
219
|
+
defer?: DeferredWork;
|
|
210
220
|
}) => Promise<CredentialCheckResult[]>;
|
|
221
|
+
/** Drain and release configured executor resources. Idempotent. */
|
|
222
|
+
close: () => Promise<void>;
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
function defaultLogger(): Logger {
|
|
@@ -504,6 +516,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
504
516
|
branding: config.branding,
|
|
505
517
|
...(toolkits ? { toolkits } : {}),
|
|
506
518
|
});
|
|
519
|
+
let closePromise: Promise<void> | undefined;
|
|
507
520
|
return {
|
|
508
521
|
fetch: (request, _env, ctx) =>
|
|
509
522
|
handler(
|
|
@@ -531,10 +544,20 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
531
544
|
),
|
|
532
545
|
);
|
|
533
546
|
}
|
|
534
|
-
return registry.checkCredentialHealth(
|
|
535
|
-
|
|
536
|
-
|
|
547
|
+
return registry.checkCredentialHealth(
|
|
548
|
+
baseUrl,
|
|
549
|
+
{
|
|
550
|
+
...(opts.force !== undefined ? { force: opts.force } : {}),
|
|
551
|
+
...(opts.ids ? { ids: opts.ids } : {}),
|
|
552
|
+
},
|
|
553
|
+
opts.defer,
|
|
554
|
+
);
|
|
555
|
+
},
|
|
556
|
+
close: async () => {
|
|
557
|
+
closePromise ??= Promise.resolve().then(async () => {
|
|
558
|
+
await config.executor?.close?.();
|
|
537
559
|
});
|
|
560
|
+
await closePromise;
|
|
538
561
|
},
|
|
539
562
|
};
|
|
540
563
|
}
|
|
@@ -590,8 +613,10 @@ export type {
|
|
|
590
613
|
ConnectorContext,
|
|
591
614
|
ConnectorStatus,
|
|
592
615
|
CredentialTestResult,
|
|
616
|
+
AdmittingExecutor,
|
|
593
617
|
ExecuteResult,
|
|
594
618
|
Executor,
|
|
619
|
+
ExecutorLease,
|
|
595
620
|
ExecutorProvider,
|
|
596
621
|
InboundAuth,
|
|
597
622
|
ToolkitBinding,
|
package/src/meta-tools.ts
CHANGED
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
type ActivityCallSource,
|
|
7
7
|
type ActivityRequestContext,
|
|
8
8
|
} from "./activity.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
closeConnectorScope,
|
|
11
|
+
type DeferredWork,
|
|
12
|
+
} from "./connector-scope.js";
|
|
10
13
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
11
14
|
import {
|
|
12
15
|
classifyCallError,
|
|
@@ -30,7 +33,7 @@ import {
|
|
|
30
33
|
import {
|
|
31
34
|
DEFAULT_PROBE_TIMEOUT_MS,
|
|
32
35
|
normalizeTimeoutMs,
|
|
33
|
-
|
|
36
|
+
withAbortableTimeout,
|
|
34
37
|
} from "./timeout.js";
|
|
35
38
|
import { credentialVerdictApplies } from "./credential-health.js";
|
|
36
39
|
import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
|
|
@@ -586,6 +589,8 @@ export function createMetaTools(
|
|
|
586
589
|
/** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
|
|
587
590
|
probeTimeoutMs?: number;
|
|
588
591
|
activity?: ActivityRequestContext;
|
|
592
|
+
/** Runtime continuation for the bounded tail of probe-owned teardown. */
|
|
593
|
+
defer?: DeferredWork;
|
|
589
594
|
} = {},
|
|
590
595
|
) {
|
|
591
596
|
// Already normalized and warned about at registry construction.
|
|
@@ -597,6 +602,18 @@ export function createMetaTools(
|
|
|
597
602
|
// identity lets remote connectors reuse one downstream client inside that
|
|
598
603
|
// request without leaking request-bound I/O into the next one.
|
|
599
604
|
const requestScope = {};
|
|
605
|
+
const withProbeDeadline = <T>(
|
|
606
|
+
label: string,
|
|
607
|
+
operation: (options: {
|
|
608
|
+
signal: AbortSignal;
|
|
609
|
+
timeoutMs: number;
|
|
610
|
+
}) => Promise<T>,
|
|
611
|
+
) =>
|
|
612
|
+
withAbortableTimeout(
|
|
613
|
+
(signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
|
|
614
|
+
probeTimeoutMs,
|
|
615
|
+
label,
|
|
616
|
+
);
|
|
600
617
|
|
|
601
618
|
interface RunCallOutcome {
|
|
602
619
|
toolResult: ToolResult;
|
|
@@ -907,8 +924,8 @@ export function createMetaTools(
|
|
|
907
924
|
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
908
925
|
const connectors = registry.listConnectors();
|
|
909
926
|
const scope = probe ? {} : requestScope;
|
|
910
|
-
const
|
|
911
|
-
|
|
927
|
+
const pending = connectors.map(
|
|
928
|
+
async (c) => {
|
|
912
929
|
const statusStarted = Date.now();
|
|
913
930
|
const observed = registry.healthFor(c.id);
|
|
914
931
|
const verdict = await registry.credentialHealthFor(c.id);
|
|
@@ -917,10 +934,10 @@ export function createMetaTools(
|
|
|
917
934
|
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
918
935
|
if (probe) {
|
|
919
936
|
try {
|
|
920
|
-
status = await
|
|
921
|
-
registry.statusFor(c.id, baseUrl, scope),
|
|
922
|
-
probeTimeoutMs,
|
|
937
|
+
status = await withProbeDeadline(
|
|
923
938
|
`list_connectors probe of "${c.id}"`,
|
|
939
|
+
(options) =>
|
|
940
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
924
941
|
);
|
|
925
942
|
} catch (err) {
|
|
926
943
|
// A probe that outran probeTimeoutMs (or otherwise threw)
|
|
@@ -1004,14 +1021,45 @@ export function createMetaTools(
|
|
|
1004
1021
|
// the first (now stale) authorization URL.
|
|
1005
1022
|
if (probe && status.state === "ok") {
|
|
1006
1023
|
try {
|
|
1007
|
-
tools = await
|
|
1008
|
-
registry.refreshTools(c.id, baseUrl, scope),
|
|
1009
|
-
probeTimeoutMs,
|
|
1024
|
+
tools = await withProbeDeadline(
|
|
1010
1025
|
`list_connectors catalog refresh of "${c.id}"`,
|
|
1026
|
+
(options) =>
|
|
1027
|
+
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
1011
1028
|
);
|
|
1012
1029
|
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
1013
1030
|
} catch (err) {
|
|
1014
|
-
|
|
1031
|
+
const details = classifyCallError(err);
|
|
1032
|
+
if (details.code === "auth_required") {
|
|
1033
|
+
let authStatus: ConnectorStatus | undefined;
|
|
1034
|
+
try {
|
|
1035
|
+
authStatus = await withProbeDeadline(
|
|
1036
|
+
`list_connectors authorization status of "${c.id}"`,
|
|
1037
|
+
(options) =>
|
|
1038
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
1039
|
+
);
|
|
1040
|
+
} catch {
|
|
1041
|
+
// The typed auth verdict is still authoritative; this second
|
|
1042
|
+
// read exists only to recover the connector's pending URL.
|
|
1043
|
+
}
|
|
1044
|
+
status =
|
|
1045
|
+
authStatus?.state === "auth_required"
|
|
1046
|
+
? authStatus
|
|
1047
|
+
: {
|
|
1048
|
+
state: "auth_required" as const,
|
|
1049
|
+
message: details.message,
|
|
1050
|
+
};
|
|
1051
|
+
await registry.recordCredentialHealth(c.id, {
|
|
1052
|
+
state: "auth_required",
|
|
1053
|
+
checkedAt,
|
|
1054
|
+
...(status.message ? { message: status.message } : {}),
|
|
1055
|
+
...("authorizationUrl" in status &&
|
|
1056
|
+
status.authorizationUrl
|
|
1057
|
+
? { authorizationUrl: status.authorizationUrl }
|
|
1058
|
+
: {}),
|
|
1059
|
+
});
|
|
1060
|
+
} else {
|
|
1061
|
+
status = { state: "error" as const, message: msg(err) };
|
|
1062
|
+
}
|
|
1015
1063
|
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
1016
1064
|
}
|
|
1017
1065
|
}
|
|
@@ -1036,17 +1084,24 @@ export function createMetaTools(
|
|
|
1036
1084
|
: {}),
|
|
1037
1085
|
...(status.message ? { message: status.message } : {}),
|
|
1038
1086
|
};
|
|
1039
|
-
}
|
|
1040
|
-
)
|
|
1041
|
-
|
|
1042
|
-
await Promise.all(
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1087
|
+
},
|
|
1088
|
+
);
|
|
1089
|
+
if (!probe) {
|
|
1090
|
+
return jsonResult({ connectors: await Promise.all(pending) });
|
|
1091
|
+
}
|
|
1092
|
+
const settled = await Promise.allSettled(pending);
|
|
1093
|
+
await Promise.all(
|
|
1094
|
+
connectors.map((connector) =>
|
|
1095
|
+
closeConnectorScope(
|
|
1096
|
+
connector,
|
|
1097
|
+
registry.contextFor(connector.id, baseUrl, scope),
|
|
1098
|
+
opts.defer,
|
|
1048
1099
|
),
|
|
1049
|
-
)
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
const out = settled.map((result) => {
|
|
1103
|
+
if (result.status === "rejected") throw result.reason;
|
|
1104
|
+
return result.value;
|
|
1050
1105
|
});
|
|
1051
1106
|
return jsonResult({ connectors: out });
|
|
1052
1107
|
},
|
|
@@ -1079,10 +1134,10 @@ export function createMetaTools(
|
|
|
1079
1134
|
}> = [];
|
|
1080
1135
|
const catalogs = await Promise.allSettled(
|
|
1081
1136
|
conns.map((c) =>
|
|
1082
|
-
|
|
1083
|
-
registry.getTools(c.id, baseUrl, requestScope),
|
|
1084
|
-
probeTimeoutMs,
|
|
1137
|
+
withProbeDeadline(
|
|
1085
1138
|
`search_tools probe of "${c.id}"`,
|
|
1139
|
+
(options) =>
|
|
1140
|
+
registry.getTools(c.id, baseUrl, requestScope, options),
|
|
1086
1141
|
),
|
|
1087
1142
|
),
|
|
1088
1143
|
);
|
|
@@ -1208,10 +1263,10 @@ export function createMetaTools(
|
|
|
1208
1263
|
];
|
|
1209
1264
|
const loaded = await Promise.allSettled(
|
|
1210
1265
|
connectorIds.map((id) =>
|
|
1211
|
-
|
|
1212
|
-
registry.getTools(id, baseUrl, requestScope),
|
|
1213
|
-
probeTimeoutMs,
|
|
1266
|
+
withProbeDeadline(
|
|
1214
1267
|
`describe_tools probe of "${id}"`,
|
|
1268
|
+
(options) =>
|
|
1269
|
+
registry.getTools(id, baseUrl, requestScope, options),
|
|
1215
1270
|
),
|
|
1216
1271
|
),
|
|
1217
1272
|
);
|
|
@@ -1538,12 +1593,14 @@ export function registerMetaTools(
|
|
|
1538
1593
|
defaultToolTimeoutMs?: number;
|
|
1539
1594
|
probeTimeoutMs?: number;
|
|
1540
1595
|
activity?: ActivityRequestContext;
|
|
1596
|
+
defer?: DeferredWork;
|
|
1541
1597
|
},
|
|
1542
1598
|
): void {
|
|
1543
1599
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1544
1600
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1545
1601
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1546
1602
|
activity: ctx.activity,
|
|
1603
|
+
defer: ctx.defer,
|
|
1547
1604
|
});
|
|
1548
1605
|
|
|
1549
1606
|
server.registerTool(
|
package/src/node.ts
CHANGED
|
@@ -32,6 +32,7 @@ const BODY_TOO_LARGE = Symbol("body-too-large");
|
|
|
32
32
|
async function toRequest(
|
|
33
33
|
req: IncomingMessage,
|
|
34
34
|
maxBodyBytes: number,
|
|
35
|
+
signal: AbortSignal,
|
|
35
36
|
): Promise<Request | typeof BODY_TOO_LARGE> {
|
|
36
37
|
const host = req.headers.host ?? "localhost";
|
|
37
38
|
const proto =
|
|
@@ -60,6 +61,7 @@ async function toRequest(
|
|
|
60
61
|
method,
|
|
61
62
|
headers,
|
|
62
63
|
body,
|
|
64
|
+
signal,
|
|
63
65
|
// Required by Node's fetch when sending a body stream.
|
|
64
66
|
...(body ? { duplex: "half" } : {}),
|
|
65
67
|
} as RequestInit);
|
|
@@ -111,8 +113,27 @@ export function listen(
|
|
|
111
113
|
|
|
112
114
|
const server = createServer((req, res) => {
|
|
113
115
|
void (async () => {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const abortRequest = () => {
|
|
118
|
+
if (!controller.signal.aborted) {
|
|
119
|
+
controller.abort(new Error("HTTP client disconnected."));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const abortIncompleteRequest = () => {
|
|
123
|
+
if (!req.complete) abortRequest();
|
|
124
|
+
};
|
|
125
|
+
const abortIncompleteResponse = () => {
|
|
126
|
+
if (!res.writableFinished) abortRequest();
|
|
127
|
+
};
|
|
128
|
+
req.once("aborted", abortRequest);
|
|
129
|
+
req.once("close", abortIncompleteRequest);
|
|
130
|
+
res.once("close", abortIncompleteResponse);
|
|
114
131
|
try {
|
|
115
|
-
const request = await toRequest(
|
|
132
|
+
const request = await toRequest(
|
|
133
|
+
req,
|
|
134
|
+
maxBodyBytes,
|
|
135
|
+
controller.signal,
|
|
136
|
+
);
|
|
116
137
|
if (request === BODY_TOO_LARGE) {
|
|
117
138
|
res.statusCode = 413;
|
|
118
139
|
// The request stream was abandoned mid-body, so this connection
|
|
@@ -125,6 +146,12 @@ export function listen(
|
|
|
125
146
|
const response = await connecta.fetch(request, undefined, ctx);
|
|
126
147
|
await writeResponse(res, response);
|
|
127
148
|
} catch (error) {
|
|
149
|
+
// A client that deliberately went away can make either the request
|
|
150
|
+
// handler or response pipeline reject. The request's abort signal is
|
|
151
|
+
// the authoritative classification: there is no caller left to answer,
|
|
152
|
+
// and logging an expected disconnect would turn cancellation into an
|
|
153
|
+
// error-log spam vector.
|
|
154
|
+
if (controller.signal.aborted) return;
|
|
128
155
|
// A headless deployment has nothing else to go on; never swallow this.
|
|
129
156
|
console.error("[connecta] request failed", error);
|
|
130
157
|
if (res.headersSent) {
|
|
@@ -136,6 +163,10 @@ export function listen(
|
|
|
136
163
|
}
|
|
137
164
|
res.statusCode = 500;
|
|
138
165
|
res.end("Internal Server Error");
|
|
166
|
+
} finally {
|
|
167
|
+
req.removeListener("aborted", abortRequest);
|
|
168
|
+
req.removeListener("close", abortIncompleteRequest);
|
|
169
|
+
res.removeListener("close", abortIncompleteResponse);
|
|
139
170
|
}
|
|
140
171
|
})();
|
|
141
172
|
});
|
|
@@ -152,11 +183,14 @@ export function listen(
|
|
|
152
183
|
process.exit(1);
|
|
153
184
|
}, timeoutMs);
|
|
154
185
|
forced.unref?.();
|
|
186
|
+
// Stop queued/running sandbox work now, not after server.close has
|
|
187
|
+
// waited for the very requests those children are holding open.
|
|
188
|
+
const executorClose = connecta.close();
|
|
155
189
|
// close() alone waits on idle keep-alive sockets, so a single browser
|
|
156
190
|
// tab would stall every shutdown until the force-exit deadline.
|
|
157
191
|
server.closeIdleConnections();
|
|
158
192
|
server.close(() => {
|
|
159
|
-
void Promise.allSettled([...pending]).then(() => {
|
|
193
|
+
void Promise.allSettled([...pending, executorClose]).then(() => {
|
|
160
194
|
clearTimeout(forced);
|
|
161
195
|
process.exit(0);
|
|
162
196
|
});
|
package/src/registry.ts
CHANGED
|
@@ -6,7 +6,11 @@ import type {
|
|
|
6
6
|
Logger,
|
|
7
7
|
ToolDef,
|
|
8
8
|
} from "./types.js";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
storedCredentialShape,
|
|
11
|
+
type CredentialVault,
|
|
12
|
+
} from "./credentials.js";
|
|
13
|
+
import { ConnectorCallError } from "./errors.js";
|
|
10
14
|
import {
|
|
11
15
|
CredentialHealthChecker,
|
|
12
16
|
type CredentialCheckOptions,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
type CredentialHealthConfig,
|
|
15
19
|
type CredentialHealthRecord,
|
|
16
20
|
} from "./credential-health.js";
|
|
21
|
+
import type { DeferredWork } from "./connector-scope.js";
|
|
17
22
|
import { splitAddress, type Toolkit } from "./toolkits.js";
|
|
18
23
|
|
|
19
24
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
@@ -154,6 +159,11 @@ function msg(err: unknown): string {
|
|
|
154
159
|
return err instanceof Error ? err.message : String(err);
|
|
155
160
|
}
|
|
156
161
|
|
|
162
|
+
type ConnectorOperationOptions = Pick<
|
|
163
|
+
ConnectorContext,
|
|
164
|
+
"signal" | "timeoutMs"
|
|
165
|
+
>;
|
|
166
|
+
|
|
157
167
|
/**
|
|
158
168
|
* The registry surface a per-connection MCP server consumes: every meta-tool
|
|
159
169
|
* (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
|
|
@@ -178,18 +188,20 @@ export interface RegistryView {
|
|
|
178
188
|
id: string,
|
|
179
189
|
baseUrl: string,
|
|
180
190
|
requestScope?: object,
|
|
191
|
+
callOptions?: ConnectorOperationOptions,
|
|
181
192
|
): Promise<ToolDef[]>;
|
|
182
193
|
refreshTools(
|
|
183
194
|
id: string,
|
|
184
195
|
baseUrl: string,
|
|
185
196
|
requestScope?: object,
|
|
197
|
+
callOptions?: ConnectorOperationOptions,
|
|
186
198
|
): Promise<ToolDef[]>;
|
|
187
199
|
peekTools(id: string): ToolDef[] | undefined;
|
|
188
200
|
contextFor(
|
|
189
201
|
id: string,
|
|
190
202
|
baseUrl: string,
|
|
191
203
|
requestScope?: object,
|
|
192
|
-
callOptions?:
|
|
204
|
+
callOptions?: ConnectorOperationOptions,
|
|
193
205
|
): ConnectorContext;
|
|
194
206
|
resultsStorage(): KVStorage;
|
|
195
207
|
recordSuccess(id: string, latencyMs: number): void;
|
|
@@ -209,6 +221,7 @@ export interface RegistryView {
|
|
|
209
221
|
id: string,
|
|
210
222
|
baseUrl: string,
|
|
211
223
|
requestScope?: object,
|
|
224
|
+
callOptions?: ConnectorOperationOptions,
|
|
212
225
|
): Promise<ConnectorStatus>;
|
|
213
226
|
invalidateStored(id: string): Promise<void>;
|
|
214
227
|
}
|
|
@@ -352,21 +365,31 @@ export class Registry implements RegistryView {
|
|
|
352
365
|
id: string,
|
|
353
366
|
baseUrl: string,
|
|
354
367
|
requestScope: object = {},
|
|
355
|
-
callOptions:
|
|
368
|
+
callOptions: ConnectorOperationOptions = {},
|
|
356
369
|
): ConnectorContext {
|
|
370
|
+
const credentialConfig = this.connectors.get(id)?.credential;
|
|
371
|
+
let credentialAccess: ConnectorContext["credential"];
|
|
372
|
+
if (this.opts.credentialVault && credentialConfig) {
|
|
373
|
+
const vault = this.opts.credentialVault;
|
|
374
|
+
const readValues = async () => {
|
|
375
|
+
const values = await vault.getAll(id);
|
|
376
|
+
const shape = storedCredentialShape(credentialConfig, values);
|
|
377
|
+
if (shape.state === "mismatch") {
|
|
378
|
+
throw new ConnectorCallError("auth_required", shape.message);
|
|
379
|
+
}
|
|
380
|
+
return values;
|
|
381
|
+
};
|
|
382
|
+
credentialAccess = {
|
|
383
|
+
get: async (field = "value") =>
|
|
384
|
+
(await readValues())?.[field] ?? null,
|
|
385
|
+
getAll: readValues,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
357
388
|
return {
|
|
358
389
|
storage: namespaced(this.opts.storage, `conn:${id}:`),
|
|
359
390
|
logger: this.opts.logger,
|
|
360
391
|
baseUrl,
|
|
361
|
-
...(
|
|
362
|
-
? {
|
|
363
|
-
credential: {
|
|
364
|
-
get: (field?: string) =>
|
|
365
|
-
this.opts.credentialVault!.get(id, field),
|
|
366
|
-
getAll: () => this.opts.credentialVault!.getAll(id),
|
|
367
|
-
},
|
|
368
|
-
}
|
|
369
|
-
: {}),
|
|
392
|
+
...(credentialAccess ? { credential: credentialAccess } : {}),
|
|
370
393
|
requestScope,
|
|
371
394
|
...callOptions,
|
|
372
395
|
};
|
|
@@ -441,13 +464,14 @@ export class Registry implements RegistryView {
|
|
|
441
464
|
id: string,
|
|
442
465
|
baseUrl: string,
|
|
443
466
|
requestScope?: object,
|
|
467
|
+
callOptions: ConnectorOperationOptions = {},
|
|
444
468
|
): Promise<ToolDef[]> {
|
|
445
469
|
const connector = this.connectors.get(id);
|
|
446
470
|
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
447
471
|
const tools = connector.staticTools
|
|
448
472
|
? connector.staticTools
|
|
449
473
|
: await connector.listTools(
|
|
450
|
-
this.contextFor(id, baseUrl, requestScope),
|
|
474
|
+
this.contextFor(id, baseUrl, requestScope, callOptions),
|
|
451
475
|
);
|
|
452
476
|
const now = Date.now();
|
|
453
477
|
const previous = this.cache.get(id);
|
|
@@ -479,6 +503,7 @@ export class Registry implements RegistryView {
|
|
|
479
503
|
id: string,
|
|
480
504
|
baseUrl: string,
|
|
481
505
|
requestScope?: object,
|
|
506
|
+
callOptions: ConnectorOperationOptions = {},
|
|
482
507
|
): Promise<ToolDef[]> {
|
|
483
508
|
const connector = this.connectors.get(id);
|
|
484
509
|
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
@@ -512,7 +537,7 @@ export class Registry implements RegistryView {
|
|
|
512
537
|
}
|
|
513
538
|
|
|
514
539
|
try {
|
|
515
|
-
return await this.refreshTools(id, baseUrl, requestScope);
|
|
540
|
+
return await this.refreshTools(id, baseUrl, requestScope, callOptions);
|
|
516
541
|
} catch (err) {
|
|
517
542
|
if (stale) {
|
|
518
543
|
this.opts.logger.warn(
|
|
@@ -594,8 +619,9 @@ export class Registry implements RegistryView {
|
|
|
594
619
|
checkCredentialHealth(
|
|
595
620
|
baseUrl: string,
|
|
596
621
|
opts?: CredentialCheckOptions,
|
|
622
|
+
defer?: DeferredWork,
|
|
597
623
|
): Promise<CredentialCheckResult[]> {
|
|
598
|
-
return this.credentialHealth.check(baseUrl, opts);
|
|
624
|
+
return this.credentialHealth.check(baseUrl, opts, defer);
|
|
599
625
|
}
|
|
600
626
|
|
|
601
627
|
/**
|
|
@@ -606,8 +632,9 @@ export class Registry implements RegistryView {
|
|
|
606
632
|
*/
|
|
607
633
|
sweepCredentialHealthIfDue(
|
|
608
634
|
baseUrl: string,
|
|
635
|
+
defer?: DeferredWork,
|
|
609
636
|
): Promise<CredentialCheckResult[]> | undefined {
|
|
610
|
-
return this.credentialHealth.sweepIfDue(baseUrl);
|
|
637
|
+
return this.credentialHealth.sweepIfDue(baseUrl, defer);
|
|
611
638
|
}
|
|
612
639
|
|
|
613
640
|
/**
|
|
@@ -626,10 +653,11 @@ export class Registry implements RegistryView {
|
|
|
626
653
|
id: string,
|
|
627
654
|
baseUrl: string,
|
|
628
655
|
requestScope: object = {},
|
|
656
|
+
callOptions: ConnectorOperationOptions = {},
|
|
629
657
|
): Promise<ConnectorStatus> {
|
|
630
658
|
const connector = this.connectors.get(id);
|
|
631
659
|
if (!connector) return { state: "error", message: "Unknown connector" };
|
|
632
|
-
const ctx = this.contextFor(id, baseUrl, requestScope);
|
|
660
|
+
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions);
|
|
633
661
|
if (connector.status) {
|
|
634
662
|
try {
|
|
635
663
|
return await connector.status(ctx);
|
|
@@ -638,7 +666,7 @@ export class Registry implements RegistryView {
|
|
|
638
666
|
}
|
|
639
667
|
}
|
|
640
668
|
try {
|
|
641
|
-
await this.getTools(id, baseUrl, requestScope);
|
|
669
|
+
await this.getTools(id, baseUrl, requestScope, callOptions);
|
|
642
670
|
return { state: "ok" };
|
|
643
671
|
} catch (err) {
|
|
644
672
|
return { state: "error", message: msg(err) };
|
|
@@ -782,11 +810,12 @@ export class ScopedRegistry implements RegistryView {
|
|
|
782
810
|
id: string,
|
|
783
811
|
baseUrl: string,
|
|
784
812
|
requestScope?: object,
|
|
813
|
+
callOptions: ConnectorOperationOptions = {},
|
|
785
814
|
): Promise<ToolDef[]> {
|
|
786
815
|
if (!this.visible(id)) throw this.unknownConnector(id);
|
|
787
816
|
return this.inScopeTools(
|
|
788
817
|
id,
|
|
789
|
-
await this.base.getTools(id, baseUrl, requestScope),
|
|
818
|
+
await this.base.getTools(id, baseUrl, requestScope, callOptions),
|
|
790
819
|
);
|
|
791
820
|
}
|
|
792
821
|
|
|
@@ -794,11 +823,12 @@ export class ScopedRegistry implements RegistryView {
|
|
|
794
823
|
id: string,
|
|
795
824
|
baseUrl: string,
|
|
796
825
|
requestScope?: object,
|
|
826
|
+
callOptions: ConnectorOperationOptions = {},
|
|
797
827
|
): Promise<ToolDef[]> {
|
|
798
828
|
if (!this.visible(id)) throw this.unknownConnector(id);
|
|
799
829
|
return this.inScopeTools(
|
|
800
830
|
id,
|
|
801
|
-
await this.base.refreshTools(id, baseUrl, requestScope),
|
|
831
|
+
await this.base.refreshTools(id, baseUrl, requestScope, callOptions),
|
|
802
832
|
);
|
|
803
833
|
}
|
|
804
834
|
|
|
@@ -812,7 +842,7 @@ export class ScopedRegistry implements RegistryView {
|
|
|
812
842
|
id: string,
|
|
813
843
|
baseUrl: string,
|
|
814
844
|
requestScope: object = {},
|
|
815
|
-
callOptions:
|
|
845
|
+
callOptions: ConnectorOperationOptions = {},
|
|
816
846
|
): ConnectorContext {
|
|
817
847
|
// Unreachable through the meta-tools (they resolve first), so a throw here
|
|
818
848
|
// is a loud backstop rather than a silent grant of connector storage and
|
|
@@ -897,12 +927,13 @@ export class ScopedRegistry implements RegistryView {
|
|
|
897
927
|
id: string,
|
|
898
928
|
baseUrl: string,
|
|
899
929
|
requestScope: object = {},
|
|
930
|
+
callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
900
931
|
): Promise<ConnectorStatus> {
|
|
901
932
|
// Same shape the unscoped registry returns for an unregistered id.
|
|
902
933
|
if (!this.visible(id)) {
|
|
903
934
|
return { state: "error", message: "Unknown connector" };
|
|
904
935
|
}
|
|
905
|
-
return this.base.statusFor(id, baseUrl, requestScope);
|
|
936
|
+
return this.base.statusFor(id, baseUrl, requestScope, callOptions);
|
|
906
937
|
}
|
|
907
938
|
|
|
908
939
|
async invalidateStored(id: string): Promise<void> {
|