@zackbart/connecta 0.24.2 → 0.24.4

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 (97) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +5 -6
  4. package/dist/auth/bearer.js +2 -0
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/branding.d.ts +31 -2
  8. package/dist/branding.js +116 -8
  9. package/dist/call-admission.d.ts +4 -0
  10. package/dist/call-admission.js +26 -0
  11. package/dist/catalog-drift.js +9 -4
  12. package/dist/catalog-service.d.ts +2 -0
  13. package/dist/catalog-service.js +25 -8
  14. package/dist/catalog.d.ts +2 -0
  15. package/dist/catalog.js +246 -121
  16. package/dist/connectors/api.d.ts +1 -1
  17. package/dist/connectors/api.js +21 -3
  18. package/dist/connectors/guarded-fetch.d.ts +6 -2
  19. package/dist/connectors/guarded-fetch.js +61 -24
  20. package/dist/connectors/remote-mcp.js +92 -57
  21. package/dist/errors.d.ts +28 -3
  22. package/dist/errors.js +60 -1
  23. package/dist/execute.js +85 -23
  24. package/dist/executor-result.js +3 -1
  25. package/dist/executors/quickjs-child.js +5 -1
  26. package/dist/executors/quickjs-protocol.d.ts +4 -0
  27. package/dist/executors/quickjs-runtime.d.ts +1 -1
  28. package/dist/executors/quickjs-runtime.js +38 -21
  29. package/dist/executors/quickjs.js +68 -27
  30. package/dist/index.d.ts +15 -1
  31. package/dist/index.js +36 -4
  32. package/dist/invocation.js +134 -93
  33. package/dist/mcp-result.js +3 -2
  34. package/dist/meta-tools.js +191 -36
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +38 -1
  37. package/dist/operator-ui/view.js +71 -0
  38. package/dist/providers/cloudflare.d.ts +14 -2
  39. package/dist/providers/cloudflare.js +107 -16
  40. package/dist/providers/linear.d.ts +26 -4
  41. package/dist/providers/linear.js +19 -4
  42. package/dist/providers/mixpanel.d.ts +16 -3
  43. package/dist/providers/mixpanel.js +13 -2
  44. package/dist/providers/notion.d.ts +8 -1
  45. package/dist/providers/notion.js +83 -10
  46. package/dist/providers/revenuecat.d.ts +30 -4
  47. package/dist/providers/revenuecat.js +42 -4
  48. package/dist/providers/stripe.d.ts +7 -1
  49. package/dist/providers/stripe.js +30 -4
  50. package/dist/providers/vercel.js +11 -1
  51. package/dist/registry.d.ts +22 -2
  52. package/dist/registry.js +101 -13
  53. package/dist/routes/mcp.d.ts +4 -1
  54. package/dist/routes/mcp.js +84 -13
  55. package/dist/routes/oauth.js +4 -0
  56. package/dist/routes/shared.d.ts +1 -0
  57. package/dist/routes/shared.js +4 -4
  58. package/dist/server.js +15 -3
  59. package/dist/skills.js +6 -5
  60. package/dist/storage/file.d.ts +6 -2
  61. package/dist/storage/file.js +312 -34
  62. package/dist/storage/memory.js +12 -1
  63. package/dist/types.d.ts +37 -0
  64. package/dist/ui.js +18 -10
  65. package/dist/validate.js +3 -3
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/documentation/architecture.md +193 -165
  69. package/documentation/auth.md +199 -145
  70. package/documentation/code-mode.md +433 -316
  71. package/documentation/meta-tools.md +363 -355
  72. package/examples/worker/AGENTS.md +2 -1
  73. package/examples/worker/README.md +12 -10
  74. package/examples/worker/src/index.ts +12 -15
  75. package/package.json +1 -2
  76. package/templates/node/.env.example +3 -3
  77. package/templates/node/AGENTS.md +5 -4
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +23 -22
  81. package/documentation/call-admission.md +0 -142
  82. package/documentation/cloudflare.md +0 -471
  83. package/documentation/connector-guides.md +0 -176
  84. package/documentation/connectors.md +0 -389
  85. package/documentation/linear.md +0 -193
  86. package/documentation/mixpanel.md +0 -160
  87. package/documentation/notion.md +0 -308
  88. package/documentation/operations.md +0 -359
  89. package/documentation/operator-ui.md +0 -135
  90. package/documentation/optional-modules-upgrade.md +0 -243
  91. package/documentation/provider-conventions.md +0 -722
  92. package/documentation/request-admission.md +0 -170
  93. package/documentation/revenuecat.md +0 -305
  94. package/documentation/storage-and-credentials.md +0 -201
  95. package/documentation/stripe.md +0 -262
  96. package/documentation/upgrading.md +0 -754
  97. package/documentation/vercel.md +0 -241
@@ -1,722 +0,0 @@
1
- # Provider conventions
2
-
3
- The seven maintained prebuilt connections grew one at a time, and until now
4
- "excellent provider" meant whatever the last author thought. This document
5
- writes the judgment down so it can be argued with, audited, and reused.
6
-
7
- There are two genuinely different provider shapes, and one convention set
8
- cannot honestly cover both:
9
-
10
- - **Hand-written HTTP providers**: `api()` surfaces where Connecta owns every
11
- tool name, schema, projection, and error. Today: Cloudflare, Notion, and the
12
- Vercel API surface.
13
- - **Hosted-MCP proxies**: `remoteMcp()` wrappers around a server somebody else
14
- operates, where the names, schemas, results, and error prose arrive as they
15
- are. Today: Cloudflare, Linear, Stripe, Mixpanel, Notion, RevenueCat, and
16
- Vercel MCP interfaces.
17
-
18
- The governing principle for every convention below is the same: **keep the
19
- model that interacts with connecta as efficient as possible.** A convention
20
- earns its place by reducing what an agent spends discovering, selecting,
21
- calling, and reading. It does not earn its place by being tidy.
22
-
23
- Nothing here overrides [ethos.md](../ethos.md). In particular, prebuilt
24
- connections remain ordinary `Connector` instances with no extra privileges,
25
- their annotations fill in downstream silence rather than replacing it, and no
26
- tool is ever generated from a schema document.
27
-
28
- ## How to read a convention
29
-
30
- Every convention is one rule, one reason, and one cost. The cost is drawn from
31
- a fixed vocabulary of four, because those are the four things an agent actually
32
- pays:
33
-
34
- | Cost | What it means |
35
- | --- | --- |
36
- | **discovery tokens** | bytes the agent reads before it can call anything |
37
- | **wrong-tool selection** | it picked the wrong tool, connector, or mode |
38
- | **argument retries** | the call was made and rejected, so it must be made again |
39
- | **result size** | bytes the agent reads back, and the round trips paging them |
40
-
41
- A convention that cannot name one of those four is a preference, not a
42
- convention, and does not belong in this document.
43
-
44
- ## What discovery actually shows
45
-
46
- Several conventions are budgets, and the budgets are not arbitrary — they are
47
- the points at which Connecta's own surface starts dropping characters on the
48
- floor. From `src/catalog.ts` and `src/catalog-service.ts`:
49
-
50
- - **A tool description is cut to 160 characters in `search_tools`** and to 240
51
- in the describe path, both with a trailing `…`, unless the caller passes
52
- `fullDescriptions: true`. Prose past those points reaches an agent only when
53
- it pays for the expansion.
54
- - **A compact schema renders into at most 1,024 UTF-8 bytes**, and any single
55
- enum or constraint annotation into at most 256. Numeric bounds, string
56
- length bounds, patterns, and formats ride beside their TypeScript-like type.
57
- Past a cap the renderer keeps what fits and degrades the rest — a prefix of
58
- the enum plus `unknown`, a shape without the constraints that did not fit, a
59
- required-first object with `unknown` types, or
60
- `unknown /* truncated */` — and flags the match, which costs a describe
61
- round trip to recover.
62
- - **`inputKeys`, `requiredInputKeys`, and `outputKeys` come only from bounded
63
- plain-object schemas.** A top-level `anyOf` has no keys to list, so a caller
64
- learns nothing about the arguments without expanding the schema.
65
- - **A guide summary is bounded at 120 characters.** A configured value past
66
- the bound refuses construction. An omitted one defaults to the guide's first
67
- meaningful body paragraph, joined across physical line wraps and shortened
68
- at a readable boundary.
69
- - **Search returns a connector's `id`, `title`, `guide`, and `guideSummary` —
70
- never its `description`.** The description reaches an agent only as the
71
- fallback summary for a guide with no usable body paragraph. Routing facts belong
72
- in the title and the guide's opening paragraph; a routing fact that lives only in
73
- the connector description has been written into a field the model does not
74
- read. Neither the `id` nor the `title` is a lexical document, so a term drawn
75
- from one of them is not a search hit — it is a no-match whose guidance names
76
- the connector and points at a scoped browse.
77
-
78
- Two construction-time checks enforce the floor beneath all of this. The hard
79
- one is `api()` itself: since
80
- [#340](https://github.com/zackbart/connecta/issues/340) a hand-written tool
81
- throws unless it carries a non-empty description, an explicit boolean
82
- `annotations.readOnlyHint`, and an `inputSchema` — where it declares one — the
83
- validator can compile. The soft one is `Registry.checkConventions()`, which
84
- warns about a connector with no description, and about a static tool from any
85
- other source with no description or no `inputSchema`.
86
-
87
- ## Hand-written HTTP providers
88
-
89
- Connecta owns the whole surface here, which means every miss is ours. These
90
- apply to `api()`-based prebuilt connections (Cloudflare, Notion, and Vercel's
91
- API surface) and are the bar any future one is written to.
92
-
93
- None of them asks an author to re-derive transport safety. URL confinement,
94
- query and body construction, `ctx.signal`, redirect refusal, credential
95
- shadowing, bounded response reads, and network-failure normalization are the
96
- [guarded fetch transport](./connectors.md#the-guarded-fetch-transport)'s job
97
- ([#341](https://github.com/zackbart/connecta/issues/341)). What the conventions
98
- below still demand — H11's error mapping and H12's credential test above all —
99
- is the provider knowledge no shared helper can hold.
100
-
101
- ### H1 — Identity is deployment-owned; the provider supplies everything else
102
-
103
- The constructor takes an `id`, a required `purpose`, an optional `title`, and
104
- optional `instructions` appended to — never replacing — the maintained guide.
105
- A blank `purpose` throws at construction.
106
-
107
- *Why:* an agent choosing between two instances of the same provider has only
108
- the title and the guide summary to choose from. *Cost:* wrong-tool selection.
109
-
110
- ### H2 — Names are `verb_object`, and the verb is the safety class
111
-
112
- `snake_case`, a leading verb from the small set the surface already uses
113
- (`list_`, `get_`, `search_`, `create_`, `update_`, `delete_`, plus a provider's
114
- own vocabulary such as `purge_` or `trash_`), and a noun that matches the
115
- provider's own word for the thing. A read never opens with a write verb, and a
116
- write never hides behind a neutral one. Escape hatches are named
117
- `<provider>_api_<class>` so they sort together and read as generic.
118
-
119
- *Why:* the name is the only thing lexical search indexes besides the
120
- description, and it is what an agent skims first. A connector's `id` and
121
- `title` are displayed, not indexed — a search for one of them matches no tool,
122
- and the miss is answered by guidance naming that connector rather than by a
123
- ranked result. *Cost:* wrong-tool selection.
124
-
125
- ### H3 — The selection sentence fits in 160 characters; the whole description in 240
126
-
127
- Sentence one says what the tool returns or does, in the imperative, and is
128
- complete inside 160 characters (roughly 40 tokens). Everything else — the
129
- constraint, the disqualifier, the handoff — fits in the remaining 80 characters
130
- (240 total, roughly 60 tokens). Detail that does not fit belongs in a property
131
- description or the usage guide, both of which are fetched only when needed.
132
-
133
- *Why:* search cuts at 160 and describe at 240 unless the caller passes
134
- `fullDescriptions: true`, so anything past the budget is delivered only to an
135
- agent that spends a second, larger read to get it. *Cost:* discovery tokens.
136
-
137
- ### H4 — The description names the disqualifier, not the pitch
138
-
139
- Say what the tool will not do when an agent is likely to assume it does:
140
- `search` "finds pages and data sources by **title**. Never searches content."
141
- One clause of honest negative space outperforms three of capability.
142
-
143
- *Why:* the cheapest wrong call is the one never made. *Cost:*
144
- wrong-tool selection.
145
-
146
- ### H5 — Input schemas are complete, closed, and bounded
147
-
148
- Every tool carries a hand-written `inputSchema`: a plain object at the top
149
- level, `additionalProperties: false`, an accurate `required` list, an `enum` on
150
- every constrained field, explicit numeric bounds on every page size and count,
151
- explicit string bounds where length or shape is constrained, and a description
152
- on every property — nested objects and array items included,
153
- because a caller composing an array element is reading that element's fields,
154
- not the parent's prose. `api()` enforces the enforceability half for free since
155
- [#340](https://github.com/zackbart/connecta/issues/340): a schema the validator
156
- cannot compile throws at construction, and one that only reveals itself on
157
- first use fails the call rather than silently admitting unvalidated input — in
158
- a surface we wrote ourselves, an unenforceable schema is our bug.
159
-
160
- *Why:* a complete schema is the difference between one call and a call, a
161
- rejection, and a repair. *Cost:* argument retries.
162
-
163
- ### H6 — A local bound says whose bound it is
164
-
165
- When a schema's bound is the provider's, the description says so; when the
166
- bound is narrower than the provider's, the description says that too. A local
167
- cap that an agent is told about beats a page size that fails somewhere inside
168
- the provider — but only if the agent is told.
169
-
170
- *Why:* an unexplained refusal reads as a bug and gets retried. *Cost:*
171
- argument retries.
172
-
173
- ### H7 — Schemas fit the compact renderer, or selection does not depend on the part that is cut
174
-
175
- Keep the common path's compact input and output shapes inside 1,024 bytes and
176
- each enum or constraint annotation inside 256. Numeric and string constraints
177
- render when they fit. Search drops complete constraints that do not fit and
178
- sets the existing truncation flag; compact describe keeps them. Where a
179
- legitimate enum genuinely cannot fit — 21 DNS record types — the truncation is
180
- acceptable only if the tool's name and description already carry enough for
181
- selection, so the caller expands the schema to *call*, not to *choose*.
182
-
183
- *Why:* a truncated compact shape costs a describe round trip. *Cost:* discovery
184
- tokens.
185
-
186
- ### H8 — Every tool declares an `outputSchema`
187
-
188
- Declared outputs produce `outputKeys` and let a program reduce a result without
189
- first fetching one to look at. Connecta measured *declared* output schemas at 0
190
- of 30 tools on one real deployment and 3 of 30 on another
191
- ([#282](https://github.com/zackbart/connecta/issues/282)) — nearly every tool an
192
- agent meets is a shape it can only learn by calling. A maintained provider has
193
- no excuse to join that majority.
194
-
195
- *Why:* an agent that knows the shape projects before it reads. *Cost:* result
196
- size.
197
-
198
- ### H9 — Every read projects, and says what it dropped
199
-
200
- Reads return the provider's payload flattened and renamed, with plan,
201
- permission, and presentation noise removed. Where the dropped detail can
202
- matter, the tool takes `raw: true` and returns the untouched response; where
203
- the provider itself truncated something, the projection surfaces that fact and
204
- the id needed to fetch the rest, rather than handing back a confident partial.
205
- The argument and result vocabulary is consistent within a connector, and the
206
- mapping from the provider's own names is either identity or one mechanical rule
207
- stated in the guide.
208
-
209
- *Why:* projection is the largest single lever on what an agent reads, and a
210
- silent truncation is worse than a large result. *Cost:* result size.
211
-
212
- ### H10 — Pagination is one convention per connector, with one field to branch on
213
-
214
- List tools take an explicit page argument and a cursor, default to a page size
215
- smaller than the provider's maximum because a first read should be cheap, and
216
- return exactly one branchable signal — `hasMore` beside a cursor. Cursors are
217
- opaque: passed back verbatim, never parsed or constructed. Where an endpoint
218
- paginates differently from the rest of the connector, the schema and the guide
219
- both say so instead of letting an agent discover it.
220
-
221
- *Why:* the loop condition should not be a research project. *Cost:* result size.
222
-
223
- ### H11 — Errors are mapped to what the caller does next
224
-
225
- The typed failure code is chosen by the caller's next move, not by the
226
- provider's name for what happened: an authorization gap an operator must fix is
227
- not `auth_required` if `authorize_connector` cannot fix it; a retryable failure
228
- carries `retryAfterMs` when the provider says how long; an ambiguous provider
229
- code gets a message that states the ambiguity rather than picking the
230
- convenient reading. A call that can only fail is refused locally as
231
- `invalid_args` before the round trip. Provider error prose is never parsed to
232
- invent a classification.
233
-
234
- **A downstream 404 is `not_found` — when the provider means it.** The code
235
- exists because the next move is none of the others': you do not wait, you do
236
- not send the agent to `authorize_connector`, you do not repair the argument
237
- object. You re-address — look the id up again, or accept the absence and carry
238
- on — and a program looping over ids inside `execute_code` can continue past
239
- `not_found` where `connector_call_failed` would have to abort the run, reading
240
- the code from a caught error or a rejected promise. The guest bridge preserves
241
- the typed classification. That
242
- control-flow difference is the H11 test being met; it is not a label for the
243
- cause.
244
-
245
- The qualifier is the whole rule. Map a status to `not_found` only where the
246
- provider distinguishes absence from a permission gap. Where it does not —
247
- Notion returns `object_not_found` both for an object that is gone and for one
248
- that was never shared with the integration, and will not say which — the honest
249
- code stays `connector_call_failed` (or `auth_required`, where a credential
250
- really is the fix) and the message states the ambiguity, exactly as the
251
- paragraph above requires. Cloudflare is the other side of the pair: a token
252
- that may not touch a resource is refused with 401 or 403, so its 404 is an
253
- absence and maps to `not_found`. Neither connector's mapping changed shape when
254
- the code arrived; one of them changed codes.
255
-
256
- Two boundaries. `not_found` is about a resource the *downstream* owns: an
257
- address connecta cannot resolve is already framed as `unknown_address` or
258
- `unknown_tool` and never reaches a connector. And it never appears on the
259
- hosted-MCP proxy path, because `P1` forbids re-shaping downstream framing and
260
- prose is never parsed to invent a classification — a proxied server's own
261
- missing-resource error arrives as that server wrote it. The two paths do not
262
- diverge on the rule; they diverge on who is entitled to apply it, which is the
263
- same split every other code already has.
264
-
265
- *Why:* a misrouted error sends an agent down a recovery path that cannot
266
- succeed. *Cost:* argument retries.
267
-
268
- ### H12 — One operator credential, one cheap test, no probing
269
-
270
- The connection declares its credential slot with a labeled field per secret and
271
- implements `testCredential`/`testCredentials` with the cheapest call that proves
272
- the secret is live, reporting the identity or workspace it authenticated as.
273
- Connecta does not check credentials behind an operator's back; the test runs
274
- when a human asks, and everything else fails loudly at use.
275
-
276
- *Why:* "which account is this?" answered once at configuration time is a
277
- question no agent has to answer by calling something. *Cost:* wrong-tool
278
- selection.
279
-
280
- ### H13 — The guide carries only what a schema cannot
281
-
282
- `usageGuide` uses the structured form: `content`, an explicit `summary`, and
283
- `required: true` only when correct use depends on a sequence or convention no
284
- complete schema can express. Imperative bullets, decision first, exact tool and
285
- argument names, constraints with their numbers. The first content line is the
286
- routing fact, because it is the summary fallback. Nothing in the guide restates
287
- a schema.
288
-
289
- *Why:* the guide is fetched into a live context window, so every line that
290
- repeats a schema is paid for twice. *Cost:* discovery tokens.
291
-
292
- ### H14 — A named tool must beat the escape hatch, and the escape hatch splits by safety
293
-
294
- Guarded raw access is an accepted shape, not a required one — a small provider
295
- whose surface is genuinely finite may deliberately have none, and say so. Where
296
- a provider is large and fast-moving enough to need one, it is split by safety
297
- class: a GET-only tool that is explicitly read-only, a JSON mutation tool that
298
- is always destructive, and an upload tool that is always destructive. The split
299
- is Connecta's, never an agent-supplied HTTP method. Paths are provider-relative
300
- and confined; the connector owns authentication, host, content type, and
301
- framing. This is not schema ingestion — a fixed handful of stable tools, never
302
- one per operation — and a *named* tool earns its place only by beating the
303
- hatch on schema, projection, or safety routing.
304
-
305
- *Why:* every named tool costs catalog bytes forever, and a thin wrapper around
306
- a call the hatch already makes costs them for nothing. *Cost:* discovery
307
- tokens.
308
-
309
- ## Hosted-MCP proxies
310
-
311
- Here the downstream owns the tool names, descriptions, input schemas, result
312
- shapes, pagination, and error prose. Conventions that legislate those things
313
- would be fiction. What Connecta owns is the endpoint, the credential, the
314
- classification, the connector's own identity, the guide, and the budget — so
315
- that is what these conventions cover.
316
-
317
- ### P1 — Normalize by adding, never by rewriting
318
-
319
- A proxy may add annotations, a title, a guide, and an admission policy. It does
320
- not rewrite a downstream tool's name, description, or schema, and it does not
321
- re-shape a downstream result. A rewritten description drifts silently away from
322
- the schema it describes, and the agent believes the description.
323
-
324
- *Why:* the catalog must stay a true report of what the downstream will accept.
325
- *Cost:* argument retries.
326
-
327
- ### P2 — Identity is deployment-owned
328
-
329
- Identical to H1: `id`, required `purpose`, optional `title`, and `instructions`
330
- appended to the maintained guide, never replacing it, and never able to change
331
- the safety classification.
332
-
333
- *Why:* two instances of the same provider are told apart only by title and
334
- guide summary. *Cost:* wrong-tool selection.
335
-
336
- ### P3 — The fact that decides routing goes in the title and the guide's first line
337
-
338
- Whichever variant an agent must not get wrong — production versus sandbox,
339
- read-only versus read-write, region, account — appears in the default `title`
340
- and as the first content line of the guide. It may also appear in the
341
- `description`; it may never appear *only* there, because search never returns
342
- the description.
343
-
344
- *Why:* the model reads title and guide summary at browse time and nothing else.
345
- *Cost:* wrong-tool selection.
346
-
347
- ### P4 — Endpoint selection is a constructor option with the safest honest default
348
-
349
- Where the provider publishes more than one endpoint, the option selects between
350
- them and the default is the safe one. Where the provider publishes one endpoint
351
- and the environment rides the credential, the mode is required with no default,
352
- and construction throws when a recognizable credential contradicts the declared
353
- mode. Deprecated transports stay unreachable.
354
-
355
- *Why:* a provider-enforced scope limit is a stronger guarantee than any
356
- annotation Connecta applies, and a wrong-mode write is not recoverable by
357
- retrying. *Cost:* wrong-tool selection.
358
-
359
- ### P5 — Classification is a reviewed allowlist that fails closed
360
-
361
- Reads are listed by name, writes are listed by name with their destructive
362
- verdict, and anything unlisted is not read-only. The lists are supersets:
363
- hosted catalogs vary by plan and feature flags, so a classified name a
364
- workspace never returns costs nothing while an unclassified new one fails
365
- closed onto `call_destructive_tool`. The classification fills in downstream
366
- silence and otherwise preserves explicit annotations, with the single
367
- fail-closed exception the [ethos](../ethos.md) accepted-prebuilt row records
368
- ([#315](https://github.com/zackbart/connecta/issues/315)) — a release-reviewed
369
- destructive verdict outranks a contradictory `readOnlyHint: true`, because that
370
- release independently established that the tool mutates existing state. An
371
- additive write leaves `destructiveHint` unset.
372
-
373
- *Why:* the fail-closed read-only invariant is not negotiable, and inflated
374
- destructive copy trains humans to approve without reading. *Cost:* wrong-tool
375
- selection.
376
-
377
- ### P6 — The guide says the catalog is not a fixed set
378
-
379
- The guide tells the agent to search this connector for what the workspace
380
- actually exposes rather than assuming a documented tool exists, and names the
381
- plan- or beta-gated areas where absence is expected.
382
-
383
- *Why:* a hosted catalog varies per account, and an agent that assumes ours is
384
- complete spends calls proving it is not. *Cost:* wrong-tool selection.
385
-
386
- ### P7 — The guide carries the reduction advice the schemas cannot
387
-
388
- A proxy cannot project a downstream result, so the guide tells the agent to
389
- page with the cursor rather than raising the page size, and to reduce inside
390
- `execute_code` before returning anything — and, where a value's rendering is
391
- the provider's rule rather than the schema's, what that value means: Mixpanel
392
- renders an absent boolean property as `false` in a breakdown, so the guide
393
- says to confirm presence before reading `false` as a signal
394
- ([#430](https://github.com/zackbart/connecta/issues/430)). Structured form,
395
- explicit `summary`, `required: true` only for a genuine cross-tool sequence or
396
- a generic wrapper.
397
-
398
- *Why:* the only projection available is the one the program writes, and a
399
- value the schema types correctly can still mislead without the provider's
400
- rendering rule beside it — the agent then re-queries to explain a signal that
401
- was never there. *Cost:* result size.
402
-
403
- ### P8 — Identity resolution comes before action
404
-
405
- Where a downstream's write arguments take ids, the guide names the read tools
406
- that produce them and says not to guess. Where the provider has a
407
- human-readable identifier alongside a UUID, the guide says which is which.
408
-
409
- *Why:* a guessed id is a call, a rejection, and a repair. *Cost:* argument
410
- retries.
411
-
412
- ### P9 — Authentication defaults to OAuth, with a documented headless alternative
413
-
414
- OAuth per connector instance, stored in connector-scoped storage, is the
415
- default. The provider's own headless credential — a personal API key, a
416
- restricted key, a service account — is supported two ways: explicit `headers`
417
- auth, documented as a secret rather than configuration, and `{ type:
418
- "credential" }`, which declares an operator slot and takes the same secret from
419
- the connection UI at `/` instead. Either way it is paired with the narrowest mode the
420
- deployment can use, and the framing matches the provider's *published* contract
421
- for the MCP endpoint — not a convention borrowed from that provider's other
422
- APIs, and not this repository's earlier example, which is the same claim wearing
423
- a circle. `requireHttps` is set. Recovery from an expired authorization is the
424
- ordinary `auth_required` → `authorize_connector` route, which returns the
425
- consent URL for permitted OAuth starts and a connection UI handoff at `/` for
426
- a declared slot when UI and vault are mounted. Missing modules yield
427
- `unavailable`; never advertise a route the deployment cannot serve.
428
-
429
- *Why:* one route back from an expired credential is what keeps a failed call
430
- from becoming an abandoned task. *Cost:* wrong-tool selection.
431
-
432
- ### P10 — Nothing probes a credential unasked; a declared slot may be tested on request
433
-
434
- A proxy declares an operator credential slot exactly when its auth is `{ type:
435
- "credential" }`, and then it inherits H12 whole
436
- ([#439](https://github.com/zackbart/connecta/issues/439)). The other two shapes
437
- declare no slot and hold nothing for the credentials page: OAuth lives in
438
- connector-scoped storage and is exercised by the authorization flow itself,
439
- while a `headers` key arrives as deployment configuration. H12 is owed in every
440
- shape, and a proxy pays it in two places that do not depend on a slot:
441
- construction throws when a recognizable credential contradicts the declared mode
442
- (P4) — a check a vault-managed key cannot get, because there is nothing in the
443
- deployment file to read — and a dead, revoked, or absent credential fails loudly
444
- at use as `auth_required` with the `authorize_connector` route attached (P9).
445
-
446
- `testCredential` exists only behind the operator-pressed Test action on
447
- the connection UI at `/`, and only for a declared slot. It connects with the stored value
448
- and reports how many tools the downstream served, which is the whole honest
449
- check for a proxy: which account, project, or mode a key reaches is the
450
- provider's answer, not Connecta's. That is not the shape
451
- [#179](https://github.com/zackbart/connecta/issues/179) removed. What was
452
- removed is the *unasked* probe — a liveness call every deployment pays on a
453
- schedule or at startup to answer a question only a misconfigured one has. A
454
- human clicking Test has asked, `api()` has had that button since the vault
455
- existed, and nothing here probes on its own: no timer, no warmup, no check on
456
- the read path.
457
-
458
- *Why:* an unasked-for liveness probe spends a call on every deployment to answer
459
- a question only a misconfigured one has; a requested one spends a call the
460
- person requesting it chose. *Cost:* result size.
461
-
462
- ### P11 — Connecta classifies the transport; the downstream owns the tool error
463
-
464
- Connecta maps what it can see from outside the tool: an authorization failure to
465
- `auth_required`, a session or scope teardown, a timeout, and a capability the
466
- proxy will not relay (`input_required`, task-required execution) to an explicit
467
- refusal that says so. A tool-level failure the downstream returns — a validation
468
- complaint, a not-found, a plan restriction — is passed back as it arrived. The
469
- proxy does not read downstream error prose to invent a Connecta classification,
470
- and does not repackage a downstream error as `invalid_args`, because it has no
471
- schema of its own to have validated against. Where a downstream reliably reports
472
- a retryable condition, the guide says how to recognize it rather than the code
473
- guessing.
474
-
475
- *Why:* a transport failure and a rejected argument need different next moves,
476
- and prose-sniffing routes the second one down the first one's path. *Cost:*
477
- argument retries.
478
-
479
- ### P12 — Declare an admission budget only when the provider documents a number
480
-
481
- Where the provider publishes a rate limit, transcribe it as a rolling-window
482
- budget and say in the guide that it is a per-runtime approximation, not an
483
- enforcement. Where the provider documents nothing, or documents a limit metered
484
- per user in a way a per-runtime counter cannot approximate, declare no budget
485
- and leave the number to the operator who knows the account — with a documented
486
- example of how to supply one. A `maxConcurrency` beside a budget is a choice
487
- Connecta made and is labeled as one.
488
-
489
- *Why:* a hardcoded ceiling either throttles a healthy deployment or fails to
490
- protect a busy one, and both look like the provider being flaky. *Cost:*
491
- argument retries.
492
-
493
- ### P13 — A drifting downstream must be visible, not absorbed
494
-
495
- The classification lists name what a release reviewed, and they are the
496
- manifest the runtime drift check compares against — one structure per provider,
497
- built once by `vettedCatalog()` and used both to classify and to compare, so
498
- the annotation a caller gets and the verdict a check reads can never disagree.
499
- When the downstream changes underneath them, the correct outcome is a loud
500
- unclassified tool on the approval path and a maintained record of the drift —
501
- never a quiet re-guess. The runtime half is
502
- [the runtime drift policy](#the-runtime-drift-policy) below; the release-time
503
- half is [the maintainer-run drift check](#the-maintainer-run-drift-check).
504
-
505
- *Why:* an allowlist nobody can tell is stale is an allowlist that is wrong.
506
- *Cost:* wrong-tool selection.
507
-
508
- ## The runtime drift policy
509
-
510
- Detection rides a refresh; it never causes one
511
- ([#343](https://github.com/zackbart/connecta/issues/343)). The comparison
512
- happens inside the wrapper's `listTools`, on the listing the downstream just
513
- returned to serve a request the deployment already made, before the
514
- classification is applied — so what it reads is the downstream's own word, not
515
- connecta's fill-in. There is no scheduled job, no background request, no
516
- credential probe, and no automatic issue filing. Proactive credential liveness
517
- stays removed ([#179](https://github.com/zackbart/connecta/issues/179)); this
518
- is the shape that does not become it.
519
-
520
- **What a manifest holds.** Every tool name a release reviewed and its verdict
521
- (`read-only`, `additive`, or `destructive`). A manifest may also carry a schema
522
- digest from a prior review, but that digest is runtime drift evidence, not a
523
- schema Connecta serves. The credential-free provider check neither requires nor
524
- updates schema digests. The live `tools/list` definition always remains the
525
- agent-facing contract.
526
-
527
- **What it counts.** Four categories, and only counts:
528
-
529
- | Category | What it means |
530
- | --- | --- |
531
- | unclassified additions | the downstream serves a tool no release classified; it already fails closed onto `call_destructive_tool` |
532
- | names no longer served | a classified name is absent from this catalog |
533
- | annotation conflicts | the downstream *explicitly* contradicts a vetted verdict — `readOnlyHint: false` or `destructiveHint: true` on a vetted read, `readOnlyHint: true` on a vetted write |
534
- | schema changes | a recorded digest no longer matches the schemas that arrived |
535
-
536
- Silence is never a conflict: filling it in is what the classification is for.
537
- A non-zero "no longer served" count is the expected reading on a plan-gated
538
- workspace, because P5's lists are deliberate supersets — it is triage input,
539
- not an alarm.
540
-
541
- **Where it surfaces.** Connector status carries the counts and the time they
542
- were observed; `/health` carries the same per connector, which is where
543
- `connecta doctor` reads them, and doctor reports drift without failing on it.
544
- Both reads are projections — four counts and a bounded timestamp, rebuilt from
545
- whatever the connector seam returned, because `/health` is unauthenticated and
546
- `Connector.catalogDrift()` is third-party code.
547
- One activity event per *change* in the counts — an identical report every TTL
548
- is a heartbeat, not news — carrying the connector id and four integers. The
549
- event type has nowhere to put a tool name, a schema, an argument, a result, or
550
- downstream error prose, which is the same construction guarantee the tool-call
551
- event makes. Which account-specific tool drifted is deliberately absent from
552
- this payload-free runtime report. The public check can name documented drift;
553
- it cannot recover a plan-gated name the provider does not publish.
554
-
555
- **How far an observation reaches.** One runtime, and no further. The
556
- observation lives in the isolate or process that served the refresh; unlike the
557
- catalog, it is not persisted, so nothing carries it across a Workers isolate, a
558
- restart, or a second Node process. Status and `/health` therefore answer for
559
- the instance that took the request: on Workers a `connecta doctor` run will
560
- usually land on an isolate that has served no refresh and print nothing, and
561
- behind more than one process it is a coin flip. Read an empty report as *this
562
- runtime has observed nothing*, never as *nothing drifted* — the durable record
563
- of a finding is the activity event a sink already stored. The public check can
564
- name a documented change. Naming an unpublished, account-only tool is outside a
565
- credential-free checker.
566
-
567
- **What a finding obliges.** A contradicted vetted verdict — the downstream
568
- calling a release-reviewed destructive tool `readOnlyHint: true`, or a vetted
569
- read `destructiveHint: true` — blocks that provider's next release until a
570
- human has re-reviewed the tool. Everything else enters ordinary issue triage.
571
- No finding changes what a caller may reach: an unclassified tool fails closed
572
- whether or not anybody noticed it arrived.
573
-
574
- ## The maintainer-run drift check
575
-
576
- `npm run providers:check` is the other half
577
- ([#351](https://github.com/zackbart/connecta/issues/351)): a human at a laptop,
578
- before a release, using only published specifications and documentation. It
579
- lives in
580
- [`scripts/drift-check.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/drift-check.mjs) and ships nowhere —
581
- `scripts/` is outside the package, no runtime module imports it, and nothing it
582
- reads becomes a runtime input.
583
-
584
- **Published MCP references.** `--docs` checks
585
- the official setup page for each maintained hosted MCP connection, including
586
- the endpoint and OAuth support. Cloudflare, Stripe, Mixpanel, Notion,
587
- RevenueCat, and Vercel also publish structured tool inventories. The checker
588
- compares their documented names with the same vetted manifests the wrappers
589
- use. Linear's setup page does not enumerate tools, so its result says that only
590
- setup metadata was checked.
591
-
592
- A documented addition with no classification is a finding. A classified tool
593
- missing from public docs is printed but does not fail the run, because account
594
- scope, staged rollout, and documentation lag can all hide a tool that an
595
- earlier release reviewed. RevenueCat's `render-paywall-screenshot` is the odd
596
- one out: the official inventory names it but leaves its Access column blank.
597
- The checker reports the reviewed exception and Connecta keeps it fail-closed.
598
-
599
- `npm run providers:check` runs this documentation check for every maintained
600
- hosted MCP connection and the touched-endpoint OpenAPI check for every
601
- hand-written HTTP connection. It never reads a provider credential. The network
602
- keeps it outside `npm run check`; a provider outage must not make the
603
- deterministic test suite flaky.
604
-
605
- MCP schema handling needs a different assertion. Connecta does not vendor or
606
- reconstruct those schemas, so there is no static MCP schema snapshot for this
607
- command to compare. The live `tools/list` response is passed through at runtime,
608
- and the deterministic `catalog-drift.test.ts` suite pins that the vetted wrapper
609
- changes safety annotations only while preserving the provider input schema,
610
- output schema, and description. A parameter table in a docs page is never
611
- promoted into a runtime schema. The report states this directly instead of
612
- printing a misleading authenticated-schema requirement.
613
-
614
- **Touched endpoints.** A hand-written provider is written against a published
615
- OpenAPI document and calls a few dozen of its operations, so
616
- [`scripts/drift/`](https://github.com/zackbart/connecta/tree/main/scripts/drift) commits exactly those: method, path, the
617
- specification revision a release reviewed the endpoint at, whether the
618
- operation was deprecated at that revision, and a digest of that endpoint's
619
- contract. `--specs` fetches each provider's published document and reports four
620
- things per touched endpoint — the path is gone, the method is gone, the
621
- operation's deprecation changed, or its contract changed since the recorded
622
- revision. Everything else in the document is ignored, which is the point: a
623
- Cloudflare release that rewrites 2,000 operations connecta never calls is not
624
- news, and a revision bump that left the touched contracts alone reports
625
- nothing.
626
-
627
- Deprecation is reported as a *transition*, not a state: a deprecation a
628
- maintainer has read and recorded stops being news, and an operation that comes
629
- back off the deprecation list is its own finding. Without that, a single
630
- reviewed deprecation would fail every release forever, and the check could
631
- never reach the "no drift" state its exit code is for.
632
-
633
- A contract digest covers the parameters, the request body, and the success
634
- responses, with local `$ref`s inlined so a change inside a shared component is
635
- visible, and with descriptions, examples, and `x-` extensions stripped so a
636
- reworded document is not a finding. Inlining runs before a response's `content`
637
- is read, because a whole response object is often a reference itself —
638
- Cloudflare writes several of connecta's touched responses that way — and
639
- reading through the reference would digest the response contract as nothing at
640
- all. Two bounds are deliberate: a `$ref` cycle stays a reference rather than an
641
- infinite walk, and failure responses are excluded because an error body is
642
- H11's business, mapped from the status. `--record` rewrites the manifests from
643
- the documents on hand; run it when a finding has been reviewed, and read the
644
- diff before committing it.
645
-
646
- Narrowing is checked against the part being run: `--specs --provider linear`
647
- and `--specs --provider stripe` exit 2 rather than checking nothing and
648
- reporting no drift. `npm run providers:check -- --provider vercel` checks the
649
- REST OpenAPI contract and public MCP documentation without credentials.
650
- `npm run drift:check` is the lower-level equivalent with the same
651
- credential-free default plus fixture and recording flags.
652
-
653
- **What it never does.** No provider credential is read at all. There is no
654
- scheduled job, background traffic in a deployment, or automatic issue filing.
655
- Public docs do not generate tools or replace the live MCP catalog. A finding is
656
- read by a human and becomes a GitHub issue they wrote, because the decision a
657
- finding needs — the provider moved this endpoint, or connecta has to stop
658
- calling it — is not one a diff can make. Published specifications remain drift
659
- evidence and nothing else: no tool is generated from one, which is the
660
- [ethos](../ethos.md)'s refusal, not a detail of this script.
661
-
662
- ## What the audit checks
663
-
664
- The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
665
- runs this document against each of the seven providers and returns a verdict per
666
- convention: **meets**, **misses** (with the fix), or **not applicable** (with
667
- the reason). A convention is never quietly skipped, and an accepted miss is
668
- recorded as a provider-specific exception with its argument, not left blank.
669
- Its seven reports live in [provider-audit.md](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md), and the
670
- mechanically checkable half of the hand-written bar runs on every test run in
671
- [`test/provider-conventions.test.ts`](https://github.com/zackbart/connecta/blob/main/test/provider-conventions.test.ts) —
672
- so a convention that was met once stays met, or fails loudly.
673
-
674
- Hand-written providers are audited against H1–H14; hosted-MCP proxies against
675
- P1–P13. Applying a hand-written convention to a proxy is a category error, not
676
- a finding.
677
-
678
- Most of the bar is mechanically checkable against the shipped surface rather
679
- than by reading:
680
-
681
- | Convention | Mechanical check |
682
- | --- | --- |
683
- | H1, P2 | constructor throws on a blank `purpose`; `instructions` appear appended to the guide |
684
- | H2 | every tool name matches `^[a-z][a-z0-9_]*$` and opens with a verb from the connector's own set |
685
- | H3 | first sentence ≤ 160 characters; whole description ≤ 240 |
686
- | H5 | every tool has an `inputSchema` that is a closed plain object with a `required` list, and every property at every depth — nested objects and array items included — carries a description or sits on a recorded exception list; `api()` refuses to construct one it cannot enforce |
687
- | H7 | every compact input and output render stays inside 1,024 bytes, or the tool is on a recorded exception list |
688
- | H8 | every tool declares an `outputSchema` |
689
- | H9 | every read either projects or documents why it does not; `raw: true` exists wherever the projection drops something recoverable |
690
- | H10 | every list tool has bounded page arguments, a default below the provider maximum, and exactly one `hasMore`-shaped signal |
691
- | H11 | every mapped provider status has a test asserting the resulting code and retryability |
692
- | H12 | `credential` declared with labeled fields, and `testCredential`/`testCredentials` implemented |
693
- | H13, P7 | `usageGuide` uses the structured form with an explicit `summary`; `required` is set only with a stated reason |
694
- | H14 | escape hatches split GET / JSON-mutate / upload, and the GET tool is annotated read-only |
695
- | P1 | the wrapper's `listTools` changes annotations only |
696
- | P3 | the routing fact appears in the default `title` and the guide's first content line |
697
- | P4 | endpoint or mode option exists, with the documented default (or no default, where none is safe) |
698
- | P5 | reads and writes are named lists; an unlisted tool resolves to not-read-only; a reviewed destructive name beats a contradictory `readOnlyHint: true` |
699
- | P6, P8 | the guide contains the catalog-varies note and the id-resolution rule |
700
- | P9 | `auth` defaults to OAuth and `requireHttps` is set; a credential-auth shape frames the key the way the provider's MCP documentation does |
701
- | P10 | a `credential` slot exactly when auth is `{ type: "credential" }`; `testCredential` runs only from the operator's Test action, never on a timer or a read path; the mode/key contradiction still throws at construction |
702
- | P11 | an authorization failure surfaces as `auth_required`; a downstream tool error is returned unchanged, with no code chosen from its prose |
703
- | P12 | a declared budget matches a citable documented limit, or the absence is justified in the guide |
704
- | P13 | classification lists are maintained in one place per provider and built into the manifest the wrapper classifies from, so the drift check compares against the same fact the caller is served |
705
-
706
- The remainder — H4, H6, and the judgment in H14 about whether a named tool
707
- beats the escape hatch — is a reading, and the audit reports it as one. The
708
- Cloudflare keep/prune half of that judgment was made in
709
- [#350](https://github.com/zackbart/connecta/issues/350): 30 keep, 18 improve,
710
- 3 prune, measured per tool in
711
- [`eval/current-version/results/issue-350-evidence.md`](https://github.com/zackbart/connecta/blob/main/eval/current-version/results/issue-350-evidence.md).
712
- Its eighteen `improve` rows were H8 and H9 misses on tools that clearly earned
713
- their place. Issue #488 resolved them with useful output keys and maintained
714
- projections; the measurements remain the reason those tools stayed rather than
715
- a second removal argument.
716
-
717
- Each provider's own guide ([Cloudflare](./cloudflare.md),
718
- [Linear](./linear.md), [Mixpanel](./mixpanel.md), [Notion](./notion.md),
719
- [RevenueCat](./revenuecat.md), [Stripe](./stripe.md)) is part of the audited
720
- surface: documentation moves with
721
- the work, and a guide describing a surface that shipped differently is itself a
722
- miss.