@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.
- package/CHANGELOG.md +225 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +33 -6
- package/dist/catalog-service.js.map +1 -1
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +5 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/execute.js +1 -1
- package/dist/execute.js.map +1 -1
- package/dist/providers/cloudflare.d.ts +54 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +3210 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/linear.d.ts +44 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +243 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/mixpanel.d.ts.map +1 -1
- package/dist/providers/mixpanel.js +15 -7
- package/dist/providers/mixpanel.js.map +1 -1
- package/dist/providers/notion.d.ts +39 -0
- package/dist/providers/notion.d.ts.map +1 -0
- package/dist/providers/notion.js +1625 -0
- package/dist/providers/notion.js.map +1 -0
- package/dist/providers/stripe.d.ts +37 -0
- package/dist/providers/stripe.d.ts.map +1 -0
- package/dist/providers/stripe.js +232 -0
- package/dist/providers/stripe.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/cloudflare.md +313 -0
- package/documentation/connectors.md +13 -7
- package/documentation/linear.md +144 -0
- package/documentation/meta-tools.md +12 -1
- package/documentation/mixpanel.md +12 -7
- package/documentation/notion.md +233 -0
- package/documentation/stripe.md +202 -0
- package/ethos.md +1 -0
- package/package.json +17 -1
- package/src/catalog-service.ts +35 -6
- package/src/connectors/api.ts +5 -1
- package/src/execute.ts +1 -1
- package/src/providers/cloudflare.ts +3803 -0
- package/src/providers/linear.ts +301 -0
- package/src/providers/mixpanel.ts +15 -7
- package/src/providers/notion.ts +1879 -0
- package/src/providers/stripe.ts +306 -0
- package/src/version.ts +1 -1
- 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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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("
|
|
76
|
-
url: "https://mcp.
|
|
77
|
-
description: "
|
|
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.
|
|
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
|
|
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
|
|
53
|
-
leaves unset and
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|