@zackbart/connecta 0.17.0 → 0.18.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.
@@ -1,6 +1,6 @@
1
1
  import { remoteMcp, } from "../connectors/remote-mcp.js";
2
2
  import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
3
- /** Stripe publishes one hosted MCP endpoint; the credential selects the mode. */
3
+ /** Stripe publishes one hosted MCP endpoint for every account and mode. */
4
4
  export const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
5
5
  /**
6
6
  * Stripe documents no MCP-specific rate limit, so this transcribes the account
@@ -89,9 +89,9 @@ const TEST_KEY = /\b(?:sk|rk|pk)_test_/;
89
89
  * Refuse a deployment whose declared mode and supplied key disagree.
90
90
  *
91
91
  * This is the one half of production/sandbox routing connecta can actually
92
- * enforce. Nothing here reads or reports key material: an unrecognizable
93
- * credential (OAuth, or a key shape this release does not know) is left alone
94
- * rather than guessed at, and a mismatch names only the two modes.
92
+ * enforce. Nothing here reads or reports key material: an unrecognizable key
93
+ * shape is left alone rather than guessed at, and a mismatch names only the
94
+ * two modes.
95
95
  */
96
96
  function assertModeMatchesKey(id, mode, auth) {
97
97
  if (auth.type !== "headers")
@@ -125,6 +125,22 @@ function resolveAuth(id, options) {
125
125
  headers: { ...auth.headers, "Stripe-Account": connectedAccount },
126
126
  };
127
127
  }
128
+ const OAUTH_ADMISSION = STRIPE_ADMISSION.sandbox;
129
+ function oauthUsageGuide(purpose, instructions) {
130
+ const accountInstructions = instructions?.trim();
131
+ return `# Stripe usage
132
+
133
+ Scope: live and sandbox accounts. Connector purpose: ${purpose}
134
+
135
+ This OAuth session may expose both live and sandbox Stripe accounts. Call \`list_available_accounts_or_orgs\`, then carry its exact \`stripe_context\` and \`livemode\` into every account-scoped call. A live-mode write moves real money; a sandbox write changes test data. Never infer the account or mode from connector metadata.
136
+
137
+ - Call \`list_available_accounts_or_orgs\` before every account-scoped read or write. Select the intended result, then carry its \`stripe_context\` and \`livemode\` unchanged. If the account, mode, or supported selector is ambiguous, stop and ask; never guess.
138
+ - Organization accounts are not Stripe Connect connected accounts. A Connect call requires a separate connector with a deployment-configured restricted key plus Stripe's documented \`Stripe-Account\` header; OAuth does not support that path.
139
+ ${sharedUsageGuide("100 requests per second in live mode and 25 in sandbox mode")}
140
+ ${accountInstructions
141
+ ? `\n## Account instructions\n\n${accountInstructions}\n`
142
+ : ""}`;
143
+ }
128
144
  const MODE_COPY = {
129
145
  production: {
130
146
  title: "Stripe (production)",
@@ -137,34 +153,40 @@ const MODE_COPY = {
137
153
  warning: "This is a SANDBOX Stripe connection. Nothing here is real money and none of these objects exist in production, so never answer a question about live revenue, payouts, or a named customer from this connector.",
138
154
  },
139
155
  };
140
- function usageGuide(mode, purpose, instructions) {
156
+ function fixedModeUsageGuide(mode, purpose, instructions) {
141
157
  const copy = MODE_COPY[mode];
142
158
  const accountInstructions = instructions?.trim();
143
- const rate = mode === "production" ? "100" : "25";
144
159
  return `# Stripe usage
145
160
 
146
161
  Mode: ${mode}. Connector purpose: ${purpose}
147
162
 
148
163
  ${copy.warning}
149
164
 
150
- - One OAuth session may cover more than one account in the same Stripe organization. The connector id, title, and purpose state routing intent; they do not prove which account a call will use.
151
- - Before every account-scoped read or write, resolve the intended organization account. Inspect the chosen tool's live input schema and carry the exact account or context field it exposes. If the account or its supported selection mechanism is ambiguous, stop and ask; never guess from the connector metadata, invent an MCP argument, or add a request header the live contract does not expose.
152
165
  - Organization accounts are not Stripe Connect connected accounts. A Connect call requires a deployment-configured restricted key plus Stripe's documented \`Stripe-Account\` header; OAuth does not support that path. Do not try to turn an organization-account call into a Connect call inside tool arguments.
166
+ ${sharedUsageGuide(`${mode === "production" ? "100" : "25"} requests per second`)}
167
+ ${accountInstructions
168
+ ? `\n## Account instructions\n\n${accountInstructions}\n`
169
+ : ""}`;
170
+ }
171
+ function sharedUsageGuide(rate) {
172
+ return `
153
173
  - Four generic tools reach any Stripe API method. Find the method with \`stripe_api_search\`, read its parameters with \`stripe_api_details\`, then call \`stripe_api_read\` (GET) or \`stripe_api_write\` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — \`stripe_api_details\` is cheaper than a rejected write.
154
174
  - Prefer a dedicated tool when one covers the task: \`get_stripe_account_info\` for account information, \`get_balance_summary\` for balances, \`create_refund\` for refunds, \`stripe_report\` for reports. One call instead of three, and a refund named \`create_refund\` reads far more clearly in the approval a human sees than the same refund buried in \`stripe_api_write\` arguments.
155
175
  - \`stripe_api_write\` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; expect approval on every call.
156
- - Lists are cursor-paginated: \`limit\` defaults to 10 and caps at 100, \`starting_after\` and \`ending_before\` take an object id and are mutually exclusive, and \`has_more\` says whether to continue. Page inside \`execute_code\` and reduce before returning.
176
+ - Lists are cursor-paginated: \`limit\` defaults to 10 and caps at 100, \`starting_after\` and \`ending_before\` take an object id and are mutually exclusive, and \`has_more\` says whether to continue. Page inside \`execute_code\`.
177
+ - Any \`stripe_api_read\` list or \`stripe_api_search\` that returns full objects belongs inside \`execute_code\`, projected to the fields the question needs before \`return\`. Neither \`limit\` nor \`expand\` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.
178
+ - Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes \`amount\`, \`created\`, \`currency\`, \`customer\`, \`status\`, \`refunded\`, \`disputed\`, \`metadata\`, \`billing_details.address.postal_code\`, and \`payment_method_details.<source>.*\` card fields — there is no \`payment_intent\` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's \`latest_charge\`) instead of retrying the search with another spelling.
179
+ - The account → \`stripe_api_search\` → \`stripe_api_details\` → \`stripe_api_read\` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.
180
+ - Decline outcomes live on the charge — \`outcome\`, \`failure_code\`, \`failure_message\` — reached from the PaymentIntent's \`latest_charge\`, so "why did this payment fail" is a PaymentIntent read followed by one charge read.
157
181
  - Resolve ids before acting; never guess one. Stripe ids are typed prefixes — \`cus_\` customer, \`sub_\` subscription, \`ch_\` charge, \`pi_\` payment intent, \`in_\` invoice, \`acct_\` account — and a plausible-looking id belongs to a different object or to nobody. Find the object with \`stripe_api_search\` (or a list endpoint through \`stripe_api_read\`) and carry the \`id\` it returned into the write.
158
182
  - This connection's tool list is not a fixed set. Stripe gates parts of its MCP catalog by account, integration, and beta enrollment, so search this connector for what it actually exposes rather than assuming a documented tool is here.
159
183
  - Amounts are integers in the currency's minor unit: \`1099\` is 10.99 USD, and zero-decimal currencies like JPY take \`10\` for 10 JPY. Never send a decimal.
160
184
  - Send an \`Idempotency-Key\` on every write you might retry, if the tool accepts it, and reuse the same key for the retry. A retry with a fresh key is a second charge, not a second attempt.
161
- - Stripe answers a rate limit with \`429\` and a \`Stripe-Rate-Limited-Reason\` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of ${rate} requests per second, and any single endpoint is capped at 25 per second regardless of mode, so paging one list is the real constraint.
185
+ - Stripe answers a rate limit with \`429\` and a \`Stripe-Rate-Limited-Reason\` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of ${rate}, and any single endpoint is capped at 25 per second regardless of mode, so paging one list is the real constraint.
162
186
  - Use \`search_stripe_documentation\` when the shape of an object or a flow is unclear; it is a read and costs nothing but a call.
163
187
  - Treat every create, update, delete, refund, and report run as a write. Connecta routes the maintained write catalog through \`call_destructive_tool\`; newly added tools also fail closed until classified.
164
188
  - An \`auth_required\` failure means this connector's Stripe authorization is missing or expired: run \`authorize_connector\` for this connector id, then retry the same call unchanged. A rejected argument or a plan restriction comes back in Stripe's own words instead — read it rather than re-authorizing.
165
- ${accountInstructions
166
- ? `\n## Account instructions\n\n${accountInstructions}\n`
167
- : ""}`;
189
+ `;
168
190
  }
169
191
  /** A maintained Stripe hosted-MCP connection. */
170
192
  export function stripe(id, options) {
@@ -172,31 +194,40 @@ export function stripe(id, options) {
172
194
  if (!purpose) {
173
195
  throw new Error("stripe() requires a non-empty account purpose.");
174
196
  }
175
- const mode = options.mode;
176
- if (mode !== "production" && mode !== "sandbox") {
177
- throw new Error(`stripe("${id}") requires mode "production" or "sandbox".`);
178
- }
179
197
  const auth = resolveAuth(id, options);
180
- assertModeMatchesKey(id, mode, auth);
181
- const copy = MODE_COPY[mode];
198
+ const mode = "mode" in options ? options.mode : undefined;
199
+ if (auth.type === "oauth" && mode !== undefined) {
200
+ throw new Error(`stripe("${id}") cannot declare a connector-wide mode for OAuth; Stripe returns mode with each account.`);
201
+ }
202
+ if (auth.type === "headers" && mode !== "production" && mode !== "sandbox") {
203
+ throw new Error(`stripe("${id}") with headers auth requires mode "production" or "sandbox".`);
204
+ }
205
+ if (auth.type === "headers") {
206
+ assertModeMatchesKey(id, mode, auth);
207
+ }
208
+ const copy = mode === undefined ? undefined : MODE_COPY[mode];
182
209
  const connector = remoteMcp(id, {
183
210
  url: STRIPE_MCP_ENDPOINT,
184
- title: options.title ?? copy.title,
185
- description: `Stripe payments (${copy.blurb}) — ${purpose}`,
211
+ title: options.title ?? copy?.title ?? "Stripe",
212
+ description: mode === undefined
213
+ ? `Stripe payments (live and sandbox accounts) — ${purpose}`
214
+ : `Stripe payments (${copy?.blurb}) — ${purpose}`,
186
215
  auth,
187
216
  requireHttps: true,
188
- callAdmission: STRIPE_ADMISSION[mode],
217
+ callAdmission: mode === undefined ? OAUTH_ADMISSION : STRIPE_ADMISSION[mode],
189
218
  usageGuide: {
190
- content: usageGuide(mode, purpose, options.instructions),
191
- // Explicit rather than derived: production versus sandbox is the fact an
192
- // agent must not get wrong. Multi-account OAuth also has to reach browse
193
- // before connector metadata can be mistaken for authenticated identity.
194
- summary: mode === "production"
195
- ? "PRODUCTION: real money. OAuth may span organization accounts; resolve the account from live schemas before acting."
196
- : "Sandbox: test data only. OAuth may span organization accounts; resolve the account from live schemas before acting.",
197
- // Not `required`. The four generic tools are the routing decision, and
198
- // the mode warning already rides the title and description; a guide
199
- // forced into every call would pay for the same paragraph repeatedly.
219
+ content: mode === undefined
220
+ ? oauthUsageGuide(purpose, options.instructions)
221
+ : fixedModeUsageGuide(mode, purpose, options.instructions),
222
+ // Explicit rather than derived: fixed credentials must lead with mode,
223
+ // while OAuth must lead with its account-scoped selector pair.
224
+ summary: mode === undefined
225
+ ? "Live and sandbox Stripe accounts. List accounts; carry the returned stripe_context and livemode before acting."
226
+ : mode === "production"
227
+ ? "PRODUCTION: real money. This static credential has one fixed live-mode scope."
228
+ : "Sandbox: test data only. This static credential has one fixed sandbox scope.",
229
+ // Not `required`. The four generic tools are the routing decision; a
230
+ // guide forced into every call would pay for the same prose repeatedly.
200
231
  },
201
232
  ...(options.maxResultBytes !== undefined
202
233
  ? { maxResultBytes: options.maxResultBytes }
package/dist/routes/ui.js CHANGED
@@ -28,7 +28,7 @@ const INERT_ICON_HEADERS = {
28
28
  "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
29
29
  "X-Content-Type-Options": "nosniff",
30
30
  };
31
- /** Per-request base64 nonce for an operator shell's scripts (Node 20+ and Workers). */
31
+ /** Per-request base64 nonce for an operator shell's scripts (Node 22+ and Workers). */
32
32
  function uiScriptNonce() {
33
33
  const bytes = crypto.getRandomValues(new Uint8Array(16));
34
34
  let binary = "";
package/dist/skills.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Connector } from "./types.js";
2
- export declare const CONNECTA_INSTRUCTIONS = "Connecta exposes seven meta-tools. For one read at an unknown address, search_tools with 2\u20134 distinctive action/object terms and includeSchemas=\"compact\", then one call_tool \u2014 a lone cold call is cheaper direct than a program. For read-only reduction, multiple or dependent calls, loops, joins, or branches, do not call top-level search_tools: make one execute_code call whose program searches, selects, calls, and reduces; never return discovery for another call. connecta.ui(html) is a guest function inside execute_code, never a connector address or search_tools result; pass one HTML string for display-only, or bind named read-only refresh/drill-down calls in its optional reads argument, and return the same initial summary data the HTML renders. Unannotated, write-capable, or destructive tools stay top level: search_tools, then call_destructive_tool; authorize_connector follows auth_required; get_result follows truncation. If this routing is unfamiliar, fetch skills({ name: \"usage\" }).";
2
+ export declare const CONNECTA_INSTRUCTIONS = "Choose a route before discovery. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. After auth_required use authorize_connector. After a truncated direct result use fields or get_result. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. Fetch skills({ name: \"usage\" }) once for program syntax, selection, repair, examples, and runtime details.";
3
3
  /** Shared Connecta routing guidance, byte-identical across deployments. */
4
4
  export declare const USAGE_SKILL: string;
5
5
  /** The always-loaded MCP `instructions` string. */
package/dist/skills.js CHANGED
@@ -1,33 +1,70 @@
1
- export const CONNECTA_INSTRUCTIONS = 'Connecta exposes seven meta-tools. For one read at an unknown address, search_tools with 2–4 distinctive action/object terms and includeSchemas="compact", then one call_tool a lone cold call is cheaper direct than a program. For read-only reduction, multiple or dependent calls, loops, joins, or branches, do not call top-level search_tools: make one execute_code call whose program searches, selects, calls, and reduces; never return discovery for another call. connecta.ui(html) is a guest function inside execute_code, never a connector address or search_tools result; pass one HTML string for display-only, or bind named read-only refresh/drill-down calls in its optional reads argument, and return the same initial summary data the HTML renders. Unannotated, write-capable, or destructive tools stay top level: search_tools, then call_destructive_tool; authorize_connector follows auth_required; get_result follows truncation. If this routing is unfamiliar, fetch skills({ name: "usage" }).';
1
+ export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. After auth_required use authorize_connector. After a truncated direct result use fields or get_result. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. Fetch skills({ name: "usage" }) once for program syntax, selection, repair, examples, and runtime details.';
2
2
  const USAGE_SKILL_BASE = `# Connecta usage
3
3
 
4
4
  ## The surface
5
5
 
6
6
  Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Broad discovery and multi-call work live in a program, not in top-level tools.
7
7
 
8
- ## Choose the smallest execution tool
8
+ The always-loaded MCP instructions are authoritative for choosing the top-level route. Read this skill at most once per task for the program workflow and recovery details below.
9
9
 
10
- Use exact addresses from discovery; never invent one. Search 2–4 distinctive action/object terms, not the whole request.
10
+ ## Inside a program
11
11
 
12
- - One read at an unknown address: \`search_tools({ query, includeSchemas: "compact" })\`, then \`call_tool\` once one cold call is cheaper direct than a program.
13
- - Anything wider — two or more calls, dependent steps, loops, joins, branching, a whole-catalog browse, or a result to reduce: one \`execute_code\` run.
14
- - Any unannotated, write-capable, or destructive call: \`call_destructive_tool\`, one at a time, after reviewing its schema and consequences.
15
- - Truncated result: retry with \`fields\`, else page it with \`get_result\`.
16
- - \`auth_required\`: \`authorize_connector\`, hand its recovery text to the operator, retry the call.
12
+ Write one plain-JavaScript async arrow function. TypeScript syntax and portable imports do not work. Return JSON-shaped data and reduce large results before returning.
17
13
 
18
- ## Inside a program
14
+ The minimum guest API is:
15
+
16
+ - \`<connectorId>.<toolName>(args)\` calls a sanitized shortcut. Non-identifier characters become \`_\`; leading digits gain \`_\`; reserved words gain a trailing \`_\`.
17
+ - \`connecta.call("connector.tool", args)\` uses the canonical address and returns the unwrapped value.
18
+ - \`connecta.search(args)\` returns \`{ tools, total, offset, limit, hasMore }\`; \`connecta.describe(args)\` returns \`{ tools }\`.
19
+ - \`connecta.batch(calls)\` runs 2–10 independent calls. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails }\`.
20
+ - \`console.log(...)\` is captured. \`connecta.emit(block)\` and \`connecta.ui(html, options?)\` produce rich output.
21
+
22
+ ## Discover and select
23
+
24
+ Search inside the run and finish the task there. A discovery-only program wastes a round trip. Use 2–4 distinctive action/object terms, not the full request. Use separate short searches for distinct operations.
25
+
26
+ For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and declared output shapes. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
27
+
28
+ - \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
29
+ - Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and declared outputs. Never select only because a result ranks first or has fewer required inputs.
30
+ - Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
31
+ - Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
32
+ - Reduce through declared output keys. Do not guess collection roots such as \`items\` or \`results\`. If a match or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
33
+
34
+ Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
35
+
36
+ ## Errors and repair
37
+
38
+ Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Batch failures expose the same classification in \`errorDetails\`. Branch on fields, never prose. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
39
+
40
+ - \`destructive_tool_requires_approval\`: stop the program and use the returned canonical address with top-level \`call_destructive_tool\`.
41
+ - \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
42
+ - A truncated direct-call result: retry \`call_tool\` with \`fields\`, or follow its \`get_result\` action. A truncated program result has no page handle; filter, map, or slice inside a new program.
43
+ - Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
44
+
45
+ For a direct call, \`fields\` selects JSON dot-paths and \`[]\` traverses arrays, for example \`results[].id\`. Projection misses return \`data\` plus \`$connecta\` feedback. \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. \`maxRetries\` is honored only for safely annotated tools. \`diagnostics: true\` adds timing.
46
+
47
+ \`get_result({ id, offset?, maxBytes? })\` returns \`{ text, offset, nextOffset?, totalBytes }\` for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1 and defaults to the deployment cap; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the response reports the served offset. Follow \`nextOffset\` to reassemble pages. An unknown or expired id is an error.
48
+
49
+ Limits: 20 host calls per run, 10 calls per batch, and a 15-second deadline per host call.
50
+
51
+ ## Runtime portability
52
+
53
+ Portable code uses only connector globals, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
54
+
55
+ ## Examples
56
+
57
+ One read-only call at a known address:
58
+
59
+ \`async () => await connecta.call("crm.get_account", { id: "acct_42" })\`
19
60
 
20
- Portable code uses only connector globals (\`<connectorId>.<toolName>(args)\`), \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch/process/timers/crypto/WebSocket. Dynamic Workers require only \`{ loader }\`; bindings/modules/globalOutbound violate it. Then env maps are empty; node:fs/http/https absent; outbound fetch/WebSocket/node:net/tls denied; DNS unresolved. Runtime builtins remain through import() and process.getBuiltinModule(), including node:path and cloudflare:workers; the set can drift. Timers/process/crypto/WebSocket and data: fetch remain. Avoid them; QuickJS fails.
61
+ Dependent calls, only when the second needs a value from the first:
21
62
 
22
- - \`connecta.search({})\` loads all catalogs; pass \`connector: "<id>"\` when obvious to load one. \`safety: "readOnly"\` keeps executable calls. Neither grants authority. Matches carry \`address\` and annotations.
23
- - Exact schemas: \`connecta.describe({ address: "connector.tool" })\` for one, \`{ addresses: [...] }\` for many; \`format: "json"\` only for exact constraints.
24
- - Caught Connecta errors have \`message\`, \`code\`, \`retryable\`, and \`details\`; branch on fields. For 2–10 independent calls, \`connecta.batch([...])\` returns success data or an \`errorDetails\` whose code and retryable flag match the throw.
25
- - Search inside the run; return only the reduction the answer needs, never raw payloads.
26
- - Only tools annotated \`readOnlyHint: true\` are reachable; the gate, credentials, and admission are enforced below the sandbox — nothing a program does widens its reach.
63
+ \`async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const address = (suffix) => { const tool = tools.find((entry) => entry.address.endsWith(suffix)); if (!tool) throw new Error("missing " + suffix); return tool.address; }; const run = await connecta.call(address(".get_run"), { runId: 42 }); const logs = await connecta.call(address(".get_job_logs"), { jobId: run.failedJobId }); return logs.map(({ timestamp, message }) => ({ timestamp, message })); }\`
27
64
 
28
65
  ## Rendering a view
29
66
 
30
- \`connecta.ui(html)\` renders one success-only display view, never for the model. Fetch and check the shape first. On empty or missing data, return a trimmed first record instead of rendering. Otherwise render returned variables; the model reads the return value, not the view.
67
+ \`connecta.emit\` accepts text, image, or audio blocks and delivers them only on success. \`connecta.ui(html)\` renders one success-only display view outside model context. One argument is display-only. Bind read-only refresh or drill-down calls with \`{ reads: { name: { address, fixedArgs?, viewArgs? } } }\`; page markup calls \`connecta.read(name, args)\`. Admission and one shared budget apply to the UI and emitted content, not separate budgets. Fetch and check the data shape first. On empty or missing data, return a trimmed first record instead of rendering. Otherwise render returned variables and return the same initial summary because the model reads the return value, not the view. A second, invalid, or over-budget UI call throws catchably.
31
68
 
32
69
  `;
33
70
  /** Deployment-scoped guide routing appended to the shared usage guide. */
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.17.0";
7
+ export declare const CONNECTA_VERSION = "0.18.1";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.17.0";
7
+ export const CONNECTA_VERSION = "0.18.1";
@@ -185,7 +185,7 @@ src/
185
185
  | The core imports no `node:` builtin and reaches no Node-only module | `test/purity.test.ts` |
186
186
  | The published surface matches the same boundary | `test/package-surface.test.ts`, `scripts/check-package.mjs` |
187
187
  | Route order, per-route auth, and byte-exact refusals | `test/server-route-contracts.test.ts` |
188
- | `/mcp` end to end, the open routes, exactly seven tools | `test/server.test.ts`, `test/code-first-surface.test.ts` |
188
+ | `/mcp` end to end, the open routes, exactly seven tools, bounded connector orientation | `test/server.test.ts`, `test/code-first-surface.test.ts` |
189
189
  | Construction-time refusals and the grouped config boundary | `test/config.test.ts`, `test/registry.test.ts` |
190
190
  | Program and top-level calls take the same enforced path | `test/execute.test.ts` |
191
191
  | Both deployment shapes still compile and configure the real thing | `test/deployment-shapes.test.ts`, `npm run check:examples` |
@@ -173,7 +173,10 @@ global whose properties are its tools, so `<connectorId>.<toolName>(args)` works
173
173
  with both parts sanitized into JavaScript identifiers — characters outside
174
174
  `[A-Za-z0-9_$]` become `_`, a leading digit gets `_` prefixed, and a reserved
175
175
  word gets `_` appended (`my-service.get.thing` → `my_service.get_thing`). The
176
- globals are lazy: no catalog is fetched until a program touches one.
176
+ globals are lazy: no catalog is fetched until a program touches one. The
177
+ bounded deployment inventory in the `execute_code` description shows each
178
+ canonical connector id and labels the shortcut only when it differs; the
179
+ [discovery guide](./meta-tools.md#discovery-context) defines that bound.
177
180
 
178
181
  **A3.** A shortcut that resolves to more than one tool fails closed with
179
182
  `ambiguous_tool_alias`, naming the colliding tool names and pointing at
@@ -256,10 +259,13 @@ const { tools } = await connecta.describe({
256
259
  });
257
260
  ```
258
261
 
259
- **S4.** Returns `{ tools }` in the order asked, one entry per address. An
260
- address that is unknown, or whose connector's catalog could not be loaded,
261
- returns an entry carrying `error` one bad address never fails the whole call.
262
- More than 100 addresses is `invalid_args`; the same 256,000-byte ceiling applies.
262
+ **S4.** Returns `{ tools }` in order, one entry per address. An unknown address
263
+ or failed catalog returns `error` plus typed `errorDetails`: `code`, `message`, and `retryable`. Misses
264
+ carry a route-aware `nextAction`; a close miss may add three canonical `suggestions`.
265
+ Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
266
+ caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
267
+ correlates a clipped address with its request; successes keep canonical addresses. More than 100
268
+ addresses is `invalid_args`; the same 256,000-byte ceiling applies.
263
269
 
264
270
  ### connecta.call
265
271
 
@@ -377,7 +383,7 @@ exactly first, by containment second — so a program that *wraps* a failure's
377
383
  message in its own text still reports the underlying typed failure. Keeping the
378
384
  type beats keeping the prose.
379
385
 
380
- **E7.** `retryable` for `unknown_address`, `unknown_tool`, `ambiguous_tool_alias`, and `destructive_tool_requires_approval` is pinned false, never inferred from an address containing `503`, `429`, or `temporar`. The first two carry `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }` — the same scoped discovery the top-level record names, keyed to the surface the caller actually has. A program cannot call `search_tools`, so it is never told to. Both the message and the derived `query` clamp the address to 512 UTF-8 bytes with a `…` marker: the address is caller-authored and lands in the message, the query, the text content, and `structuredContent`, so an invented 50 KB one would otherwise produce a refusal orders of magnitude past the deployment's result cap. A clipped address still identifies the mistake; a short one — the common case — is exact and untagged.
386
+ **E7.** `retryable` for `unknown_address`, `unknown_tool`, `ambiguous_tool_alias`, and `destructive_tool_requires_approval` is pinned false, never inferred from an address containing `503`, `429`, or `temporar`. The first two carry `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }` — the same scoped discovery the top-level record names, keyed to the surface the caller actually has. A program cannot call `search_tools`, so it is never told to. The message, the derived `query`, and a failed describe entry's `address` clamp caller-authored text to 512 UTF-8 bytes with an `…` marker. Those values land in the text content and `structuredContent`, so an invented 50 KB address would otherwise produce a refusal orders of magnitude past the deployment's result cap. A clipped address still identifies the mistake by its position; a short one — the common case — is exact and untagged.
381
387
 
382
388
  **E8.** A remote MCP tool whose advertised schema rejects the call fails before provider dispatch with `invalid_args`, carrying bounded, value-free `{ path, code, expected }` findings and scoped search recovery keyed `function: "connecta.search"` like every other in-program miss. A declared property reports the schema keyword that failed, never the validator's duplicate `additionalProperties` branch; a truly undeclared property still reports `additionalProperties`. Unsupported schemas pass through; unrecognized provider prose remains `connector_call_failed`.
383
389
 
@@ -585,14 +591,14 @@ the same mistake as automatic host-side projection, refused in `ethos.md`
585
591
  ([#282](https://github.com/zackbart/connecta/issues/282)).
586
592
 
587
593
  **U13.** The always-loaded MCP instructions locate `connecta.ui(html)` before an
588
- agent chooses a route: it is a guest function inside `execute_code`, never a
589
- connector address or catalog result, takes one HTML string, and carries `U12`'s
590
- mirrored-return duty. The detailed tool description proved too late to stop cold
591
- agents from searching downstream catalogs for UI; the location distinction
592
- therefore rides `initialize`, under a 1,000-character ceiling for the complete
594
+ agent chooses a route: it exists only inside `execute_code`, never in connector
595
+ search, and carries `U12`'s mirrored-return duty. The detailed call, binding,
596
+ budget, and repair rules live in the on-demand `usage` skill. The location
597
+ distinction rides `initialize`, under a 1,000-character ceiling for the complete
593
598
  instructions string. This promotes existing contract, not capability: the
594
599
  seven-tool surface, guest API, catalog, Apps delivery, and runtime do not change
595
- ([#286](https://github.com/zackbart/connecta/issues/286)).
600
+ ([#286](https://github.com/zackbart/connecta/issues/286),
601
+ [#418](https://github.com/zackbart/connecta/issues/418)).
596
602
 
597
603
  Bounded view reads follow normative [`V1`–`V8`](./program-ui-read-calls.md) ([#287](https://github.com/zackbart/connecta/issues/287), [#289](https://github.com/zackbart/connecta/issues/289)).
598
604
 
@@ -645,6 +651,8 @@ because connecta enforces them above the sandbox:
645
651
  | Deadline per host call | 15 s |
646
652
  | Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
647
653
  | `describe` addresses | ≤ 100 |
654
+ | `describe` nearby suggestions | ≤ 3 canonical addresses per failed entry |
655
+ | Caller text echoed by `describe` recovery | ≤ 512 UTF-8 bytes per field, plus `…` |
648
656
  | Result | 24,000 serialized characters |
649
657
  | Logs presented to the model | 4,000 characters |
650
658
 
@@ -828,13 +836,13 @@ the upstream `Executor` shape assignable.
828
836
  | `P2`, `X5` | `test/guest-api-contract.test.ts` (Dynamic globals plus loader-only filesystem, HTTP, environment, egress, DNS, and local `data:` boundaries), `test/guest-api-contract-quickjs.test.ts` (exact absent globals and blocked imports), `test/deployment-shapes.test.ts` (loader-only Worker construction) |
829
837
  | `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
830
838
  | `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
831
- | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (sanitizing) |
839
+ | `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (sanitizing), `test/server.test.ts` (bounded live connector inventory) |
832
840
  | `A3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (colliding alias) |
833
841
  | `A4` | `test/execute.test.ts` (namespace collisions, reserved namespace) |
834
842
  | `A5` | verdict; `A1`–`A3` are its enforcement |
835
843
  | `S1`, `S2` | `test/guest-api-contract.test.ts` (flat page, connector guides, schema keys, and the unfiltered browse that replaces `list_connectors`), `test/execute.test.ts` (guide pagination/partial/no-match behavior and `$ref`/`allOf`), `test/meta-tools.test.ts` (mixed complete/partial ranking and stable pagination) |
836
844
  | `S3` | `test/guest-api-contract.test.ts` (typed uncaught bound), `test/execute.test.ts` (count limits, fan-out bound) |
837
- | `S4` | `test/guest-api-contract.test.ts` (unknown address in `describe`) |
845
+ | `S4` | both guest-contract executors (ordered mixed describe results with unknown-address, unknown-tool suggestion, and catalog-failure details), `test/meta-tools.test.ts` (top-level routing, no-suggestion, catalog-failure, and hostile-input bounds) |
838
846
  | `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
839
847
  | `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
840
848
  | `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
@@ -875,7 +883,7 @@ the upstream `Executor` shape assignable.
875
883
  | `U7`, `U8` | two arms passing one case table, `test/codemode-compat.test.ts` |
876
884
  | `U9` | `test/execute-ui.test.ts` (a `ui` byte aggregate distinct from `emitted`, absent when nothing was accepted) |
877
885
  | `U12` | `test/server.test.ts` (the `connecta.ui` bullet carries the return-value clause); a duty on program authors, so the description is the only place it can be enforced |
878
- | `U13` | `test/code-first-surface.test.ts`, `test/server.test.ts` (served `initialize.instructions` locate UI inside `execute_code`, exclude it from catalog search, state the one-string call and mirrored return, and stay within the complete 1,000-character budget) |
886
+ | `U13` | `test/code-first-surface.test.ts`, `test/server.test.ts` (served `initialize.instructions` locate UI inside `execute_code`, exclude it from connector search, state the mirrored-return duty, and stay within the complete 1,000-character budget; the usage skill carries detailed call rules) |
879
887
  | `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
880
888
  | `X4` | `test/guest-api-contract.test.ts` (string logs only) |
881
889
  | `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
@@ -60,15 +60,12 @@ unknown name, unknown connector, connector with no guide — is an explicit
60
60
  error. Nothing silently falls back to the generic guide, because a generic
61
61
  answer to a specific question is worse than no answer.
62
62
 
63
- Discovery text is conditional on the deployment actually having a guide. The
64
- guide sentences in the `skills`, `search_tools`, `call_destructive_tool`, and
63
+ Discovery text is conditional on the deployment actually having a guide. Short
64
+ pointers in the `skills`, `search_tools`, `call_destructive_tool`, and
65
65
  `execute_code` descriptions appear only when at least one visible connector
66
- declares one the connector set is fixed at construction, so this is stable
67
- per deployment, and a deployment with no guides pays no always-loaded context
68
- for a feature it does not use. The built-in `usage` skill is the deliberate
69
- exception: it stays byte-identical across every deployment, including its
70
- per-connector-guides section, so an agent that has read it once in a task never
71
- needs a deployment-local copy of it.
66
+ declares one. The detailed selection rules live only in the built-in `usage`
67
+ skill. That skill stays byte-identical across deployments, including its
68
+ per-connector-guides section, so an agent reads it at most once per task.
72
69
 
73
70
  ## What belongs in a guide
74
71
 
@@ -175,5 +172,5 @@ each branch, the `guide` pointer in search output, and `guideRequired`
175
172
  appearing for connector-required conventions, approval-bound tools, and
176
173
  truncated schemas — and being absent from a search that asked for no schemas.
177
174
  `test/server.test.ts` owns the conditional half: it compares a guide-free
178
- deployment's four tool descriptions against a guided one's, and asserts the
179
- `usage` skill is byte-identical between them.
175
+ deployment's four short pointers against a guided one's, and asserts the
176
+ complete `usage` skill is byte-identical between them.
@@ -122,6 +122,7 @@ Maintained provider guides:
122
122
  - [Linear](./linear.md)
123
123
  - [Mixpanel](./mixpanel.md)
124
124
  - [Notion](./notion.md)
125
+ - [RevenueCat](./revenuecat.md)
125
126
  - [Stripe](./stripe.md)
126
127
 
127
128
  ## The `api()` construction contract
@@ -60,6 +60,14 @@ their smallest successful one-tool shapes:
60
60
 
61
61
  ## Discovery context
62
62
 
63
+ The deployment-derived `execute_code` description includes a live connector
64
+ inventory before any catalog search. It preserves registry order and uses each
65
+ canonical id, adding `shortcut <name>` only when the program namespace differs.
66
+ The complete inventory line is capped at 256 UTF-8 bytes. Entries stay whole,
67
+ and a truncated line ends with the exact `+N more` count. This reads only the
68
+ configured registry: it loads no catalog, probes no credential, grants no
69
+ capability, and does not replace canonical discovery or addressing.
70
+
63
71
  Start an unknown-address lookup with two to four distinctive action/object
64
72
  terms, not the full request, and omit `limit` so the default eight-result page
65
73
  stays small. When the integration is obvious, set `connector` to its id: a
@@ -146,8 +154,12 @@ the zero-tool page.
146
154
  The built-in `usage` skill is byte-identical across deployments and says to
147
155
  read it at most once per task. Connector guides remain scoped to the deployment
148
156
  that listed them, even when two deployments happen to use identical content.
149
- Deployments without connector guides receive none of the conditional guide
150
- sentences in their always-loaded tool descriptions.
157
+ The always-loaded instructions and seven tool definitions own route selection,
158
+ the fail-closed boundary, and the minimum guest syntax. The usage skill owns
159
+ program selection detail, examples, runtime differences, and repair guidance.
160
+ This split avoids two normative copies while preserving a valid first program
161
+ for clients that never fetch the skill. Deployments without connector guides
162
+ receive none of the short conditional guide pointers in their definitions.
151
163
 
152
164
  ## Result representation
153
165
 
@@ -316,6 +328,16 @@ a tool. A read path that reaches an unannotated, write-capable, or destructive
316
328
  tool returns `nextAction` for `call_destructive_tool` with the canonical
317
329
  address. Nothing is executed by these records.
318
330
 
331
+ `connecta.describe` keeps failures inline so one miss cannot discard the other
332
+ schemas. Each failed entry keeps its human `error` and adds `errorDetails` with
333
+ the equivalent invocation `code` and `retryable`. Address and tool misses use
334
+ the same route-aware discovery action above. A close tool-name miss on a known
335
+ connector may also carry `suggestions`: at most three deterministically ranked
336
+ canonical addresses, with no scores or descriptions. An unknown connector
337
+ stays unscoped and has no suggestions. A catalog-load failure carries only
338
+ `code`, bounded `message`, `retryable`, and any `retryAfterMs`; discovery does
339
+ not inherit later additions to the call-failure envelope.
340
+
319
341
  That route echoes the caller's own arguments back only while they fit a
320
342
  512-byte budget, and then whole — never clipped. An error envelope is not
321
343
  size-guarded the way a result is, so an unbounded echo would let a large
@@ -393,3 +415,6 @@ duplicate `additionalProperties` branches never reach the caller. A schema the l
393
415
  validator cannot evaluate passes through to the provider. Provider error prose
394
416
  is not parsed or guessed, so an unknown format remains
395
417
  `connector_call_failed`.
418
+
419
+ Describe's nearby-address list uses the same three-item recovery bound. It
420
+ contains addresses only; it never serializes ranking scores or result prose.
@@ -237,6 +237,23 @@ and all deliberately absent: this is a deliberate tool surface, not a mirror of
237
237
  the API. Anything missing is reachable through a custom `api()` connector
238
238
  beside this one, which remains a first-class path.
239
239
 
240
+ The 2026-03-11 contract also offers more fields on create and update. They were
241
+ reviewed after the 0.17.0 drift check and remain deliberately absent:
242
+
243
+ - `create_page` does not create workspace-private pages, apply templates,
244
+ choose page placement, or accept expanded icon and cover forms. Those change
245
+ ownership, start asynchronous content work, control ordering, or depend on
246
+ file surfaces. They are not extensions of the maintained page/row authoring
247
+ contract (#408).
248
+ - `update_page_properties` does not lock pages, apply templates, or erase page
249
+ content. Locking is coordination state, templates finish asynchronously, and
250
+ `erase_content` permanently deletes every child block through the API. None
251
+ belongs under an approval named for property replacement (#409).
252
+
253
+ `trash_page` stays separate and reversible. The current `create_page`,
254
+ `update_page_properties`, and `trash_page` request subsets remain valid against
255
+ the expanded published contract.
256
+
240
257
  There is also **no guarded raw-REST escape hatch** — no `notion_api_get`, no
241
258
  `notion_api_mutate`. The convention that permits one
242
259
  ([H14](./provider-conventions.md#h14--a-named-tool-must-beat-the-escape-hatch-and-the-escape-hatch-splits-by-safety))