@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/server.ts
CHANGED
|
@@ -222,8 +222,8 @@ async function authorize(
|
|
|
222
222
|
| {
|
|
223
223
|
ok: true;
|
|
224
224
|
actor: ActivityActor;
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
/** True only when the admitting provider can also authorize UI mutation. */
|
|
226
|
+
uiAdminEligible?: boolean;
|
|
227
227
|
/** The admitting identity's toolkit binding (docs/toolkits.md). */
|
|
228
228
|
toolkitBinding?: ToolkitBinding;
|
|
229
229
|
}
|
|
@@ -261,8 +261,9 @@ async function authorize(
|
|
|
261
261
|
kind: provider.kind,
|
|
262
262
|
...(subjectId ? { id: subjectId } : {}),
|
|
263
263
|
},
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
...(result.userId && provider.uiAuth?.kind === "clerk"
|
|
265
|
+
? { uiAdminEligible: true }
|
|
266
|
+
: {}),
|
|
266
267
|
...(binding.binding ? { toolkitBinding: binding.binding } : {}),
|
|
267
268
|
};
|
|
268
269
|
}
|
|
@@ -896,6 +897,9 @@ async function serveMcp(
|
|
|
896
897
|
activity,
|
|
897
898
|
defaultToolTimeoutMs: opts.defaultToolTimeoutMs,
|
|
898
899
|
probeTimeoutMs: opts.probeTimeoutMs,
|
|
900
|
+
...(runtimeContext
|
|
901
|
+
? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
|
|
902
|
+
: {}),
|
|
899
903
|
});
|
|
900
904
|
if (opts.executor) {
|
|
901
905
|
registerExecuteTool(server, registry, {
|
|
@@ -903,6 +907,7 @@ async function serveMcp(
|
|
|
903
907
|
executor: opts.executor,
|
|
904
908
|
logger: opts.logger,
|
|
905
909
|
activity,
|
|
910
|
+
requestSignal: request.signal,
|
|
906
911
|
});
|
|
907
912
|
}
|
|
908
913
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
@@ -1044,6 +1049,9 @@ export function createFetchHandler(
|
|
|
1044
1049
|
const url = new URL(request.url);
|
|
1045
1050
|
const baseUrl = publicUrl ?? url.origin;
|
|
1046
1051
|
const path = url.pathname;
|
|
1052
|
+
const defer = runtimeContext
|
|
1053
|
+
? runtimeContext.waitUntil.bind(runtimeContext)
|
|
1054
|
+
: undefined;
|
|
1047
1055
|
|
|
1048
1056
|
/**
|
|
1049
1057
|
* Piggyback a DUE credential liveness sweep on traffic that has already been
|
|
@@ -1059,7 +1067,7 @@ export function createFetchHandler(
|
|
|
1059
1067
|
// throws while deciding whether anything is due. Nothing about a
|
|
1060
1068
|
// background health check may turn a served request into a 500.
|
|
1061
1069
|
try {
|
|
1062
|
-
const sweep = registry.sweepCredentialHealthIfDue(baseUrl);
|
|
1070
|
+
const sweep = registry.sweepCredentialHealthIfDue(baseUrl, defer);
|
|
1063
1071
|
if (!sweep) return;
|
|
1064
1072
|
const settled = sweep.then(
|
|
1065
1073
|
() => {},
|
|
@@ -1067,7 +1075,7 @@ export function createFetchHandler(
|
|
|
1067
1075
|
opts.logger.warn("[connecta] credential health sweep failed", err);
|
|
1068
1076
|
},
|
|
1069
1077
|
);
|
|
1070
|
-
if (
|
|
1078
|
+
if (defer) defer(settled);
|
|
1071
1079
|
else void settled;
|
|
1072
1080
|
} catch (err) {
|
|
1073
1081
|
opts.logger.warn("[connecta] credential health sweep failed", err);
|
|
@@ -1197,7 +1205,7 @@ export function createFetchHandler(
|
|
|
1197
1205
|
|
|
1198
1206
|
const operatorPage = operatorPageForPath(path);
|
|
1199
1207
|
if (operatorPage) {
|
|
1200
|
-
if (request.method !== "GET") {
|
|
1208
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1201
1209
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
1202
1210
|
}
|
|
1203
1211
|
// Open shell — carries no operator data; everything comes from the
|
|
@@ -1212,7 +1220,9 @@ export function createFetchHandler(
|
|
|
1212
1220
|
// — only script execution, the XSS sink, is gated.
|
|
1213
1221
|
const nonce = uiScriptNonce();
|
|
1214
1222
|
return new Response(
|
|
1215
|
-
|
|
1223
|
+
request.method === "HEAD"
|
|
1224
|
+
? null
|
|
1225
|
+
: renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage),
|
|
1216
1226
|
{
|
|
1217
1227
|
status: 200,
|
|
1218
1228
|
headers: {
|
|
@@ -1235,8 +1245,7 @@ export function createFetchHandler(
|
|
|
1235
1245
|
// After the restriction check, not before: an identity that may not
|
|
1236
1246
|
// read this surface should not get to trigger background work from it.
|
|
1237
1247
|
sweepCredentials();
|
|
1238
|
-
const eligibleClerkOperator =
|
|
1239
|
-
authz.providerKind === "clerk" && Boolean(authz.userId);
|
|
1248
|
+
const eligibleClerkOperator = authz.uiAdminEligible === true;
|
|
1240
1249
|
const credentialManagement = credentialManagementCapability({
|
|
1241
1250
|
eligibleClerkOperator,
|
|
1242
1251
|
hasCredentialSlots: registry
|
|
@@ -1253,6 +1262,8 @@ export function createFetchHandler(
|
|
|
1253
1262
|
eligibleClerkOperator ? opts.credentialVault : undefined,
|
|
1254
1263
|
Boolean(opts.activity?.list),
|
|
1255
1264
|
credentialManagement,
|
|
1265
|
+
opts.toolkits,
|
|
1266
|
+
defer,
|
|
1256
1267
|
);
|
|
1257
1268
|
return privateJson(data);
|
|
1258
1269
|
}
|
package/src/timeout.ts
CHANGED
|
@@ -21,10 +21,9 @@ export function normalizeTimeoutMs(
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Reject `promise` after `ms` if it has not settled, so one hung downstream
|
|
24
|
-
* cannot stall a whole fan-out.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (AbortSignal plumbed through the registry) is a deferred follow-up.
|
|
24
|
+
* cannot stall a whole fan-out. This form bounds only the caller-facing wait;
|
|
25
|
+
* use `withAbortableTimeout` when the operation accepts an AbortSignal and the
|
|
26
|
+
* underlying work must stop too.
|
|
28
27
|
*/
|
|
29
28
|
export function withTimeout<T>(
|
|
30
29
|
promise: Promise<T>,
|
|
@@ -47,3 +46,41 @@ export function withTimeout<T>(
|
|
|
47
46
|
);
|
|
48
47
|
});
|
|
49
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Give one operation a caller-facing deadline and the matching cancellation
|
|
52
|
+
* signal. The timeout rejects with the stable, labelled error while aborting
|
|
53
|
+
* any in-flight work that honors the signal.
|
|
54
|
+
*/
|
|
55
|
+
export function withAbortableTimeout<T>(
|
|
56
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
57
|
+
ms: number,
|
|
58
|
+
label: string,
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
return new Promise<T>((resolve, reject) => {
|
|
62
|
+
const timeoutError = new Error(`${label} timed out after ${ms}ms`);
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
controller.abort(timeoutError);
|
|
65
|
+
reject(timeoutError);
|
|
66
|
+
}, ms);
|
|
67
|
+
let promise: Promise<T>;
|
|
68
|
+
try {
|
|
69
|
+
promise = operation(controller.signal);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(err);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
promise.then(
|
|
76
|
+
(value) => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
resolve(value);
|
|
79
|
+
},
|
|
80
|
+
(err) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
reject(err);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/toolkits.ts
CHANGED
|
@@ -41,6 +41,14 @@ export type ToolkitConfig = Record<string, ToolkitDefinition>;
|
|
|
41
41
|
export interface Toolkit {
|
|
42
42
|
readonly name: string;
|
|
43
43
|
readonly description?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The validated config this scope came from. Kept as immutable data so
|
|
46
|
+
* deployment-wide operator surfaces can explain the running configuration
|
|
47
|
+
* without inventing a second source of truth.
|
|
48
|
+
*/
|
|
49
|
+
readonly connectors: readonly string[];
|
|
50
|
+
readonly includeTools: readonly string[];
|
|
51
|
+
readonly excludeTools: readonly string[];
|
|
44
52
|
/** True when `connectorId` is inside this toolkit's scope. */
|
|
45
53
|
hasConnector(connectorId: string): boolean;
|
|
46
54
|
/** True when `<connectorId>.<toolName>` is inside this toolkit's scope. */
|
|
@@ -178,6 +186,9 @@ function resolveToolkit(
|
|
|
178
186
|
return {
|
|
179
187
|
name,
|
|
180
188
|
...(definition.description ? { description: definition.description } : {}),
|
|
189
|
+
connectors: Object.freeze([...connectorIds]),
|
|
190
|
+
includeTools: Object.freeze([...(definition.includeTools ?? [])]),
|
|
191
|
+
excludeTools: Object.freeze([...(definition.excludeTools ?? [])]),
|
|
181
192
|
hasConnector: (connectorId) => connectorIds.has(connectorId),
|
|
182
193
|
hasTool: (connectorId, toolName) => {
|
|
183
194
|
if (!connectorIds.has(connectorId)) return false;
|
package/src/types.ts
CHANGED
|
@@ -113,9 +113,9 @@ export interface ConnectorContext {
|
|
|
113
113
|
* when omitted.
|
|
114
114
|
*/
|
|
115
115
|
requestScope?: object;
|
|
116
|
-
/** Best-effort cancellation signal for this
|
|
116
|
+
/** Best-effort cancellation signal for this connector operation. */
|
|
117
117
|
signal?: AbortSignal;
|
|
118
|
-
/** Requested
|
|
118
|
+
/** Requested connector-operation deadline in milliseconds. */
|
|
119
119
|
timeoutMs?: number;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -264,6 +264,23 @@ export interface ExecutorProvider {
|
|
|
264
264
|
*/
|
|
265
265
|
export interface Executor {
|
|
266
266
|
execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
|
|
267
|
+
/** Release runtime resources. Node's built-in pool implements this. */
|
|
268
|
+
close?(): void | Promise<void>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Optional admission capability used by bounded executors. The acquired lease
|
|
273
|
+
* carries execution so an already-admitted caller cannot accidentally acquire
|
|
274
|
+
* a second slot and deadlock a pool of one.
|
|
275
|
+
*/
|
|
276
|
+
export interface AdmittingExecutor extends Executor {
|
|
277
|
+
acquire(options?: { signal?: AbortSignal }): Promise<ExecutorLease>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface ExecutorLease {
|
|
281
|
+
execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
|
|
282
|
+
/** Idempotent. Call from finally even when provider construction fails. */
|
|
283
|
+
release(): void;
|
|
267
284
|
}
|
|
268
285
|
|
|
269
286
|
/**
|
package/src/ui.ts
CHANGED
|
@@ -4,8 +4,12 @@ import {
|
|
|
4
4
|
storedCredentialShape,
|
|
5
5
|
} from "./credentials.js";
|
|
6
6
|
import type { CredentialVault } from "./credentials.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
closeConnectorScope,
|
|
9
|
+
type DeferredWork,
|
|
10
|
+
} from "./connector-scope.js";
|
|
8
11
|
import type { Registry } from "./registry.js";
|
|
12
|
+
import type { Toolkit } from "./toolkits.js";
|
|
9
13
|
import type { ConnectaBranding, UiAuthConfig } from "./types.js";
|
|
10
14
|
|
|
11
15
|
/** Connecta's default monochrome "C" mark. */
|
|
@@ -296,10 +300,21 @@ export interface UiConnector {
|
|
|
296
300
|
export interface UiData {
|
|
297
301
|
serverInfo: { name: string; version: string };
|
|
298
302
|
connectors: UiConnector[];
|
|
303
|
+
/** Read-only projection of the validated deployment config. */
|
|
304
|
+
toolkits: UiToolkit[];
|
|
299
305
|
activityEnabled: boolean;
|
|
300
306
|
credentialManagement: CredentialManagementCapability;
|
|
301
307
|
}
|
|
302
308
|
|
|
309
|
+
export interface UiToolkit {
|
|
310
|
+
name: string;
|
|
311
|
+
connectors: string[];
|
|
312
|
+
includeTools: string[];
|
|
313
|
+
excludeTools: string[];
|
|
314
|
+
/** Tools currently loaded through healthy connectors and visible in this view. */
|
|
315
|
+
toolCount: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
303
318
|
export type CredentialManagementCapability =
|
|
304
319
|
| "available"
|
|
305
320
|
| "requires_clerk"
|
|
@@ -391,10 +406,12 @@ export async function buildUiData(
|
|
|
391
406
|
credentialManagement: CredentialManagementCapability = credentialVault
|
|
392
407
|
? "available"
|
|
393
408
|
: "requires_clerk",
|
|
409
|
+
toolkits?: ReadonlyMap<string, Toolkit>,
|
|
410
|
+
defer?: DeferredWork,
|
|
394
411
|
): Promise<UiData> {
|
|
395
412
|
const requestScope = {};
|
|
396
413
|
const connectorSet = registry.listConnectors();
|
|
397
|
-
const
|
|
414
|
+
const settled = await Promise.allSettled(
|
|
398
415
|
connectorSet.map(async (c): Promise<UiConnector> => {
|
|
399
416
|
const status = await registry.statusFor(c.id, baseUrl, requestScope);
|
|
400
417
|
const credentialCheck = await registry.credentialHealthFor(c.id);
|
|
@@ -530,19 +547,42 @@ export async function buildUiData(
|
|
|
530
547
|
...(credential ? { credential } : {}),
|
|
531
548
|
};
|
|
532
549
|
}),
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
550
|
+
);
|
|
551
|
+
await Promise.all(
|
|
552
|
+
connectorSet.map((connector) =>
|
|
553
|
+
closeConnectorScope(
|
|
554
|
+
connector,
|
|
555
|
+
registry.contextFor(connector.id, baseUrl, requestScope),
|
|
556
|
+
defer,
|
|
540
557
|
),
|
|
541
|
-
)
|
|
558
|
+
),
|
|
559
|
+
);
|
|
560
|
+
const connectors = settled.map((result) => {
|
|
561
|
+
if (result.status === "rejected") throw result.reason;
|
|
562
|
+
return result.value;
|
|
542
563
|
});
|
|
564
|
+
const toolkitData: UiToolkit[] = [...(toolkits?.values() ?? [])].map(
|
|
565
|
+
(toolkit) => ({
|
|
566
|
+
name: toolkit.name,
|
|
567
|
+
connectors: [...toolkit.connectors],
|
|
568
|
+
includeTools: [...toolkit.includeTools],
|
|
569
|
+
excludeTools: [...toolkit.excludeTools],
|
|
570
|
+
toolCount: connectors.reduce(
|
|
571
|
+
(count, connector) =>
|
|
572
|
+
count +
|
|
573
|
+
(toolkit.hasConnector(connector.id)
|
|
574
|
+
? connector.tools.filter((tool) =>
|
|
575
|
+
toolkit.hasTool(connector.id, tool.name),
|
|
576
|
+
).length
|
|
577
|
+
: 0),
|
|
578
|
+
0,
|
|
579
|
+
),
|
|
580
|
+
}),
|
|
581
|
+
);
|
|
543
582
|
return {
|
|
544
583
|
serverInfo,
|
|
545
584
|
connectors,
|
|
585
|
+
toolkits: toolkitData,
|
|
546
586
|
activityEnabled,
|
|
547
587
|
credentialManagement,
|
|
548
588
|
};
|
|
@@ -876,6 +916,56 @@ ${clerkScript}
|
|
|
876
916
|
.connector-description { margin-top: .25rem; max-width: 40rem; }
|
|
877
917
|
.connector-message,
|
|
878
918
|
.connector-auth { margin-top: .75rem; }
|
|
919
|
+
.connector-toolkits {
|
|
920
|
+
align-items: baseline;
|
|
921
|
+
display: flex;
|
|
922
|
+
flex-wrap: wrap;
|
|
923
|
+
gap: .25rem .5rem;
|
|
924
|
+
margin-top: .5rem;
|
|
925
|
+
}
|
|
926
|
+
.scope-label {
|
|
927
|
+
border: 1px solid var(--rule);
|
|
928
|
+
display: inline-block;
|
|
929
|
+
font-family: var(--mono);
|
|
930
|
+
font-size: .72rem;
|
|
931
|
+
padding: .05rem .35rem;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.toolkit-copy { margin-bottom: 1rem; max-width: 42rem; }
|
|
935
|
+
.toolkit-ledger { border-bottom: 1px solid var(--rule); }
|
|
936
|
+
.toolkit-card {
|
|
937
|
+
border-top: 1px solid var(--rule);
|
|
938
|
+
padding: .9rem 0 1rem;
|
|
939
|
+
}
|
|
940
|
+
.toolkit-head {
|
|
941
|
+
display: grid;
|
|
942
|
+
gap: var(--gap);
|
|
943
|
+
grid-template-columns: minmax(0, 2fr) minmax(10rem, 1fr);
|
|
944
|
+
}
|
|
945
|
+
.toolkit-name {
|
|
946
|
+
align-items: baseline;
|
|
947
|
+
display: flex;
|
|
948
|
+
flex-wrap: wrap;
|
|
949
|
+
gap: .5rem;
|
|
950
|
+
}
|
|
951
|
+
.toolkit-name h3 { overflow-wrap: anywhere; }
|
|
952
|
+
.toolkit-state { text-align: right; }
|
|
953
|
+
.toolkit-connectors {
|
|
954
|
+
display: flex;
|
|
955
|
+
flex-wrap: wrap;
|
|
956
|
+
gap: .5rem;
|
|
957
|
+
margin-top: .75rem;
|
|
958
|
+
}
|
|
959
|
+
.toolkit-connector {
|
|
960
|
+
border-left: 1px solid var(--ink);
|
|
961
|
+
display: inline-flex;
|
|
962
|
+
flex-wrap: wrap;
|
|
963
|
+
gap: .4rem;
|
|
964
|
+
padding-left: .5rem;
|
|
965
|
+
}
|
|
966
|
+
.toolkit-rules { margin-top: .75rem; }
|
|
967
|
+
.toolkit-rules > * + * { margin-top: .25rem; }
|
|
968
|
+
.toolkit-endpoint { margin-top: .75rem; }
|
|
879
969
|
|
|
880
970
|
.credential-ledger { border-bottom: 1px solid var(--rule); }
|
|
881
971
|
.credential-card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
@@ -991,9 +1081,11 @@ ${clerkScript}
|
|
|
991
1081
|
.section,
|
|
992
1082
|
.section + .section { margin-top: 2.5rem; }
|
|
993
1083
|
.connector-head,
|
|
1084
|
+
.toolkit-head,
|
|
994
1085
|
.tool,
|
|
995
1086
|
.activity-item { grid-template-columns: 1fr; }
|
|
996
|
-
.connector-state
|
|
1087
|
+
.connector-state,
|
|
1088
|
+
.toolkit-state { text-align: left; }
|
|
997
1089
|
.credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
|
|
998
1090
|
input { min-height: 2.75rem; }
|
|
999
1091
|
}
|
|
@@ -1068,6 +1160,16 @@ ${clerkScript}
|
|
|
1068
1160
|
<div id="list" class="connector-tools" aria-busy="false"></div>
|
|
1069
1161
|
</div>
|
|
1070
1162
|
</section>
|
|
1163
|
+
<section class="section pgrid" aria-labelledby="toolkitLedgerHeading">
|
|
1164
|
+
<h2 class="pcap" id="toolkitLedgerHeading">Toolkits</h2>
|
|
1165
|
+
<div class="pbody">
|
|
1166
|
+
<p class="toolkit-copy meta">
|
|
1167
|
+
Read-only views from deployment config. Change the config and redeploy
|
|
1168
|
+
to update them.
|
|
1169
|
+
</p>
|
|
1170
|
+
<div id="toolkitList" class="toolkit-ledger"></div>
|
|
1171
|
+
</div>
|
|
1172
|
+
</section>
|
|
1071
1173
|
</section>
|
|
1072
1174
|
|
|
1073
1175
|
<section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
|
|
@@ -1129,6 +1231,7 @@ let ACTIVITY_LOADED = false;
|
|
|
1129
1231
|
let CURRENT_PAGE = INITIAL_PAGE;
|
|
1130
1232
|
let SESSION_GENERATION = 0;
|
|
1131
1233
|
let ACTIVITY_GENERATION = 0;
|
|
1234
|
+
let CLERK_SESSION_ID = null;
|
|
1132
1235
|
$("mcpUrl").textContent = MCP_URL;
|
|
1133
1236
|
|
|
1134
1237
|
function esc(s) {
|
|
@@ -1184,6 +1287,7 @@ function clearIdentityState() {
|
|
|
1184
1287
|
DATA = null;
|
|
1185
1288
|
clearActivityState();
|
|
1186
1289
|
$("list").innerHTML = "";
|
|
1290
|
+
$("toolkitList").innerHTML = "";
|
|
1187
1291
|
$("filter").value = "";
|
|
1188
1292
|
$("credentialList").innerHTML = "";
|
|
1189
1293
|
$("credentialList").setAttribute("aria-busy", "false");
|
|
@@ -1267,7 +1371,10 @@ function activatePage(page, options) {
|
|
|
1267
1371
|
loadActivity(true);
|
|
1268
1372
|
}
|
|
1269
1373
|
}
|
|
1270
|
-
if (next !== "credentials")
|
|
1374
|
+
if (next !== "credentials") {
|
|
1375
|
+
$("credentialList").innerHTML = "";
|
|
1376
|
+
setNotice("");
|
|
1377
|
+
}
|
|
1271
1378
|
// Focus what is actually on screen. While gated the page views are hidden, so
|
|
1272
1379
|
// focusing their heading is a silent no-op that drops focus to <body> and
|
|
1273
1380
|
// restarts the next Tab from the top of the document — the gate's own h1 is
|
|
@@ -1452,6 +1559,71 @@ async function loadActivity(reset) {
|
|
|
1452
1559
|
}
|
|
1453
1560
|
}
|
|
1454
1561
|
|
|
1562
|
+
function toolkitMcpUrl(name) {
|
|
1563
|
+
const url = new URL(MCP_URL, window.location.href);
|
|
1564
|
+
url.searchParams.set("toolkit", name);
|
|
1565
|
+
return url.toString();
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function renderToolkits() {
|
|
1569
|
+
const list = $("toolkitList");
|
|
1570
|
+
const toolkits = DATA.toolkits || [];
|
|
1571
|
+
list.innerHTML = "";
|
|
1572
|
+
if (!toolkits.length) {
|
|
1573
|
+
list.innerHTML =
|
|
1574
|
+
'<p class="empty">No toolkits are configured. Unscoped clients see the full connector registry.</p>';
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const connectors = new Map(
|
|
1578
|
+
DATA.connectors.map((connector) => [connector.id, connector]),
|
|
1579
|
+
);
|
|
1580
|
+
for (const toolkit of toolkits) {
|
|
1581
|
+
const el = document.createElement("article");
|
|
1582
|
+
el.className = "toolkit-card";
|
|
1583
|
+
const connectorCount = toolkit.connectors.length;
|
|
1584
|
+
const toolCount = toolkit.toolCount || 0;
|
|
1585
|
+
let body =
|
|
1586
|
+
'<div class="toolkit-head"><div><div class="toolkit-name">' +
|
|
1587
|
+
'<h3 class="mono">' + esc(toolkit.name) + "</h3>" +
|
|
1588
|
+
'<span class="cap">configured view</span></div>';
|
|
1589
|
+
body += '</div><div class="toolkit-state cap">' +
|
|
1590
|
+
connectorCount + (connectorCount === 1 ? " connector" : " connectors") +
|
|
1591
|
+
" · " + toolCount + (toolCount === 1 ? " loaded tool" : " loaded tools") +
|
|
1592
|
+
"</div></div>";
|
|
1593
|
+
body += '<ul class="toolkit-connectors" aria-label="Included connectors">';
|
|
1594
|
+
for (const id of toolkit.connectors) {
|
|
1595
|
+
const connector = connectors.get(id);
|
|
1596
|
+
body += '<li class="toolkit-connector"><span>' +
|
|
1597
|
+
esc(connector?.title || id) + '</span><code class="mono">' +
|
|
1598
|
+
esc(id) + "</code></li>";
|
|
1599
|
+
}
|
|
1600
|
+
body += "</ul>";
|
|
1601
|
+
body += '<div class="toolkit-rules meta">';
|
|
1602
|
+
if (toolkit.includeTools.length) {
|
|
1603
|
+
body += '<p>Per-connector allowlist: <span class="mono">' +
|
|
1604
|
+
toolkit.includeTools.map(esc).join('</span>, <span class="mono">') +
|
|
1605
|
+
"</span>. Connectors without an allowlist keep all tools.</p>";
|
|
1606
|
+
}
|
|
1607
|
+
if (toolkit.excludeTools.length) {
|
|
1608
|
+
body += '<p>Hidden: <span class="mono">' +
|
|
1609
|
+
toolkit.excludeTools.map(esc).join('</span>, <span class="mono">') +
|
|
1610
|
+
"</span></p>";
|
|
1611
|
+
}
|
|
1612
|
+
if (!toolkit.includeTools.length && !toolkit.excludeTools.length) {
|
|
1613
|
+
body += "<p>All tools on the included connectors are visible.</p>";
|
|
1614
|
+
}
|
|
1615
|
+
body += "</div>";
|
|
1616
|
+
const endpoint = toolkitMcpUrl(toolkit.name);
|
|
1617
|
+
body += '<div class="endpoint toolkit-endpoint"><div class="endpoint-row">' +
|
|
1618
|
+
'<code class="mono">' + esc(endpoint) + "</code>" +
|
|
1619
|
+
'<button class="linklike" type="button" data-toolkit-copy="' +
|
|
1620
|
+
esc(toolkit.name) + '" aria-label="Copy endpoint for ' +
|
|
1621
|
+
esc(toolkit.name) + '">Copy URL</button></div></div>';
|
|
1622
|
+
el.innerHTML = body;
|
|
1623
|
+
list.appendChild(el);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1455
1627
|
function renderConnections() {
|
|
1456
1628
|
const q = $("filter").value.trim().toLowerCase();
|
|
1457
1629
|
const list = $("list");
|
|
@@ -1472,6 +1644,15 @@ function renderConnections() {
|
|
|
1472
1644
|
if (c.description) {
|
|
1473
1645
|
head += '<p class="connector-description meta">' + esc(c.description) + "</p>";
|
|
1474
1646
|
}
|
|
1647
|
+
const memberships = (DATA.toolkits || [])
|
|
1648
|
+
.filter((toolkit) => toolkit.connectors.includes(c.id))
|
|
1649
|
+
.map((toolkit) => toolkit.name);
|
|
1650
|
+
if (memberships.length) {
|
|
1651
|
+
head += '<div class="connector-toolkits meta"><span>Toolkits</span>' +
|
|
1652
|
+
memberships.map((name) =>
|
|
1653
|
+
'<span class="scope-label">' + esc(name) + "</span>"
|
|
1654
|
+
).join("") + "</div>";
|
|
1655
|
+
}
|
|
1475
1656
|
head += '</div><div class="connector-state cap">' + esc(status) + " · " +
|
|
1476
1657
|
c.toolCount + (c.toolCount === 1 ? " tool" : " tools") +
|
|
1477
1658
|
'<br><span class="mono">' + esc(c.id) + "</span></div></div>";
|
|
@@ -1525,6 +1706,7 @@ function renderConnections() {
|
|
|
1525
1706
|
: "No connectors are declared in this deployment.") +
|
|
1526
1707
|
"</p>";
|
|
1527
1708
|
}
|
|
1709
|
+
renderToolkits();
|
|
1528
1710
|
}
|
|
1529
1711
|
|
|
1530
1712
|
function renderCredentials() {
|
|
@@ -1771,6 +1953,19 @@ $("copyMcpUrl").onclick = async () => {
|
|
|
1771
1953
|
}
|
|
1772
1954
|
window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
|
|
1773
1955
|
};
|
|
1956
|
+
$("toolkitList").onclick = async (event) => {
|
|
1957
|
+
const button = event.target.closest("[data-toolkit-copy]");
|
|
1958
|
+
if (!button) return;
|
|
1959
|
+
try {
|
|
1960
|
+
await navigator.clipboard.writeText(
|
|
1961
|
+
toolkitMcpUrl(button.dataset.toolkitCopy),
|
|
1962
|
+
);
|
|
1963
|
+
button.textContent = "Copied";
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
button.textContent = "Copy failed";
|
|
1966
|
+
}
|
|
1967
|
+
window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
|
|
1968
|
+
};
|
|
1774
1969
|
$("signin").onclick = () => Clerk.redirectToSignIn({
|
|
1775
1970
|
signInFallbackRedirectUrl: window.location.href,
|
|
1776
1971
|
signUpFallbackRedirectUrl: window.location.href,
|
|
@@ -1820,6 +2015,17 @@ async function init() {
|
|
|
1820
2015
|
signUpFallbackRedirectUrl: window.location.href,
|
|
1821
2016
|
afterSignOutUrl: window.location.href,
|
|
1822
2017
|
});
|
|
2018
|
+
CLERK_SESSION_ID = Clerk.session?.id ?? null;
|
|
2019
|
+
Clerk.addListener((resources) => {
|
|
2020
|
+
const nextSessionId = resources.session?.id ?? null;
|
|
2021
|
+
if (nextSessionId === CLERK_SESSION_ID) return;
|
|
2022
|
+
CLERK_SESSION_ID = nextSessionId;
|
|
2023
|
+
// Clerk has already updated its public session before notifying
|
|
2024
|
+
// listeners. Clear synchronously so stale identity-scoped data cannot
|
|
2025
|
+
// be repainted while the replacement identity is being fetched.
|
|
2026
|
+
showGate("");
|
|
2027
|
+
void load();
|
|
2028
|
+
});
|
|
1823
2029
|
} catch (e) {
|
|
1824
2030
|
return showGate("Clerk could not initialize: " + e.message);
|
|
1825
2031
|
}
|
package/src/version.ts
CHANGED