@zackbart/connecta 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +225 -0
  2. package/dist/catalog-service.d.ts.map +1 -1
  3. package/dist/catalog-service.js +33 -6
  4. package/dist/catalog-service.js.map +1 -1
  5. package/dist/connectors/api.d.ts.map +1 -1
  6. package/dist/connectors/api.js +5 -1
  7. package/dist/connectors/api.js.map +1 -1
  8. package/dist/execute.js +1 -1
  9. package/dist/execute.js.map +1 -1
  10. package/dist/providers/cloudflare.d.ts +54 -0
  11. package/dist/providers/cloudflare.d.ts.map +1 -0
  12. package/dist/providers/cloudflare.js +3210 -0
  13. package/dist/providers/cloudflare.js.map +1 -0
  14. package/dist/providers/linear.d.ts +44 -0
  15. package/dist/providers/linear.d.ts.map +1 -0
  16. package/dist/providers/linear.js +243 -0
  17. package/dist/providers/linear.js.map +1 -0
  18. package/dist/providers/mixpanel.d.ts.map +1 -1
  19. package/dist/providers/mixpanel.js +15 -7
  20. package/dist/providers/mixpanel.js.map +1 -1
  21. package/dist/providers/notion.d.ts +39 -0
  22. package/dist/providers/notion.d.ts.map +1 -0
  23. package/dist/providers/notion.js +1625 -0
  24. package/dist/providers/notion.js.map +1 -0
  25. package/dist/providers/stripe.d.ts +37 -0
  26. package/dist/providers/stripe.d.ts.map +1 -0
  27. package/dist/providers/stripe.js +232 -0
  28. package/dist/providers/stripe.js.map +1 -0
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/documentation/cloudflare.md +313 -0
  32. package/documentation/connectors.md +13 -7
  33. package/documentation/linear.md +144 -0
  34. package/documentation/meta-tools.md +12 -1
  35. package/documentation/mixpanel.md +12 -7
  36. package/documentation/notion.md +233 -0
  37. package/documentation/stripe.md +202 -0
  38. package/ethos.md +1 -0
  39. package/package.json +17 -1
  40. package/src/catalog-service.ts +35 -6
  41. package/src/connectors/api.ts +5 -1
  42. package/src/execute.ts +1 -1
  43. package/src/providers/cloudflare.ts +3803 -0
  44. package/src/providers/linear.ts +301 -0
  45. package/src/providers/mixpanel.ts +15 -7
  46. package/src/providers/notion.ts +1879 -0
  47. package/src/providers/stripe.ts +306 -0
  48. package/src/version.ts +1 -1
  49. package/templates/node/package.json +1 -1
@@ -0,0 +1,313 @@
1
+ # Cloudflare prebuilt connection
2
+
3
+ Import `cloudflare()` independently from
4
+ `@zackbart/connecta/providers/cloudflare`. It is a deliberate, hand-written
5
+ surface over Cloudflare's v4 REST API. Fifty-five tools combine ergonomic,
6
+ fully described operations for common work with three guarded escape hatches
7
+ for the rest of Cloudflare's fast-moving control plane. Reads, JSON mutations,
8
+ and raw/multipart uploads remain separate so safety routing does not depend on
9
+ an agent-supplied HTTP method. The connection keeps lean projections, typed
10
+ failures, and a rate-limit budget matching the documented one. It adds no
11
+ provider dependency, imports nothing outside Connecta, and is not reachable
12
+ from Connecta's root entry.
13
+
14
+ ```ts
15
+ import { cloudflare } from "@zackbart/connecta/providers/cloudflare";
16
+
17
+ const edge = cloudflare("cloudflare_prod", {
18
+ title: "Production edge",
19
+ purpose: "DNS and cache administration for the production estate",
20
+ zoneId: "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
21
+ accountId: "9f8e7d6c5b4a30291817263544332211",
22
+ instructions: "Never purge the whole zone during business hours.",
23
+ });
24
+ ```
25
+
26
+ The `id` owns the ordinary connector namespaces; use a different id for every
27
+ Cloudflare account or estate. `purpose` is required because an agent choosing
28
+ between a production and a staging instance needs to know which one answers the
29
+ question. Account `instructions` are appended to the maintained guide and
30
+ cannot change the connector's safety classification.
31
+
32
+ ## No SDK, on purpose
33
+
34
+ Cloudflare publishes an official `cloudflare` npm SDK, and this connection does
35
+ not use it. The SDK's value is typed request wrappers and pagination helpers.
36
+ Both are things this connection replaces rather than consumes: an agent needs a
37
+ projected result and a `page.hasMore` boolean, not Cloudflare's full response
38
+ object, so the SDK's types would be re-projected away at the boundary. What the
39
+ dependency would cost is real — an optional peer with its own install step and
40
+ version skew, an import that never belongs in the root graph, and a second
41
+ opinion about what a Cloudflare call looks like.
42
+
43
+ The API itself does not need one. It is Bearer-token `fetch` with a uniform
44
+ `{ success, errors, messages, result, result_info }` response envelope. JSON,
45
+ raw bytes, and multipart request bodies all use Web APIs, which keeps the
46
+ provider Workers-clean and means `@zackbart/connecta/providers/cloudflare`
47
+ installs and runs with nothing extra. `test/package-surface.test.ts` pins the
48
+ claim: the `cloudflare` package must not appear in `dependencies`,
49
+ `peerDependencies`, or `devDependencies`, and every import in the provider
50
+ must be relative.
51
+
52
+ ## Credentials
53
+
54
+ The connection declares one operator-managed credential: a scoped Cloudflare
55
+ API token, sent as `Authorization: Bearer <token>`. Create it under My Profile →
56
+ API Tokens → Create Token. Do not use a Global API Key — it carries every
57
+ permission on the account and cannot be scoped.
58
+
59
+ Grant only what the deployment needs:
60
+
61
+ | Tools | Token permission | Scope |
62
+ | --- | --- | --- |
63
+ | Zone discovery and settings reads | Zone Read | Zone |
64
+ | `update_zone_setting` | Zone Settings Write | Zone |
65
+ | Zone ruleset reads | Relevant Rules product Read permission, such as Transform Rules Read or Firewall Services Read | Zone |
66
+ | `list_dns_records`, `get_dns_record` | DNS Read | Zone |
67
+ | `create_dns_record`, `update_dns_record`, `delete_dns_record` | DNS Write | Zone |
68
+ | `purge_cache` | Cache Purge | Zone |
69
+ | Worker script/deployment reads | Workers Scripts Read | Account |
70
+ | Worker writes through named or raw tools | Workers Scripts Write | Account |
71
+ | KV reads | Workers KV Storage Read | Account |
72
+ | KV creates, renames, writes, and deletes | Workers KV Storage Write | Account |
73
+ | R2 reads | Workers R2 Storage Read | Account |
74
+ | R2 creates, changes, uploads, deletes, and configuration writes | Workers R2 Storage Write | Account |
75
+ | Pages reads | Cloudflare Pages Read | Account |
76
+ | Pages retries, rollbacks, domains, purges, uploads, and deletes | Cloudflare Pages Write | Account |
77
+ | Images, Stream, Email Routing, D1, Queues, and other raw calls | Matching product Read or Write permission | Account or Zone |
78
+
79
+ Most names above appear directly in the token editor. Ruleset access is split
80
+ by product and phase, so grant the narrow Rules permission for the phases the
81
+ agent must inspect rather than looking for one generic "Zone Rulesets Read"
82
+ scope. "Cache Purge" is a single permission with no Read/Write split, and
83
+ Cloudflare's own reference renders a few labels differently between its
84
+ Dashboard and API tabs.
85
+
86
+ `verify_api_token` needs no permission beyond the token existing, which is what
87
+ makes it the right first call when something fails. The `/credentials` Test
88
+ action runs the same verification against the candidate token before it is
89
+ stored.
90
+
91
+ Cloudflare rate-limits *authentication failures* aggressively and separately
92
+ from the global limit: a few requests with a bad token return HTTP 429 with
93
+ code `10502`, "Too many authentication failures". That surfaces here as
94
+ `rate_limited`, not `auth_required`, which is correct — the token may well be
95
+ fine by the time the window clears — but it means a broken token should be
96
+ diagnosed once with `verify_api_token` rather than by retrying real calls.
97
+
98
+ ## Scoping
99
+
100
+ `zoneId` and `accountId` are deployment defaults, not restrictions. When a
101
+ default is set, the corresponding argument drops out of the tool's `required`
102
+ list and calls that omit it use the default; a call may still pass a different
103
+ id. When no default is set, the argument is required and the schema's own
104
+ description names the discovery tool that produces it — `list_zones` for a
105
+ zone, `list_accounts` for an account.
106
+
107
+ That is the discovery flow worth knowing: Cloudflare addresses almost
108
+ everything by an opaque 32-character id, and an agent that only knows a domain
109
+ name must call `list_zones` with `name: "example.com"` first. Configuring
110
+ `zoneId` removes that hop entirely for a single-zone deployment.
111
+
112
+ `list_zones` is the one tool a configured `accountId` deliberately does *not*
113
+ reach. It is the discovery step, and a default that quietly filtered it would
114
+ be a restriction in all but name — one with no argument that escapes it, since
115
+ an empty `accountId` would fall back to the default again. A deployment that
116
+ wants zones from one account passes `accountId` explicitly, and the property
117
+ says so.
118
+
119
+ ## Tools
120
+
121
+ The named surface covers workflows that benefit most from concise schemas and
122
+ projections:
123
+
124
+ | Area | Reads | Writes |
125
+ | --- | --- | --- |
126
+ | Zones | discovery, details, settings, rulesets | update a setting |
127
+ | DNS/cache | list and get records | create, update, delete, targeted/full purge |
128
+ | Workers | scripts, settings, deployments | delete a script |
129
+ | KV | namespaces, keys, bulk values | create/rename/delete namespace, bulk write/delete |
130
+ | R2 | buckets, object metadata, metrics, CORS | create/update/delete bucket, delete object, replace/delete CORS |
131
+ | Pages | projects, deployments, domains | retry/rollback/delete deployments, add/delete domains, purge build cache, delete project |
132
+
133
+ Every named tool carries a complete hand-written input schema: closed
134
+ (`additionalProperties: false`), with an accurate `required` list, an `enum` on
135
+ every constrained field, endpoint-specific pagination bounds, and a description
136
+ on every property. `test/cloudflare-provider.test.ts` walks the surface and
137
+ asserts those properties rather than leaving them as a claim.
138
+
139
+ ### The whole-v4 escape hatch
140
+
141
+ Cloudflare adds products and endpoints faster than a curated connector should
142
+ grow tool names. Three provider-relative tools cover the rest without turning
143
+ method classification into user input:
144
+
145
+ - `cloudflare_api_get` accepts only GET and is explicitly read-only. JSON is the
146
+ default; `responseType: "text" | "base64"` retrieves scripts, logs, R2
147
+ objects, and media bodies without pretending they have a JSON envelope.
148
+ - `cloudflare_api_mutate` accepts JSON POST, PUT, PATCH, and DELETE. It is always
149
+ destructive, even when a particular POST is merely additive.
150
+ - `cloudflare_api_upload` accepts POST or PUT plus exactly one of raw text,
151
+ base64 bytes, or multipart fields/files. It is always destructive and reads
152
+ no local files.
153
+
154
+ All three accept explicit endpoint-specific headers, which supports R2
155
+ jurisdictions, conditional requests, encryption controls, and object metadata.
156
+ Authentication, host selection, content type, content length, and transfer
157
+ framing remain connector-owned and cannot be overridden.
158
+
159
+ Paths are relative to `/client/v4`. Absolute URLs, protocol-relative paths,
160
+ `..` traversal, fragments, and embedded query strings are refused locally;
161
+ query parameters are explicit name/value pairs. These tools reuse the same
162
+ credential, admission budget, abort signal, envelope parsing, and typed failure
163
+ mapping as named tools. They do not widen the token's Cloudflare permissions.
164
+
165
+ This is intentionally not OpenAPI ingestion: it creates three stable tools,
166
+ not one tool per Cloudflare operation. For example, an agent can list Images at
167
+ `/accounts/{accountId}/images/v1`, manage Stream at
168
+ `/accounts/{accountId}/stream`, manage Email Routing at
169
+ `/zones/{zoneId}/email/routing/rules`, reach D1 at
170
+ `/accounts/{accountId}/d1/database`, and reach Queues at
171
+ `/accounts/{accountId}/queues`. The endpoint-specific query and body shape still
172
+ comes from Cloudflare's API reference.
173
+
174
+ ### Where the `perPage` bounds come from
175
+
176
+ `strictValidation` is on, so an out-of-range `perPage` is refused locally
177
+ before it reaches Cloudflare. That is only a favor when the bound is really
178
+ Cloudflare's, so the schemas record which ones are and the descriptions say so
179
+ out loud:
180
+
181
+ | Tool | `perPage` | Default | Whose bound |
182
+ | --- | --- | --- | --- |
183
+ | `list_accounts`, `list_zones` | 5–50 | 20 | Cloudflare's, as documented |
184
+ | `list_kv_namespaces` | 1–1000 | 20 | Cloudflare's, as documented |
185
+ | `list_dns_records` | 1–1000 | 100 | Cloudflare's minimum; the ceiling is ours |
186
+ | `list_pages_projects` | 1–100 | — | Ours entirely |
187
+
188
+ Two need the note. Cloudflare's schema documents `per_page` on
189
+ `/zones/{id}/dns_records` as 1 to **5,000,000** — a nominal ceiling no listing
190
+ will honor — so this connection caps it at 1,000, the same conservative-reading
191
+ move as the [one-variant purge rule](#cache-purging): a local cap an agent is
192
+ told about beats a page size that fails somewhere inside Cloudflare. And
193
+ `/accounts/{id}/pages/projects` documents no bounds and no default at all, so
194
+ 1 to 100 is a choice made here and labeled as one.
195
+
196
+ ### DNS record types
197
+
198
+ Cloudflare accepts 21 record types, exported as `CLOUDFLARE_DNS_RECORD_TYPES`.
199
+ Eight of them take a single `content` string; the other thirteen (CAA, CERT,
200
+ DNSKEY, DS, HTTPS, LOC, NAPTR, SMIMEA, SRV, SSHFP, SVCB, TLSA, URI) take a
201
+ per-type structured `data` object with its own field set.
202
+
203
+ `list_dns_records` filters on all 21. `create_dns_record` and
204
+ `update_dns_record` accept only the eight content-based types, exported as
205
+ `CLOUDFLARE_CONTENT_DNS_RECORD_TYPES`. Supporting the rest would mean either a
206
+ free-form `data` passthrough — the untyped `{}` this connection exists to
207
+ avoid — or thirteen more hand-written schemas for record types that are rare in
208
+ day-to-day zone administration. Structured-data records stay fully readable.
209
+ The named create/update tools omit them and the enum says so rather than letting
210
+ the call reach Cloudflare and 400; an operator who needs one can use the
211
+ approval-gated raw mutation tool with Cloudflare's documented per-type `data`
212
+ body.
213
+
214
+ ### Cache purging
215
+
216
+ `purge_cache` takes exactly one variant per call: `everything: true`, or one of
217
+ `files`, `tags`, `hosts`, or `prefixes`. Cloudflare caps a purge at 100
218
+ operations per request (500 files on Enterprise), and all four targeted methods
219
+ are available on every plan — tag, host, and prefix purging is no longer
220
+ Enterprise-only.
221
+
222
+ The one-variant rule is this connection's contract, not a documented API
223
+ restriction. Cloudflare's schema models the body as `anyOf`, which does not
224
+ forbid combining, and the only explicit exclusivity statement in its
225
+ documentation is about the Workers cache binding rather than the REST endpoint.
226
+ Refusing a combined call locally is the conservative reading: an agent gets a
227
+ clear `invalid_args` naming the conflict instead of a purge whose actual scope
228
+ is ambiguous. If a future deployment needs combined tag-and-prefix purging,
229
+ that is a deliberate change to make here, not something to discover in
230
+ production.
231
+
232
+ ## Results
233
+
234
+ Reads return Cloudflare's `result` unwrapped and projected: identity and
235
+ description fields kept, plan/permission/meta noise dropped, `snake_case`
236
+ renamed to `camelCase`. A zone comes back as `id`, `name`, `status`, `paused`,
237
+ `type`, `accountId`, `accountName`, `plan`, `nameServers`, and timestamps —
238
+ not the forty-field object Cloudflare sends.
239
+
240
+ Paginated lists add a `page` object derived from `result_info`:
241
+ `{ page, perPage, count, totalCount, totalPages, hasMore }`. `hasMore` is the
242
+ field to branch on.
243
+
244
+ Some endpoints do not work that way, and the schemas say so rather than leaving
245
+ an agent to discover it. `list_r2_buckets`, `list_r2_objects`, and
246
+ `list_kv_keys` paginate by cursor and return `nextCursor` instead of `page`.
247
+ `list_worker_scripts` reports no counters at all and omits `page` entirely.
248
+
249
+ Projected resource reads expose `raw: true` where the provider's larger object
250
+ is commonly useful. `cloudflare_api_get` is the universal unprojected escape
251
+ hatch. Raw shapes can hit a deployment's result cap, so programs should still
252
+ filter and project before returning them.
253
+
254
+ ## Typed failures
255
+
256
+ Cloudflare's error envelope carries an array of `{ code, message }` entries and
257
+ sometimes a nested `error_chain`; the connection flattens the whole chain into
258
+ the failure message so the provider's own code number survives to the agent.
259
+
260
+ | Cloudflare | Connecta failure | Agent behavior |
261
+ | --- | --- | --- |
262
+ | 429 | `rate_limited`, retryable | Waits `retryAfterMs` — the `retry-after` header when present, otherwise the full five-minute window |
263
+ | 401 or 403 | `auth_required`, not retryable | Stops and reports which permission is missing |
264
+ | 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 |
265
+ | 400, 409, 422 | `invalid_args`, not retryable | Repairs the arguments |
266
+ | 404 | `connector_call_failed`, not retryable | Re-runs discovery for the id |
267
+ | 5xx or a transport error | `unavailable`, retryable | Retries |
268
+
269
+ The six credential-shaped codes deserve a caveat: Cloudflare publishes no
270
+ official table mapping error codes to causes, so that set is assembled from
271
+ community reports and probing, not from documentation. The same goes for the
272
+ claim below that `10000` is overloaded — that is an observation about responses
273
+ seen in practice. Treat both as well-supported readings that Cloudflare could
274
+ invalidate without notice, and prefer `verify_api_token` over the code list
275
+ when a diagnosis actually matters.
276
+
277
+ Two ordering decisions are deliberate. The 429 branch is checked before the
278
+ authentication codes, because Cloudflare reuses the generic `10000` code on
279
+ throttled responses and reading a rate limit as an auth failure would tell an
280
+ agent to stop when it should wait. And `10000` is *not* itself treated as an
281
+ auth code: Cloudflare returns it for "Authentication error" but also for
282
+ ordinary validation failures like "Invalid pagination cursor" and
283
+ "domain_name is required", so routing on it would tell an agent its token was
284
+ broken when its arguments were. Genuine `10000` auth failures arrive with 401
285
+ or 403 and are caught by status.
286
+
287
+ Because the connection declares an operator-managed credential rather than an
288
+ OAuth flow, an `auth_required` failure resolves to the `operator_config`
289
+ recovery mode — the fix is a human updating the token, not an authorization
290
+ URL the agent can open. A missing token fails that way before any request is
291
+ made.
292
+
293
+ Some failures never reach Cloudflare at all. A blank scope id, a `purge_cache`
294
+ call with no variant or two, and an `update_dns_record` with nothing to change
295
+ are all refused locally as `invalid_args` with a validation issue attached,
296
+ because a round trip that can only 400 is a wasted call and a worse
297
+ explanation.
298
+
299
+ ## Rate limits
300
+
301
+ Cloudflare documents a global limit of
302
+ [1,200 requests per five minutes per user](https://developers.cloudflare.com/fundamentals/api/reference/limits/),
303
+ counted cumulatively across the dashboard, API keys, and API tokens. The
304
+ connection declares a matching rolling-window admission budget plus a
305
+ `maxConcurrency` of 6, overridable with the `maxConcurrency` option.
306
+
307
+ The budget is a best-effort approximation of the per-user limit, not an
308
+ enforcement of it. Each runtime keeps its own counter, so N Worker isolates or
309
+ Node processes serving one deployment can each admit up to 1,200 — and the
310
+ dashboard traffic of a human sharing the account is counted by Cloudflare but
311
+ not by Connecta. `maxConcurrency` is the bound that actually protects a shared
312
+ token, because a single `execute_code` program can fan out far faster than the
313
+ window notices.
@@ -48,10 +48,12 @@ instances of the same provider are isolated in exactly the same way as two
48
48
  hand-written connectors with different ids.
49
49
 
50
50
  A prebuilt connection's vetted annotations are fill-in only. They classify what
51
- the downstream leaves unannotated and may always tighten a classification; they
52
- never overrule an explicit downstream `destructiveHint: true` or
53
- `readOnlyHint: false`. The fail-closed read-only invariant is unchanged by the
54
- authoring path.
51
+ the downstream leaves unannotated and do not argue with what it states — not an
52
+ explicit `destructiveHint: true` or `readOnlyHint: false` on a name the
53
+ connection files as a read, nor an explicit `readOnlyHint: true` on a name no
54
+ release has classified at all. Silence on an unclassified name still means not
55
+ read-only, so catalog drift fails closed. The fail-closed read-only invariant
56
+ is unchanged by the authoring path.
55
57
 
56
58
  Prebuilt means preferred when available, not mandatory. A deployment may mix
57
59
  prebuilt connections, custom `remoteMcp()` connections, and custom `api()`
@@ -72,9 +74,9 @@ export const connecta = createConnecta({
72
74
  purpose: "Production product decisions for the growth team",
73
75
  }),
74
76
  // Custom downstream MCP server, no prebuilt connection needed.
75
- remoteMcp("linear", {
76
- url: "https://mcp.linear.app/mcp",
77
- description: "Issue tracking for the platform team",
77
+ remoteMcp("deploy_tools", {
78
+ url: "https://mcp.internal.example/deploys",
79
+ description: "In-house deployment and rollback tooling",
78
80
  }),
79
81
  // Deliberate in-house HTTP surface, hand-written tool by hand-written tool.
80
82
  api("billing", {
@@ -109,7 +111,11 @@ them. Nothing in the list is privileged by how it was authored.
109
111
 
110
112
  Maintained provider guides:
111
113
 
114
+ - [Cloudflare](./cloudflare.md)
115
+ - [Linear](./linear.md)
112
116
  - [Mixpanel](./mixpanel.md)
117
+ - [Notion](./notion.md)
118
+ - [Stripe](./stripe.md)
113
119
 
114
120
  ## MCP version skew
115
121
 
@@ -0,0 +1,144 @@
1
+ # Linear prebuilt connection
2
+
3
+ Import `linear()` independently from `@zackbart/connecta/providers/linear`. It
4
+ wraps Linear's hosted MCP server with endpoint selection, OAuth by default, a
5
+ task-oriented usage guide, and a vetted safety classification. It adds no
6
+ provider dependency and is not reachable from Connecta's root entry.
7
+
8
+ ```ts
9
+ import { linear } from "@zackbart/connecta/providers/linear";
10
+
11
+ const tracker = linear("product_tracker", {
12
+ title: "Product issue tracking",
13
+ purpose: "Issue and project planning for the platform team",
14
+ instructions: "File bugs into the Platform team unless the request names another.",
15
+ });
16
+ ```
17
+
18
+ The `id` owns the ordinary connector namespaces; use a different id for every
19
+ Linear workspace or access mode. `purpose` is required because an agent
20
+ choosing between two instances needs to know which workspace answers the
21
+ question. Workspace `instructions` are appended to the maintained guide and
22
+ cannot change the connector's safety classification.
23
+
24
+ ## Access modes
25
+
26
+ Linear publishes two hosted endpoints, and `access` selects between them:
27
+
28
+ | `access` | Endpoint | OAuth scopes |
29
+ | --- | --- | --- |
30
+ | `"read-write"` (default) | `https://mcp.linear.app/mcp` | `read`, `write` |
31
+ | `"read-only"` | `https://mcp.linear.app/mcp/readonly` | `read` |
32
+
33
+ Read-only is not a client-side filter. The endpoint advertises the `read` scope
34
+ alone, so the token minted for it cannot reach Linear's write APIs — a stronger
35
+ guarantee than any annotation Connecta applies. A deployment that only reports
36
+ on delivery should use it, and can run it beside a read-write instance under a
37
+ different id:
38
+
39
+ ```ts
40
+ linear("delivery_reporting", {
41
+ purpose: "Executive delivery reporting",
42
+ access: "read-only",
43
+ });
44
+ ```
45
+
46
+ The mode is legible at browse time, not only after the guide is fetched. A
47
+ read-only connection titles itself `Linear (read-only)` unless the operator
48
+ gives a `title`, and its guide opens with the access note rather than the
49
+ workspace purpose — `search_tools` renders a connector's title and guide
50
+ summary but never its description, and the summary is the guide's first content
51
+ line.
52
+
53
+ Linear's deprecated `/sse` transport is deliberately unreachable from this
54
+ connection; it now answers 404.
55
+
56
+ ## Authentication
57
+
58
+ OAuth 2.1 with dynamic client registration is the default and keeps each
59
+ connector instance's flow and tokens in its connector-scoped storage. Linear
60
+ also accepts a bearer token or a personal API key passed directly in the
61
+ `Authorization` header, which suits a headless deployment:
62
+
63
+ ```ts
64
+ linear("automation_tracker", {
65
+ purpose: "Headless release reporting",
66
+ auth: {
67
+ type: "headers",
68
+ headers: { Authorization: env.LINEAR_API_KEY },
69
+ },
70
+ });
71
+ ```
72
+
73
+ Keep that key in the runtime's secret store; it is a password, not ordinary
74
+ configuration. A personal API key carries the acting user's full workspace
75
+ permissions, so pair it with `access: "read-only"` unless the deployment
76
+ genuinely writes.
77
+
78
+ ## Safety classification
79
+
80
+ The wrapper classifies Linear's documented `list_*`, `get_*`, and
81
+ `search_documentation` tools as reads, and its `save_*`, `create_*`, `delete_*`,
82
+ `resolve_*`, `submit_*`, and `merge_*` tools as writes. An unfamiliar tool the
83
+ downstream leaves unannotated fails closed onto `call_destructive_tool` until a
84
+ Connecta release reviews it.
85
+
86
+ That classification is **fill-in only**, and unconditionally so: it supplies
87
+ the annotations Linear leaves unset and contradicts an explicit downstream
88
+ annotation in neither direction. A tool on the read allowlist arriving with
89
+ `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
90
+ downstream said and stays behind `call_destructive_tool`. A tool on neither
91
+ maintained list arriving with `readOnlyHint: true` keeps that too, and stays
92
+ callable from `execute_code`. Both are the downstream telling you this
93
+ release's allowlist is stale, and on a name no release has reviewed its word is
94
+ the only evidence there is. The one classification that still outranks the
95
+ downstream is a name this release reviewed and filed destructive: a `save_*`
96
+ tool claiming `readOnlyHint: true` is a downstream bug rather than news, and
97
+ stays on the approval path.
98
+
99
+ One detail of Linear's own design shapes the classification: **`save_*` tools
100
+ are upserts.** Omitting a record id creates; supplying one updates in place.
101
+ Because an upsert can overwrite, every `save_*` is classified destructive even
102
+ though some calls only create. The genuine creates are `create_issue_label` and
103
+ `create_initiative_label`, plus the attachment upload tools, which assert
104
+ `readOnlyHint: false` without claiming a destruction they do not perform.
105
+
106
+ ## The catalog is not a fixed set
107
+
108
+ Linear's hosted `tools/list` varies by workspace plan and enabled features:
109
+ customer requests, releases, and code review do not appear in every workspace.
110
+ The maintained allowlists are therefore a superset — a classified name a
111
+ workspace never returns costs nothing, and a genuinely new tool fails closed.
112
+ Agents should search this connector's catalog for what the workspace actually
113
+ exposes rather than assuming a tool exists; the usage guide says so explicitly.
114
+
115
+ ## Rate limits
116
+
117
+ Linear documents no MCP-specific rate limit. The MCP server rides the
118
+ [GraphQL API limits](https://linear.app/developers/rate-limiting), which are
119
+ metered **per user per hour** and shared with everything else that credential
120
+ does. Linear's own page is internally inconsistent on the API-key request
121
+ figure — the prose says 5,000 requests per hour while the table below it says
122
+ 2,500 for an API key and 5,000 for an OAuth app, against 600 unauthenticated —
123
+ and limits are raised dynamically for workspace-level OAuth apps using Actor
124
+ Authorization.
125
+
126
+ For that reason this connection declares **no call-admission budget by
127
+ default**. Connecta's counter is per runtime, not per user, so a hardcoded
128
+ ceiling would either throttle a healthy deployment or fail to protect a busy
129
+ one. An operator who knows their workspace can supply one explicitly:
130
+
131
+ ```ts
132
+ linear("product_tracker", {
133
+ purpose: "Issue and project planning for the platform team",
134
+ callAdmission: {
135
+ rules: [
136
+ { budget: { kind: "rolling-window", maxCalls: 1_000, windowMs: 3_600_000 } },
137
+ ],
138
+ },
139
+ });
140
+ ```
141
+
142
+ A budget-only rule needs no queue. If you add `maxConcurrency` you are asking
143
+ for a queue, and the admission controller then requires the rest of the queue
144
+ settings at construction.
@@ -194,7 +194,18 @@ the bounded classified failure (`code`, `message`, `retryable`, and any
194
194
  `retryAfterMs`) so the caller can tell a transient outage from one a deployment
195
195
  operator must clear. It carries nothing else the call-path classifier knows: a
196
196
  discovery read is not a call. Unscoped searches keep the count only — one
197
- connector's failure is not another search's context. Analysis
197
+ connector's failure is not another search's context. An empty query browses
198
+ rather than searches, so it reports no term analysis — except when the scope
199
+ itself failed, where the same fields apply. A browse scoped to an unavailable
200
+ connector carries `unavailableConnectorCount`, `catalogError`, and guidance,
201
+ and an unscoped browse again carries the count alone. A browse scoped to an ID
202
+ that is not configured at all carries `connectorScope`, `unknownConnector`, and
203
+ the same omit-the-connector guidance the term-bearing path gives — nothing was
204
+ attempted, so there is no count and no `catalogError` — and it names no
205
+ connector but the one the caller supplied. A connector that correctly exposes
206
+ no tools still reports no analysis, so the two do not serialize alike. The
207
+ advice to browse a connector with an empty query must not land in silence that
208
+ reads like a connector with no tools. Analysis
198
209
  from a connector-filtered search includes `connectorScope` and speaks only
199
210
  about that connector; `unknownConnector` distinguishes an unconfigured ID from
200
211
  a known connector with no match. Analysis covers at most eight distinct terms
@@ -46,16 +46,21 @@ headless.
46
46
 
47
47
  The wrapper classifies the documented observational tools as reads and the
48
48
  documented create, update, edit, merge, dismiss, duplicate, and delete tools as
49
- writes. An unfamiliar tool added by the downstream fails closed onto
49
+ writes. An unfamiliar tool the downstream leaves unannotated fails closed onto
50
50
  `call_destructive_tool` until a Connecta release reviews it.
51
51
 
52
- That classification is **fill-in only**. It supplies the annotations Mixpanel
53
- leaves unset and may always tighten one — but it never contradicts an explicit
54
- downstream annotation. A tool on the read allowlist that arrives carrying
52
+ That classification is **fill-in only**, and unconditionally so: it supplies
53
+ the annotations Mixpanel leaves unset and contradicts an explicit downstream
54
+ annotation in neither direction. A tool on the read allowlist arriving with
55
55
  `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
56
- downstream said and stays behind `call_destructive_tool`: the downstream is
57
- telling you this release's allowlist is stale, and the fail-closed invariant
58
- does not bend for a maintained connection. Maintained writes that only create
56
+ downstream said and stays behind `call_destructive_tool`. A tool on neither
57
+ maintained list arriving with `readOnlyHint: true` keeps that too, and stays
58
+ callable from `execute_code`. Both are the downstream telling you this
59
+ release's allowlist is stale, and on a name no release has reviewed its word is
60
+ the only evidence there is. The one classification that still outranks the
61
+ downstream is a name this release reviewed and filed destructive: a
62
+ `Delete-Dashboard` claiming `readOnlyHint: true` is a downstream bug rather
63
+ than news, and stays on the approval path. Maintained writes that only create
59
64
  something new (`Create-Dashboard`, `Create-Cohort`, `Create-Metric`, and the
60
65
  rest) leave `destructiveHint` unset; `readOnlyHint: false` already routes them
61
66
  through the destructive path, and asserting destruction only inflates the