@zackbart/connecta 0.9.1 → 0.10.1
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 +128 -0
- package/README.md +49 -92
- package/dist/access-tokens.d.ts +32 -0
- package/dist/access-tokens.d.ts.map +1 -0
- package/dist/access-tokens.js +225 -0
- package/dist/access-tokens.js.map +1 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +1 -4
- package/dist/catalog-service.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +3 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +79 -18
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.d.ts.map +1 -1
- package/dist/executor-result.js +37 -6
- package/dist/executor-result.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +12 -0
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-protocol.js +14 -0
- 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 +6 -3
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +10 -4
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +27 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +1 -4
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +26 -5
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +84 -40
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +2 -0
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/routes/access-tokens.d.ts +7 -0
- package/dist/routes/access-tokens.d.ts.map +1 -0
- package/dist/routes/access-tokens.js +84 -0
- package/dist/routes/access-tokens.js.map +1 -0
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +8 -2
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +11 -2
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +9 -1
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +15 -3
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +63 -10
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +5 -0
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +8 -0
- package/dist/storage/memory.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +44 -1
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
- package/src/access-tokens.ts +289 -0
- package/src/catalog-service.ts +1 -5
- package/src/errors.ts +28 -0
- package/src/execute.ts +123 -48
- package/src/executor-result.ts +50 -6
- package/src/executors/quickjs-protocol.ts +19 -0
- package/src/executors/quickjs-runtime.ts +6 -2
- package/src/executors/quickjs.ts +10 -3
- package/src/index.ts +85 -5
- package/src/invocation.ts +1 -5
- package/src/meta-tools.ts +116 -53
- package/src/operator-ui/browser.css +63 -0
- package/src/operator-ui/browser.ts +288 -2
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +6 -0
- package/src/routes/access-tokens.ts +115 -0
- package/src/routes/mcp.ts +8 -2
- package/src/routes/shared.ts +11 -1
- package/src/routes/ui.ts +9 -0
- package/src/server.ts +5 -0
- package/src/skills.ts +79 -9
- package/src/storage/file.ts +5 -0
- package/src/storage/memory.ts +8 -0
- package/src/types.ts +20 -0
- package/src/ui.ts +50 -1
- package/src/version.ts +1 -1
|
@@ -57,6 +57,25 @@ export interface ExecutionPayload {
|
|
|
57
57
|
timedOut?: boolean;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* How a host call should be named in an error a program will read. The lazy
|
|
62
|
+
* connector namespaces all dispatch through one internal function, so the raw
|
|
63
|
+
* provider/function pair would report every shortcut call as
|
|
64
|
+
* `connecta.__callNamespace` — an internal name that appears nowhere in the
|
|
65
|
+
* documented surface. Report the address the program actually called.
|
|
66
|
+
*/
|
|
67
|
+
export function hostCallLabel(payload: {
|
|
68
|
+
namespace: string;
|
|
69
|
+
functionName: string;
|
|
70
|
+
args: unknown[];
|
|
71
|
+
}): string {
|
|
72
|
+
if (payload.functionName === "__callNamespace") {
|
|
73
|
+
const [connectorId, toolAlias] = payload.args;
|
|
74
|
+
return `${String(connectorId)}.${String(toolAlias)}`;
|
|
75
|
+
}
|
|
76
|
+
return `${payload.namespace}.${payload.functionName}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
export function serializedBytes(text: string): number {
|
|
61
80
|
return new TextEncoder().encode(text).length;
|
|
62
81
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "quickjs-emscripten";
|
|
16
16
|
import type { ExecuteResult, ExecutorProvider } from "../types.js";
|
|
17
17
|
import {
|
|
18
|
+
hostCallLabel,
|
|
18
19
|
MAX_QUICKJS_LOG_TRANSPORT_BYTES,
|
|
19
20
|
serializedBytes,
|
|
20
21
|
} from "./quickjs-protocol.js";
|
|
@@ -245,18 +246,21 @@ function installBridge(
|
|
|
245
246
|
: undefined;
|
|
246
247
|
if (!f) throw new Error(`Unknown function ${ns}.${fn}`);
|
|
247
248
|
const args = JSON.parse(argsJson) as unknown[];
|
|
249
|
+
// Name the address the program called, never the internal dispatcher the
|
|
250
|
+
// lazy connector namespaces share.
|
|
251
|
+
const label = hostCallLabel({ namespace: ns, functionName: fn, args });
|
|
248
252
|
const value = await f(...args);
|
|
249
253
|
let json: string;
|
|
250
254
|
try {
|
|
251
255
|
json = JSON.stringify({ ok: true, value });
|
|
252
256
|
} catch (err) {
|
|
253
257
|
throw new Error(
|
|
254
|
-
`Host result from ${
|
|
258
|
+
`Host result from ${label} could not be serialized: ${msg(err)}`,
|
|
255
259
|
);
|
|
256
260
|
}
|
|
257
261
|
if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
|
|
258
262
|
throw new Error(
|
|
259
|
-
`Host result from ${
|
|
263
|
+
`Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`,
|
|
260
264
|
);
|
|
261
265
|
}
|
|
262
266
|
return json;
|
package/src/executors/quickjs.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ExecutorProvider,
|
|
19
19
|
} from "../types.js";
|
|
20
20
|
import {
|
|
21
|
+
hostCallLabel,
|
|
21
22
|
MAX_QUICKJS_IPC_BYTES,
|
|
22
23
|
MAX_QUICKJS_HOST_RPC_BYTES,
|
|
23
24
|
type ChildToParentMessage,
|
|
@@ -593,6 +594,9 @@ class QuickJsChildPool implements AdmittingExecutor {
|
|
|
593
594
|
serializedBytes(message.payloadJson) >
|
|
594
595
|
MAX_QUICKJS_HOST_RPC_BYTES
|
|
595
596
|
) {
|
|
597
|
+
// Refused before parsing, so there is no address to name here: parsing
|
|
598
|
+
// an over-limit payload to improve its error message would spend the
|
|
599
|
+
// work the limit exists to refuse.
|
|
596
600
|
throw new RangeError("Host call arguments exceeded the IPC limit.");
|
|
597
601
|
}
|
|
598
602
|
const payload = JSON.parse(message.payloadJson) as HostCallPayload;
|
|
@@ -610,14 +614,17 @@ class QuickJsChildPool implements AdmittingExecutor {
|
|
|
610
614
|
try {
|
|
611
615
|
payloadJson = stringifyBounded(
|
|
612
616
|
{ ok: true, value } satisfies HostResultPayload,
|
|
613
|
-
`Host result from ${payload
|
|
617
|
+
`Host result from ${hostCallLabel(payload)}`,
|
|
614
618
|
MAX_QUICKJS_HOST_RPC_BYTES,
|
|
615
619
|
);
|
|
616
620
|
} catch (err) {
|
|
621
|
+
// The guest reads this text, so it names the address the program called
|
|
622
|
+
// rather than the internal dispatcher every shortcut namespace shares.
|
|
623
|
+
const label = hostCallLabel(payload);
|
|
617
624
|
const detail =
|
|
618
625
|
err instanceof RangeError
|
|
619
|
-
? `Host result from ${
|
|
620
|
-
: `Host result from ${
|
|
626
|
+
? `Host result from ${label} exceeds the ${MAX_QUICKJS_HOST_RPC_BYTES}-byte serialized bridge limit.`
|
|
627
|
+
: `Host result from ${label} could not be serialized: ${msg(err)}`;
|
|
621
628
|
payloadJson = errorPayload(detail);
|
|
622
629
|
}
|
|
623
630
|
} catch (err) {
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
credentialTestRule,
|
|
4
4
|
describeCredentialTestMismatch,
|
|
5
5
|
} from "./credentials.js";
|
|
6
|
+
import { AccessTokenManager } from "./access-tokens.js";
|
|
6
7
|
import { Registry } from "./registry.js";
|
|
7
8
|
import { createFetchHandler } from "./server.js";
|
|
8
9
|
import { droppedBrandingUrls, droppedUiAuthUrls } from "./ui.js";
|
|
@@ -17,6 +18,7 @@ import type { ActivityReadGate, ActivityStore } from "./activity.js";
|
|
|
17
18
|
import type {
|
|
18
19
|
Connector,
|
|
19
20
|
ConnectaBranding,
|
|
21
|
+
ConnectaSurface,
|
|
20
22
|
Executor,
|
|
21
23
|
InboundAuth,
|
|
22
24
|
KVStorage,
|
|
@@ -48,6 +50,12 @@ export interface ConnectaCredentialsConfig {
|
|
|
48
50
|
encryptionKey?: string;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/** Operator-issued credentials for clients connecting to this deployment. */
|
|
54
|
+
export interface ConnectaAccessTokensConfig {
|
|
55
|
+
/** Maximum simultaneously active access tokens. Defaults to 100. */
|
|
56
|
+
maxActive?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
52
60
|
export interface ConnectaDiscoveryConfig {
|
|
53
61
|
/**
|
|
@@ -147,6 +155,11 @@ export interface ConnectaConfig {
|
|
|
147
155
|
activity?: ConnectaActivityConfig;
|
|
148
156
|
/** Operator credential vault settings. */
|
|
149
157
|
credentials?: ConnectaCredentialsConfig;
|
|
158
|
+
/**
|
|
159
|
+
* Named, revocable Bearer tokens for MCP clients. Creation and mutation
|
|
160
|
+
* require an eligible Clerk operator; token secrets are returned once.
|
|
161
|
+
*/
|
|
162
|
+
accessTokens?: ConnectaAccessTokensConfig;
|
|
150
163
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
151
164
|
discovery?: ConnectaDiscoveryConfig;
|
|
152
165
|
/** Deployment-wide call deadlines and result paging threshold. */
|
|
@@ -169,12 +182,22 @@ export interface ConnectaConfig {
|
|
|
169
182
|
/** Deployment metadata exposed by /health (for example a Worker version). */
|
|
170
183
|
deploymentInfo?: Record<string, unknown>;
|
|
171
184
|
/**
|
|
172
|
-
* Sandbox for
|
|
173
|
-
*
|
|
174
|
-
* `new DynamicWorkerExecutor({ loader: env.LOADER })`
|
|
175
|
-
* `@cloudflare/codemode`. Node: `quickJsExecutor()` from
|
|
185
|
+
* Sandbox for `execute_code`, and the switch that decides the surface: with
|
|
186
|
+
* an executor a model sees the seven code-first tools, without one the nine
|
|
187
|
+
* classic ones. Workers: `new DynamicWorkerExecutor({ loader: env.LOADER })`
|
|
188
|
+
* from `@cloudflare/codemode`. Node: `quickJsExecutor()` from
|
|
189
|
+
* "@zackbart/connecta/quickjs".
|
|
176
190
|
*/
|
|
177
191
|
executor?: Executor;
|
|
192
|
+
/**
|
|
193
|
+
* Override the surface the `executor` implies. The only reason to set it is
|
|
194
|
+
* `"classic"` alongside an executor — ten tools, the shape the eval gate's
|
|
195
|
+
* *incremental* arm measures ("does adding `execute_code` to classic help on
|
|
196
|
+
* its own?"). The gate's control arm is executor-free classic, which needs no
|
|
197
|
+
* override. `"code-first"` is the default wherever an executor exists and
|
|
198
|
+
* throws without one.
|
|
199
|
+
*/
|
|
200
|
+
surface?: ConnectaSurface;
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
export interface Connecta {
|
|
@@ -406,8 +429,43 @@ function warnInsecureConfig(
|
|
|
406
429
|
}
|
|
407
430
|
}
|
|
408
431
|
|
|
432
|
+
/**
|
|
433
|
+
* The advertised surface: the executor is the switch. Configure one and the
|
|
434
|
+
* deployment serves the seven-tool code-first surface; omit it and there is no
|
|
435
|
+
* program to fold discovery and batching into, so it serves classic.
|
|
436
|
+
*
|
|
437
|
+
* Two mistakes are structural rather than recoverable, so neither is warned
|
|
438
|
+
* past: a surface name connecta does not implement, which would otherwise
|
|
439
|
+
* resolve to something the operator did not ask for; and `code-first` without
|
|
440
|
+
* an executor, which would advertise six tools and no program surface.
|
|
441
|
+
*/
|
|
442
|
+
function resolveSurface(config: ConnectaConfig): ConnectaSurface {
|
|
443
|
+
const surface = config.surface;
|
|
444
|
+
if (surface === undefined) {
|
|
445
|
+
return config.executor ? "code-first" : "classic";
|
|
446
|
+
}
|
|
447
|
+
if (surface !== "classic" && surface !== "code-first") {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`ConnectaConfig.surface must be "classic" or "code-first", not ` +
|
|
450
|
+
`${JSON.stringify(surface)}.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (surface === "code-first" && !config.executor) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
'ConnectaConfig.surface "code-first" requires an executor: it folds ' +
|
|
456
|
+
"list_connectors, describe_tools, and batch_call into connecta.search, " +
|
|
457
|
+
"connecta.describe, and connecta.batch inside execute_code, so without " +
|
|
458
|
+
"an executor there is nothing left to reach them through. Configure " +
|
|
459
|
+
"one (quickJsExecutor() from \"@zackbart/connecta/quickjs\" on Node, " +
|
|
460
|
+
"new DynamicWorkerExecutor({ loader: env.LOADER }) on Workers).",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return surface;
|
|
464
|
+
}
|
|
465
|
+
|
|
409
466
|
export function createConnecta(config: ConnectaConfig): Connecta {
|
|
410
467
|
assertNoLegacyConfig(config);
|
|
468
|
+
const surface = resolveSurface(config);
|
|
411
469
|
const storage = config.storage ?? memoryStorage();
|
|
412
470
|
const logger = config.logger ?? defaultLogger();
|
|
413
471
|
const credentialConnectors = config.connectors.filter((c) => c.credential);
|
|
@@ -422,6 +480,19 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
422
480
|
const credentialVault = encryptionKey
|
|
423
481
|
? new CredentialVault(storage, encryptionKey)
|
|
424
482
|
: undefined;
|
|
483
|
+
const configuredAuth = normalizeAuth(config.auth);
|
|
484
|
+
const accessTokens = config.accessTokens
|
|
485
|
+
? new AccessTokenManager(storage, config.accessTokens)
|
|
486
|
+
: undefined;
|
|
487
|
+
if (
|
|
488
|
+
accessTokens &&
|
|
489
|
+
!configuredAuth.some((provider) => provider.uiAuth?.kind === "clerk")
|
|
490
|
+
) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
"accessTokens requires a Clerk auth provider: only an eligible Clerk " +
|
|
493
|
+
"operator may create, rename, or revoke deployment access tokens",
|
|
494
|
+
);
|
|
495
|
+
}
|
|
425
496
|
const registry = new Registry(config.connectors, {
|
|
426
497
|
storage,
|
|
427
498
|
logger,
|
|
@@ -442,7 +513,9 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
442
513
|
? { maxBatchResultBytes: config.calls.maxBatchResultBytes }
|
|
443
514
|
: {}),
|
|
444
515
|
});
|
|
445
|
-
const inboundAuth = normalizeAuth(
|
|
516
|
+
const inboundAuth = normalizeAuth(
|
|
517
|
+
accessTokens ? [accessTokens.auth, ...configuredAuth] : configuredAuth,
|
|
518
|
+
);
|
|
446
519
|
warnInsecureConfig(config, inboundAuth, logger);
|
|
447
520
|
const requestAdmission = admissionController(
|
|
448
521
|
config.admission?.requests,
|
|
@@ -484,6 +557,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
484
557
|
? { activityDeploymentId: config.activity.deploymentId }
|
|
485
558
|
: {}),
|
|
486
559
|
...(executor !== undefined ? { executor } : {}),
|
|
560
|
+
surface,
|
|
487
561
|
requestAdmission,
|
|
488
562
|
...(config.calls?.defaultTimeoutMs !== undefined
|
|
489
563
|
? { defaultToolTimeoutMs: config.calls.defaultTimeoutMs }
|
|
@@ -495,6 +569,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
495
569
|
? { discoveryConcurrency: config.discovery.concurrency }
|
|
496
570
|
: {}),
|
|
497
571
|
...(credentialVault !== undefined ? { credentialVault } : {}),
|
|
572
|
+
...(accessTokens !== undefined ? { accessTokens } : {}),
|
|
498
573
|
...(config.deploymentInfo !== undefined
|
|
499
574
|
? { deploymentInfo: config.deploymentInfo }
|
|
500
575
|
: {}),
|
|
@@ -533,6 +608,10 @@ export { validateToolInput } from "./validate.js";
|
|
|
533
608
|
export type { ValidateToolInputOptions } from "./validate.js";
|
|
534
609
|
export { bearerToken } from "./auth/bearer.js";
|
|
535
610
|
export type { BearerTokenOptions } from "./auth/bearer.js";
|
|
611
|
+
export type {
|
|
612
|
+
AccessTokenMetadata,
|
|
613
|
+
CreatedAccessToken,
|
|
614
|
+
} from "./access-tokens.js";
|
|
536
615
|
export { memoryStorage } from "./storage/memory.js";
|
|
537
616
|
export { CONNECTA_VERSION } from "./version.js";
|
|
538
617
|
// Registry is reachable through `Connecta.registry`, so its type is public;
|
|
@@ -553,6 +632,7 @@ export type {
|
|
|
553
632
|
ConnectorCallAdmissionRule,
|
|
554
633
|
ConnectorRollingWindowBudget,
|
|
555
634
|
ConnectaBranding,
|
|
635
|
+
ConnectaSurface,
|
|
556
636
|
ConnectorCredentialAccess,
|
|
557
637
|
ConnectorCredentialConfig,
|
|
558
638
|
ConnectorCredentialFieldConfig,
|
package/src/invocation.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
classifyCallError,
|
|
14
14
|
ConnectorCallError,
|
|
15
|
-
|
|
15
|
+
framingError,
|
|
16
16
|
type AuthRecoveryMode,
|
|
17
17
|
type CallErrorDetails,
|
|
18
18
|
} from "./errors.js";
|
|
@@ -70,10 +70,6 @@ function retrySafe(definition: ToolDef): boolean {
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function framingError(code: string, message: string): CallErrorDetails {
|
|
74
|
-
return { code, message, retryable: messageLooksRetryable(message) };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
function callerCancelledDetails(): CallErrorDetails {
|
|
78
74
|
return {
|
|
79
75
|
code: "cancelled",
|
package/src/meta-tools.ts
CHANGED
|
@@ -49,7 +49,11 @@ import {
|
|
|
49
49
|
normalizeTimeoutMs,
|
|
50
50
|
withAbortableTimeout,
|
|
51
51
|
} from "./timeout.js";
|
|
52
|
-
import type {
|
|
52
|
+
import type {
|
|
53
|
+
ConnectaSurface,
|
|
54
|
+
ConnectorStatus,
|
|
55
|
+
KVStorage,
|
|
56
|
+
} from "./types.js";
|
|
53
57
|
|
|
54
58
|
export {
|
|
55
59
|
MAX_DESCRIBE_ADDRESSES,
|
|
@@ -473,10 +477,12 @@ export interface SkillArgs {
|
|
|
473
477
|
}
|
|
474
478
|
|
|
475
479
|
/**
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
+
* Every base meta-tool handler over a registry — all nine, whichever surface is
|
|
481
|
+
* advertised, since folding a tool away only skips its registration and never
|
|
482
|
+
* its handler. Exported for direct testing; registerMetaTools() wires the ones
|
|
483
|
+
* this surface advertises onto an McpServer. `opts.defaultToolTimeoutMs`
|
|
484
|
+
* supplies a deadline for calls that don't carry one. (execute_code is
|
|
485
|
+
* registered separately by registerExecuteTool.)
|
|
480
486
|
*
|
|
481
487
|
* Deployment-wide result-size caps are read off the registry view rather than
|
|
482
488
|
* passed in: `ConnectaConfig.calls.maxResultBytes`, its per-connector override,
|
|
@@ -498,8 +504,15 @@ export function createMetaTools(
|
|
|
498
504
|
requestSignal?: AbortSignal;
|
|
499
505
|
/** Runtime continuation for the bounded tail of probe-owned teardown. */
|
|
500
506
|
defer?: DeferredWork;
|
|
507
|
+
/**
|
|
508
|
+
* The advertised surface, which the `skills` guidance must match: a
|
|
509
|
+
* code-first deployment never gets guidance naming a tool it does not
|
|
510
|
+
* advertise. Default `classic`.
|
|
511
|
+
*/
|
|
512
|
+
surface?: ConnectaSurface;
|
|
501
513
|
} = {},
|
|
502
514
|
) {
|
|
515
|
+
const surface: ConnectaSurface = opts.surface ?? "classic";
|
|
503
516
|
// Already normalized and warned about at registry construction.
|
|
504
517
|
const globalCap = registry.maxResultBytes;
|
|
505
518
|
const batchCap = registry.maxBatchResultBytes;
|
|
@@ -666,14 +679,14 @@ export function createMetaTools(
|
|
|
666
679
|
type: "text",
|
|
667
680
|
text:
|
|
668
681
|
'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
|
|
669
|
-
listSkills(connectors)
|
|
682
|
+
listSkills(connectors, surface)
|
|
670
683
|
.map((skill) => `- \`${skill.name}\` — ${skill.description}`)
|
|
671
684
|
.join("\n"),
|
|
672
685
|
},
|
|
673
686
|
],
|
|
674
687
|
};
|
|
675
688
|
}
|
|
676
|
-
const skill = resolveSkill(args.name, connectors);
|
|
689
|
+
const skill = resolveSkill(args.name, connectors, surface);
|
|
677
690
|
if (!skill.found) return errorResult(skill.message);
|
|
678
691
|
return { content: [{ type: "text", text: skill.content }] };
|
|
679
692
|
},
|
|
@@ -1133,6 +1146,23 @@ const AUTHORIZE_DESC =
|
|
|
1133
1146
|
const SKILLS_DESC =
|
|
1134
1147
|
'List or fetch concise guidance for choosing among Connecta meta-tools. Call skills({ name: "usage" }) once when the routing workflow is unfamiliar; do not refetch it in the same task.';
|
|
1135
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Code-first replacements for the descriptions that route work between tools.
|
|
1151
|
+
* Every one of these mentions a tool the consolidated surface removed, so on a
|
|
1152
|
+
* code-first deployment the routing sentence has to point at the in-program
|
|
1153
|
+
* function that took the work over — a description naming `batch_call` on a
|
|
1154
|
+
* surface without one teaches a call that cannot succeed.
|
|
1155
|
+
*
|
|
1156
|
+
* The classic strings above are left byte-for-byte alone: classic is the
|
|
1157
|
+
* compatibility surface and the eval's control arm, and rewording it would
|
|
1158
|
+
* change what that control measures.
|
|
1159
|
+
*/
|
|
1160
|
+
const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
|
|
1161
|
+
const CODE_FIRST_CALL_DESC =
|
|
1162
|
+
'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1163
|
+
const CODE_FIRST_GET_RESULT_DESC =
|
|
1164
|
+
"Page a truncated result stashed by call_tool or call_destructive_tool; a program's oversized return is not paged, so reduce it in code instead. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
|
|
1165
|
+
|
|
1136
1166
|
/**
|
|
1137
1167
|
* Sentences appended to a meta-tool description only when this connection
|
|
1138
1168
|
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
@@ -1191,7 +1221,17 @@ const CALL_INPUT_SCHEMA = {
|
|
|
1191
1221
|
diagnostics: z.boolean().optional(),
|
|
1192
1222
|
};
|
|
1193
1223
|
|
|
1194
|
-
/**
|
|
1224
|
+
/**
|
|
1225
|
+
* Register the base meta-tools onto an McpServer instance: nine on the classic
|
|
1226
|
+
* surface, six on the code-first one, where `list_connectors`,
|
|
1227
|
+
* `describe_tools`, and `batch_call` have folded into the program surface
|
|
1228
|
+
* (`registerExecuteTool` adds the seventh, `execute_code`).
|
|
1229
|
+
*
|
|
1230
|
+
* Only the registrations differ. Every handler still exists on the object
|
|
1231
|
+
* `createMetaTools` returns, and a folded tool's behavior is reached through
|
|
1232
|
+
* `connecta.search` / `connecta.describe` / `connecta.batch` inside a program —
|
|
1233
|
+
* the same code paths, one layer down.
|
|
1234
|
+
*/
|
|
1195
1235
|
export function registerMetaTools(
|
|
1196
1236
|
server: McpServer,
|
|
1197
1237
|
registry: RegistryView,
|
|
@@ -1203,9 +1243,14 @@ export function registerMetaTools(
|
|
|
1203
1243
|
activity?: ActivityRequestContext;
|
|
1204
1244
|
requestSignal?: AbortSignal;
|
|
1205
1245
|
defer?: DeferredWork;
|
|
1246
|
+
/** The advertised surface. Default `classic`. */
|
|
1247
|
+
surface?: ConnectaSurface;
|
|
1206
1248
|
},
|
|
1207
1249
|
): void {
|
|
1250
|
+
const surface: ConnectaSurface = ctx.surface ?? "classic";
|
|
1251
|
+
const codeFirst = surface === "code-first";
|
|
1208
1252
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1253
|
+
surface,
|
|
1209
1254
|
...(ctx.defaultToolTimeoutMs !== undefined
|
|
1210
1255
|
? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
|
|
1211
1256
|
: {}),
|
|
@@ -1232,20 +1277,30 @@ export function registerMetaTools(
|
|
|
1232
1277
|
async (args) => mt.skills(args as SkillArgs),
|
|
1233
1278
|
);
|
|
1234
1279
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1280
|
+
// Folded on the code-first surface: a program browses the same inventory with
|
|
1281
|
+
// connecta.search({}) (every catalog) or connecta.search({ connector }) (one).
|
|
1282
|
+
// Live connector probing is an operator concern, not a model one — it stays on
|
|
1283
|
+
// the operator pages and /health, which is where the ethos puts observability.
|
|
1284
|
+
if (!codeFirst) {
|
|
1285
|
+
server.registerTool(
|
|
1286
|
+
"list_connectors",
|
|
1287
|
+
{
|
|
1288
|
+
description: LIST_DESC,
|
|
1289
|
+
inputSchema: z.object({ probe: z.boolean().optional() }),
|
|
1290
|
+
annotations: READ_ONLY_REMOTE,
|
|
1291
|
+
},
|
|
1292
|
+
async (args) => mt.listConnectors(args as ListArgs),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1244
1295
|
|
|
1245
1296
|
server.registerTool(
|
|
1246
1297
|
"search_tools",
|
|
1247
1298
|
{
|
|
1248
|
-
description: describedFor(
|
|
1299
|
+
description: describedFor(
|
|
1300
|
+
registry,
|
|
1301
|
+
codeFirst ? CODE_FIRST_SEARCH_DESC : SEARCH_DESC,
|
|
1302
|
+
"search",
|
|
1303
|
+
),
|
|
1249
1304
|
inputSchema: z.object({
|
|
1250
1305
|
query: z.string().optional(),
|
|
1251
1306
|
connector: z.string().optional(),
|
|
@@ -1259,24 +1314,28 @@ export function registerMetaTools(
|
|
|
1259
1314
|
async (args) => mt.searchTools(args as SearchArgs),
|
|
1260
1315
|
);
|
|
1261
1316
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1317
|
+
// Folded on the code-first surface: connecta.describe takes the same
|
|
1318
|
+
// addresses, format, and per-address error reporting inside a program.
|
|
1319
|
+
if (!codeFirst) {
|
|
1320
|
+
server.registerTool(
|
|
1321
|
+
"describe_tools",
|
|
1322
|
+
{
|
|
1323
|
+
description: describedFor(registry, DESCRIBE_DESC, "describe"),
|
|
1324
|
+
inputSchema: z.object({
|
|
1325
|
+
addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
|
|
1326
|
+
format: z.enum(["compact", "json"]).optional(),
|
|
1327
|
+
fullDescriptions: z.boolean().optional(),
|
|
1328
|
+
}),
|
|
1329
|
+
annotations: READ_ONLY_REMOTE,
|
|
1330
|
+
},
|
|
1331
|
+
async (args) => mt.describeTools(args as DescribeArgs),
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1275
1334
|
|
|
1276
1335
|
server.registerTool(
|
|
1277
1336
|
"call_tool",
|
|
1278
1337
|
{
|
|
1279
|
-
description: CALL_DESC,
|
|
1338
|
+
description: codeFirst ? CODE_FIRST_CALL_DESC : CALL_DESC,
|
|
1280
1339
|
inputSchema: z.object(CALL_INPUT_SCHEMA),
|
|
1281
1340
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1282
1341
|
// anything else is refused and routed to call_destructive_tool.
|
|
@@ -1321,7 +1380,7 @@ export function registerMetaTools(
|
|
|
1321
1380
|
server.registerTool(
|
|
1322
1381
|
"get_result",
|
|
1323
1382
|
{
|
|
1324
|
-
description: GET_RESULT_DESC,
|
|
1383
|
+
description: codeFirst ? CODE_FIRST_GET_RESULT_DESC : GET_RESULT_DESC,
|
|
1325
1384
|
inputSchema: z.object({
|
|
1326
1385
|
id: z.string(),
|
|
1327
1386
|
// Both bounds are the shared rules (isValidResultOffset,
|
|
@@ -1336,24 +1395,28 @@ export function registerMetaTools(
|
|
|
1336
1395
|
async (args) => mt.getResult(args as GetResultArgs),
|
|
1337
1396
|
);
|
|
1338
1397
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1398
|
+
// Folded on the code-first surface: connecta.batch runs the same 1–10
|
|
1399
|
+
// parallel read-only calls and returns the same typed per-call outcomes.
|
|
1400
|
+
if (!codeFirst) {
|
|
1401
|
+
server.registerTool(
|
|
1402
|
+
"batch_call",
|
|
1403
|
+
{
|
|
1404
|
+
description: BATCH_DESC,
|
|
1405
|
+
inputSchema: z.object({
|
|
1406
|
+
calls: z
|
|
1407
|
+
.array(z.object(CALL_INPUT_SCHEMA))
|
|
1408
|
+
.min(1)
|
|
1409
|
+
.max(10),
|
|
1410
|
+
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1411
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
1412
|
+
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1413
|
+
diagnostics: z.boolean().optional(),
|
|
1414
|
+
}),
|
|
1415
|
+
// Same gate as call_tool: every call in the batch must be explicitly
|
|
1416
|
+
// read-only or the batch is refused.
|
|
1417
|
+
annotations: READ_ONLY_REMOTE,
|
|
1418
|
+
},
|
|
1419
|
+
async (args) => mt.batchCall(args as BatchArgs),
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1359
1422
|
}
|
|
@@ -285,6 +285,69 @@
|
|
|
285
285
|
.credential-field input { min-width: 0; width: 100%; }
|
|
286
286
|
.danger { text-decoration-style: double; }
|
|
287
287
|
|
|
288
|
+
.token-create {
|
|
289
|
+
border-bottom: 1px solid var(--rule);
|
|
290
|
+
border-top: 1px solid var(--rule);
|
|
291
|
+
padding: .75rem 0;
|
|
292
|
+
}
|
|
293
|
+
.token-create > label {
|
|
294
|
+
display: block;
|
|
295
|
+
margin-bottom: .5rem;
|
|
296
|
+
}
|
|
297
|
+
.token-create input { flex: 1 1 18rem; }
|
|
298
|
+
.token-create button,
|
|
299
|
+
.token-card button,
|
|
300
|
+
.token-reveal button {
|
|
301
|
+
align-items: center;
|
|
302
|
+
display: inline-flex;
|
|
303
|
+
min-height: 2.75rem;
|
|
304
|
+
}
|
|
305
|
+
.token-reveal {
|
|
306
|
+
background: var(--ink);
|
|
307
|
+
color: var(--paper);
|
|
308
|
+
margin-top: 1.5rem;
|
|
309
|
+
padding: 1rem 1.25rem;
|
|
310
|
+
}
|
|
311
|
+
.token-reveal .meta,
|
|
312
|
+
.token-reveal .cap { color: #bbb; }
|
|
313
|
+
.token-reveal-head,
|
|
314
|
+
.token-card-head {
|
|
315
|
+
align-items: baseline;
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-wrap: wrap;
|
|
318
|
+
gap: .25rem var(--gap);
|
|
319
|
+
justify-content: space-between;
|
|
320
|
+
}
|
|
321
|
+
.token-secret {
|
|
322
|
+
border-bottom: 1px solid #555;
|
|
323
|
+
border-top: 1px solid #555;
|
|
324
|
+
margin-top: .75rem;
|
|
325
|
+
}
|
|
326
|
+
.token-secret code {
|
|
327
|
+
color: var(--paper);
|
|
328
|
+
user-select: all;
|
|
329
|
+
}
|
|
330
|
+
.token-ledger {
|
|
331
|
+
border-bottom: 1px solid var(--rule);
|
|
332
|
+
margin-top: 1.5rem;
|
|
333
|
+
}
|
|
334
|
+
.token-card {
|
|
335
|
+
border-top: 1px solid var(--rule);
|
|
336
|
+
padding: .75rem 0 .75rem 1.25rem;
|
|
337
|
+
position: relative;
|
|
338
|
+
}
|
|
339
|
+
.token-card::before {
|
|
340
|
+
background: var(--ink);
|
|
341
|
+
bottom: 0;
|
|
342
|
+
content: "";
|
|
343
|
+
left: .25rem;
|
|
344
|
+
position: absolute;
|
|
345
|
+
top: 0;
|
|
346
|
+
width: 1px;
|
|
347
|
+
}
|
|
348
|
+
.token-card.revoked { color: var(--muted); }
|
|
349
|
+
.token-card.revoked::before { background: var(--rule); }
|
|
350
|
+
|
|
288
351
|
details { margin-top: .75rem; }
|
|
289
352
|
summary { cursor: pointer; list-style: none; width: max-content; }
|
|
290
353
|
summary::-webkit-details-marker { display: none; }
|