@zackbart/connecta 0.16.1 → 0.18.0

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 (46) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +4 -0
  3. package/dist/catalog-service.d.ts +10 -0
  4. package/dist/catalog-service.js +77 -5
  5. package/dist/catalog.js +114 -12
  6. package/dist/errors.d.ts +4 -6
  7. package/dist/execute.d.ts +7 -0
  8. package/dist/execute.js +262 -168
  9. package/dist/invocation.js +3 -1
  10. package/dist/meta-tools.d.ts +4 -0
  11. package/dist/meta-tools.js +55 -23
  12. package/dist/operator-ui/generated.d.ts +1 -1
  13. package/dist/operator-ui/generated.js +1 -1
  14. package/dist/operator-ui/model.d.ts +3 -1
  15. package/dist/providers/mixpanel.d.ts +3 -5
  16. package/dist/providers/mixpanel.js +73 -5
  17. package/dist/providers/stripe.d.ts +25 -24
  18. package/dist/providers/stripe.js +64 -35
  19. package/dist/registry.d.ts +32 -9
  20. package/dist/registry.js +217 -33
  21. package/dist/routes/mcp.js +6 -0
  22. package/dist/routes/ui.js +1 -1
  23. package/dist/skills.d.ts +5 -1
  24. package/dist/skills.js +206 -30
  25. package/dist/types.d.ts +14 -2
  26. package/dist/ui.js +4 -1
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/documentation/architecture.md +8 -5
  30. package/documentation/code-mode.md +68 -68
  31. package/documentation/connector-guides.md +29 -27
  32. package/documentation/connectors.md +13 -1
  33. package/documentation/meta-tools.md +53 -19
  34. package/documentation/mixpanel.md +20 -0
  35. package/documentation/notion.md +17 -0
  36. package/documentation/operations.md +24 -21
  37. package/documentation/operator-ui.md +12 -2
  38. package/documentation/provider-audit.md +15 -7
  39. package/documentation/provider-conventions.md +26 -13
  40. package/documentation/stripe.md +66 -59
  41. package/documentation/upgrading.md +46 -4
  42. package/ethos.md +7 -7
  43. package/examples/worker/README.md +4 -3
  44. package/package.json +2 -2
  45. package/templates/node/README.md +7 -0
  46. package/templates/node/package.json +5 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,186 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.18.0 — 2026-08-13
6
+
7
+ This minor release spends fewer tokens before the first call and answers
8
+ better when a call goes wrong. The always-loaded MCP instructions and tool
9
+ definitions are 44.6% lighter, the `execute_code` description now names the
10
+ deployment's configured connector IDs before the first catalog search, and a
11
+ failed `connecta.describe` entry carries the same typed recovery as a failed
12
+ call. The compatibility change is the floor: Node 22 is now the minimum
13
+ supported release, matching the shipped Docker template, and CI runs one job
14
+ per pull request against it; the Cloudflare Worker target is unchanged. Stripe
15
+ OAuth deployments must drop the connector-wide `mode` — each returned account
16
+ now carries its own. An evaluation of erasable TypeScript syntax in
17
+ `execute_code` programs ended in a recorded refusal, so portable programs
18
+ remain plain JavaScript. A deployment already on Node 22 that lets accounts
19
+ carry their Stripe mode needs no configuration change.
20
+
21
+ ### Added
22
+
23
+ - **`execute_code` now names the deployment's connectors up front.** Its
24
+ description carries a connector inventory derived only from the configured
25
+ registry: canonical IDs in registry order, the executable shortcut when
26
+ sanitization changes the ID, bounded to 256 serialized bytes with an exact
27
+ omitted count. A cold model can scope its first catalog search without
28
+ guessing, and rendering the inventory performs no catalog request and no
29
+ credential access (#416).
30
+
31
+ - **Failed `connecta.describe` entries are now as repairable as failed calls.**
32
+ Each failed entry carries `errorDetails` beside the preserved human text: the
33
+ invocation path's stable `code` and `retryable`, a route-aware `nextAction`,
34
+ and up to three deterministic nearby canonical addresses when the connector
35
+ is known. Caller-authored addresses are clamped everywhere they echo, so
36
+ hostile input can no longer displace partial results (#417).
37
+
38
+ ### Changed
39
+
40
+ - **The always-loaded surface got 44.6% lighter.** The MCP instructions and the
41
+ seven tool definitions now carry route selection, the fail-closed read-only
42
+ boundary, and the minimum guest syntax — 6,850 bytes instead of 12,358.
43
+ Selection discipline, runtime differences, worked examples, direct-call
44
+ options, `get_result` rules, the `connecta.ui` reads-binding shape, and
45
+ repair guidance moved into the on-demand `usage` skill, which grew to a
46
+ deliberate 9,000-byte cap and stays byte-identical across deployments.
47
+ Clients that never fetch the skill keep correct routing from the compact
48
+ definitions alone (#418).
49
+
50
+ - **Node 22 is now the minimum supported Node release.** CI and the published
51
+ engine range now match the Node 22 runtime used by the shipped deployment
52
+ template. The Worker test project continues to cover the other deployment
53
+ target (#422).
54
+
55
+ - **Stripe OAuth mode now belongs to each returned account.** OAuth-backed
56
+ connectors no longer accept a connector-wide `mode`. Their neutral metadata
57
+ and guide support mixed live and sandbox accounts, require
58
+ `list_available_accounts_or_orgs`, and carry its exact `stripe_context` and
59
+ `livemode` into each account-scoped call. They use the stricter sandbox
60
+ admission ceiling. Header credentials still require one fixed mode, retain
61
+ key-prefix contradiction checks, and keep Stripe Connect behavior (#414).
62
+
63
+ - **The reviewed Notion writes stay narrow.** Newly published workspace-private
64
+ creation, templates, placement, richer media, locking, and irreversible
65
+ content erasure do not join the maintained surface. They are distinct
66
+ ownership, asynchronous, ordering, file, coordination, or deletion workflows,
67
+ not extra fields on `create_page` or `update_page_properties`. The existing
68
+ request subsets remain valid, and `trash_page` stays isolated and reversible
69
+ (#408, #409).
70
+
71
+ ### Fixed
72
+
73
+ - **The Node template now declares its one reviewed install script.** npm 11 no
74
+ longer warns that esbuild's install is unreviewed, and npm 12 will not block
75
+ it. The approval is pinned to `esbuild@0.28.2`; the package smoke reads the
76
+ generated lockfile and fails if any resolved dependency has an unapproved
77
+ install script, including a future esbuild version. Local and container
78
+ installs keep the same `tsx` runtime path (#375).
79
+
80
+ - The documentation gate now ignores `.claude`, whose nested agent worktrees
81
+ are separate historical checkouts rather than source in the current tree.
82
+
83
+ ## 0.17.0 — 2026-08-13
84
+
85
+ This minor release makes catalog discovery faster and its answers more exact.
86
+ Agent reads can use a verified stale catalog while one bounded refresh runs,
87
+ guide summaries now read Markdown as prose, compact schemas retain more declared
88
+ constraints, array projection distinguishes misses from genuine nulls, and
89
+ Mixpanel carries the conditional rules its live tools enforce. One construction
90
+ contract tightens: an explicit `usageGuide.summary` over 120 characters now
91
+ refuses to boot. Deployments whose summaries fit, or which let Connecta derive
92
+ them, need no configuration change. Operator catalog reads retain their blocking
93
+ freshness behavior. This release also makes the two code sandboxes' runtime
94
+ differences explicit. The shipped
95
+ Worker example is already loader-only; deployments that added executor
96
+ `bindings`, `modules`, or `globalOutbound` must remove them. Existing portable
97
+ `execute_code` programs keep their behavior. Programs can now classify a caught
98
+ Connecta failure without parsing its message.
99
+
100
+ ### Changed
101
+
102
+ - An explicit `usageGuide.summary` longer than 120 characters after whitespace
103
+ normalization now throws during registry construction. Exactly 120 remains
104
+ valid, and a blank explicit summary still falls back to derivation (#392).
105
+
106
+ - **Agent catalog reads now serve a verified stale entry while they refresh it.**
107
+ Search, describe, and code-mode calls no longer await a downstream listing
108
+ when the runtime already holds a complete catalog inside its stale window.
109
+ The inbound request still causes the refresh; there is no timer, warmup, or
110
+ credential probe. One bounded refresh per connector owns and closes a fresh
111
+ scope, while operator status stays blocking and shows whether the last agent
112
+ read in this runtime was fresh or stale (#396).
113
+
114
+ ### Fixed
115
+
116
+ - **Caught `execute_code` failures now keep their machine-readable type.**
117
+ Calls, connector shortcuts, discovery, emitted-output and UI validation,
118
+ batch validation, and host-call budgets still throw with the same human
119
+ message, but now expose `code`, `retryable`, and full `details`. Batch entries
120
+ use the same codes. A per-run authenticated frame prevents connector prose or
121
+ guest code from forging the host transport on either executor (#393).
122
+
123
+ - **`execute_code` now tells the truth about each shipped sandbox.** QuickJS
124
+ has no `fetch`, `process`, timers, `crypto`, or `WebSocket`, and blocks
125
+ imports. Loader-only Dynamic Workers deny outbound fetch, WebSocket,
126
+ `node:net`, and `node:tls`; leave DNS unresolved; expose no environment
127
+ bindings or filesystem/HTTP builtins; but retain local `data:` fetch,
128
+ runtime globals, and a non-contract builtin set that can drift. The example
129
+ pins the required loader-only construction, and agent guidance tells portable
130
+ programs to use none of that Dynamic-only authority (#390).
131
+
132
+ - **Clerk-authenticated operator pages now wait for ClerkJS before booting.**
133
+ The loader runs before the later inline operator bundle instead of deferring
134
+ until after parsing, so a fresh page no longer mistakes normal script order
135
+ for a network failure. Clerk's major-to-pinned version redirect remains
136
+ supported, and a real loader failure keeps the existing clear error (#403).
137
+
138
+ - **Stripe's guide now treats connector identity as routing intent, not account
139
+ proof.** One OAuth session may cover several accounts in one organization,
140
+ so agents resolve the intended account through the live tool schema and stop
141
+ when the target or selector is ambiguous. The guide also keeps organization
142
+ accounts separate from the restricted-key-only Stripe Connect path (#404).
143
+
144
+ - **The no-account-model constitution now matches provider-owned sessions.**
145
+ Connecta still has no account dimension: credentials, storage, admission,
146
+ and health remain connector-scoped. A provider may expose its own account
147
+ scope only through its live schema; metadata never proves identity, and an
148
+ ambiguous target or selector stops instead of becoming a guess (#410).
149
+
150
+ - **The reviewed Notion page contracts are current again.** Notion added
151
+ create-page template and placement options plus update-page locking,
152
+ template, and erase options. The existing parent, properties, Markdown,
153
+ children, emoji, and trash request subsets remain valid, so this release
154
+ records the two changed endpoint digests without adding the new capabilities.
155
+ Their product decisions remain in #408 and #409.
156
+
157
+ - **Array field misses now report what happened.** A path that misses every
158
+ element appears in `unmatchedFields` instead of returning a clean array of
159
+ false nulls. A heterogeneous array keeps its positional result and names the
160
+ path in `partialFields`, so genuine downstream nulls remain distinguishable.
161
+ Schema-backed misses keep the same bounded guidance through nested arrays;
162
+ schema-free projections still report their observed misses (#394).
163
+ - Derived guide summaries now join a hard-wrapped opening paragraph before
164
+ selecting a complete sentence or shortening at a clause or word boundary.
165
+ Frontmatter, fences, headings, rules, tables, and description fallbacks keep
166
+ their prior roles; multi-line HTML comments are now skipped whole (#392).
167
+
168
+ - **Compact schemas now carry declared numeric and string constraints.** Search
169
+ and compact describe show numeric bounds, multiples, string length bounds,
170
+ patterns, and formats beside the affected type. Search keeps its 1,024-byte
171
+ schema ceiling and 256-byte node budget: a constraint that does not fit is
172
+ dropped whole, and the existing truncation flag sends the caller to describe
173
+ for the complete shape (#391).
174
+
175
+ - **Carry Mixpanel's three enforced conditional-input rules in its maintained
176
+ guide.** A live read-only audit confirmed that `Get-Business-Context`,
177
+ `Get-Property-Values`, and `List-Properties` accept shapes in their advertised
178
+ schemas that their implementations reject. Connecta still preserves the
179
+ hosted schemas unchanged; the guide now prevents those rejected calls, the
180
+ vetted manifest records schema digests for all 63 tools, and the provider
181
+ defect is tracked upstream. The maintainer drift check also frames
182
+ Mixpanel's service account as its documented `Bearer Basic` value instead of
183
+ ordinary HTTP Basic (#395).
184
+
5
185
  ## 0.16.1 — 2026-08-13
6
186
 
7
187
  This is the cleanup that follows 0.16.0 out the door: the packaging housekeeping
package/README.md CHANGED
@@ -110,6 +110,10 @@ connector tools are reachable. Unannotated or write-capable calls stay
110
110
  individual and cross `call_destructive_tool`, where the MCP host can ask the
111
111
  operator for approval.
112
112
 
113
+ The Node template also pins its one approved dependency install script:
114
+ esbuild, which `tsx` needs to run the deployment source. A dependency update
115
+ that adds or changes an install script fails the package smoke until reviewed.
116
+
113
117
  There are two deployment shapes and no others:
114
118
 
115
119
  - [Node, local or Docker](./templates/node/) — what `init` copies
@@ -1,5 +1,6 @@
1
1
  import type { CallErrorDetails } from "./errors.js";
2
2
  import type { ConnectorOperationOptions, RegistryView } from "./registry.js";
3
+ import type { DeferredWork } from "./connector-scope.js";
3
4
  import type { Connector, ToolDef } from "./types.js";
4
5
  export declare const DEFAULT_SEARCH_LIMIT = 8;
5
6
  export declare const MAX_SEARCH_LIMIT = 100;
@@ -71,6 +72,11 @@ interface CatalogFailureDetail {
71
72
  retryable: boolean;
72
73
  retryAfterMs?: number;
73
74
  }
75
+ interface CatalogDescriptionFailureDetail extends CatalogFailureDetail {
76
+ nextAction?: NonNullable<CallErrorDetails["nextAction"]>;
77
+ /** Nearby canonical addresses, ranked deterministically by tool name. */
78
+ suggestions?: string[];
79
+ }
74
80
  export interface CatalogSearchPage {
75
81
  entries: CatalogSearchEntry[];
76
82
  total: number;
@@ -108,6 +114,7 @@ export interface CatalogDescription {
108
114
  outputSchema?: unknown;
109
115
  annotations?: ToolDef["annotations"];
110
116
  error?: string;
117
+ errorDetails?: CatalogDescriptionFailureDetail;
111
118
  }
112
119
  export interface ResolvedCatalogTool {
113
120
  connector: Connector;
@@ -138,6 +145,7 @@ export declare class CatalogService {
138
145
  private readonly probeTimeoutMs;
139
146
  private readonly concurrency;
140
147
  private readonly searchRoute;
148
+ private readonly readOptions;
141
149
  private readonly loaded;
142
150
  private readonly loading;
143
151
  constructor(registry: RegistryView, baseUrl: string, options?: {
@@ -146,6 +154,8 @@ export declare class CatalogService {
146
154
  concurrency?: number;
147
155
  /** The discovery route recovery records name. Default `search_tools`. */
148
156
  searchRoute?: SearchRoute;
157
+ /** Runtime-owned tail for stale-while-revalidate catalog reads. */
158
+ defer?: DeferredWork;
149
159
  });
150
160
  /**
151
161
  * Send a caller back to discovery through the surface it can actually reach.
@@ -16,6 +16,7 @@ const MAX_QUERY_TERM_LENGTH = 64;
16
16
  * of the deployment.
17
17
  */
18
18
  const MAX_IDENTITY_CONNECTORS = 3;
19
+ const MAX_DESCRIBE_SUGGESTIONS = 3;
19
20
  const encoder = new TextEncoder();
20
21
  /** Clip one echoed query term without splitting a non-BMP code point. */
21
22
  function boundedQueryTerm(term) {
@@ -80,6 +81,39 @@ function recoveryQuery(address) {
80
81
  const candidate = separator >= 0 ? address.slice(separator + 1) : address;
81
82
  return boundedEchoText(candidate.replaceAll(/[._-]+/g, " ").trim() || address);
82
83
  }
84
+ function editDistance(left, right) {
85
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
86
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
87
+ const current = [leftIndex + 1];
88
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
89
+ current.push(Math.min(previous[rightIndex + 1] + 1, current[rightIndex] + 1, previous[rightIndex] +
90
+ (left[leftIndex] === right[rightIndex] ? 0 : 1)));
91
+ }
92
+ previous = current;
93
+ }
94
+ return previous[right.length];
95
+ }
96
+ /** Nearby names only; descriptions never influence describe-miss recovery. */
97
+ function describeSuggestions(connectorId, attemptedName, tools) {
98
+ const attempted = attemptedName.toLowerCase();
99
+ return tools
100
+ .map((tool, order) => {
101
+ const name = tool.name.toLowerCase();
102
+ return { tool, order, distance: editDistance(attempted, name) };
103
+ })
104
+ .filter(({ tool, distance }) => {
105
+ const longest = Math.max(attempted.length, tool.name.length);
106
+ return (attempted.includes(tool.name.toLowerCase()) ||
107
+ tool.name.toLowerCase().includes(attempted) ||
108
+ distance <= Math.max(2, Math.floor(longest * 0.4)));
109
+ })
110
+ .sort((left, right) => left.distance - right.distance || left.order - right.order)
111
+ .map(({ tool }) => `${connectorId}.${tool.name}`)
112
+ // A clipped address would no longer be canonical. Omit an implausibly
113
+ // large catalog name instead of letting one suggestion erase the page.
114
+ .filter((address) => boundedEchoText(address) === address)
115
+ .slice(0, MAX_DESCRIBE_SUGGESTIONS);
116
+ }
83
117
  /** Serialize once and count the exact bytes the MCP adapter would emit. */
84
118
  export function boundedDiscoveryText(value, hint) {
85
119
  const text = JSON.stringify(value);
@@ -167,6 +201,7 @@ export class CatalogService {
167
201
  probeTimeoutMs;
168
202
  concurrency;
169
203
  searchRoute;
204
+ readOptions;
170
205
  loaded = new Map();
171
206
  loading = new Map();
172
207
  constructor(registry, baseUrl, options = {}) {
@@ -177,6 +212,12 @@ export class CatalogService {
177
212
  normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
178
213
  this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
179
214
  this.searchRoute = options.searchRoute ?? "search_tools";
215
+ this.readOptions = options.defer
216
+ ? {
217
+ defer: options.defer,
218
+ refreshTimeoutMs: this.probeTimeoutMs,
219
+ }
220
+ : undefined;
180
221
  }
181
222
  /**
182
223
  * Send a caller back to discovery through the surface it can actually reach.
@@ -207,7 +248,7 @@ export class CatalogService {
207
248
  if (inFlight)
208
249
  return inFlight;
209
250
  const loading = this.registry
210
- .getTools(id, this.baseUrl, this.requestScope, callOptions)
251
+ .getTools(id, this.baseUrl, this.requestScope, callOptions, this.readOptions)
211
252
  .then((tools) => {
212
253
  this.loaded.set(id, tools);
213
254
  return tools;
@@ -727,17 +768,48 @@ export class CatalogService {
727
768
  });
728
769
  return resolved.map(({ address, resolved: addressResolution }) => {
729
770
  if (!addressResolution) {
730
- return { address, error: `Unknown address "${address}"` };
771
+ const message = `Unknown address "${boundedEchoText(address)}"`;
772
+ return {
773
+ address: boundedEchoText(address),
774
+ error: message,
775
+ errorDetails: {
776
+ ...framingError("unknown_address", message),
777
+ nextAction: this.searchRecovery({ query: recoveryQuery(address) }, "Find the configured canonical address before retrying."),
778
+ },
779
+ };
731
780
  }
732
781
  const catalog = catalogs.get(addressResolution.connector.id);
733
782
  if (catalog instanceof Error) {
734
- return { address, error: catalog.message };
783
+ const classified = classifyCallError(catalog, "catalog_lookup_failed");
784
+ const message = boundedEchoText(classified.message);
785
+ return {
786
+ address: boundedEchoText(address),
787
+ error: message,
788
+ errorDetails: {
789
+ code: classified.code,
790
+ message,
791
+ retryable: classified.retryable,
792
+ ...(classified.retryAfterMs === undefined
793
+ ? {}
794
+ : { retryAfterMs: classified.retryAfterMs }),
795
+ },
796
+ };
735
797
  }
736
798
  const tool = catalog?.find((item) => item.name === addressResolution.toolName);
737
799
  if (!tool) {
800
+ const message = `Unknown tool "${boundedEchoText(addressResolution.toolName)}" on connector "${addressResolution.connector.id}"`;
801
+ const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, catalog ?? []);
738
802
  return {
739
- address,
740
- error: `Unknown tool "${addressResolution.toolName}" on connector "${addressResolution.connector.id}"`,
803
+ address: boundedEchoText(address),
804
+ error: message,
805
+ errorDetails: {
806
+ ...framingError("unknown_tool", message),
807
+ nextAction: this.searchRecovery({
808
+ query: recoveryQuery(addressResolution.toolName),
809
+ connector: addressResolution.connector.id,
810
+ }, "Find the connector's current canonical tool address."),
811
+ ...(suggestions.length > 0 ? { suggestions } : {}),
812
+ },
741
813
  };
742
814
  }
743
815
  const input = tool.inputSchema ?? { type: "object" };
package/dist/catalog.js CHANGED
@@ -2,6 +2,7 @@ const DEFAULT_DESCRIPTION_LENGTH = 240;
2
2
  const DISCOVERY_DESCRIPTION_LENGTH = 160;
3
3
  export const MAX_COMPACT_DISCOVERY_SCHEMA_BYTES = 1_024;
4
4
  const MAX_COMPACT_DISCOVERY_ENUM_BYTES = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES / 4;
5
+ const MAX_COMPACT_DISCOVERY_CONSTRAINT_BYTES = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES / 4;
5
6
  const schemaEncoder = new TextEncoder();
6
7
  const COMPACT_DISCOVERY_TRUNCATION = " /* truncated */";
7
8
  export function summarizeDescription(text, full) {
@@ -323,7 +324,8 @@ function declaresShape(s) {
323
324
  s.const !== undefined ||
324
325
  s.items !== undefined ||
325
326
  s.properties !== undefined ||
326
- s.type !== undefined);
327
+ s.type !== undefined ||
328
+ constraintEntries(s).length > 0);
327
329
  }
328
330
  /**
329
331
  * Parenthesize a top-level union so it doesn't read as part of a surrounding
@@ -366,6 +368,56 @@ function renderEnum(values, byteLimit, onTruncated) {
366
368
  }
367
369
  return rendered;
368
370
  }
371
+ function safeConstraintValue(value) {
372
+ return JSON.stringify(value).replaceAll("*/", "*\\/");
373
+ }
374
+ function constraintEntries(schema) {
375
+ const entries = [];
376
+ const number = (keyword, label) => {
377
+ const value = schema[keyword];
378
+ if (typeof value === "number" && Number.isFinite(value)) {
379
+ entries.push(`${label} ${value}`);
380
+ }
381
+ };
382
+ const integer = (keyword, label) => {
383
+ const value = schema[keyword];
384
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
385
+ entries.push(`${label} ${value}`);
386
+ }
387
+ };
388
+ number("minimum", ">=");
389
+ number("exclusiveMinimum", ">");
390
+ number("maximum", "<=");
391
+ number("exclusiveMaximum", "<");
392
+ number("multipleOf", "multiple of");
393
+ integer("minLength", "length >=");
394
+ integer("maxLength", "length <=");
395
+ if (typeof schema.format === "string") {
396
+ entries.push(`format ${safeConstraintValue(schema.format)}`);
397
+ }
398
+ if (typeof schema.pattern === "string") {
399
+ entries.push(`pattern ${safeConstraintValue(schema.pattern)}`);
400
+ }
401
+ return entries;
402
+ }
403
+ function renderConstraints(base, schema, byteLimit, onTruncated) {
404
+ const entries = constraintEntries(schema);
405
+ if (entries.length === 0)
406
+ return base;
407
+ const kept = [];
408
+ for (const entry of entries) {
409
+ const candidate = ` /* ${[...kept, entry].join("; ")} */`;
410
+ if (byteLimit !== undefined &&
411
+ schemaEncoder.encode(candidate).length > byteLimit) {
412
+ onTruncated?.();
413
+ continue;
414
+ }
415
+ kept.push(entry);
416
+ }
417
+ return kept.length === 0
418
+ ? base
419
+ : `${grouped(base)} /* ${kept.join("; ")} */`;
420
+ }
369
421
  function renderSchema(schema, defs, seen, depth, options) {
370
422
  if (depth > 4)
371
423
  return "…";
@@ -405,24 +457,36 @@ function renderSchema(schema, defs, seen, depth, options) {
405
457
  seen.add(name);
406
458
  const rendered = renderSchema(target, defs, seen, depth, options);
407
459
  seen.delete(name);
408
- return rendered;
460
+ return options.renderConstraints
461
+ ? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
462
+ : rendered;
409
463
  }
410
464
  const union = (s.oneOf ?? s.anyOf);
411
465
  if (Array.isArray(union)) {
412
- return (union
466
+ const rendered = union
413
467
  .map((u) => renderSchema(u, defs, seen, depth + 1, options))
414
468
  .join(" | ") ||
415
- "unknown");
469
+ "unknown";
470
+ return options.renderConstraints
471
+ ? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
472
+ : rendered;
416
473
  }
417
474
  if (Array.isArray(s.enum)) {
418
- return renderEnum(s.enum, options.enumByteLimit, options.onEnumTruncated);
475
+ const rendered = renderEnum(s.enum, options.enumByteLimit, options.onEnumTruncated);
476
+ return options.renderConstraints
477
+ ? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
478
+ : rendered;
419
479
  }
420
480
  // Checked before type/properties so a discriminator like
421
481
  // { type: "string", const: "emoji" } renders as "emoji" rather than string.
422
482
  // JSON.stringify(undefined) returns undefined (not a string), so an explicit
423
483
  // `const: undefined` must fall through to the regular type rendering.
424
- if (s.const !== undefined)
425
- return JSON.stringify(s.const);
484
+ if (s.const !== undefined) {
485
+ const rendered = JSON.stringify(s.const);
486
+ return options.renderConstraints
487
+ ? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
488
+ : rendered;
489
+ }
426
490
  const type = s.type;
427
491
  if (type === "array" || s.items) {
428
492
  const items = s.items
@@ -454,10 +518,20 @@ function renderSchema(schema, defs, seen, depth, options) {
454
518
  })
455
519
  .join(", ")} }`;
456
520
  }
457
- if (typeof type === "string")
458
- return type;
459
- if (Array.isArray(type))
460
- return type.join(" | ");
521
+ if (typeof type === "string") {
522
+ return options.renderConstraints
523
+ ? renderConstraints(type, s, options.constraintByteLimit, options.onConstraintTruncated)
524
+ : type;
525
+ }
526
+ if (Array.isArray(type)) {
527
+ const rendered = type.join(" | ");
528
+ return options.renderConstraints
529
+ ? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
530
+ : rendered;
531
+ }
532
+ if (options.renderConstraints && constraintEntries(s).length > 0) {
533
+ return renderConstraints("unknown", s, options.constraintByteLimit, options.onConstraintTruncated);
534
+ }
461
535
  return JSON.stringify(schema);
462
536
  }
463
537
  const compactSchemas = new WeakMap();
@@ -475,6 +549,7 @@ export function compactSchema(schema) {
475
549
  rendered = renderSchema(schema, defs, new Set(), 0, {
476
550
  propertyDescriptions: true,
477
551
  requiredFirst: false,
552
+ renderConstraints: true,
478
553
  });
479
554
  }
480
555
  catch {
@@ -534,6 +609,7 @@ export function compactDiscoverySchema(schema) {
534
609
  };
535
610
  let rendered;
536
611
  let enumTruncated = false;
612
+ let constraintTruncated = false;
537
613
  try {
538
614
  rendered = renderSchema(schema, defs, new Set(), 0, {
539
615
  propertyDescriptions: false,
@@ -545,15 +621,41 @@ export function compactDiscoverySchema(schema) {
545
621
  onEnumTruncated: () => {
546
622
  enumTruncated = true;
547
623
  },
624
+ renderConstraints: true,
625
+ constraintByteLimit: MAX_COMPACT_DISCOVERY_CONSTRAINT_BYTES,
626
+ onConstraintTruncated: () => {
627
+ constraintTruncated = true;
628
+ },
548
629
  });
549
630
  }
550
631
  catch {
551
632
  rendered = JSON.stringify(schema);
552
633
  }
634
+ if (schemaEncoder.encode(rendered).length >
635
+ MAX_COMPACT_DISCOVERY_SCHEMA_BYTES) {
636
+ try {
637
+ rendered = renderSchema(schema, defs, new Set(), 0, {
638
+ propertyDescriptions: false,
639
+ requiredFirst: true,
640
+ enumByteLimit: MAX_COMPACT_DISCOVERY_ENUM_BYTES,
641
+ onEnumTruncated: () => {
642
+ enumTruncated = true;
643
+ },
644
+ renderConstraints: false,
645
+ });
646
+ constraintTruncated = true;
647
+ }
648
+ catch {
649
+ rendered = JSON.stringify(schema);
650
+ }
651
+ }
553
652
  const bytes = schemaEncoder.encode(rendered);
554
653
  let result;
555
654
  if (bytes.length <= MAX_COMPACT_DISCOVERY_SCHEMA_BYTES) {
556
- result = { text: rendered, truncated: enumTruncated };
655
+ result = {
656
+ text: rendered,
657
+ truncated: enumTruncated || constraintTruncated,
658
+ };
557
659
  }
558
660
  else {
559
661
  result = {
package/dist/errors.d.ts CHANGED
@@ -11,12 +11,10 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "rate_limited
11
11
  * Its own code because the next move is none of the others': not a retry,
12
12
  * not `authorize_connector`, not a reshaped argument object, but
13
13
  * re-addressing — look the identifier up again, or accept the absence and
14
- * carry on. Inside `execute_code` a program reads that difference off a
15
- * `connecta.batch` entry's `errorDetails.code` — continue past this one,
16
- * abort on `connector_call_failed` or lets the failure escape uncaught so
17
- * the model sees the typed envelope. Never off a caught error: the guest
18
- * bridge reduces a rejected host call to `new Error(message)` and drops
19
- * every own property, and message prose cannot be classified.
14
+ * carry on. Inside `execute_code` a program reads that difference from a
15
+ * caught error's `code` or a `connecta.batch` entry's `errorDetails.code` —
16
+ * continue past this one, abort on `connector_call_failed`. Message prose
17
+ * cannot be classified.
20
18
  *
21
19
  * Use it only where the provider distinguishes absence from a permission
22
20
  * gap. A status that means both "it is not there" and "you cannot see it" —
package/dist/execute.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import type { ActivityRequestContext } from "./activity.js";
3
+ import type { DeferredWork } from "./connector-scope.js";
3
4
  import { type ToolResult } from "./meta-tools.js";
4
5
  import { InvocationFailure } from "./invocation.js";
5
6
  import type { RegistryView } from "./registry.js";
6
7
  import type { Executor, ExecutorProvider, Logger } from "./types.js";
7
8
  export declare const EXECUTE_MAX_BATCH_CALLS = 10;
9
+ /** Complete entries plus an exact omission count, all inside this byte cap. */
10
+ export declare const CONNECTOR_INVENTORY_MAX_BYTES = 256;
8
11
  /**
9
12
  * Default budgets for `connecta.emit`. The byte budget is a transport bound,
10
13
  * not a context bound — emitted image/audio blocks reach the model as media,
@@ -157,6 +160,8 @@ export declare function buildSandboxProviders(registry: RegistryView, baseUrl: s
157
160
  * blocks nobody will ever return.
158
161
  */
159
162
  emitCollector?: EmitCollector;
163
+ /** Runtime-owned tail for stale catalog refreshes. */
164
+ defer?: DeferredWork;
160
165
  }): Promise<ExecutorProvider[]>;
161
166
  /** The execute_code handler. Exported for direct testing. */
162
167
  export declare function createExecuteTool(registry: RegistryView, baseUrl: string, executor: Executor, logger: Logger, activity?: ActivityRequestContext, config?: {
@@ -164,6 +169,7 @@ export declare function createExecuteTool(registry: RegistryView, baseUrl: strin
164
169
  probeTimeoutMs?: number;
165
170
  maxEmittedBytes?: number;
166
171
  maxEmittedBlocks?: number;
172
+ defer?: DeferredWork;
167
173
  }): ({ code, diagnostics: diagnosticsRequested }: {
168
174
  code: string;
169
175
  diagnostics?: boolean;
@@ -189,5 +195,6 @@ export declare function registerExecuteTool(server: McpServer, registry: Registr
189
195
  maxEmittedBytes?: number;
190
196
  /** Block-count budget for connecta.emit. Default 32. */
191
197
  maxEmittedBlocks?: number;
198
+ defer?: DeferredWork;
192
199
  }): void;
193
200
  export {};