@zackbart/connecta 0.16.0 → 0.17.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 (51) hide show
  1. package/AGENTS.md +12 -5
  2. package/CHANGELOG.md +289 -0
  3. package/README.md +6 -1
  4. package/dist/catalog-service.d.ts +4 -0
  5. package/dist/catalog-service.js +49 -5
  6. package/dist/catalog.d.ts +11 -0
  7. package/dist/catalog.js +134 -12
  8. package/dist/errors.d.ts +28 -2
  9. package/dist/errors.js +1 -0
  10. package/dist/execute.d.ts +5 -0
  11. package/dist/execute.js +229 -161
  12. package/dist/invocation.js +3 -1
  13. package/dist/meta-tools.d.ts +4 -0
  14. package/dist/meta-tools.js +46 -14
  15. package/dist/operator-ui/generated.d.ts +1 -1
  16. package/dist/operator-ui/generated.js +1 -1
  17. package/dist/operator-ui/model.d.ts +3 -1
  18. package/dist/providers/cloudflare.js +13 -25
  19. package/dist/providers/mixpanel.d.ts +3 -5
  20. package/dist/providers/mixpanel.js +73 -5
  21. package/dist/providers/stripe.d.ts +2 -2
  22. package/dist/providers/stripe.js +13 -11
  23. package/dist/registry.d.ts +32 -9
  24. package/dist/registry.js +217 -33
  25. package/dist/routes/mcp.js +6 -0
  26. package/dist/skills.d.ts +4 -0
  27. package/dist/skills.js +157 -18
  28. package/dist/types.d.ts +14 -2
  29. package/dist/ui.js +4 -1
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/documentation/architecture.md +7 -4
  33. package/documentation/cloudflare.md +40 -8
  34. package/documentation/code-first-exploration.md +2 -2
  35. package/documentation/code-mode.md +45 -53
  36. package/documentation/connector-guides.md +24 -19
  37. package/documentation/connectors.md +13 -1
  38. package/documentation/meta-tools.md +33 -18
  39. package/documentation/mixpanel.md +20 -0
  40. package/documentation/notion.md +7 -2
  41. package/documentation/operations.md +74 -29
  42. package/documentation/operator-ui.md +12 -2
  43. package/documentation/provider-audit.md +4 -4
  44. package/documentation/provider-conventions.md +68 -19
  45. package/documentation/stripe.md +45 -14
  46. package/documentation/upgrading.md +478 -0
  47. package/ethos.md +4 -4
  48. package/examples/worker/README.md +13 -6
  49. package/package.json +7 -2
  50. package/templates/node/AGENTS.md +5 -0
  51. package/templates/node/package.json +1 -1
@@ -234,6 +234,9 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
234
234
  ? { discoveryConcurrency: opts.discoveryConcurrency }
235
235
  : {}),
236
236
  requestSignal: request.signal,
237
+ ...(runtimeContext?.waitUntil
238
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
239
+ : {}),
237
240
  });
238
241
  registerExecuteTool(server, registry, {
239
242
  baseUrl,
@@ -241,6 +244,9 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
241
244
  logger: opts.logger,
242
245
  ...(activity ? { activity } : {}),
243
246
  requestSignal: request.signal,
247
+ ...(runtimeContext?.waitUntil
248
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
249
+ : {}),
244
250
  ...(opts.discoveryConcurrency !== undefined
245
251
  ? { discoveryConcurrency: opts.discoveryConcurrency }
246
252
  : {}),
package/dist/skills.d.ts CHANGED
@@ -10,6 +10,10 @@ export declare function hasConnectorGuides(connectors: readonly Connector[]): bo
10
10
  export declare function connectorSkillName(connectorId: string): string;
11
11
  /** The connector's guide, or undefined when it declares none (or a blank one). */
12
12
  export declare function connectorGuide(connector: Connector): string | undefined;
13
+ /** Discovery budget for one connector-guide summary, including an ellipsis. */
14
+ export declare const GUIDE_SUMMARY_LENGTH = 120;
15
+ /** Normalize authored and derived summaries under one construction contract. */
16
+ export declare function normalizeGuideSummary(summary: string): string | undefined;
13
17
  /** Bounded, decision-useful discovery summary for a connector guide. */
14
18
  export declare function connectorGuideSummary(connector: Connector): string | undefined;
15
19
  /** Whether correct use always depends on conventions outside the tool schema. */
package/dist/skills.js CHANGED
@@ -17,24 +17,24 @@ Use exact addresses from discovery; never invent one. Search 2–4 distinctive a
17
17
 
18
18
  ## Inside a program
19
19
 
20
- One async arrow function. The only capabilities are one global per connector (\`<connectorId>.<toolName>(args)\`), the \`connecta\` functions, and \`console.log\`.
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.
21
21
 
22
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
23
  - Exact schemas: \`connecta.describe({ address: "connector.tool" })\` for one, \`{ addresses: [...] }\` for many; \`format: "json"\` only for exact constraints.
24
- - Two to ten independent calls: \`connecta.batch([...])\`. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails: { code, retryable } }\` how a program tells a policy refusal from a transient failure.
25
- - Search inside the run, not before it; return only the reduction the answer needs, never raw payloads.
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
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.
27
27
 
28
28
  ## Rendering a view
29
29
 
30
- \`connecta.ui(html)\` renders a display-only view on success for the client, never for the model. Fetch first, check the shape in code. On a surprise — empty array, missing key return a trimmed first record instead of rendering: the wrong view becomes the sample you needed. Otherwise render from the variables you return; the model reads the return value, not the view.
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.
31
31
 
32
32
  `;
33
33
  /** Deployment-scoped guide routing appended to the shared usage guide. */
34
34
  const CONNECTOR_GUIDES_SECTION = `
35
35
  ## Per-connector guides
36
36
 
37
- When a connector here ships a deployment-scoped usage guide, \`skills({})\` and discovery return the exact \`guide\` name plus a bounded \`guideSummary\` saying what it covers. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop: fetch before calling. \`guideRequiredReasons\` says why \`connector_required\` and \`approval_required\` stand however you expand the schema; \`schema_truncated\` clears once describe returns the exact one. Otherwise fetch when the summary names a connector-specific sequence, unit, pagination rule, alias, or generic API convention relevant to the task. A read-only call whose compact schema is complete and unambiguous may proceed without fetching an otherwise irrelevant guide. Connector guides do not replace the shared Connecta usage guide and never apply to another deployment implicitly.
37
+ Connector guides appear in \`skills({})\` and discovery with an exact \`guide\` name and bounded \`guideSummary\`. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop. \`guideRequiredReasons\` says why: \`connector_required\` and \`approval_required\` stand after schema expansion; \`schema_truncated\` clears after exact describe. Otherwise fetch for a relevant sequence, unit, pagination rule, alias, or API convention. A read-only call with a complete compact schema may skip an irrelevant guide. Connector guides never apply to another deployment.
38
38
  `;
39
39
  /** Shared Connecta routing guidance, byte-identical across deployments. */
40
40
  export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
@@ -80,17 +80,63 @@ export function connectorGuide(connector) {
80
80
  const content = typeof guide === "string" ? guide : guide?.content;
81
81
  return content && content.trim() !== "" ? content : undefined;
82
82
  }
83
- const SUMMARY_LENGTH = 120;
83
+ /** Discovery budget for one connector-guide summary, including an ellipsis. */
84
+ export const GUIDE_SUMMARY_LENGTH = 120;
84
85
  /** A `---`/`***`/`___` rule, which also opens and closes YAML frontmatter. */
85
86
  const RULE_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
86
87
  /** A fenced code block's delimiter. */
87
88
  const FENCE_RE = /^\s*(?:```|~~~)/;
89
+ /** Markdown blocks that end a paragraph without a blank physical line. */
90
+ const HEADING_RE = /^\s*#{1,6}/;
91
+ const LIST_ITEM_RE = /^\s*(?:[-*+]\s+|\d+[.)]\s+)/;
92
+ const SETEXT_UNDERLINE_RE = /^\s*=+\s*$/;
93
+ const TABLE_DELIMITER_RE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
94
+ const OPENS_CLAUSE_RE = /^(?:The|A|An)\b/u;
95
+ const ARTICLE_RE = /^(?:the|a|an)\b/u;
88
96
  /**
89
97
  * Markup that carries no summary text of its own: horizontal rules, HTML
90
98
  * comments, and table rows. Skipped so a guide that opens with one is
91
99
  * summarized by its first real line instead of by punctuation.
92
100
  */
93
101
  const NOT_SUMMARY_RE = /^\s*(?:<!--|\|)|^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
102
+ /** A standard Markdown table starts with a pipe-bearing row and delimiter. */
103
+ function startsTable(lines, index) {
104
+ const header = lines[index] ?? "";
105
+ const delimiter = lines[index + 1] ?? "";
106
+ return header.includes("|") && TABLE_DELIMITER_RE.test(delimiter);
107
+ }
108
+ /** True when a sentence-looking period belongs to an abbreviation. */
109
+ function isAbbreviation(text, end) {
110
+ const token = text.slice(0, end).match(/\S+$/u)?.[0] ?? "";
111
+ // These introduce an example or restatement even before a capitalized word.
112
+ if (/^(?:e\.g|i\.e)\.$/iu.test(token))
113
+ return true;
114
+ // An initial or title belongs to the proper name that follows it.
115
+ if (/^[A-Z]\.$/u.test(token))
116
+ return true;
117
+ if (/^(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St)\.$/iu.test(token))
118
+ return true;
119
+ if (!/^(?:[A-Za-z]\.){2,}$/u.test(token) &&
120
+ !/^(?:vs|etc|approx|dept|fig|no)\.$/iu.test(token)) {
121
+ return false;
122
+ }
123
+ // Initialisms can end a sentence or extend a name ("U.S. East region").
124
+ // The mistakes are asymmetric: a false ending presents a fragment as a
125
+ // complete thought, while a missed ending gets an honest ellipsis. Count
126
+ // the period only with narrow evidence of a new clause: an article in one
127
+ // of its first two words. This is grammar evidence, not a starter-word list.
128
+ const following = text
129
+ .slice(end)
130
+ .match(/^[)\]}'"”’]*\s+(\S+)(?:\s+(\S+))?/u);
131
+ if (!following)
132
+ return false;
133
+ const [, nextWord, afterNext] = following;
134
+ const startsClause = nextWord !== undefined &&
135
+ /^\p{Lu}/u.test(nextWord) &&
136
+ (OPENS_CLAUSE_RE.test(nextWord) ||
137
+ (afterNext !== undefined && ARTICLE_RE.test(afterNext)));
138
+ return !startsClause;
139
+ }
94
140
  /** Drop a leading YAML frontmatter block — metadata, not summary text. */
95
141
  function withoutFrontmatter(lines) {
96
142
  let start = 0;
@@ -102,33 +148,107 @@ function withoutFrontmatter(lines) {
102
148
  const close = lines.findIndex((line, i) => i > start && RULE_RE.test(line));
103
149
  return close === -1 ? lines : lines.slice(close + 1);
104
150
  }
151
+ /** Normalize authored and derived summaries under one construction contract. */
152
+ export function normalizeGuideSummary(summary) {
153
+ const normalized = summary.replace(/\s+/g, " ").trim();
154
+ return normalized === "" ? undefined : normalized;
155
+ }
105
156
  /**
106
- * One line describing a guide, for the cheap list view: the guide's first
107
- * meaningful line (heading marks and list bullets stripped), falling back to
108
- * the connector's own description when the guide opens with nothing but
109
- * markup.
157
+ * Shorten a normalized summary at the strongest readable boundary available.
158
+ * A complete sentence needs no ellipsis; clause and word cuts do, so discovery
159
+ * never presents an unfinished fragment as the guide's complete thought.
110
160
  */
111
161
  function boundedSummary(summary) {
112
- const line = summary.replace(/\s+/g, " ").trim();
113
- if (line === "")
162
+ const normalized = normalizeGuideSummary(summary);
163
+ if (!normalized)
114
164
  return undefined;
115
- return line.length <= SUMMARY_LENGTH
116
- ? line
117
- : `${line.slice(0, SUMMARY_LENGTH - 1).trimEnd()}…`;
165
+ if (normalized.length <= GUIDE_SUMMARY_LENGTH)
166
+ return normalized;
167
+ const contentBudget = GUIDE_SUMMARY_LENGTH - 1;
168
+ let sentenceEnd = 0;
169
+ const sentenceBoundary = /[.!?…。!?](?:[)\]}'"”’]+)?(?=\s|$)/gu;
170
+ for (const match of normalized.matchAll(sentenceBoundary)) {
171
+ const end = (match.index ?? 0) + match[0].length;
172
+ if (end > GUIDE_SUMMARY_LENGTH)
173
+ break;
174
+ const punctuationEnd = (match.index ?? 0) + 1;
175
+ if (match[0].startsWith(".") &&
176
+ isAbbreviation(normalized, punctuationEnd)) {
177
+ continue;
178
+ }
179
+ // Do not mistake another short fragment for a useful complete thought.
180
+ if (end >= 24)
181
+ sentenceEnd = end;
182
+ }
183
+ if (sentenceEnd > 0)
184
+ return normalized.slice(0, sentenceEnd);
185
+ const available = normalized.slice(0, contentBudget);
186
+ let clauseEnd = 0;
187
+ const clauseBoundary = /[,;:](?=\s)|\s[—–-](?=\s)/g;
188
+ for (const match of available.matchAll(clauseBoundary)) {
189
+ const end = match.index ?? 0;
190
+ // Prefer a clause only when it retains most of the discovery budget.
191
+ if (end >= 80)
192
+ clauseEnd = end;
193
+ }
194
+ if (clauseEnd > 0) {
195
+ return `${available.slice(0, clauseEnd).trimEnd()}…`;
196
+ }
197
+ const wordEnd = available.search(/\s+\S*$/);
198
+ if (wordEnd > 0) {
199
+ const prefix = available
200
+ .slice(0, wordEnd)
201
+ .trimEnd()
202
+ .replace(/[,;:([{—–-]+$/u, "")
203
+ .trimEnd();
204
+ if (prefix !== "")
205
+ return `${prefix}…`;
206
+ }
207
+ let hardEnd = contentBudget;
208
+ const code = normalized.charCodeAt(hardEnd - 1);
209
+ if (code >= 0xd800 && code <= 0xdbff)
210
+ hardEnd--;
211
+ return `${normalized.slice(0, hardEnd)}…`;
118
212
  }
213
+ /**
214
+ * One thought describing a guide for the cheap list view: the first meaningful
215
+ * paragraph, joined across physical lines, with headings and the connector
216
+ * description as fallbacks when the guide opens with markup alone.
217
+ */
119
218
  function summarizeGuide(connector, guide) {
219
+ const lines = withoutFrontmatter(guide.split("\n"));
120
220
  let inFence = false;
221
+ let inComment = false;
121
222
  let headingFallback;
122
- for (const raw of withoutFrontmatter(guide.split("\n"))) {
223
+ for (let index = 0; index < lines.length; index++) {
224
+ const raw = lines[index] ?? "";
225
+ if (inComment) {
226
+ if (raw.includes("-->"))
227
+ inComment = false;
228
+ continue;
229
+ }
123
230
  if (FENCE_RE.test(raw)) {
124
231
  inFence = !inFence;
125
232
  continue;
126
233
  }
127
234
  if (inFence)
128
235
  continue;
236
+ if (raw.trimStart().startsWith("<!--")) {
237
+ if (!raw.includes("-->"))
238
+ inComment = true;
239
+ continue;
240
+ }
241
+ if (startsTable(lines, index)) {
242
+ index++;
243
+ while (index + 1 < lines.length &&
244
+ (lines[index + 1] ?? "").includes("|")) {
245
+ index++;
246
+ }
247
+ continue;
248
+ }
129
249
  if (raw.trim() === "" || NOT_SUMMARY_RE.test(raw))
130
250
  continue;
131
- const heading = /^\s*#{1,6}/.test(raw);
251
+ const heading = HEADING_RE.test(raw);
132
252
  const line = raw
133
253
  // `\s*` (not `\s+`) so a bare `#` strips to nothing and is skipped, and
134
254
  // an unspaced `#Heading` is still read as a heading.
@@ -142,7 +262,24 @@ function summarizeGuide(connector, guide) {
142
262
  headingFallback ??= boundedSummary(line);
143
263
  continue;
144
264
  }
145
- return boundedSummary(line) ?? line;
265
+ const paragraph = [line];
266
+ while (index + 1 < lines.length) {
267
+ const next = lines[index + 1] ?? "";
268
+ if (next.trim() === "" ||
269
+ startsTable(lines, index + 1) ||
270
+ FENCE_RE.test(next) ||
271
+ HEADING_RE.test(next) ||
272
+ LIST_ITEM_RE.test(next) ||
273
+ SETEXT_UNDERLINE_RE.test(next) ||
274
+ NOT_SUMMARY_RE.test(next)) {
275
+ break;
276
+ }
277
+ paragraph.push(next.trim());
278
+ index++;
279
+ if (paragraph.join(" ").length > GUIDE_SUMMARY_LENGTH)
280
+ break;
281
+ }
282
+ return boundedSummary(paragraph.join(" ")) ?? line;
146
283
  }
147
284
  if (headingFallback)
148
285
  return headingFallback;
@@ -155,6 +292,8 @@ export function connectorGuideSummary(connector) {
155
292
  if (!guide)
156
293
  return undefined;
157
294
  const configured = typeof connector.usageGuide === "object"
295
+ // Registry construction rejects over-budget configured summaries. Keep
296
+ // normalization here so direct Connector callers see the same text.
158
297
  ? boundedSummary(connector.usageGuide.summary ?? "")
159
298
  : undefined;
160
299
  return configured ?? summarizeGuide(connector, guide);
package/dist/types.d.ts CHANGED
@@ -180,6 +180,12 @@ export interface CatalogDriftReport extends CatalogDriftCounts {
180
180
  /** When the observation was taken; never when a probe was scheduled. */
181
181
  observedAt: string;
182
182
  }
183
+ /** The last agent-facing catalog read this runtime served for one connector. */
184
+ export interface CatalogAccessObservation {
185
+ /** Fresh means the read used a live or unexpired catalog; stale means SWR. */
186
+ state: "fresh" | "stale";
187
+ observedAt: string;
188
+ }
183
189
  export interface ConnectorStatus {
184
190
  state: ConnectorStatusState;
185
191
  /** When state === "auth_required", the URL the operator should open. */
@@ -193,6 +199,11 @@ export interface ConnectorStatus {
193
199
  * isolate or process has seen nothing, not that nothing drifted.
194
200
  */
195
201
  catalogDrift?: CatalogDriftReport;
202
+ /**
203
+ * The last agent-facing catalog read in this runtime. This payload-free
204
+ * observation is not persisted and operator reads do not replace it.
205
+ */
206
+ catalogAccess?: CatalogAccessObservation;
196
207
  }
197
208
  /** The whole plugin contract — the one open seam. */
198
209
  export interface Connector {
@@ -313,8 +324,9 @@ export interface ConnectorUsageGuide {
313
324
  /** Markdown returned verbatim by `skills({ name: "connector:<id>" })`. */
314
325
  content: string;
315
326
  /**
316
- * Bounded discovery hint describing the conventions the guide covers. When
317
- * omitted, Connecta derives a summary from the guide's first meaningful line.
327
+ * Discovery hint describing the conventions the guide covers. Whitespace is
328
+ * normalized, and a value over 120 characters throws at construction. When
329
+ * omitted, Connecta derives a bounded summary from the first body paragraph.
318
330
  */
319
331
  summary?: string;
320
332
  /**
package/dist/ui.js CHANGED
@@ -374,6 +374,9 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
374
374
  // `boundedCatalogDrift`, so what lands here cannot carry a name or a
375
375
  // schema even if the plugin seam returned one.
376
376
  ...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
377
+ ...(status.catalogAccess
378
+ ? { catalogAccess: status.catalogAccess }
379
+ : {}),
377
380
  ...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
378
381
  ...(credential ? { credential } : {}),
379
382
  };
@@ -461,7 +464,7 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
461
464
  : `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
462
465
  : "";
463
466
  const clerkScript = clerk && clerkScriptOrigin
464
- ? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
467
+ ? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
465
468
  : "";
466
469
  return `<!doctype html>
467
470
  <html lang="en">
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.16.0";
7
+ export declare const CONNECTA_VERSION = "0.17.0";
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.16.0";
7
+ export const CONNECTA_VERSION = "0.17.0";
@@ -26,7 +26,10 @@ ethos invariant, not a style preference: a client retained across requests on
26
26
  Workers is a cross-request capability leak, and a promise awaited after the
27
27
  response is work the runtime may have already torn down. Deferred work has one
28
28
  sanctioned channel — `ctx.waitUntil`, threaded through `fetch(request, env,
29
- ctx)` and used for best-effort activity writes.
29
+ ctx)`. Best-effort activity writes use it. An agent read that already demanded
30
+ an expired catalog refresh may also use it while serving a complete catalog
31
+ inside its stale window. That refresh owns a new scope and deadline; it never
32
+ carries the inbound scope or signal past the request.
30
33
 
31
34
  The registry is deliberately on the long side of that line and the MCP server
32
35
  deliberately on the short side. A fresh `McpServer` per request is what makes
@@ -87,8 +90,8 @@ owns or hands out, and a change usually belongs in exactly one of them:
87
90
 
88
91
  | Module | Owns |
89
92
  | --- | --- |
90
- | `src/registry.ts` | The connector set, id validation, address resolution, connector health, per-connector call limiters, and the drift snapshot. Construction-time refusal of structural mistakes lives here. |
91
- | `src/catalog-service.ts` | Tool listing: cold-load coalescing, TTL, persistence as manifest plus revision-addressed chunks, stale fallback, and the completeness rule a partial catalog is a failure, never a cache write. |
93
+ | `src/registry.ts` | The connector set, address resolution, catalog TTL/persistence/completeness, shared refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
94
+ | `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
92
95
  | `src/invocation.ts` | One tool call: argument validation, call admission, per-attempt timeout, retry with the connector's own `Retry-After` honoured exactly or declined, result unwrapping, size capping, and the activity record. |
93
96
  | `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
94
97
 
@@ -137,7 +140,7 @@ src/
137
140
  apps-shell.ts the one build-time MCP Apps template
138
141
  skills.ts MCP instructions, the usage skill, connector guides
139
142
  registry.ts connector set, addresses, health, call limiters
140
- catalog-service.ts catalog loading, caching, persistence, stale fallback
143
+ catalog-service.ts request-local catalog access, search, and describe
141
144
  catalog.ts ranking, summaries, compact schema rendering
142
145
  invocation.ts one tool call, end to end
143
146
  catalog-drift.ts vetted manifests and the counts a refresh produces
@@ -2,7 +2,7 @@
2
2
 
3
3
  Import `cloudflare()` independently from
4
4
  `@zackbart/connecta/providers/cloudflare`. It is a deliberate, hand-written
5
- surface over Cloudflare's v4 REST API. Fifty-two tools combine ergonomic,
5
+ surface over Cloudflare's v4 REST API. Fifty-one tools combine ergonomic,
6
6
  fully described operations for common work with three guarded escape hatches
7
7
  for the rest of Cloudflare's fast-moving control plane. Reads, JSON mutations,
8
8
  and raw/multipart uploads remain separate so safety routing does not depend on
@@ -140,7 +140,7 @@ projections:
140
140
 
141
141
  | Area | Reads | Writes |
142
142
  | --- | --- | --- |
143
- | Zones | discovery, details, settings, rulesets | update a setting |
143
+ | Zones | discovery, details, one setting at a time, rulesets | update a setting |
144
144
  | DNS/cache | list and get records | create, update, delete, targeted/full purge |
145
145
  | Workers | scripts, settings, deployments | delete a script |
146
146
  | KV | namespaces, keys, bulk values | create/rename/delete namespace, bulk write/delete |
@@ -158,9 +158,10 @@ asserts those properties rather than leaving them as a claim.
158
158
  A named tool is a permanent line item in every deployment's catalog, so the
159
159
  surface was measured against the escape hatches rather than assumed to beat
160
160
  them ([#350](https://github.com/zackbart/connecta/issues/350), evidence in
161
- [`eval/current-version/results/issue-350-evidence.md`](../eval/current-version/results/issue-350-evidence.md)).
162
- Two named tools lost that comparison, and a third followed one of them off the
163
- surface to keep a policy pair on one route:
161
+ [`eval/current-version/results/issue-350-evidence.md`](https://github.com/zackbart/connecta/blob/main/eval/current-version/results/issue-350-evidence.md)).
162
+ Two named tools lost that comparison, a third followed one of them off the
163
+ surface to keep a policy pair on one route, and a fourth left because
164
+ Cloudflare deprecated the only endpoint it could call:
164
165
 
165
166
  - **R2 CORS writes.** `set_r2_cors` declared its rule list as free-form objects
166
167
  — the untyped body this connection refuses everywhere else — so its schema
@@ -181,8 +182,31 @@ surface to keep a policy pair on one route:
181
182
  path, and returned the response untouched — `cloudflare_api_get` at
182
183
  `/accounts/{accountId}/r2/metrics` does the same thing without a permanent
183
184
  catalog line.
184
-
185
- The surviving 48 named tools all refuse malformed arguments locally, which is
185
+ - **The bulk zone-settings read.** `list_zone_settings` called
186
+ `GET /zones/{zoneId}/settings`, which Cloudflare's published document now
187
+ marks `deprecated: true` along with its `PATCH` sibling
188
+ ([#361](https://github.com/zackbart/connecta/issues/361)). There is no bulk
189
+ replacement to repoint it at: the supported operations are the per-setting
190
+ `GET` and `PATCH /zones/{zoneId}/settings/{settingId}` that `get_zone_setting`
191
+ and `update_zone_setting` already call. Keeping the tool would have meant
192
+ maintaining a name over a contract Cloudflare has announced it intends to
193
+ stop honouring — and #350 had already measured this one as the sharpest
194
+ passthrough on the surface: it took a zone id, projected nothing, and *grew*
195
+ the payload 22.7% by wrapping Cloudflare's settings array in a page object an
196
+ unpaginated endpoint never filled. Read one setting with `get_zone_setting`.
197
+ Where the whole set is genuinely wanted — an audit, a config diff — an
198
+ operator can still ask for it explicitly through `cloudflare_api_get` at
199
+ `/zones/{zoneId}/settings`, which is the honest place for a call whose
200
+ endpoint is on its way out: named by the caller, at the caller's risk, rather
201
+ than promised by connecta's catalog.
202
+
203
+ Because `scripts/drift/cloudflare-endpoints.json` records the endpoints this
204
+ connection *calls*, dropping the tool drops the row — so
205
+ `npm run drift:check -- --specs` is quiet about zone settings by construction
206
+ rather than by a recorded exception. A path reached only through a hatch is
207
+ named by the caller, so it was never a touched endpoint.
208
+
209
+ The surviving 47 named tools all refuse malformed arguments locally, which is
186
210
  the one thing no escape hatch can do: a hatch's path is an opaque string, so it
187
211
  can only check that a path is a path.
188
212
 
@@ -324,9 +348,17 @@ the failure message so the provider's own code number survives to the agent.
324
348
  | 401 or 403 | `auth_required`, not retryable | Stops and reports which permission is missing |
325
349
  | 400 with a credential-shaped code (1001, 6003, 6111, 9103, 9106, 9107) | `auth_required`, not retryable | Stops; the header or key is malformed, not the arguments |
326
350
  | 400, 409, 422 | `invalid_args`, not retryable | Repairs the arguments |
327
- | 404 | `connector_call_failed`, not retryable | Re-runs discovery for the id |
351
+ | 404 | `not_found`, not retryable | Re-runs discovery for the id |
328
352
  | 5xx or a transport error | `unavailable`, retryable | Retries |
329
353
 
354
+ The 404 row is the unambiguous half of the `not_found` rule
355
+ ([H11](./provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next)).
356
+ Cloudflare refuses a token that may not touch a resource with 401 or 403, so a
357
+ 404 is an absence rather than a permission gap wearing a miss, and an agent can
358
+ act on it: confirm the zone or account id with `list_zones` or `list_accounts`
359
+ and address the right one. Notion's 404, which proves neither, stays
360
+ `connector_call_failed`.
361
+
330
362
  The six credential-shaped codes deserve a caveat: Cloudflare publishes no
331
363
  official table mapping error codes to causes, so that set is assembled from
332
364
  community reports and probing, not from documentation. The same goes for the
@@ -13,7 +13,7 @@
13
13
  > constraint below was dropped.** The owner decided the default directly on
14
14
  > 2026-07-30 and [#224](https://github.com/zackbart/connecta/issues/224)
15
15
  > shipped it; the ethos records the eval-as-gate as `removed` and
16
- > [`eval/code-first-gate`](../eval/code-first-gate/README.md) continues as
16
+ > [`eval/code-first-gate`](https://github.com/zackbart/connecta/blob/main/eval/code-first-gate/README.md) continues as
17
17
  > measurement that nothing waits on.
18
18
 
19
19
  ## Executive summary
@@ -227,7 +227,7 @@ changing the default surface. Run at least 20 independent samples per task and
227
227
  model, with prompt variation.
228
228
 
229
229
  That suite was built under
230
- [`eval/code-first-gate`](../eval/code-first-gate/README.md). Its recorded results
230
+ [`eval/code-first-gate`](https://github.com/zackbart/connecta/blob/main/eval/code-first-gate/README.md). Its recorded results
231
231
  remain as measurement history; the runnable comparison was retired when #273
232
232
  removed the alternate deployment shapes.
233
233