@zackbart/connecta 0.24.3 → 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.
- package/AGENTS.md +18 -20
- package/CHANGELOG.md +64 -1
- package/README.md +5 -6
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.d.ts +5 -1
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.js +8 -4
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -1
- package/dist/meta-tools.js +105 -29
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +12 -4
- package/dist/registry.js +22 -8
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -181
- package/documentation/auth.md +197 -176
- package/documentation/code-mode.md +426 -321
- package/documentation/meta-tools.md +356 -416
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
|
@@ -1,431 +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
|
-
An `unavailable` failure can carry `details.host` and `details.code` in its
|
|
192
|
-
classified error. `host` is an HTTP(S) origin, including a non-default port,
|
|
193
|
-
with no userinfo, path, query, or fragment and at most 253 UTF-8 bytes. `code`
|
|
194
|
-
is an allowlisted network errno, an undici transport code, or `timeout`, at
|
|
195
|
-
most 32 bytes. Invalid or oversized fields are omitted, never clipped.
|
|
196
|
-
`remoteMcp()` and the guarded transport know the failed fetch destination;
|
|
197
|
-
`api()` can classify a handler's structured runtime code but cannot recover
|
|
198
|
-
its destination from `ctx.baseUrl`, which names Connecta itself. A handler can
|
|
199
|
-
supply a known URL in a typed `ConnectorCallError` and the constructor reduces
|
|
200
|
-
it to an origin. AbortError and TimeoutError become diagnostic `timeout`;
|
|
201
|
-
workerd outbound denial supplies no errno, so a fetch boundary reports the
|
|
202
|
-
known origin alone. No provider prose supplies a code or retryability.
|
|
203
|
-
These diagnostics belong in tool failures and the bounded warning log, never
|
|
204
|
-
in payload-free activity records.
|
|
205
|
-
|
|
206
|
-
## The guarded fetch transport
|
|
207
|
-
|
|
208
|
-
Every hand-written HTTP surface re-derives the same safety machinery, and two
|
|
209
|
-
of them had already derived it slightly differently. `src/connectors/guarded-fetch.ts`
|
|
210
|
-
is that machinery extracted once ([#341](https://github.com/zackbart/connecta/issues/341)):
|
|
211
|
-
one `guardedFetch({ provider, baseUrl, headers, maxResponseBytes, authenticate })`
|
|
212
|
-
factory returning the transport a connector sends every request through.
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
const send = guardedFetch({
|
|
216
|
-
provider: "Billing",
|
|
217
|
-
baseUrl: "https://billing.internal.example/v1",
|
|
218
|
-
maxResponseBytes: 4 * 1024 * 1024,
|
|
219
|
-
headers: { Accept: "application/json" },
|
|
220
|
-
authenticate: async (ctx) => {
|
|
221
|
-
const token = await ctx.credential?.get();
|
|
222
|
-
if (!token) throw new ConnectorCallError("auth_required", "...");
|
|
223
|
-
return { Authorization: `Bearer ${token}` };
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const invoice = await send(
|
|
228
|
-
{ method: "GET", path: `/invoices/${id}` },
|
|
229
|
-
ctx,
|
|
230
|
-
(response) => {
|
|
231
|
-
if (!response.ok) throw billingFailure(response.status);
|
|
232
|
-
return response.json();
|
|
233
|
-
},
|
|
234
|
-
);
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
What it owns is mechanical and provider-independent:
|
|
238
|
-
|
|
239
|
-
- **Confinement.** A request path is provider-relative, carries no query or
|
|
240
|
-
fragment, and is re-checked against the base origin and path prefix *after*
|
|
241
|
-
`new URL` normalization — because normalization is how a path escapes a
|
|
242
|
-
prefix, not something to trust before it.
|
|
243
|
-
- **Construction.** Query parameters are encoded rather than concatenated, a
|
|
244
|
-
JSON body is serialized with the `Content-Type` to match, and a pre-framed
|
|
245
|
-
body gets none, so `fetch` still picks the multipart boundary.
|
|
246
|
-
- **Credential forwarding.** `authenticate` is called once per request and its
|
|
247
|
-
headers are applied last; a request header wearing one of their names is
|
|
248
|
-
refused rather than allowed to shadow it. A 3xx is refused outright — a
|
|
249
|
-
redirect is an instruction to re-send the credential to whatever origin the
|
|
250
|
-
`Location` names, and a confinement a redirect can undo was never one.
|
|
251
|
-
- **Bounded reads.** `maxResponseBytes` is required, not defaulted: what counts
|
|
252
|
-
as an absurd response is a fact about the API, not about HTTP. A declared
|
|
253
|
-
`Content-Length` past the ceiling fails before a byte is read, and a
|
|
254
|
-
streaming body is abandoned at the ceiling rather than buffered past it.
|
|
255
|
-
Without a body stream, text is measured in UTF-8 bytes before it is accepted;
|
|
256
|
-
JSON is parsed from that same bounded text. Such runtimes still buffer their
|
|
257
|
-
read internally, but cannot return an oversized body as a successful result.
|
|
258
|
-
- **Normalization.** An unreachable provider becomes a retryable `unavailable`
|
|
259
|
-
instead of whatever `TypeError` the runtime threw, and `ctx.signal` rides
|
|
260
|
-
every request. `Retry-After` hints accept delta-seconds and HTTP dates;
|
|
261
|
-
a date in the past means no further wait.
|
|
262
|
-
|
|
263
|
-
What it deliberately does not own is meaning. It never reads a status code and
|
|
264
|
-
never invents an authentication scheme: the provider's `authenticate` callback
|
|
265
|
-
supplies the headers, and the provider's mapper turns one `GuardedResponse`
|
|
266
|
-
into a result or a typed failure. That split is not fastidiousness. Notion's
|
|
267
|
-
403 means a capability the integration was never granted — re-authorizing
|
|
268
|
-
cannot fix it — while Cloudflare's means a token scope, and the two want
|
|
269
|
-
opposite next moves. A helper that guessed would be wrong for one of them.
|
|
270
|
-
|
|
271
|
-
Cloudflare, Notion, and Vercel run on it. The first two existing suites carried
|
|
272
|
-
over unchanged when the helper was extracted, which proves that migration kept
|
|
273
|
-
the behavior those suites cover — not that nothing changed. Three things did,
|
|
274
|
-
and the changelog names them: a 3xx is refused where both providers used to
|
|
275
|
-
follow it, both now fail past their byte ceiling, and `cloudflare()`'s
|
|
276
|
-
`baseUrl` is validated at construction. Each suite gained one test for the
|
|
277
|
-
ceiling, because the one
|
|
278
|
-
guard the helper was written to add is the one a provider's own mapper can
|
|
279
|
-
most easily disarm: a bare `catch` around `response.json()` swallows the
|
|
280
|
-
transport's refusal along with a parse error, and turns a response nobody was
|
|
281
|
-
allowed to read into an empty success. A mapper re-throws
|
|
282
|
-
`ConnectorCallError` and swallows only what it recognizes.
|
|
283
|
-
|
|
284
|
-
It is **not exported this release**: the two migrations proved the shape
|
|
285
|
-
preserves behavior for connectors that already had this machinery, not that it
|
|
286
|
-
is the right shape for an author starting from nothing, and an unexported
|
|
287
|
-
symbol costs nothing to reshape while a published one is a promise.
|
|
288
|
-
The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
|
|
289
|
-
supplies the third caller that would settle it.
|
|
290
|
-
|
|
291
|
-
## MCP version skew
|
|
292
|
-
|
|
293
|
-
Connecta deliberately sits between protocol generations
|
|
294
|
-
([full revision inventory](https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md)):
|
|
295
|
-
|
|
296
|
-
- **Inbound:** `/mcp` serves both the 2026-07-28 revision and legacy 2025
|
|
297
|
-
clients. Modern clients negotiate with `server/discover` and do not send
|
|
298
|
-
`initialize`; legacy clients retain their initialize flow. The endpoint
|
|
299
|
-
remains stateless in both cases.
|
|
300
|
-
- **Outbound:** `remoteMcp()` probes modern downstreams and falls back to the
|
|
301
|
-
byte-compatible legacy flow. Legacy downstreams are normal supported
|
|
302
|
-
deployments, not a temporary exception. Automatic negotiation remains the
|
|
303
|
-
default. A known-legacy server that crashes or returns a server error for the
|
|
304
|
-
pre-initialize probe can set `versionNegotiation: "legacy"` on that one
|
|
305
|
-
connector; Connecta then starts directly with `initialize` and never sends
|
|
306
|
-
`server/discover`. Keep the default unless the downstream requires this
|
|
307
|
-
compatibility concession, so modern protocol support is still discovered.
|
|
308
|
-
- **Legacy sessions:** Connecta's own endpoint creates no protocol session, but
|
|
309
|
-
a stateful legacy downstream can still issue `Mcp-Session-Id`. Closing a
|
|
310
|
-
request scope, rotating credentials, retiring an OAuth generation, and
|
|
311
|
-
abandoning a connect all send the legacy DELETE before closing the transport.
|
|
312
|
-
The DELETE gets at most one second and failures are logged. Rotation and
|
|
313
|
-
abandoned connects start cleanup without waiting; the connector context has
|
|
314
|
-
no deferred-work hook. Scope teardown gives its bounded tail back to core,
|
|
315
|
-
which can attach the runtime's deferred channel. SDK v2 `Client.close()` does
|
|
316
|
-
not send DELETE on Connecta's behalf.
|
|
317
|
-
- **Modern cache hints:** `tools/list` is deployment-fixed and returns a
|
|
318
|
-
one-hour private cache hint. Downstream hints do not alter Connecta's existing
|
|
319
|
-
five-minute fingerprinted catalog cache; that remains gated in
|
|
320
|
-
[#206](https://github.com/zackbart/connecta/issues/206).
|
|
321
|
-
- **Multi-round-trip results:** a downstream `input_required` result becomes a
|
|
322
|
-
non-retryable `input_required_unsupported` failure. `call_tool` and the
|
|
323
|
-
`execute_code` host bridge preserve the structured code. Relaying the
|
|
324
|
-
opaque `requestState` is architecturally possible but gated until real hosts
|
|
325
|
-
and downstreams adopt it.
|
|
326
|
-
|
|
327
|
-
The compatibility policy has no automatic sunset. Dropping a revision,
|
|
328
|
-
session cleanup, or cursor tolerance requires an explicit design decision and
|
|
329
|
-
replacement evidence.
|
|
330
|
-
|
|
331
|
-
## Catalog contract
|
|
332
|
-
|
|
333
|
-
A downstream catalog is complete or it is a failure. Follow every page until
|
|
334
|
-
the cursor ends, preserve schemas and annotations, and never cache or serve a
|
|
335
|
-
partial walk. The fixed TTL is paired with a schema fingerprint so a changed
|
|
336
|
-
catalog invalidates persisted results even within the time window.
|
|
337
|
-
|
|
338
|
-
Agent reads use a complete entry inside `staleCatalogSeconds` immediately and
|
|
339
|
-
defer the refresh that read already demanded. Every live refresh is
|
|
340
|
-
single-flight per connector in one runtime. A blocking operator or direct read
|
|
341
|
-
joins an agent-owned refresh and awaits it; an agent stale read joins an
|
|
342
|
-
operator-owned refresh without awaiting it. The first refresh owns the context
|
|
343
|
-
and deadline. A deferred first refresh owns a fresh scope and signal, then
|
|
344
|
-
closes that scope. Operator status and direct registry reads still await
|
|
345
|
-
freshness. The operator page reports whether the last agent read in this runtime
|
|
346
|
-
was fresh or stale; this payload-free timestamp is not persisted. No timer or
|
|
347
|
-
idle warmup originates downstream traffic.
|
|
348
|
-
|
|
349
|
-
Tool calls must use the shared invocation path. That keeps direct calls and code-mode host calls aligned on safety, retry hints, admission,
|
|
350
|
-
timeouts, validation, result guards, and typed failures.
|
|
351
|
-
|
|
352
|
-
That path also learns an observed output schema after a successful explicitly
|
|
353
|
-
read-only call when the catalog declared none. The observation retains field
|
|
354
|
-
names and broad JSON types rather than a result. Object fields stay optional,
|
|
355
|
-
additional fields stay allowed, discovery labels the source `observed`, and any
|
|
356
|
-
later provider declaration wins. Property names can be user-authored data, so
|
|
357
|
-
the bounded cache stays in this process or Worker isolate and expires entries
|
|
358
|
-
after 24 hours. An exact tool-definition comparison rejects stale shapes. This
|
|
359
|
-
does not weaken the catalog completeness rule or the refusal of result sampling:
|
|
360
|
-
no catalog read executes a tool.
|
|
361
|
-
|
|
362
|
-
Connector usage guides are configuration too. `usageGuide` accepts the
|
|
363
|
-
historical markdown string or `{ content, summary?, required? }`; the latter
|
|
364
|
-
lets discovery explain what the guide covers without loading it. The summary
|
|
365
|
-
is a 120-character routing hint; a longer configured value refuses construction
|
|
366
|
-
instead of being silently shortened. Mark a guide `required` only when no complete
|
|
367
|
-
tool schema can describe correct use, such as a generic operation wrapper or a
|
|
368
|
-
mandatory cross-tool sequence. Mutations and truncated compact schemas already
|
|
369
|
-
produce automatic review requirements. Two deployments may reuse the same
|
|
370
|
-
constant and override its summary or requirement in their own config, but
|
|
371
|
-
Connecta stores no runtime template and never lets one deployment's guide apply
|
|
372
|
-
to another.
|
|
373
|
-
|
|
374
|
-
For remote MCP tools, that path checks the catalog's advertised `inputSchema`
|
|
375
|
-
before provider dispatch. Supported mismatches become bounded, payload-free
|
|
376
|
-
`invalid_args` findings; a schema the local validator cannot evaluate passes
|
|
377
|
-
through unchanged. Connecta does not parse provider error prose to invent a
|
|
378
|
-
validation classification. A downstream JSON-RPC `-32602` is an explicit
|
|
379
|
-
`invalid_args` refusal, with a bounded server message and no retry. HTTP 4xx
|
|
380
|
-
refusals preserve a bounded JSON `message`, including `error.message`, and are
|
|
381
|
-
non-retryable; 429 becomes `rate_limited` and 408 becomes `timeout`. The SDK
|
|
382
|
-
still owns OAuth challenges and step-up authorization.
|
|
383
|
-
|
|
384
|
-
## Authentication
|
|
385
|
-
|
|
386
|
-
OAuth-backed MCP connectors persist their registration and tokens through the
|
|
387
|
-
connector-scoped storage context. Those values are bound to the authorization
|
|
388
|
-
server issuer discovered and validated by the SDK; see
|
|
389
|
-
[storage and credentials](./storage-and-credentials.md#downstream-oauth).
|
|
390
|
-
The callback route validates `state` before passing the complete callback query
|
|
391
|
-
to the SDK so RFC 9207 `iss` validation is not lost.
|
|
392
|
-
|
|
393
|
-
Within one runtime, overlapping refreshes share one completion gate per
|
|
394
|
-
credential partition and generation. A valid token response is a consumed
|
|
395
|
-
refresh token, so the coordinator keeps that response's tokens on the flight.
|
|
396
|
-
If the owner then fails before the SDK saves them — cancelled, redirected to
|
|
397
|
-
authorization, or invalidated — the host persists the rotation itself, holds
|
|
398
|
-
contenders behind the mutation marker until that write lands, and hands them
|
|
399
|
-
the saved rotation; a retired token is never redeemed twice. A write already
|
|
400
|
-
running in `saveTokens` clears the marker on its own success or failure, and a
|
|
401
|
-
late duplicate save from a detached owner is harmless. Nothing can leave a
|
|
402
|
-
permanent 503 gate ([#526](https://github.com/zackbart/connecta/issues/526)).
|
|
403
|
-
Refresh token response validation
|
|
404
|
-
reads at most 65,536 bytes, and credential reads stop after 64 revision races
|
|
405
|
-
with `temporarily_unavailable` so churn cannot spin indefinitely. Request
|
|
406
|
-
cancellation is checked before each attempt.
|
|
407
|
-
|
|
408
|
-
A remote MCP connector that authenticates with a static key has two ways to
|
|
409
|
-
receive one. `{ type: "headers", headers }` bakes the literal value into the
|
|
410
|
-
deployment file, which suits a secret the runtime already holds.
|
|
411
|
-
`{ type: "credential" }` declares the slot instead and lets an operator paste
|
|
412
|
-
the key at the connection UI at `/`, where it is encrypted at rest and rotatable without
|
|
413
|
-
a redeploy:
|
|
414
|
-
|
|
415
|
-
```ts
|
|
416
|
-
remoteMcp("revenuecat_bepresent", {
|
|
417
|
-
url: "https://mcp.revenuecat.ai/mcp",
|
|
418
|
-
auth: {
|
|
419
|
-
type: "credential",
|
|
420
|
-
credential: { label: "API v2 secret key" },
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
Header name and framing are configurable — `header` defaults to
|
|
426
|
-
`Authorization`, `scheme` to `Bearer`, `scheme: null` sends the value bare, and
|
|
427
|
-
a `Basic` framing base64-encodes a `user:secret` pair. Every maintained hosted
|
|
428
|
-
connection takes the shape through its existing `auth` option and fills in its
|
|
429
|
-
own label and framing. The full behavior, including rotation and the empty-slot
|
|
430
|
-
state, is in
|
|
431
|
-
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
package/documentation/linear.md
DELETED
|
@@ -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).
|