@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,389 +0,0 @@
1
- # Connectors
2
-
3
- Every connector may set `authScope: "shared" | "personal"`. Shared is the
4
- default and keeps one deployment-wide downstream grant. Personal auth requires
5
- a stable human principal and partitions connector state, credentials, OAuth,
6
- catalogs, and observed shapes by that principal. Connector visibility is a
7
- separate deployment rule under `identity.connectorAccess`; hiding a connector
8
- does not change who owns its auth. See [shared and personal auth](./storage-and-credentials.md#shared-and-personal-auth).
9
-
10
- `authScope` partitions connecta-owned context, not arbitrary variables captured
11
- by connector code. A custom personal connector must read auth from
12
- `ctx.credential` or `ctx.storage`; a secret closed over by its handler remains
13
- shared JavaScript state. `remoteMcp()` rejects the equivalent mistake when
14
- literal headers are combined with personal scope.
15
-
16
- Connectors are the boundary between Connecta's fixed meta-tool surface and
17
- downstream capabilities. Prefer a prebuilt connection when Connecta maintains
18
- one for the provider. Use `api()` to define a deliberate HTTP API surface and
19
- `remoteMcp()` to aggregate any other MCP endpoint. All three authoring paths
20
- produce ordinary `Connector` instances and pass through the same catalog,
21
- read-only admission, credentials, storage, invocation, result-size, and
22
- activity paths.
23
-
24
- Custom HTTP routes belong to the deployment fetch handler. Connectors expose
25
- tools and the documented OAuth hooks; a removed `handleRequest` declaration
26
- refuses construction.
27
-
28
- Connector instances are deployment configuration. They are not registered or
29
- reconfigured at runtime. Request-local clients, transports, abort signals, and
30
- catalogs must be released with the request that created them.
31
-
32
- ## Prebuilt connections
33
-
34
- A prebuilt connection is an independently imported provider constructor, not a
35
- registry or a second connector interface. It packages behavior Connecta can
36
- maintain universally: provider endpoints and authentication defaults, tool
37
- definitions or downstream catalog behavior, schemas and annotations, lean
38
- result shapes, typed errors, pagination and retry conventions, and a short
39
- usage guide where schemas cannot carry the advice.
40
-
41
- The deployment still supplies the account-specific identity and policy:
42
-
43
- - a unique connector `id`, which owns its address, storage, credential,
44
- catalog, admission, and activity namespaces;
45
- - a human-readable `title` and a concrete `purpose` or audience;
46
- - supported authentication overrides; and
47
- - account-specific instructions appended to, rather than replacing, the safe
48
- provider guidance.
49
-
50
- Imports and registration stay explicit and a la carte:
51
-
52
- ```ts
53
- import { mixpanel } from "@zackbart/connecta/providers/mixpanel";
54
-
55
- const analytics = mixpanel("product_analytics", {
56
- title: "Product analytics",
57
- purpose: "Production product decisions for the growth team",
58
- });
59
- ```
60
-
61
- The constructor may use `remoteMcp()` or `api()` internally. When a provider's
62
- official MCP and HTTP API expose materially different capabilities or schema
63
- ownership, the constructor may offer an explicit deployment-time surface
64
- choice. It must document the difference, keep a backward-compatible default,
65
- and never let an agent switch surfaces during a run. The choice grants no
66
- different runtime privileges. Two instances of the same provider are isolated
67
- in exactly the same way as two hand-written connectors with different ids.
68
-
69
- That choice exists only when the two interfaces are genuinely different:
70
-
71
- | Provider | Maintained interfaces | Why |
72
- | --- | --- | --- |
73
- | Cloudflare | API and MCP | The API interface has 48 projected named tools plus three safety-split hatches. The official MCP compresses more than 2,500 endpoints into `search` and approval-gated `execute`. |
74
- | Notion | API and MCP | The API interface has stable lean projections. The official MCP adds Notion-owned live schemas, workspace search, files, views, agents, and sessions. |
75
- | Vercel | API and MCP | The API interface has projected deployment operations. The official MCP owns a broader, independently changing catalog. |
76
- | Linear | MCP | Vendoring its GraphQL API would create a second schema system rather than a distinct maintained interface. |
77
- | Mixpanel | MCP | Its hosted service already joins several Mixpanel APIs; flattening those APIs would recreate the catalog problem. |
78
- | RevenueCat | MCP | Its official server is generated from API v2, so a second wrapper would duplicate the same contract. |
79
- | Stripe | MCP | Its official server already exposes both named workflows and supported API methods. A second raw API interface would duplicate it. |
80
-
81
- This is not a requirement that every provider have two labels. A second choice
82
- must change capability, result shape, or schema ownership enough to justify a
83
- second contract. Otherwise it only gives agents two names for the same thing.
84
-
85
- A prebuilt connection's vetted annotations fill in downstream silence and
86
- otherwise preserve explicit annotations. This includes an explicit
87
- `destructiveHint: true` or `readOnlyHint: false` on a vetted read, and an
88
- explicit `readOnlyHint: true` on a name no release has classified. One narrow
89
- exception stays fail-closed: a release-reviewed destructive classification
90
- overrides a contradictory `readOnlyHint: true`, because Connecta has
91
- independently established that the tool mutates existing state. Silence on an
92
- unclassified name still means not read-only. The authoring path never weakens
93
- the fail-closed read-only invariant.
94
-
95
- Prebuilt means preferred when available, not mandatory. A deployment may mix
96
- prebuilt connections, custom `remoteMcp()` connections, and custom `api()`
97
- connections. Connecta makes no completeness promise: providers without a
98
- maintained prebuilt connection continue to use the public primitives without
99
- loss of support.
100
-
101
- ```ts
102
- import { createConnecta, remoteMcp, api } from "@zackbart/connecta";
103
- import { mixpanel } from "@zackbart/connecta/providers/mixpanel";
104
- import { quickJsExecutor } from "@zackbart/connecta/quickjs";
105
-
106
- export const connecta = createConnecta({
107
- executor: quickJsExecutor(),
108
- connectors: [
109
- // Maintained prebuilt connection.
110
- mixpanel("product_analytics", {
111
- purpose: "Production product decisions for the growth team",
112
- }),
113
- // Custom downstream MCP server, no prebuilt connection needed.
114
- remoteMcp("deploy_tools", {
115
- url: "https://mcp.internal.example/deploys",
116
- description: "In-house deployment and rollback tooling",
117
- }),
118
- // Deliberate in-house HTTP surface, hand-written tool by hand-written tool.
119
- api("billing", {
120
- description: "Internal billing reads",
121
- credential: { label: "Billing API token" },
122
- tools: [
123
- {
124
- name: "get_invoice",
125
- description: "Fetch one invoice by id.",
126
- annotations: { readOnlyHint: true },
127
- inputSchema: {
128
- type: "object",
129
- properties: { id: { type: "string" } },
130
- required: ["id"],
131
- },
132
- handler: async ({ id }, ctx) => {
133
- const response = await fetch(
134
- `https://billing.internal.example/invoices/${id}`,
135
- { headers: { Authorization: `Bearer ${await ctx.credential?.get()}` } },
136
- );
137
- return response.json();
138
- },
139
- },
140
- ],
141
- }),
142
- ],
143
- });
144
- ```
145
-
146
- All three are ordinary `Connector` instances by the time the registry sees
147
- them. Nothing in the list is privileged by how it was authored.
148
-
149
- What a maintained connection owes an agent is written down rather than
150
- inherited from whoever wrote it last:
151
- [provider conventions](./provider-conventions.md), one set for hand-written
152
- HTTP surfaces and one for hosted-MCP proxies.
153
-
154
- Maintained provider guides:
155
-
156
- - [Cloudflare](./cloudflare.md)
157
- - [Linear](./linear.md)
158
- - [Mixpanel](./mixpanel.md)
159
- - [Notion](./notion.md)
160
- - [RevenueCat](./revenuecat.md)
161
- - [Stripe](./stripe.md)
162
- - [Vercel](./vercel.md)
163
-
164
- ## The `api()` construction contract
165
-
166
- `api()` is the path every custom integration takes, and whatever it accepts is
167
- what an agent eventually reads. Three things are refused at construction rather
168
- than discovered in production
169
- ([#340](https://github.com/zackbart/connecta/issues/340)):
170
-
171
- - **A tool with no description.** Discovery has nothing else to route on, and a
172
- guess costs a call.
173
- - **A tool with no explicit boolean `annotations.readOnlyHint`.** `true`
174
- declares a read and admits the tool to `call_tool` and `execute_code`;
175
- `false` declares work that crosses `call_destructive_tool`, where the host
176
- can ask a human. Connecta never infers the classification from a tool name,
177
- description, schema, HTTP method, or the other annotations — an unclassified
178
- tool is a bug in the deployment, not a puzzle to solve.
179
- - **An `inputSchema` the validator cannot compile.** Declaring one is optional;
180
- declaring one that cannot be enforced is not. A schema that only reveals
181
- itself on first use — an unresolvable `$ref`, say — fails that call as
182
- non-retryable `invalid_args` rather than forwarding raw arguments to the
183
- handler. `validateArgs: false` still opts out of enforcement for deployments
184
- that want loose coercion; it does not opt out of the schema being real.
185
-
186
- None of this reaches a proxied catalog: hosted-MCP tools arrive as their
187
- downstream wrote them, and an unannotated or contradictory one stays
188
- fail-closed onto `call_destructive_tool`. The contract binds the surfaces we
189
- write, not the catalogs we relay.
190
-
191
- ## The guarded fetch transport
192
-
193
- Every hand-written HTTP surface re-derives the same safety machinery, and two
194
- of them had already derived it slightly differently. `src/connectors/guarded-fetch.ts`
195
- is that machinery extracted once ([#341](https://github.com/zackbart/connecta/issues/341)):
196
- one `guardedFetch({ provider, baseUrl, headers, maxResponseBytes, authenticate })`
197
- factory returning the transport a connector sends every request through.
198
-
199
- ```ts
200
- const send = guardedFetch({
201
- provider: "Billing",
202
- baseUrl: "https://billing.internal.example/v1",
203
- maxResponseBytes: 4 * 1024 * 1024,
204
- headers: { Accept: "application/json" },
205
- authenticate: async (ctx) => {
206
- const token = await ctx.credential?.get();
207
- if (!token) throw new ConnectorCallError("auth_required", "...");
208
- return { Authorization: `Bearer ${token}` };
209
- },
210
- });
211
-
212
- const invoice = await send(
213
- { method: "GET", path: `/invoices/${id}` },
214
- ctx,
215
- (response) => {
216
- if (!response.ok) throw billingFailure(response.status);
217
- return response.json();
218
- },
219
- );
220
- ```
221
-
222
- What it owns is mechanical and provider-independent:
223
-
224
- - **Confinement.** A request path is provider-relative, carries no query or
225
- fragment, and is re-checked against the base origin and path prefix *after*
226
- `new URL` normalization — because normalization is how a path escapes a
227
- prefix, not something to trust before it.
228
- - **Construction.** Query parameters are encoded rather than concatenated, a
229
- JSON body is serialized with the `Content-Type` to match, and a pre-framed
230
- body gets none, so `fetch` still picks the multipart boundary.
231
- - **Credential forwarding.** `authenticate` is called once per request and its
232
- headers are applied last; a request header wearing one of their names is
233
- refused rather than allowed to shadow it. A 3xx is refused outright — a
234
- redirect is an instruction to re-send the credential to whatever origin the
235
- `Location` names, and a confinement a redirect can undo was never one.
236
- - **Bounded reads.** `maxResponseBytes` is required, not defaulted: what counts
237
- as an absurd response is a fact about the API, not about HTTP. A declared
238
- `Content-Length` past the ceiling fails before a byte is read, and a
239
- streaming body is abandoned at the ceiling rather than buffered past it.
240
- - **Normalization.** An unreachable provider becomes a retryable `unavailable`
241
- instead of whatever `TypeError` the runtime threw, and `ctx.signal` rides
242
- every request.
243
-
244
- What it deliberately does not own is meaning. It never reads a status code and
245
- never invents an authentication scheme: the provider's `authenticate` callback
246
- supplies the headers, and the provider's mapper turns one `GuardedResponse`
247
- into a result or a typed failure. That split is not fastidiousness. Notion's
248
- 403 means a capability the integration was never granted — re-authorizing
249
- cannot fix it — while Cloudflare's means a token scope, and the two want
250
- opposite next moves. A helper that guessed would be wrong for one of them.
251
-
252
- Cloudflare, Notion, and Vercel run on it. The first two existing suites carried
253
- over unchanged when the helper was extracted, which proves that migration kept
254
- the behavior those suites cover — not that nothing changed. Three things did,
255
- and the changelog names them: a 3xx is refused where both providers used to
256
- follow it, both now fail past their byte ceiling, and `cloudflare()`'s
257
- `baseUrl` is validated at construction. Each suite gained one test for the
258
- ceiling, because the one
259
- guard the helper was written to add is the one a provider's own mapper can
260
- most easily disarm: a bare `catch` around `response.json()` swallows the
261
- transport's refusal along with a parse error, and turns a response nobody was
262
- allowed to read into an empty success. A mapper re-throws
263
- `ConnectorCallError` and swallows only what it recognizes.
264
-
265
- It is **not exported this release**: the two migrations proved the shape
266
- preserves behavior for connectors that already had this machinery, not that it
267
- is the right shape for an author starting from nothing, and an unexported
268
- symbol costs nothing to reshape while a published one is a promise.
269
- The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
270
- supplies the third caller that would settle it.
271
-
272
- ## MCP version skew
273
-
274
- Connecta deliberately sits between protocol generations
275
- ([full revision inventory](https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md)):
276
-
277
- - **Inbound:** `/mcp` serves both the 2026-07-28 revision and legacy 2025
278
- clients. Modern clients negotiate with `server/discover` and do not send
279
- `initialize`; legacy clients retain their initialize flow. The endpoint
280
- remains stateless in both cases.
281
- - **Outbound:** `remoteMcp()` probes modern downstreams and falls back to the
282
- byte-compatible legacy flow. Legacy downstreams are normal supported
283
- deployments, not a temporary exception. Automatic negotiation remains the
284
- default. A known-legacy server that crashes or returns a server error for the
285
- pre-initialize probe can set `versionNegotiation: "legacy"` on that one
286
- connector; Connecta then starts directly with `initialize` and never sends
287
- `server/discover`. Keep the default unless the downstream requires this
288
- compatibility concession, so modern protocol support is still discovered.
289
- - **Legacy sessions:** Connecta's own endpoint creates no protocol session, but
290
- a stateful legacy downstream can still issue `Mcp-Session-Id`. Closing a
291
- request scope explicitly sends the legacy DELETE before closing its transport.
292
- SDK v2 `Client.close()` does not do that on Connecta's behalf, so
293
- `terminateSession` remains required and tested.
294
- - **Modern cache hints:** `tools/list` is deployment-fixed and returns a
295
- one-hour private cache hint. Downstream hints do not alter Connecta's existing
296
- five-minute fingerprinted catalog cache; that remains gated in
297
- [#206](https://github.com/zackbart/connecta/issues/206).
298
- - **Multi-round-trip results:** a downstream `input_required` result becomes a
299
- non-retryable `input_required_unsupported` failure. `call_tool` and the
300
- `execute_code` host bridge preserve the structured code. Relaying the
301
- opaque `requestState` is architecturally possible but gated until real hosts
302
- and downstreams adopt it.
303
-
304
- The compatibility policy has no automatic sunset. Dropping a revision,
305
- session cleanup, or cursor tolerance requires an explicit design decision and
306
- replacement evidence.
307
-
308
- ## Catalog contract
309
-
310
- A downstream catalog is complete or it is a failure. Follow every page until
311
- the cursor ends, preserve schemas and annotations, and never cache or serve a
312
- partial walk. The fixed TTL is paired with a schema fingerprint so a changed
313
- catalog invalidates persisted results even within the time window.
314
-
315
- Agent reads use a complete entry inside `staleCatalogSeconds` immediately and
316
- defer the refresh that read already demanded. Every live refresh is
317
- single-flight per connector in one runtime. A blocking operator or direct read
318
- joins an agent-owned refresh and awaits it; an agent stale read joins an
319
- operator-owned refresh without awaiting it. The first refresh owns the context
320
- and deadline. A deferred first refresh owns a fresh scope and signal, then
321
- closes that scope. Operator status and direct registry reads still await
322
- freshness. The operator page reports whether the last agent read in this runtime
323
- was fresh or stale; this payload-free timestamp is not persisted. No timer or
324
- idle warmup originates downstream traffic.
325
-
326
- Tool calls must use the shared invocation path. That keeps direct calls and code-mode host calls aligned on safety, retry hints, admission,
327
- timeouts, validation, result guards, and typed failures.
328
-
329
- That path also learns an observed output schema after a successful explicitly
330
- read-only call when the catalog declared none. The observation retains field
331
- names and broad JSON types rather than a result. Object fields stay optional,
332
- additional fields stay allowed, discovery labels the source `observed`, and any
333
- later provider declaration wins. Property names can be user-authored data, so
334
- the bounded cache stays in this process or Worker isolate and expires entries
335
- after 24 hours. An exact tool-definition comparison rejects stale shapes. This
336
- does not weaken the catalog completeness rule or the refusal of result sampling:
337
- no catalog read executes a tool.
338
-
339
- Connector usage guides are configuration too. `usageGuide` accepts the
340
- historical markdown string or `{ content, summary?, required? }`; the latter
341
- lets discovery explain what the guide covers without loading it. The summary
342
- is a 120-character routing hint; a longer configured value refuses construction
343
- instead of being silently shortened. Mark a guide `required` only when no complete
344
- tool schema can describe correct use, such as a generic operation wrapper or a
345
- mandatory cross-tool sequence. Mutations and truncated compact schemas already
346
- produce automatic review requirements. Two deployments may reuse the same
347
- constant and override its summary or requirement in their own config, but
348
- Connecta stores no runtime template and never lets one deployment's guide apply
349
- to another.
350
-
351
- For remote MCP tools, that path checks the catalog's advertised `inputSchema`
352
- before provider dispatch. Supported mismatches become bounded, payload-free
353
- `invalid_args` findings; a schema the local validator cannot evaluate passes
354
- through unchanged. Connecta does not parse provider error prose to invent a
355
- validation classification.
356
-
357
- ## Authentication
358
-
359
- OAuth-backed MCP connectors persist their registration and tokens through the
360
- connector-scoped storage context. Those values are bound to the authorization
361
- server issuer discovered and validated by the SDK; see
362
- [storage and credentials](./storage-and-credentials.md#downstream-oauth).
363
- The callback route validates `state` before passing the complete callback query
364
- to the SDK so RFC 9207 `iss` validation is not lost.
365
-
366
- A remote MCP connector that authenticates with a static key has two ways to
367
- receive one. `{ type: "headers", headers }` bakes the literal value into the
368
- deployment file, which suits a secret the runtime already holds.
369
- `{ type: "credential" }` declares the slot instead and lets an operator paste
370
- the key at the connection UI at `/`, where it is encrypted at rest and rotatable without
371
- a redeploy:
372
-
373
- ```ts
374
- remoteMcp("revenuecat_bepresent", {
375
- url: "https://mcp.revenuecat.ai/mcp",
376
- auth: {
377
- type: "credential",
378
- credential: { label: "API v2 secret key" },
379
- },
380
- });
381
- ```
382
-
383
- Header name and framing are configurable — `header` defaults to
384
- `Authorization`, `scheme` to `Bearer`, `scheme: null` sends the value bare, and
385
- a `Basic` framing base64-encodes a `user:secret` pair. Every maintained hosted
386
- connection takes the shape through its existing `auth` option and fills in its
387
- own label and framing. The full behavior, including rotation and the empty-slot
388
- state, is in
389
- [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
@@ -1,193 +0,0 @@
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
- access: "read-write",
15
- instructions: "File bugs into the Platform team unless the request names another.",
16
- });
17
- ```
18
-
19
- The `id` owns the ordinary connector namespaces; use a different id for every
20
- Linear workspace or access mode. `purpose` is required because an agent
21
- choosing between two instances needs to know which workspace answers the
22
- question. Workspace `instructions` are appended to the maintained guide and
23
- cannot change the connector's safety classification.
24
-
25
- ## Access modes
26
-
27
- Linear publishes two hosted endpoints, and `access` selects between them. It is
28
- **required**, with no default:
29
-
30
- | `access` | Endpoint | OAuth scopes |
31
- | --- | --- | --- |
32
- | `"read-write"` | `https://mcp.linear.app/mcp` | `read`, `write` |
33
- | `"read-only"` | `https://mcp.linear.app/mcp/readonly` | `read` |
34
-
35
- There is no safe default to pick. Defaulting to `"read-write"` hands a
36
- deployment write access it never asked for; defaulting to `"read-only"` turns a
37
- deployment that does write into one whose every write fails at Linear — at
38
- runtime, where no agent can repair it. So the operator declares which one this
39
- is, and a deployment that forgot fails at construction with a message naming
40
- both options.
41
-
42
- Read-only is not a client-side filter. The endpoint advertises the `read` scope
43
- alone, so the token minted for it cannot reach Linear's write APIs — a stronger
44
- guarantee than any annotation Connecta applies. A deployment that only reports
45
- on delivery should use it, and can run it beside a read-write instance under a
46
- different id:
47
-
48
- ```ts
49
- linear("delivery_reporting", {
50
- purpose: "Executive delivery reporting",
51
- access: "read-only",
52
- });
53
- ```
54
-
55
- The mode is legible at browse time, not only after the guide is fetched. A
56
- read-only connection titles itself `Linear (read-only)` unless the operator
57
- gives a `title`, and its guide opens with the access note rather than the
58
- workspace purpose — `search_tools` renders a connector's title and guide
59
- summary but never its description, and the summary is the guide's first content
60
- line.
61
-
62
- Linear's deprecated `/sse` transport is deliberately unreachable from this
63
- connection; it now answers 404.
64
-
65
- ## Authentication
66
-
67
- OAuth 2.1 with dynamic client registration is the default and keeps each
68
- connector instance's flow and tokens in its connector-scoped storage. Linear's
69
- MCP server also "supports passing OAuth token and API keys directly in the
70
- `Authorization: Bearer <yourtoken>` header instead of using the interactive
71
- authentication flow" ([Linear MCP docs](https://linear.app/docs/mcp)), which
72
- suits a headless deployment. Note the framing: the bare-`Authorization`
73
- convention is Linear's *GraphQL* API, and this endpoint is not that.
74
-
75
- ```ts
76
- linear("automation_tracker", {
77
- purpose: "Headless release reporting",
78
- access: "read-only",
79
- auth: {
80
- type: "headers",
81
- headers: { Authorization: `Bearer ${env.LINEAR_API_KEY}` },
82
- },
83
- });
84
- ```
85
-
86
- Keep that key in the runtime's secret store; it is a password, not ordinary
87
- configuration. A personal API key carries the acting user's full workspace
88
- permissions, so pair it with `access: "read-only"` unless the deployment
89
- genuinely writes.
90
-
91
- The same key can arrive from the connection UI at `/` instead, which is what a deployment
92
- with no secret store — or an operator who rotates keys without a redeploy —
93
- wants:
94
-
95
- ```ts
96
- linear("automation_tracker", {
97
- purpose: "Headless release reporting",
98
- access: "read-only",
99
- auth: { type: "credential" },
100
- });
101
- ```
102
-
103
- The slot renders as "Personal API key" and Connecta sends the stored value as
104
- `Authorization: Bearer <key>`, the framing Linear's MCP page documents; pass
105
- `credential` or `scheme` to override either. Until the operator saves a value
106
- the connector is present and reports `auth_required`. See
107
- [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
108
-
109
- ## Safety classification
110
-
111
- The wrapper classifies Linear's documented `list_*`, `get_*`, and
112
- `search_documentation` tools as reads, and its `save_*`, `create_*`, `delete_*`,
113
- `resolve_*`, `submit_*`, `merge_*`, `share_issue`, and `unshare_issue` tools as
114
- writes. Sharing changes an existing issue's access, so both halves are
115
- destructive. An unfamiliar tool the downstream leaves unannotated fails closed
116
- onto `call_destructive_tool` until a Connecta release reviews it.
117
-
118
- That classification fills in downstream silence and otherwise preserves
119
- explicit annotations. A tool on the read allowlist arriving with
120
- `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
121
- downstream said and stays behind `call_destructive_tool`. A tool on neither
122
- maintained list arriving with `readOnlyHint: true` keeps that too, and stays
123
- callable from `execute_code`. Both are the downstream telling you this
124
- release's allowlist is stale, and on a name no release has reviewed its word is
125
- the only evidence there is. One narrow fail-closed exception applies to a name
126
- this release reviewed and filed destructive: a `save_*` tool claiming
127
- `readOnlyHint: true` is a downstream bug rather than news, and stays on the
128
- approval path.
129
-
130
- One detail of Linear's own design shapes the classification: **`save_*` tools
131
- are upserts.** Omitting a record id creates; supplying one updates in place.
132
- Because an upsert can overwrite, every `save_*` is classified destructive even
133
- though some calls only create. The genuine creates are `create_issue_label` and
134
- `create_initiative_label`, plus the attachment upload tools, which assert
135
- `readOnlyHint: false` without claiming a destruction they do not perform.
136
-
137
- ## The catalog is not a fixed set
138
-
139
- Linear's hosted `tools/list` varies by workspace plan and enabled features:
140
- customer requests, releases, code review, templates, and explicit issue sharing
141
- do not appear in every workspace.
142
- The maintained allowlists are therefore a superset — a classified name a
143
- workspace never returns costs nothing, and a genuinely new tool fails closed.
144
- Agents should search this connector's catalog for what the workspace actually
145
- exposes rather than assuming a tool exists; the usage guide says so explicitly.
146
-
147
- ## Rate limits
148
-
149
- Linear documents no MCP-specific rate limit. The MCP server rides the
150
- [GraphQL API limits](https://linear.app/developers/rate-limiting), which are
151
- metered **per user per hour** and shared with everything else that credential
152
- does. Linear's own page is internally inconsistent on the API-key request
153
- figure — the prose says 5,000 requests per hour while the table below it says
154
- 2,500 for an API key and 5,000 for an OAuth app, against 600 unauthenticated —
155
- and limits are raised dynamically for workspace-level OAuth apps using Actor
156
- Authorization.
157
-
158
- For that reason this connection declares **no call-admission budget by
159
- default**. Connecta's counter is per runtime, not per user, so a hardcoded
160
- ceiling would either throttle a healthy deployment or fail to protect a busy
161
- one. An operator who knows their workspace can supply one explicitly:
162
-
163
- ```ts
164
- linear("product_tracker", {
165
- purpose: "Issue and project planning for the platform team",
166
- access: "read-write",
167
- callAdmission: {
168
- rules: [
169
- { budget: { kind: "rolling-window", maxCalls: 1_000, windowMs: 3_600_000 } },
170
- ],
171
- },
172
- });
173
- ```
174
-
175
- A budget-only rule needs no queue. If you add `maxConcurrency` you are asking
176
- for a queue, and the admission controller then requires the rest of the queue
177
- settings at construction.
178
-
179
- ## Public contract check
180
-
181
- `npm run drift:check -- --docs --provider linear` checks Linear's official MCP
182
- setup page for the read-write endpoint and OAuth support without using a
183
- credential. Linear does not publish an exact tool inventory there, so the
184
- command says `setup metadata only` and makes no claim about names or schemas.
185
- At runtime the live `tools/list` response remains the schema authority and is
186
- passed through without a vendored replacement.
187
-
188
- ## Conventions
189
-
190
- This connection is audited against
191
- [the provider conventions](./provider-conventions.md). Its verdict per
192
- convention, including every recorded exception, is the Linear section of
193
- [the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).