@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +49 -92
  3. package/dist/access-tokens.d.ts +32 -0
  4. package/dist/access-tokens.d.ts.map +1 -0
  5. package/dist/access-tokens.js +225 -0
  6. package/dist/access-tokens.js.map +1 -0
  7. package/dist/catalog-service.d.ts.map +1 -1
  8. package/dist/catalog-service.js +1 -4
  9. package/dist/catalog-service.js.map +1 -1
  10. package/dist/errors.d.ts +5 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/errors.js +26 -0
  13. package/dist/errors.js.map +1 -1
  14. package/dist/execute.d.ts +3 -1
  15. package/dist/execute.d.ts.map +1 -1
  16. package/dist/execute.js +79 -18
  17. package/dist/execute.js.map +1 -1
  18. package/dist/executor-result.d.ts.map +1 -1
  19. package/dist/executor-result.js +37 -6
  20. package/dist/executor-result.js.map +1 -1
  21. package/dist/executors/quickjs-protocol.d.ts +12 -0
  22. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  23. package/dist/executors/quickjs-protocol.js +14 -0
  24. package/dist/executors/quickjs-protocol.js.map +1 -1
  25. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  26. package/dist/executors/quickjs-runtime.js +6 -3
  27. package/dist/executors/quickjs-runtime.js.map +1 -1
  28. package/dist/executors/quickjs.d.ts.map +1 -1
  29. package/dist/executors/quickjs.js +10 -4
  30. package/dist/executors/quickjs.js.map +1 -1
  31. package/dist/index.d.ts +27 -6
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +43 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/invocation.d.ts.map +1 -1
  36. package/dist/invocation.js +1 -4
  37. package/dist/invocation.js.map +1 -1
  38. package/dist/meta-tools.d.ts +26 -5
  39. package/dist/meta-tools.d.ts.map +1 -1
  40. package/dist/meta-tools.js +84 -40
  41. package/dist/meta-tools.js.map +1 -1
  42. package/dist/operator-ui/generated.d.ts +2 -2
  43. package/dist/operator-ui/generated.d.ts.map +1 -1
  44. package/dist/operator-ui/generated.js +2 -2
  45. package/dist/operator-ui/generated.js.map +1 -1
  46. package/dist/operator-ui/model.d.ts +2 -0
  47. package/dist/operator-ui/model.d.ts.map +1 -1
  48. package/dist/operator-ui/model.js.map +1 -1
  49. package/dist/routes/access-tokens.d.ts +7 -0
  50. package/dist/routes/access-tokens.d.ts.map +1 -0
  51. package/dist/routes/access-tokens.js +84 -0
  52. package/dist/routes/access-tokens.js.map +1 -0
  53. package/dist/routes/mcp.d.ts.map +1 -1
  54. package/dist/routes/mcp.js +8 -2
  55. package/dist/routes/mcp.js.map +1 -1
  56. package/dist/routes/shared.d.ts +11 -2
  57. package/dist/routes/shared.d.ts.map +1 -1
  58. package/dist/routes/shared.js.map +1 -1
  59. package/dist/routes/ui.d.ts.map +1 -1
  60. package/dist/routes/ui.js +9 -1
  61. package/dist/routes/ui.js.map +1 -1
  62. package/dist/server.d.ts.map +1 -1
  63. package/dist/server.js +5 -0
  64. package/dist/server.js.map +1 -1
  65. package/dist/skills.d.ts +15 -3
  66. package/dist/skills.d.ts.map +1 -1
  67. package/dist/skills.js +63 -10
  68. package/dist/skills.js.map +1 -1
  69. package/dist/storage/file.d.ts.map +1 -1
  70. package/dist/storage/file.js +5 -0
  71. package/dist/storage/file.js.map +1 -1
  72. package/dist/storage/memory.d.ts.map +1 -1
  73. package/dist/storage/memory.js +8 -0
  74. package/dist/storage/memory.js.map +1 -1
  75. package/dist/types.d.ts +19 -0
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/ui.d.ts +4 -4
  78. package/dist/ui.d.ts.map +1 -1
  79. package/dist/ui.js +44 -1
  80. package/dist/ui.js.map +1 -1
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.d.ts.map +1 -1
  83. package/dist/version.js +1 -1
  84. package/dist/version.js.map +1 -1
  85. package/package.json +2 -2
  86. package/src/access-tokens.ts +289 -0
  87. package/src/catalog-service.ts +1 -5
  88. package/src/errors.ts +28 -0
  89. package/src/execute.ts +123 -48
  90. package/src/executor-result.ts +50 -6
  91. package/src/executors/quickjs-protocol.ts +19 -0
  92. package/src/executors/quickjs-runtime.ts +6 -2
  93. package/src/executors/quickjs.ts +10 -3
  94. package/src/index.ts +85 -5
  95. package/src/invocation.ts +1 -5
  96. package/src/meta-tools.ts +116 -53
  97. package/src/operator-ui/browser.css +63 -0
  98. package/src/operator-ui/browser.ts +288 -2
  99. package/src/operator-ui/generated.ts +2 -2
  100. package/src/operator-ui/model.ts +6 -0
  101. package/src/routes/access-tokens.ts +115 -0
  102. package/src/routes/mcp.ts +8 -2
  103. package/src/routes/shared.ts +11 -1
  104. package/src/routes/ui.ts +9 -0
  105. package/src/server.ts +5 -0
  106. package/src/skills.ts +79 -9
  107. package/src/storage/file.ts +5 -0
  108. package/src/storage/memory.ts +8 -0
  109. package/src/types.ts +20 -0
  110. package/src/ui.ts +50 -1
  111. 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 ${ns}.${fn} could not be serialized: ${msg(err)}`,
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 ${ns}.${fn} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`,
263
+ `Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`,
260
264
  );
261
265
  }
262
266
  return json;
@@ -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.namespace}.${payload.functionName}`,
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 ${payload.namespace}.${payload.functionName} exceeds the ${MAX_QUICKJS_HOST_RPC_BYTES}-byte serialized bridge limit.`
620
- : `Host result from ${payload.namespace}.${payload.functionName} could not be serialized: ${msg(err)}`;
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 the optional execute_code meta-tool (code mode). Omit the
173
- * tool is not registered and connecta serves the nine base tools. Workers:
174
- * `new DynamicWorkerExecutor({ loader: env.LOADER })` from
175
- * `@cloudflare/codemode`. Node: `quickJsExecutor()` from "@zackbart/connecta/quickjs".
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(config.auth);
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
- messageLooksRetryable,
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 { ConnectorStatus, KVStorage } from "./types.js";
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
- * The nine meta-tool handlers over a registry. Exported for direct testing;
477
- * registerMetaTools() wires them onto an McpServer. `opts.defaultToolTimeoutMs`
478
- * supplies a deadline for calls that don't carry one. (execute_code, the
479
- * optional tenth tool, is registered separately by registerExecuteTool.)
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
- /** Register the nine meta-tools onto an McpServer instance. */
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
- server.registerTool(
1236
- "list_connectors",
1237
- {
1238
- description: LIST_DESC,
1239
- inputSchema: z.object({ probe: z.boolean().optional() }),
1240
- annotations: READ_ONLY_REMOTE,
1241
- },
1242
- async (args) => mt.listConnectors(args as ListArgs),
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(registry, SEARCH_DESC, "search"),
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
- server.registerTool(
1263
- "describe_tools",
1264
- {
1265
- description: describedFor(registry, DESCRIBE_DESC, "describe"),
1266
- inputSchema: z.object({
1267
- addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
1268
- format: z.enum(["compact", "json"]).optional(),
1269
- fullDescriptions: z.boolean().optional(),
1270
- }),
1271
- annotations: READ_ONLY_REMOTE,
1272
- },
1273
- async (args) => mt.describeTools(args as DescribeArgs),
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
- server.registerTool(
1340
- "batch_call",
1341
- {
1342
- description: BATCH_DESC,
1343
- inputSchema: z.object({
1344
- calls: z
1345
- .array(z.object(CALL_INPUT_SCHEMA))
1346
- .min(1)
1347
- .max(10),
1348
- resultMode: z.enum(["mcp", "value"]).optional(),
1349
- timeoutMs: z.number().int().positive().optional(),
1350
- maxRetries: z.number().int().min(0).max(2).optional(),
1351
- diagnostics: z.boolean().optional(),
1352
- }),
1353
- // Same gate as call_tool: every call in the batch must be explicitly
1354
- // read-only or the batch is refused.
1355
- annotations: READ_ONLY_REMOTE,
1356
- },
1357
- async (args) => mt.batchCall(args as BatchArgs),
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; }