@zackbart/connecta 0.18.1 → 0.18.2
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 +53 -0
- package/dist/connectors/remote-mcp.d.ts +49 -2
- package/dist/connectors/remote-mcp.js +302 -7
- package/dist/providers/linear.d.ts +7 -1
- package/dist/providers/linear.js +12 -2
- package/dist/providers/mixpanel.d.ts +5 -1
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/revenuecat.d.ts +3 -1
- package/dist/providers/revenuecat.js +12 -3
- package/dist/providers/stripe.d.ts +9 -3
- package/dist/providers/stripe.js +24 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/connectors.md +25 -0
- package/documentation/linear.md +25 -4
- package/documentation/meta-tools.md +5 -0
- package/documentation/mixpanel.md +19 -0
- package/documentation/operations.md +1 -0
- package/documentation/provider-conventions.md +37 -21
- package/documentation/revenuecat.md +23 -1
- package/documentation/storage-and-credentials.md +55 -0
- package/documentation/stripe.md +22 -2
- package/documentation/upgrading.md +21 -4
- package/examples/worker/src/index.ts +5 -0
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.18.2 — 2026-08-18
|
|
6
|
+
|
|
7
|
+
This patch closes the gap the RevenueCat rollout exposed on the same day 0.18.1
|
|
8
|
+
shipped: a hosted-MCP connection could authenticate with OAuth from the
|
|
9
|
+
Connections page or with a header literal baked into the deployment file, but
|
|
10
|
+
never with a key an operator pastes on `/credentials` the way every `api()`
|
|
11
|
+
connector already can. Now it can. Nothing breaks, no constructor changes, and a
|
|
12
|
+
deployment that keeps its static keys in runtime secrets can ignore this release.
|
|
13
|
+
Two things are worth reading before adopting the new shape: the operator vault
|
|
14
|
+
(`credentials.encryptionKey`) must be configured, and a key pasted with a
|
|
15
|
+
wrapped newline is refused before it is framed rather than echoed back by the
|
|
16
|
+
runtime that rejects it.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`auth: { type: "credential" }` for `remoteMcp()` and every maintained
|
|
21
|
+
hosted connection.** The connector declares an operator credential slot on
|
|
22
|
+
`/credentials` (label and guidance from the provider, overridable), reads the
|
|
23
|
+
stored value on every request before trusting a cached client, and frames it
|
|
24
|
+
as `Authorization: Bearer <value>` by default — `header` and `scheme` are
|
|
25
|
+
configurable, `scheme: null` sends the value bare, and a scheme ending in
|
|
26
|
+
`Basic` base64-encodes it, which is how Mixpanel's documented
|
|
27
|
+
`Bearer Basic <base64(user:secret)>` is spelled. A missing or empty value is
|
|
28
|
+
`auth_required`, and `authorize_connector` returns the operator handoff to
|
|
29
|
+
`/credentials`; a deployment without a vault warns at construction and answers
|
|
30
|
+
`recovery: "unavailable"` at use. Rotation on `/credentials` takes effect on
|
|
31
|
+
the next call: a SHA-256 digest of the connected value is compared per
|
|
32
|
+
request (including against an in-flight connect) and a differing digest
|
|
33
|
+
closes the old client. The `/credentials` Test action connects with the
|
|
34
|
+
candidate and counts the catalog, and only when an operator presses it — the
|
|
35
|
+
#179 refusal of proactive probing stands, now written into P10. Provider
|
|
36
|
+
defaults: Stripe (requires `mode`, refuses `connectedAccount`), Linear
|
|
37
|
+
(Bearer, per Linear's MCP docs), Mixpanel (`Bearer Basic`, service account),
|
|
38
|
+
RevenueCat (single-project branch, "API v2 secret key") (#439).
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **A stored credential never reaches an agent or operator surface, even
|
|
43
|
+
malformed.** The value is refused before framing if it carries a control
|
|
44
|
+
character (the wrapped-newline paste), with a message that names the problem
|
|
45
|
+
and not the value; behind that, any error whose message or `cause` chain
|
|
46
|
+
quotes the raw or framed value is replaced whole rather than redacted. Both
|
|
47
|
+
layers are tested independently (#439).
|
|
48
|
+
|
|
49
|
+
- **Provider conventions P9 and P10 say what is now true.** P9 names both
|
|
50
|
+
headless shapes and requires the framing to match the provider's published
|
|
51
|
+
MCP contract; P10 is retitled to allow an operator-requested credential test
|
|
52
|
+
while forbidding any unasked probe (#439).
|
|
53
|
+
|
|
54
|
+
- **Linear's headers example uses `Bearer`.** Linear's MCP server documents
|
|
55
|
+
`Authorization: Bearer <token>` for API keys; the bare form is its GraphQL
|
|
56
|
+
convention. Existing `headers` connectors are untouched (#439).
|
|
57
|
+
|
|
5
58
|
## 0.18.1 — 2026-08-18
|
|
6
59
|
|
|
7
60
|
This patch is the response to one long investigation run against a live
|
|
@@ -1,12 +1,58 @@
|
|
|
1
1
|
import type { FetchLike, Transport } from "@modelcontextprotocol/client";
|
|
2
2
|
import { ConnectorCallError } from "../errors.js";
|
|
3
|
-
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorUsageGuide, Logger } from "../types.js";
|
|
3
|
+
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* A static downstream credential the operator supplies at `/credentials`
|
|
6
|
+
* rather than the deployment baking into its source.
|
|
7
|
+
*
|
|
8
|
+
* The connector, its endpoint, and the credential *slot* stay declared in
|
|
9
|
+
* code; only the secret arrives through the operator route, exactly as for
|
|
10
|
+
* `api()`. One reserved `value` field, deliberately: a header is assembled
|
|
11
|
+
* from a name, a framing scheme, and one secret, and anything that needs two
|
|
12
|
+
* secrets composed into one header is a provider integration, not a proxy
|
|
13
|
+
* config ([#439](https://github.com/zackbart/connecta/issues/439)).
|
|
14
|
+
*/
|
|
15
|
+
interface RemoteMcpCredentialAuth {
|
|
16
|
+
type: "credential";
|
|
17
|
+
/**
|
|
18
|
+
* Operator-facing slot description rendered on `/credentials`. Defaults to
|
|
19
|
+
* `{ label: "API key" }`; a maintained provider passes the name the provider
|
|
20
|
+
* itself uses. Named `fields` are refused — this shape reads the reserved
|
|
21
|
+
* `value` field only.
|
|
22
|
+
*/
|
|
23
|
+
credential?: ConnectorCredentialConfig;
|
|
24
|
+
/** Header the credential rides. Defaults to `Authorization`. */
|
|
25
|
+
header?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Framing token placed before the value. Defaults to `"Bearer"`. `null` (or
|
|
28
|
+
* an empty string) sends the stored value verbatim, for an endpoint that
|
|
29
|
+
* reads a bare key. A scheme whose last token is `Basic` declares
|
|
30
|
+
* HTTP Basic credentials: the stored `user:secret` is base64-encoded first,
|
|
31
|
+
* so `"Basic"` produces `Basic <base64>` and Mixpanel's documented
|
|
32
|
+
* `"Bearer Basic"` produces `Bearer Basic <base64>`.
|
|
33
|
+
*/
|
|
34
|
+
scheme?: string | null;
|
|
35
|
+
}
|
|
4
36
|
export type RemoteMcpAuth = {
|
|
5
37
|
type: "headers";
|
|
6
38
|
headers: Record<string, string>;
|
|
7
|
-
} | {
|
|
39
|
+
} | RemoteMcpCredentialAuth | {
|
|
8
40
|
type: "oauth";
|
|
9
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Apply a maintained provider's slot copy and header framing to credential
|
|
44
|
+
* auth the deployment left bare.
|
|
45
|
+
*
|
|
46
|
+
* A provider knows what its own key is called and how the endpoint expects it
|
|
47
|
+
* framed; a deployment that states either one keeps its answer. Every other
|
|
48
|
+
* auth shape passes through untouched, so a provider can hand this its whole
|
|
49
|
+
* `auth` option without branching first.
|
|
50
|
+
*/
|
|
51
|
+
export declare function withCredentialDefaults(auth: RemoteMcpAuth, defaults: {
|
|
52
|
+
credential: ConnectorCredentialConfig;
|
|
53
|
+
/** Provider framing; omit to leave the `Bearer` default in place. */
|
|
54
|
+
scheme?: string | null;
|
|
55
|
+
}): RemoteMcpAuth;
|
|
10
56
|
export type RemoteMcpRedirectPolicy = "none" | "same-origin";
|
|
11
57
|
export interface RemoteMcpOptions {
|
|
12
58
|
url: string;
|
|
@@ -90,3 +136,4 @@ export declare function redirectSafeFetch(connectorId: string, policy?: RemoteMc
|
|
|
90
136
|
* server or hide other connectors).
|
|
91
137
|
*/
|
|
92
138
|
export declare function remoteMcp(id: string, opts: RemoteMcpOptions): Connector;
|
|
139
|
+
export {};
|
|
@@ -3,6 +3,26 @@ import { KvOAuthProvider } from "../auth/downstream-oauth.js";
|
|
|
3
3
|
import { MAX_CATALOG_TOOLS } from "../catalog-limits.js";
|
|
4
4
|
import { ConnectorCallError } from "../errors.js";
|
|
5
5
|
import { CONNECTA_VERSION } from "../version.js";
|
|
6
|
+
/**
|
|
7
|
+
* Apply a maintained provider's slot copy and header framing to credential
|
|
8
|
+
* auth the deployment left bare.
|
|
9
|
+
*
|
|
10
|
+
* A provider knows what its own key is called and how the endpoint expects it
|
|
11
|
+
* framed; a deployment that states either one keeps its answer. Every other
|
|
12
|
+
* auth shape passes through untouched, so a provider can hand this its whole
|
|
13
|
+
* `auth` option without branching first.
|
|
14
|
+
*/
|
|
15
|
+
export function withCredentialDefaults(auth, defaults) {
|
|
16
|
+
if (auth.type !== "credential")
|
|
17
|
+
return auth;
|
|
18
|
+
return {
|
|
19
|
+
...auth,
|
|
20
|
+
credential: auth.credential ?? defaults.credential,
|
|
21
|
+
...(auth.scheme === undefined && defaults.scheme !== undefined
|
|
22
|
+
? { scheme: defaults.scheme }
|
|
23
|
+
: {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* How long a downstream gets to answer the session-termination DELETE before
|
|
8
28
|
* teardown stops waiting. This is a network round-trip budget, deliberately
|
|
@@ -160,6 +180,65 @@ async function terminateSession(transport, logger, connectorId) {
|
|
|
160
180
|
});
|
|
161
181
|
});
|
|
162
182
|
}
|
|
183
|
+
const encoder = new TextEncoder();
|
|
184
|
+
/** Base64 of a UTF-8 string, Web-API only so the core still runs on workerd. */
|
|
185
|
+
function base64Utf8(value) {
|
|
186
|
+
let binary = "";
|
|
187
|
+
for (const byte of encoder.encode(value)) {
|
|
188
|
+
binary += String.fromCharCode(byte);
|
|
189
|
+
}
|
|
190
|
+
return btoa(binary);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Hex SHA-256 of a credential, used only to notice that a cached client
|
|
194
|
+
* connected with a value the vault no longer holds. WebCrypto rather than a
|
|
195
|
+
* `node:` hash: the whole core has to keep running unchanged on Workers.
|
|
196
|
+
*/
|
|
197
|
+
async function digestOf(value) {
|
|
198
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
199
|
+
let hex = "";
|
|
200
|
+
for (const byte of bytes)
|
|
201
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
202
|
+
return hex;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Assemble the one header a credential-auth connector sends.
|
|
206
|
+
*
|
|
207
|
+
* A scheme ending in `Basic` names HTTP Basic credentials wherever a provider
|
|
208
|
+
* nests it, so the `user:secret` it frames is base64-encoded — that is what
|
|
209
|
+
* makes plain `Basic` and Mixpanel's `Bearer Basic` one rule instead of two.
|
|
210
|
+
*/
|
|
211
|
+
function credentialHeaderValue(scheme, value) {
|
|
212
|
+
if (scheme === null)
|
|
213
|
+
return value;
|
|
214
|
+
return /(?:^|\s)basic$/i.test(scheme)
|
|
215
|
+
? `${scheme} ${base64Utf8(value)}`
|
|
216
|
+
: `${scheme} ${value}`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* True when a stored credential carries a character a header cannot.
|
|
220
|
+
*
|
|
221
|
+
* The fetch specification refuses NUL, CR, and LF outright, and the runtime
|
|
222
|
+
* that refuses them says so in a `TypeError` that quotes the whole offending
|
|
223
|
+
* value — a message that would otherwise travel to the agent. The remaining C0
|
|
224
|
+
* controls and DEL are refused here too: no real API key contains one, and a
|
|
225
|
+
* paste that picked one up is a paste to redo rather than a request to send.
|
|
226
|
+
* Leading and trailing whitespace is already gone by the time this runs.
|
|
227
|
+
*
|
|
228
|
+
* A scan rather than a regular expression, because a character class over the
|
|
229
|
+
* control range is exactly what `no-control-regex` exists to flag, and the
|
|
230
|
+
* suppression would be less readable than the loop it suppressed.
|
|
231
|
+
*/
|
|
232
|
+
function carriesIllegalHeaderChar(value) {
|
|
233
|
+
for (let index = 0; index < value.length; index++) {
|
|
234
|
+
const code = value.charCodeAt(index);
|
|
235
|
+
if (code <= 0x1f || code === 0x7f)
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
/** RFC 9110 field-name token, checked once at construction. */
|
|
241
|
+
const HEADER_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
163
242
|
function isLoopbackHost(hostname) {
|
|
164
243
|
return (hostname === "localhost" ||
|
|
165
244
|
hostname === "127.0.0.1" ||
|
|
@@ -273,6 +352,29 @@ export function remoteMcp(id, opts) {
|
|
|
273
352
|
const closedScopes = new WeakSet();
|
|
274
353
|
const isOauth = opts.auth?.type === "oauth";
|
|
275
354
|
const logger = opts.logger ?? console;
|
|
355
|
+
const credentialAuth = opts.auth?.type === "credential" ? opts.auth : undefined;
|
|
356
|
+
if (credentialAuth?.credential?.fields?.length) {
|
|
357
|
+
throw new Error(`[connecta] connector "${id}" credential auth declares named fields; ` +
|
|
358
|
+
"this shape sends one secret in one header and reads the reserved " +
|
|
359
|
+
"`value` field only.");
|
|
360
|
+
}
|
|
361
|
+
const credentialConfig = credentialAuth?.credential ?? {
|
|
362
|
+
label: "API key",
|
|
363
|
+
};
|
|
364
|
+
const credentialHeader = credentialAuth?.header?.trim() || "Authorization";
|
|
365
|
+
// A structural mistake in the deployment file, beside the `fields` refusal
|
|
366
|
+
// above: an unsendable header name is not worth discovering on the first
|
|
367
|
+
// request, where only a runtime error can report it.
|
|
368
|
+
if (credentialAuth && !HEADER_NAME_TOKEN.test(credentialHeader)) {
|
|
369
|
+
throw new Error(`[connecta] connector "${id}" credential auth declares header ` +
|
|
370
|
+
`"${credentialHeader}", which is not a valid HTTP field name.`);
|
|
371
|
+
}
|
|
372
|
+
// `undefined` means "not stated" and takes the bearer default; `null` and
|
|
373
|
+
// `""` both mean "send the stored value verbatim", which some providers
|
|
374
|
+
// require for a bare API key.
|
|
375
|
+
const credentialScheme = credentialAuth === undefined || credentialAuth.scheme === undefined
|
|
376
|
+
? "Bearer"
|
|
377
|
+
: (credentialAuth.scheme?.trim() ?? "") || null;
|
|
276
378
|
// Check the destination scheme once at construction: buildTransport (and the
|
|
277
379
|
// SDK's fetch) attach any static credentials to every request, so an http://
|
|
278
380
|
// endpoint sends bearer tokens / API keys in cleartext. Loopback is exempt
|
|
@@ -283,7 +385,10 @@ export function remoteMcp(id, opts) {
|
|
|
283
385
|
if (opts.requireHttps) {
|
|
284
386
|
throw new Error(`[connecta] connector "${id}" url ${opts.url} is not https:// (and not loopback) — refusing to connect (requireHttps).`);
|
|
285
387
|
}
|
|
286
|
-
|
|
388
|
+
// Both static shapes send a secret on every request; that an operator
|
|
389
|
+
// typed one into /credentials rather than a deployment file changes who
|
|
390
|
+
// owns it, not whether the wire carries it in the clear.
|
|
391
|
+
if (opts.auth?.type === "headers" || credentialAuth) {
|
|
287
392
|
logger.warn(`[connecta] connector "${id}" sends static credentials to ${opts.url} over a non-https:// connection — those tokens will be transmitted in cleartext.`);
|
|
288
393
|
}
|
|
289
394
|
}
|
|
@@ -295,6 +400,77 @@ export function remoteMcp(id, opts) {
|
|
|
295
400
|
}
|
|
296
401
|
}
|
|
297
402
|
const operatorDisconnectedError = () => new OperatorDisconnectedError();
|
|
403
|
+
/**
|
|
404
|
+
* The credential slot is declared but the vault has nothing in it (or has no
|
|
405
|
+
* key to read it with). Deliberately the same `auth_required` code an absent
|
|
406
|
+
* OAuth grant produces: the agent's next move is `authorize_connector`
|
|
407
|
+
* either way, and that tool reads `connector.credential` to hand the
|
|
408
|
+
* operator `/credentials` instead of a consent URL.
|
|
409
|
+
*/
|
|
410
|
+
class CredentialRequiredError extends ConnectorCallError {
|
|
411
|
+
constructor(message) {
|
|
412
|
+
super("auth_required", message);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Read this request's credential. Called before every connect and before
|
|
417
|
+
* trusting any cached client, so an operator's replacement is picked up
|
|
418
|
+
* without a redeploy. The value stays in the caller's local scope.
|
|
419
|
+
*/
|
|
420
|
+
const readCredential = async (ctx) => {
|
|
421
|
+
if (!ctx.credential) {
|
|
422
|
+
throw new CredentialRequiredError(`Connector "${id}" needs an operator-managed credential, but ` +
|
|
423
|
+
"credential storage is not configured. Set " +
|
|
424
|
+
"credentials.encryptionKey and redeploy.");
|
|
425
|
+
}
|
|
426
|
+
// A stored-shape mismatch already arrives as a typed auth_required from
|
|
427
|
+
// the registry's accessor; nothing to reclassify here.
|
|
428
|
+
const value = (await ctx.credential.get())?.trim();
|
|
429
|
+
if (!value) {
|
|
430
|
+
throw new CredentialRequiredError(`Connector "${id}" has no stored credential — call ` +
|
|
431
|
+
`authorize_connector({ connector: "${id}" }) and follow the ` +
|
|
432
|
+
"operator handoff it returns.");
|
|
433
|
+
}
|
|
434
|
+
// Refuse a value a header cannot carry BEFORE anything frames it. A
|
|
435
|
+
// wrapped newline in a pasted key is the ordinary way this happens, and
|
|
436
|
+
// the runtime that rejects the header quotes the whole value back in its
|
|
437
|
+
// TypeError — a message that travels to status, to the activity log, and
|
|
438
|
+
// to the agent. So the check lives here, and says only what is wrong.
|
|
439
|
+
if (carriesIllegalHeaderChar(value)) {
|
|
440
|
+
throw new CredentialRequiredError(`Connector "${id}"'s stored credential contains a character a header ` +
|
|
441
|
+
"cannot carry (a line break or other control character) — re-enter " +
|
|
442
|
+
"it on /credentials. The value is not shown or logged.");
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Replace, never edit.
|
|
448
|
+
*
|
|
449
|
+
* Any error whose message quotes the credential — the raw value or the
|
|
450
|
+
* framed header it becomes — is discarded whole and replaced with a fixed
|
|
451
|
+
* sentence. Nothing is substringed, masked, or truncated out of the original:
|
|
452
|
+
* a redaction that keeps part of a secret is still a leak, and the original
|
|
453
|
+
* error is not worth one. This is defense in depth behind the validation
|
|
454
|
+
* above, which is what keeps an unsendable value from reaching a transport
|
|
455
|
+
* at all.
|
|
456
|
+
*/
|
|
457
|
+
const withoutCredential = (err, ...secrets) => {
|
|
458
|
+
const quoted = secrets.filter((secret) => typeof secret === "string" && secret !== "");
|
|
459
|
+
if (quoted.length === 0)
|
|
460
|
+
return err;
|
|
461
|
+
const seen = new Set();
|
|
462
|
+
let current = err;
|
|
463
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
464
|
+
seen.add(current);
|
|
465
|
+
if (quoted.some((secret) => current instanceof Error && current.message.includes(secret))) {
|
|
466
|
+
return new CredentialRequiredError(`Connector "${id}" could not send its stored credential as a ` +
|
|
467
|
+
"header — re-enter it on /credentials. The value is not shown or " +
|
|
468
|
+
"logged.");
|
|
469
|
+
}
|
|
470
|
+
current = current.cause;
|
|
471
|
+
}
|
|
472
|
+
return err;
|
|
473
|
+
};
|
|
298
474
|
const scopeEndedError = () => new Error(`Connector "${id}" scope ended during connection.`);
|
|
299
475
|
const requestOptions = (ctx) => ctx.timeoutMs || ctx.signal
|
|
300
476
|
? {
|
|
@@ -334,6 +510,7 @@ export function remoteMcp(id, opts) {
|
|
|
334
510
|
authRequired: false,
|
|
335
511
|
provider: null,
|
|
336
512
|
connectedGeneration: null,
|
|
513
|
+
credentialDigest: null,
|
|
337
514
|
closed: false,
|
|
338
515
|
};
|
|
339
516
|
states.set(scope, state);
|
|
@@ -345,7 +522,13 @@ export function remoteMcp(id, opts) {
|
|
|
345
522
|
return state.provider;
|
|
346
523
|
};
|
|
347
524
|
const newProvider = (ctx) => new KvOAuthProvider(id, ctx.storage, `${ctx.baseUrl}/oauth/callback/${id}`);
|
|
348
|
-
const buildTransport = (ctx, provider
|
|
525
|
+
const buildTransport = (ctx, provider,
|
|
526
|
+
/**
|
|
527
|
+
* This attempt's assembled header value, for credential auth only — framed
|
|
528
|
+
* by the caller so the raw secret is not passed around twice. Never
|
|
529
|
+
* retained past the transport it configures.
|
|
530
|
+
*/
|
|
531
|
+
credentialFramed = null) => {
|
|
349
532
|
if (opts._transportFactory)
|
|
350
533
|
return opts._transportFactory(ctx);
|
|
351
534
|
const url = new URL(opts.url);
|
|
@@ -356,7 +539,11 @@ export function remoteMcp(id, opts) {
|
|
|
356
539
|
fetch: guardedFetch,
|
|
357
540
|
});
|
|
358
541
|
}
|
|
359
|
-
const headers = opts.auth?.type === "headers"
|
|
542
|
+
const headers = opts.auth?.type === "headers"
|
|
543
|
+
? opts.auth.headers
|
|
544
|
+
: credentialAuth && credentialFramed !== null
|
|
545
|
+
? { [credentialHeader]: credentialFramed }
|
|
546
|
+
: undefined;
|
|
360
547
|
return new StreamableHTTPClientTransport(url, {
|
|
361
548
|
...(headers ? { requestInit: { headers } } : {}),
|
|
362
549
|
fetch: guardedFetch,
|
|
@@ -370,6 +557,7 @@ export function remoteMcp(id, opts) {
|
|
|
370
557
|
state.authRequired = false;
|
|
371
558
|
state.provider = null;
|
|
372
559
|
state.connectedGeneration = null;
|
|
560
|
+
state.credentialDigest = null;
|
|
373
561
|
// `closed` is deliberately not cleared — see ConnectionState.
|
|
374
562
|
};
|
|
375
563
|
const ensureConnected = async (ctx, state) => {
|
|
@@ -415,6 +603,43 @@ export function remoteMcp(id, opts) {
|
|
|
415
603
|
reset(state);
|
|
416
604
|
}
|
|
417
605
|
}
|
|
606
|
+
// The static-credential counterpart of the epoch read above, and
|
|
607
|
+
// deliberately beside it: the vault is read before any cached client is
|
|
608
|
+
// trusted, so an operator's rotation on /credentials takes effect on the
|
|
609
|
+
// next call rather than the next deploy. Compared by digest — the
|
|
610
|
+
// plaintext lives in this function's scope and never reaches `state`.
|
|
611
|
+
let credentialValue = null;
|
|
612
|
+
let credentialFramed = null;
|
|
613
|
+
let credentialDigest = null;
|
|
614
|
+
if (credentialAuth) {
|
|
615
|
+
credentialValue = await readCredential(ctx);
|
|
616
|
+
credentialFramed = credentialHeaderValue(credentialScheme, credentialValue);
|
|
617
|
+
credentialDigest = await digestOf(credentialValue);
|
|
618
|
+
// Gated on a connect in flight as well as a cached client, exactly like
|
|
619
|
+
// the epoch read above: a rotation that lands while the first caller is
|
|
620
|
+
// still connecting must not let the second one ride the key the vault
|
|
621
|
+
// has already replaced.
|
|
622
|
+
if ((state.client || state.connecting) &&
|
|
623
|
+
state.credentialDigest !== null &&
|
|
624
|
+
state.credentialDigest !== credentialDigest) {
|
|
625
|
+
if (state.closed)
|
|
626
|
+
throw scopeEndedError();
|
|
627
|
+
const connecting = state.connecting;
|
|
628
|
+
const client = state.client;
|
|
629
|
+
const transport = state.transport;
|
|
630
|
+
reset(state);
|
|
631
|
+
void connecting?.catch(() => { });
|
|
632
|
+
try {
|
|
633
|
+
if (client)
|
|
634
|
+
await client.close();
|
|
635
|
+
else
|
|
636
|
+
await transport?.close();
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// The rotated-away client is discarded either way.
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
418
643
|
if (state.closed)
|
|
419
644
|
throw scopeEndedError();
|
|
420
645
|
if (state.client)
|
|
@@ -458,7 +683,7 @@ export function remoteMcp(id, opts) {
|
|
|
458
683
|
// below as one structured, non-retryable connector failure.
|
|
459
684
|
inputRequired: { autoFulfill: false },
|
|
460
685
|
});
|
|
461
|
-
const t = buildTransport(ctx, provider);
|
|
686
|
+
const t = buildTransport(ctx, provider, credentialFramed);
|
|
462
687
|
if (!ownsAttempt())
|
|
463
688
|
await abandon(t);
|
|
464
689
|
state.transport = t;
|
|
@@ -506,7 +731,11 @@ export function remoteMcp(id, opts) {
|
|
|
506
731
|
if (err instanceof UnauthorizedError) {
|
|
507
732
|
throw authRequiredError(err);
|
|
508
733
|
}
|
|
509
|
-
|
|
734
|
+
// Defense in depth for the one error class that can quote the
|
|
735
|
+
// credential: a runtime refusing the assembled header. `readCredential`
|
|
736
|
+
// already rejects a value that cannot ride one, so reaching this is a
|
|
737
|
+
// gap in that check rather than a routine outcome.
|
|
738
|
+
throw withoutCredential(err, credentialValue, credentialFramed);
|
|
510
739
|
}
|
|
511
740
|
finally {
|
|
512
741
|
// Force reset may have abandoned this attempt and installed a new one
|
|
@@ -517,6 +746,10 @@ export function remoteMcp(id, opts) {
|
|
|
517
746
|
}
|
|
518
747
|
})();
|
|
519
748
|
state.connecting = attempt;
|
|
749
|
+
// Published with the attempt, not with its result: a rotation that lands
|
|
750
|
+
// while this connect is in flight has to be visible to the next caller,
|
|
751
|
+
// which would otherwise wait on a client bound to the older key.
|
|
752
|
+
state.credentialDigest = credentialDigest;
|
|
520
753
|
}
|
|
521
754
|
return state.connecting;
|
|
522
755
|
};
|
|
@@ -562,6 +795,54 @@ export function remoteMcp(id, opts) {
|
|
|
562
795
|
? { callAdmission: opts.callAdmission }
|
|
563
796
|
: {}),
|
|
564
797
|
...(opts.usageGuide !== undefined ? { usageGuide: opts.usageGuide } : {}),
|
|
798
|
+
// Declaring the slot is what makes the rest of the operator surface work:
|
|
799
|
+
// /credentials renders it, the shape check compares against it, and
|
|
800
|
+
// authorize_connector reads it to return the operator handoff rather than
|
|
801
|
+
// an OAuth URL this connector has none of.
|
|
802
|
+
...(credentialAuth
|
|
803
|
+
? {
|
|
804
|
+
credential: credentialConfig,
|
|
805
|
+
/**
|
|
806
|
+
* The honest test for a proxy is the catalog: connect with the
|
|
807
|
+
* stored value and count what the downstream serves. Nothing else
|
|
808
|
+
* here is connecta's to verify — the credential's scope, project,
|
|
809
|
+
* and mode are the provider's answer, not ours.
|
|
810
|
+
*/
|
|
811
|
+
testCredential: async (value, ctx) => {
|
|
812
|
+
try {
|
|
813
|
+
// The connect below reads the vault itself — the header is
|
|
814
|
+
// assembled deep inside `ensureConnected`, and handing a
|
|
815
|
+
// candidate down that path would mean threading a second secret
|
|
816
|
+
// through the whole connection state. `/ui/credentials/<id>/test`
|
|
817
|
+
// reads the stored value and passes it here, so today the two are
|
|
818
|
+
// the same string. Check rather than assume: a route that later
|
|
819
|
+
// tested an unsaved candidate would otherwise silently report on
|
|
820
|
+
// the old value, which is the one answer worse than refusing.
|
|
821
|
+
const stored = (await ctx.credential?.get())?.trim();
|
|
822
|
+
if (stored !== value.trim()) {
|
|
823
|
+
return {
|
|
824
|
+
ok: false,
|
|
825
|
+
message: "This connector tests the credential that is currently " +
|
|
826
|
+
"saved. Save the value first, then test it.",
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
const tools = await connector.listTools(ctx);
|
|
830
|
+
return {
|
|
831
|
+
ok: true,
|
|
832
|
+
message: `Connected — the downstream served ${tools.length} tool${tools.length === 1 ? "" : "s"}.`,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
return { ok: false, message: msg(err) };
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
// A test owns the scope it just opened; leaving the session for
|
|
840
|
+
// the downstream to age out is not this button's to spend.
|
|
841
|
+
await connector.closeScope?.(ctx);
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
}
|
|
845
|
+
: {}),
|
|
565
846
|
// `tools/list` is cursor-paginated: the server chooses the page size and
|
|
566
847
|
// signals "there is more" with a `nextCursor`, which the SDK's
|
|
567
848
|
// Client.listTools() returns without following. Collect the whole chain
|
|
@@ -744,6 +1025,7 @@ export function remoteMcp(id, opts) {
|
|
|
744
1025
|
state.connecting = null;
|
|
745
1026
|
state.authRequired = false;
|
|
746
1027
|
state.connectedGeneration = null;
|
|
1028
|
+
state.credentialDigest = null;
|
|
747
1029
|
// Ask the downstream to drop its session first — closing only aborts our
|
|
748
1030
|
// side, and the DELETE that frees the server's rides on the very
|
|
749
1031
|
// AbortSignal the close is about to trip.
|
|
@@ -766,12 +1048,25 @@ export function remoteMcp(id, opts) {
|
|
|
766
1048
|
return { state: "ok" };
|
|
767
1049
|
}
|
|
768
1050
|
catch (err) {
|
|
1051
|
+
// An empty slot, or one with no vault behind it, is reported the way a
|
|
1052
|
+
// missing grant is: present, unauthenticated, and repairable — never a
|
|
1053
|
+
// boot failure and never a silently absent connector.
|
|
1054
|
+
if (err instanceof CredentialRequiredError) {
|
|
1055
|
+
return { state: "auth_required", message: err.message };
|
|
1056
|
+
}
|
|
769
1057
|
if (state.authRequired) {
|
|
770
|
-
|
|
1058
|
+
// Only an OAuth connector has a pending consent URL to offer. A
|
|
1059
|
+
// credential connector's downstream 401 is repaired on /credentials,
|
|
1060
|
+
// so do not reach into OAuth storage to look for one.
|
|
1061
|
+
const url = isOauth
|
|
1062
|
+
? await getProvider(ctx, state).pendingAuthorizationUrl()
|
|
1063
|
+
: undefined;
|
|
771
1064
|
return {
|
|
772
1065
|
state: "auth_required",
|
|
773
1066
|
...(url !== undefined ? { authorizationUrl: url } : {}),
|
|
774
|
-
message:
|
|
1067
|
+
message: credentialAuth
|
|
1068
|
+
? "Authorization required — the downstream rejected this connector's stored credential."
|
|
1069
|
+
: "Authorization required — open the URL to connect.",
|
|
775
1070
|
};
|
|
776
1071
|
}
|
|
777
1072
|
if (err instanceof OperatorDisconnectedError) {
|
|
@@ -32,7 +32,13 @@ export interface LinearOptions {
|
|
|
32
32
|
* ([#342](https://github.com/zackbart/connecta/issues/342)).
|
|
33
33
|
*/
|
|
34
34
|
access: LinearAccess;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* OAuth by default. A Linear personal API key works either as a literal
|
|
37
|
+
* header or as an operator-managed credential (`{ type: "credential" }`).
|
|
38
|
+
* Linear's MCP documentation asks for `Authorization: Bearer <yourtoken>`
|
|
39
|
+
* for both API keys and OAuth tokens (https://linear.app/docs/mcp), which is
|
|
40
|
+
* the framing default, so the credential shape needs no `scheme` of its own.
|
|
41
|
+
*/
|
|
36
42
|
auth?: RemoteMcpAuth;
|
|
37
43
|
/** Workspace-specific conventions appended to the maintained provider guide. */
|
|
38
44
|
instructions?: string;
|
package/dist/providers/linear.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remoteMcp, } from "../connectors/remote-mcp.js";
|
|
1
|
+
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
3
|
/**
|
|
4
4
|
* Linear publishes two hosted endpoints. `read-only` is not a client-side
|
|
@@ -189,7 +189,17 @@ export function linear(id, options) {
|
|
|
189
189
|
description: access === "read-only"
|
|
190
190
|
? `Linear issue tracking and project planning (read-only) — ${purpose}`
|
|
191
191
|
: `Linear issue tracking and project planning — ${purpose}`,
|
|
192
|
-
|
|
192
|
+
// Linear's MCP endpoint takes an API key the same way it takes an OAuth
|
|
193
|
+
// token — `Authorization: Bearer <yourtoken>` — so only the slot copy is
|
|
194
|
+
// provider-specific and the bearer framing default stands. The bare-header
|
|
195
|
+
// convention belongs to Linear's GraphQL API, not to this endpoint.
|
|
196
|
+
auth: withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
197
|
+
credential: {
|
|
198
|
+
label: "Personal API key",
|
|
199
|
+
description: "A Linear personal API key. It carries the issuing user's full workspace access and is stored encrypted; the read-only endpoint still limits what it can reach.",
|
|
200
|
+
placeholder: "lin_api_…",
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
193
203
|
requireHttps: true,
|
|
194
204
|
usageGuide: {
|
|
195
205
|
content: usageGuide(purpose, access, options.instructions),
|
|
@@ -18,7 +18,11 @@ export interface MixpanelOptions {
|
|
|
18
18
|
* rather than a convenient one.
|
|
19
19
|
*/
|
|
20
20
|
region?: MixpanelRegion;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* OAuth by default; static headers support Mixpanel service accounts, and
|
|
23
|
+
* `{ type: "credential" }` takes the same service account as an
|
|
24
|
+
* operator-managed `username:secret` that Connecta frames for the endpoint.
|
|
25
|
+
*/
|
|
22
26
|
auth?: RemoteMcpAuth;
|
|
23
27
|
/** Account-specific conventions appended to the maintained provider guide. */
|
|
24
28
|
instructions?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remoteMcp, } from "../connectors/remote-mcp.js";
|
|
1
|
+
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
3
|
export const MIXPANEL_MCP_ENDPOINTS = {
|
|
4
4
|
us: "https://mcp.mixpanel.com/mcp",
|
|
@@ -214,7 +214,18 @@ export function mixpanel(id, options) {
|
|
|
214
214
|
// an agent must not get wrong between two Mixpanel connections.
|
|
215
215
|
title: options.title ?? `Mixpanel (${region})`,
|
|
216
216
|
description: `Mixpanel product analytics (${REGION_COPY[region]} residency) — ${purpose}`,
|
|
217
|
-
|
|
217
|
+
// Mixpanel's beta service-account scheme is deliberately not ordinary HTTP
|
|
218
|
+
// Basic: the endpoint wants `Bearer Basic <base64(user:secret)>`. The
|
|
219
|
+
// operator therefore pastes the pair, not an encoded blob, and Connecta
|
|
220
|
+
// does the framing — the same shaping the maintainer drift check applies.
|
|
221
|
+
auth: withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
222
|
+
credential: {
|
|
223
|
+
label: "Service account",
|
|
224
|
+
description: "A Mixpanel service account as `username:secret`. Connecta encodes and frames it the way the hosted endpoint requires; it is stored encrypted and never displayed.",
|
|
225
|
+
placeholder: "username:secret",
|
|
226
|
+
},
|
|
227
|
+
scheme: "Bearer Basic",
|
|
228
|
+
}),
|
|
218
229
|
requireHttps: true,
|
|
219
230
|
usageGuide: {
|
|
220
231
|
content: usageGuide(purpose, region, options.instructions),
|
|
@@ -21,7 +21,9 @@ export interface RevenueCatOptions {
|
|
|
21
21
|
purpose: string;
|
|
22
22
|
/**
|
|
23
23
|
* OAuth by default; static headers support a RevenueCat API v2 secret key
|
|
24
|
-
* as `Authorization: Bearer sk_…`.
|
|
24
|
+
* as `Authorization: Bearer sk_…`. `{ type: "credential" }` is the same
|
|
25
|
+
* single-project scope with the key pasted at `/credentials` instead — which
|
|
26
|
+
* is what two projects on two keys wants, since each is its own connector.
|
|
25
27
|
*/
|
|
26
28
|
auth?: RemoteMcpAuth;
|
|
27
29
|
/** Project-specific conventions appended to the maintained provider guide. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remoteMcp, } from "../connectors/remote-mcp.js";
|
|
1
|
+
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
3
|
/** RevenueCat publishes one hosted MCP endpoint, streamable HTTP. */
|
|
4
4
|
export const REVENUECAT_MCP_ENDPOINT = "https://mcp.revenuecat.ai/mcp";
|
|
@@ -274,8 +274,17 @@ export function revenuecat(id, options) {
|
|
|
274
274
|
if (!purpose) {
|
|
275
275
|
throw new Error("revenuecat() requires a non-empty project purpose.");
|
|
276
276
|
}
|
|
277
|
-
const auth = options.auth ?? { type: "oauth" }
|
|
278
|
-
|
|
277
|
+
const auth = withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
278
|
+
credential: {
|
|
279
|
+
label: "API v2 secret key",
|
|
280
|
+
description: "A RevenueCat API v2 secret key. It reaches exactly one project, which is why two projects are two connectors; it is stored encrypted and never displayed.",
|
|
281
|
+
placeholder: "sk_…",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
// Both static shapes reach one project. Where the key came from — the
|
|
285
|
+
// deployment file or the operator page — changes nothing an agent must know
|
|
286
|
+
// about scope, so the title, description, and guide follow the scope alone.
|
|
287
|
+
const scoped = auth.type !== "oauth";
|
|
279
288
|
const connector = remoteMcp(id, {
|
|
280
289
|
url: REVENUECAT_MCP_ENDPOINT,
|
|
281
290
|
// The scope shape rides the title because browse-time discovery renders
|
|
@@ -22,10 +22,16 @@ export interface StripeOAuthOptions extends StripeCommonOptions {
|
|
|
22
22
|
mode?: never;
|
|
23
23
|
connectedAccount?: never;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Static credentials have one fixed mode, including Stripe Connect calls.
|
|
27
|
+
*
|
|
28
|
+
* Both static shapes belong here: a key the deployment supplies as a literal
|
|
29
|
+
* header, and one the operator pastes at `/credentials`. Neither can discover
|
|
30
|
+
* its own mode — a restricted key answers for exactly one — so both declare it.
|
|
31
|
+
*/
|
|
26
32
|
export interface StripeHeaderOptions extends StripeCommonOptions {
|
|
27
|
-
auth:
|
|
28
|
-
type: "
|
|
33
|
+
auth: Exclude<RemoteMcpAuth, {
|
|
34
|
+
type: "oauth";
|
|
29
35
|
}>;
|
|
30
36
|
mode: StripeMode;
|
|
31
37
|
/** Act as one Connect account by sending Stripe's `Stripe-Account` header. */
|
package/dist/providers/stripe.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remoteMcp, } from "../connectors/remote-mcp.js";
|
|
1
|
+
import { remoteMcp, withCredentialDefaults, } from "../connectors/remote-mcp.js";
|
|
2
2
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
3
3
|
/** Stripe publishes one hosted MCP endpoint for every account and mode. */
|
|
4
4
|
export const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
|
|
@@ -109,17 +109,31 @@ function assertModeMatchesKey(id, mode, auth) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
function resolveAuth(id, options) {
|
|
112
|
-
const auth = options.auth ?? { type: "oauth" }
|
|
112
|
+
const auth = withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
113
|
+
credential: {
|
|
114
|
+
label: "Secret or restricted API key",
|
|
115
|
+
description: "A Stripe secret or restricted API key for this connector's declared mode. Stripe sends it as a bearer token; it is stored encrypted and never displayed.",
|
|
116
|
+
placeholder: "sk_… or rk_…",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
113
119
|
const connectedAccount = options.connectedAccount?.trim();
|
|
114
120
|
if (connectedAccount === undefined || connectedAccount === "")
|
|
115
121
|
return auth;
|
|
116
122
|
if (!connectedAccount.startsWith("acct_")) {
|
|
117
123
|
throw new Error(`stripe("${id}") connectedAccount must be a Stripe account id ("acct_...").`);
|
|
118
124
|
}
|
|
119
|
-
if (auth.type
|
|
125
|
+
if (auth.type === "oauth") {
|
|
120
126
|
throw new Error(`stripe("${id}") cannot reach a connected account over OAuth; Stripe ` +
|
|
121
127
|
`requires a restricted API key for Stripe-Account calls.`);
|
|
122
128
|
}
|
|
129
|
+
if (auth.type === "credential") {
|
|
130
|
+
// `Stripe-Account` is a second header beside the credential's own, and the
|
|
131
|
+
// credential shape assembles exactly one. A Connect connector therefore
|
|
132
|
+
// still takes its restricted key as a literal header.
|
|
133
|
+
throw new Error(`stripe("${id}") cannot reach a connected account with an ` +
|
|
134
|
+
`operator-managed credential; Stripe-Account is a second static ` +
|
|
135
|
+
`header, so declare auth: { type: "headers" } for this connector.`);
|
|
136
|
+
}
|
|
123
137
|
return {
|
|
124
138
|
type: "headers",
|
|
125
139
|
headers: { ...auth.headers, "Stripe-Account": connectedAccount },
|
|
@@ -199,9 +213,14 @@ export function stripe(id, options) {
|
|
|
199
213
|
if (auth.type === "oauth" && mode !== undefined) {
|
|
200
214
|
throw new Error(`stripe("${id}") cannot declare a connector-wide mode for OAuth; Stripe returns mode with each account.`);
|
|
201
215
|
}
|
|
202
|
-
if (auth.type
|
|
203
|
-
throw new Error(`stripe("${id}") with headers auth requires mode
|
|
216
|
+
if (auth.type !== "oauth" && mode !== "production" && mode !== "sandbox") {
|
|
217
|
+
throw new Error(`stripe("${id}") with headers or credential auth requires mode ` +
|
|
218
|
+
`"production" or "sandbox".`);
|
|
204
219
|
}
|
|
220
|
+
// Only a literal header can be inspected. An operator-managed credential is
|
|
221
|
+
// not readable at construction — there is nothing in the deployment file to
|
|
222
|
+
// read — so the declared mode stands alone, and a key pointed at the other
|
|
223
|
+
// one is Stripe's own refusal to report.
|
|
205
224
|
if (auth.type === "headers") {
|
|
206
225
|
assertModeMatchesKey(id, mode, auth);
|
|
207
226
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -294,3 +294,28 @@ server issuer discovered and validated by the SDK; see
|
|
|
294
294
|
[storage and credentials](./storage-and-credentials.md#downstream-oauth).
|
|
295
295
|
The callback route validates `state` before passing the complete callback query
|
|
296
296
|
to the SDK so RFC 9207 `iss` validation is not lost.
|
|
297
|
+
|
|
298
|
+
A remote MCP connector that authenticates with a static key has two ways to
|
|
299
|
+
receive one. `{ type: "headers", headers }` bakes the literal value into the
|
|
300
|
+
deployment file, which suits a secret the runtime already holds.
|
|
301
|
+
`{ type: "credential" }` declares the slot instead and lets an operator paste
|
|
302
|
+
the key at `/credentials`, where it is encrypted at rest and rotatable without
|
|
303
|
+
a redeploy:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
remoteMcp("revenuecat_bepresent", {
|
|
307
|
+
url: "https://mcp.revenuecat.ai/mcp",
|
|
308
|
+
auth: {
|
|
309
|
+
type: "credential",
|
|
310
|
+
credential: { label: "API v2 secret key" },
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Header name and framing are configurable — `header` defaults to
|
|
316
|
+
`Authorization`, `scheme` to `Bearer`, `scheme: null` sends the value bare, and
|
|
317
|
+
a `Basic` framing base64-encodes a `user:secret` pair. Every maintained hosted
|
|
318
|
+
connection takes the shape through its existing `auth` option and fills in its
|
|
319
|
+
own label and framing. The full behavior, including rotation and the empty-slot
|
|
320
|
+
state, is in
|
|
321
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
package/documentation/linear.md
CHANGED
|
@@ -65,9 +65,12 @@ connection; it now answers 404.
|
|
|
65
65
|
## Authentication
|
|
66
66
|
|
|
67
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
|
|
69
|
-
also
|
|
70
|
-
`Authorization
|
|
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.
|
|
71
74
|
|
|
72
75
|
```ts
|
|
73
76
|
linear("automation_tracker", {
|
|
@@ -75,7 +78,7 @@ linear("automation_tracker", {
|
|
|
75
78
|
access: "read-only",
|
|
76
79
|
auth: {
|
|
77
80
|
type: "headers",
|
|
78
|
-
headers: { Authorization: env.LINEAR_API_KEY },
|
|
81
|
+
headers: { Authorization: `Bearer ${env.LINEAR_API_KEY}` },
|
|
79
82
|
},
|
|
80
83
|
});
|
|
81
84
|
```
|
|
@@ -85,6 +88,24 @@ configuration. A personal API key carries the acting user's full workspace
|
|
|
85
88
|
permissions, so pair it with `access: "read-only"` unless the deployment
|
|
86
89
|
genuinely writes.
|
|
87
90
|
|
|
91
|
+
The same key can arrive from `/credentials` 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
|
+
|
|
88
109
|
## Safety classification
|
|
89
110
|
|
|
90
111
|
The wrapper classifies Linear's documented `list_*`, `get_*`, and
|
|
@@ -310,6 +310,11 @@ handoff:
|
|
|
310
310
|
declared credential label and field names/guidance; or
|
|
311
311
|
- `unavailable`: an honest deployment/configuration message.
|
|
312
312
|
|
|
313
|
+
The class follows what the connector declares, not how it was authored: a
|
|
314
|
+
`remoteMcp()` connection using `auth: { type: "credential" }` declares a slot
|
|
315
|
+
and no OAuth flow, so it lands in `operator_config` beside every `api()`
|
|
316
|
+
credential.
|
|
317
|
+
|
|
313
318
|
The tool accepts no secret. `force` applies only to OAuth and may discard its
|
|
314
319
|
stored grant before restarting consent. Static credential values are written
|
|
315
320
|
only through the same-origin, Clerk-operator credential route. After OAuth
|
|
@@ -50,6 +50,25 @@ password, not ordinary configuration. Mixpanel currently labels service-account
|
|
|
50
50
|
MCP authentication beta. Prefer OAuth unless the deployment is intentionally
|
|
51
51
|
headless.
|
|
52
52
|
|
|
53
|
+
The same service account can arrive from `/credentials` instead, and there the
|
|
54
|
+
operator pastes the readable pair rather than an encoded blob:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
mixpanel("automation_analytics", {
|
|
58
|
+
purpose: "Headless release-health reporting",
|
|
59
|
+
auth: { type: "credential" },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The slot renders as "Service account" and takes `username:secret`. Connecta
|
|
64
|
+
base64-encodes it and sends Mixpanel's documented `Bearer Basic` framing, so the
|
|
65
|
+
operator never has to encode anything by hand. **The two paths take different
|
|
66
|
+
strings:** the `headers` example above wants the already-encoded blob
|
|
67
|
+
(`echo -n "username:secret" | base64`), and this one wants the plaintext pair.
|
|
68
|
+
Migrating from one to the other means decoding, not copying. Until a value is
|
|
69
|
+
saved the connector is present and reports `auth_required`. See
|
|
70
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
71
|
+
|
|
53
72
|
## Conditional input contracts
|
|
54
73
|
|
|
55
74
|
Mixpanel's hosted descriptions enforce three cross-field conditions that its
|
|
@@ -247,6 +247,7 @@ in.
|
|
|
247
247
|
| `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), their compact discovery schemas staying complete (H7), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
|
|
248
248
|
| `registry.test.ts` | construction and id validation, startup warnings, address resolution, catalog TTL/persistence/completeness, agent-only stale-while-revalidate with cross-request single-flight shared with blocking reads in both start orders, owned teardown, invalidation/fingerprint guards, blocking diagnostics, and broken-connector isolation |
|
|
249
249
|
| `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
|
|
250
|
+
| `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from `/credentials`: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, `lastError`, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
|
|
250
251
|
| `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
|
|
251
252
|
| `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
|
|
252
253
|
| `revenuecat-provider.test.ts` / `revenuecat-registry.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot`; then two project-scoped keys as two connectors in a real deployment |
|
|
@@ -412,33 +412,49 @@ retries.
|
|
|
412
412
|
|
|
413
413
|
OAuth per connector instance, stored in connector-scoped storage, is the
|
|
414
414
|
default. The provider's own headless credential — a personal API key, a
|
|
415
|
-
restricted key, a service account — is supported
|
|
416
|
-
auth, documented as a secret rather than configuration, and
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
415
|
+
restricted key, a service account — is supported two ways: explicit `headers`
|
|
416
|
+
auth, documented as a secret rather than configuration, and `{ type:
|
|
417
|
+
"credential" }`, which declares an operator slot and takes the same secret from
|
|
418
|
+
`/credentials` instead. Either way it is paired with the narrowest mode the
|
|
419
|
+
deployment can use, and the framing matches the provider's *published* contract
|
|
420
|
+
for the MCP endpoint — not a convention borrowed from that provider's other
|
|
421
|
+
APIs, and not this repository's earlier example, which is the same claim wearing
|
|
422
|
+
a circle. `requireHttps` is set. Recovery from an expired authorization is the
|
|
423
|
+
ordinary `auth_required` → `authorize_connector` route, which returns the
|
|
424
|
+
consent URL for OAuth and the `/credentials` handoff for a declared slot.
|
|
420
425
|
|
|
421
426
|
*Why:* one route back from an expired credential is what keeps a failed call
|
|
422
427
|
from becoming an abandoned task. *Cost:* wrong-tool selection.
|
|
423
428
|
|
|
424
|
-
### P10 —
|
|
429
|
+
### P10 — Nothing probes a credential unasked; a declared slot may be tested on request
|
|
425
430
|
|
|
426
|
-
A proxy declares
|
|
427
|
-
|
|
428
|
-
|
|
431
|
+
A proxy declares an operator credential slot exactly when its auth is `{ type:
|
|
432
|
+
"credential" }`, and then it inherits H12 whole
|
|
433
|
+
([#439](https://github.com/zackbart/connecta/issues/439)). The other two shapes
|
|
434
|
+
declare no slot and hold nothing for the credentials page: OAuth lives in
|
|
429
435
|
connector-scoped storage and is exercised by the authorization flow itself,
|
|
430
|
-
while a
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
`authorize_connector` route attached (P9).
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
436
|
+
while a `headers` key arrives as deployment configuration. H12 is owed in every
|
|
437
|
+
shape, and a proxy pays it in two places that do not depend on a slot:
|
|
438
|
+
construction throws when a recognizable credential contradicts the declared mode
|
|
439
|
+
(P4) — a check a vault-managed key cannot get, because there is nothing in the
|
|
440
|
+
deployment file to read — and a dead, revoked, or absent credential fails loudly
|
|
441
|
+
at use as `auth_required` with the `authorize_connector` route attached (P9).
|
|
442
|
+
|
|
443
|
+
`testCredential` exists only behind the operator-pressed Test action on
|
|
444
|
+
`/credentials`, and only for a declared slot. It connects with the stored value
|
|
445
|
+
and reports how many tools the downstream served, which is the whole honest
|
|
446
|
+
check for a proxy: which account, project, or mode a key reaches is the
|
|
447
|
+
provider's answer, not Connecta's. That is not the shape
|
|
448
|
+
[#179](https://github.com/zackbart/connecta/issues/179) removed. What was
|
|
449
|
+
removed is the *unasked* probe — a liveness call every deployment pays on a
|
|
450
|
+
schedule or at startup to answer a question only a misconfigured one has. A
|
|
451
|
+
human clicking Test has asked, `api()` has had that button since the vault
|
|
452
|
+
existed, and nothing here probes on its own: no timer, no warmup, no check on
|
|
453
|
+
the read path.
|
|
439
454
|
|
|
440
455
|
*Why:* an unasked-for liveness probe spends a call on every deployment to answer
|
|
441
|
-
a question only a misconfigured one has
|
|
456
|
+
a question only a misconfigured one has; a requested one spends a call the
|
|
457
|
+
person requesting it chose. *Cost:* result size.
|
|
442
458
|
|
|
443
459
|
### P11 — Connecta classifies the transport; the downstream owns the tool error
|
|
444
460
|
|
|
@@ -666,8 +682,8 @@ than by reading:
|
|
|
666
682
|
| P4 | endpoint or mode option exists, with the documented default (or no default, where none is safe) |
|
|
667
683
|
| P5 | reads and writes are named lists; an unlisted tool resolves to not-read-only; a reviewed destructive name beats a contradictory `readOnlyHint: true` |
|
|
668
684
|
| P6, P8 | the guide contains the catalog-varies note and the id-resolution rule |
|
|
669
|
-
| P9 | `auth` defaults to OAuth and `requireHttps` is set |
|
|
670
|
-
| P10 |
|
|
685
|
+
| P9 | `auth` defaults to OAuth and `requireHttps` is set; a credential-auth shape frames the key the way the provider's MCP documentation does |
|
|
686
|
+
| P10 | a `credential` slot exactly when auth is `{ type: "credential" }`; `testCredential` runs only from the operator's Test action, never on a timer or a read path; the mode/key contradiction still throws at construction |
|
|
671
687
|
| P11 | an authorization failure surfaces as `auth_required`; a downstream tool error is returned unchanged, with no code chosen from its prose |
|
|
672
688
|
| P12 | a declared budget matches a citable documented limit, or the absence is justified in the guide |
|
|
673
689
|
| P13 | classification lists are maintained in one place per provider and built into the manifest the wrapper classifies from, so the drift check compares against the same fact the caller is served |
|
|
@@ -80,6 +80,26 @@ connectors: [
|
|
|
80
80
|
]
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
Neither key has to be a runtime secret. Declare the slot instead and each
|
|
84
|
+
connector's key is pasted, tested, and rotated on `/credentials`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
connectors: [
|
|
88
|
+
revenuecat("bepresent_ios", {
|
|
89
|
+
purpose: "Subscription state for the BePresent iOS project",
|
|
90
|
+
auth: { type: "credential", credential: { label: "API v2 secret key" } },
|
|
91
|
+
}),
|
|
92
|
+
revenuecat("biblescroll", {
|
|
93
|
+
purpose: "Subscription state for the BibleScroll project",
|
|
94
|
+
auth: { type: "credential", credential: { label: "API v2 secret key" } },
|
|
95
|
+
}),
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Two ids, two slots, two single-project catalogs — the `credential` option is
|
|
100
|
+
optional, and omitting it gives the same "API v2 secret key" label. See
|
|
101
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
102
|
+
|
|
83
103
|
That is config-as-code doing what an account model would otherwise do: one
|
|
84
104
|
credential per connector, each with its own catalog, storage namespace, health,
|
|
85
105
|
and admission counters. The two share a title, because Connecta cannot know
|
|
@@ -116,7 +136,9 @@ Keys are prefixed `sk_`, are issued read-only or write-enabled, and can be
|
|
|
116
136
|
revoked at any time by a project Admin. RevenueCat's setup guidance is to "use
|
|
117
137
|
a write-enabled key if you plan to create/modify resources"; "a read-only key
|
|
118
138
|
works if you only need to view data". Keep the key in the runtime's secret
|
|
119
|
-
store, never in the deployment file
|
|
139
|
+
store, never in the deployment file — or declare
|
|
140
|
+
`auth: { type: "credential" }` and let the operator hold it in the vault
|
|
141
|
+
instead, which is the shape the two-project example above uses.
|
|
120
142
|
|
|
121
143
|
**Connecta does not filter writes for a read-only key.** It has no way to tell
|
|
122
144
|
which kind a key is without spending a call, so every write in the catalog is
|
|
@@ -34,6 +34,61 @@ Credential mutation is intentionally narrower than MCP access:
|
|
|
34
34
|
The vault is read for each call. Once an operator saves a replacement,
|
|
35
35
|
the agent can retry immediately without restarting or redeploying Connecta.
|
|
36
36
|
|
|
37
|
+
## A remote MCP connector's static credential
|
|
38
|
+
|
|
39
|
+
`remoteMcp()` accepts a third auth shape beside OAuth and literal headers:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
remoteMcp("revenuecat_bepresent", {
|
|
43
|
+
url: "https://mcp.revenuecat.ai/mcp",
|
|
44
|
+
auth: { type: "credential", credential: { label: "API v2 secret key" } },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The connector, its endpoint, and the credential *slot* stay declared in code;
|
|
49
|
+
only the secret arrives through `/credentials`. That is the same boundary
|
|
50
|
+
`api()` has always had, and the reason a project-wide key no longer has to be a
|
|
51
|
+
Worker secret or an environment variable
|
|
52
|
+
([#439](https://github.com/zackbart/connecta/issues/439)).
|
|
53
|
+
|
|
54
|
+
`header` defaults to `Authorization` and `scheme` to `Bearer`. `scheme: null`
|
|
55
|
+
sends the stored value verbatim, which is what Linear's personal API keys
|
|
56
|
+
expect. A scheme whose last token is `Basic` declares HTTP Basic credentials, so
|
|
57
|
+
the stored `user:secret` is base64-encoded first — `"Basic"` produces
|
|
58
|
+
`Basic <base64>`, and Mixpanel's documented `"Bearer Basic"` produces
|
|
59
|
+
`Bearer Basic <base64>`. There is one reserved `value` field and no multi-field
|
|
60
|
+
header composition: named `credential.fields` are refused at construction.
|
|
61
|
+
|
|
62
|
+
A stored value is checked before anything frames it: a line break or other
|
|
63
|
+
control character — what a key pasted across two lines leaves behind — is
|
|
64
|
+
refused as `auth_required` with a message naming the problem and never the
|
|
65
|
+
value. That check exists because the runtime that rejects such a header quotes
|
|
66
|
+
the whole offending value back in its `TypeError`, and that message would
|
|
67
|
+
otherwise reach the agent, the operator page, and the activity log. Behind it,
|
|
68
|
+
any error whose message quotes the credential or the header it became is
|
|
69
|
+
discarded whole and replaced; nothing is masked or truncated, because a
|
|
70
|
+
redaction that keeps part of a secret is still a leak.
|
|
71
|
+
|
|
72
|
+
An empty slot is not a boot failure and not a silently absent connector. The
|
|
73
|
+
connector is present, its status reads `auth_required`, calls fail with the same
|
|
74
|
+
typed error a missing OAuth grant produces, and `authorize_connector` returns
|
|
75
|
+
the `/credentials` handoff. With no vault configured at all, the failure names
|
|
76
|
+
`credentials.encryptionKey`, and Connecta already warned at startup.
|
|
77
|
+
|
|
78
|
+
The vault is read before any cached downstream client is trusted, so a rotation
|
|
79
|
+
lands on the next call rather than the next deploy. Connecta compares a SHA-256
|
|
80
|
+
digest of the value the cached client connected with; a different digest closes
|
|
81
|
+
that client and reconnects. The plaintext lives in the connect attempt's local
|
|
82
|
+
scope, never on connector state, never in a log, and never in a status or error
|
|
83
|
+
message. A cleartext `http://` destination warns at construction here exactly as
|
|
84
|
+
it does for literal headers — who owns the secret changed, not what the wire
|
|
85
|
+
carries.
|
|
86
|
+
|
|
87
|
+
`/credentials`' Test action connects with the stored value and reports how many
|
|
88
|
+
tools the downstream served. That is the whole honest check for a proxy: which
|
|
89
|
+
account, project, or mode the key reaches is the provider's answer, not
|
|
90
|
+
Connecta's.
|
|
91
|
+
|
|
37
92
|
## Downstream OAuth
|
|
38
93
|
|
|
39
94
|
`remoteMcp()` stores dynamic client registration, tokens, PKCE material, state,
|
package/documentation/stripe.md
CHANGED
|
@@ -95,12 +95,32 @@ Use a restricted key, not a secret key, and scope it to the operations the
|
|
|
95
95
|
agent actually needs; Stripe's own guidance is to "limit your agent's access to
|
|
96
96
|
exactly the functionality it requires". Keep it in the runtime's secret store.
|
|
97
97
|
|
|
98
|
+
The same key can come from `/credentials` instead:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
stripe("stripe_sandbox", {
|
|
102
|
+
mode: "sandbox",
|
|
103
|
+
purpose: "Automated billing rehearsal",
|
|
104
|
+
auth: { type: "credential" },
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`mode` is required either way — a static key answers for exactly one
|
|
109
|
+
environment and cannot report which. The literal-header form is checked against
|
|
110
|
+
the key's `_live_`/`_test_` prefix at construction; an operator-managed key is
|
|
111
|
+
not in the deployment file to read, so the declared mode stands alone and a key
|
|
112
|
+
pointed at the other environment fails at Stripe. Declare the mode carefully:
|
|
113
|
+
that check is the one guard Connecta can offer, and this shape does not get it.
|
|
114
|
+
See
|
|
115
|
+
[storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
|
|
116
|
+
|
|
98
117
|
Organization accounts in one OAuth session are not Stripe Connect connected
|
|
99
118
|
accounts. Connect platforms can act as a connected account with
|
|
100
119
|
`connectedAccount`, which adds Stripe's documented `Stripe-Account` header at
|
|
101
120
|
connector construction. Stripe does not support OAuth for connected-account
|
|
102
|
-
calls,
|
|
103
|
-
|
|
121
|
+
calls, and `Stripe-Account` is a second header beside the credential's own,
|
|
122
|
+
which the operator-managed shape does not assemble — so this requires a
|
|
123
|
+
restricted key through `headers` auth and throws otherwise:
|
|
104
124
|
|
|
105
125
|
```ts
|
|
106
126
|
stripe("merchant_42", {
|
|
@@ -57,7 +57,7 @@ exist so far:
|
|
|
57
57
|
| --- | --- | --- |
|
|
58
58
|
| **pre-template** | before 0.10.2 | no `connecta init` existed; hand-written, or copied from the retired `examples/node` |
|
|
59
59
|
| **A** | 0.10.2 – 0.15.1 | `.env.example`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `src/index.ts`, `tsconfig.json` |
|
|
60
|
-
| **B** | 0.16.0 – 0.18.
|
|
60
|
+
| **B** | 0.16.0 – 0.18.2 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
|
|
61
61
|
|
|
62
62
|
Generation A is a decade in template years and identifying it precisely does
|
|
63
63
|
not matter, because you are about to reconstruct it exactly rather than guess
|
|
@@ -106,7 +106,7 @@ know what to preserve, once to know what to re-verify at the end.
|
|
|
106
106
|
### Bump the pin and install
|
|
107
107
|
|
|
108
108
|
```sh
|
|
109
|
-
npm pkg set dependencies.@zackbart/connecta=0.18.
|
|
109
|
+
npm pkg set dependencies.@zackbart/connecta=0.18.2
|
|
110
110
|
npm install
|
|
111
111
|
```
|
|
112
112
|
|
|
@@ -130,7 +130,7 @@ Generate the *current* template beside the base you already made, into the same
|
|
|
130
130
|
`$SCRATCH`:
|
|
131
131
|
|
|
132
132
|
```sh
|
|
133
|
-
(cd "$SCRATCH" && npx @zackbart/connecta@0.18.
|
|
133
|
+
(cd "$SCRATCH" && npx @zackbart/connecta@0.18.2 init current)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
You now have a three-way merge with a real base: `$SCRATCH/base` is what this
|
|
@@ -186,7 +186,7 @@ A deployment older than 0.10.2 has no base to diff against. Do not try to
|
|
|
186
186
|
manufacture one. Instead:
|
|
187
187
|
|
|
188
188
|
1. `SCRATCH=$(mktemp -d)`, then
|
|
189
|
-
`(cd "$SCRATCH" && npx @zackbart/connecta@0.18.
|
|
189
|
+
`(cd "$SCRATCH" && npx @zackbart/connecta@0.18.2 init current)` — there is no
|
|
190
190
|
`base` leg here, only the current template to read from.
|
|
191
191
|
2. Copy `$SCRATCH/current` into the deployment file by file, **skipping
|
|
192
192
|
`src/index.ts`**.
|
|
@@ -207,6 +207,23 @@ first, so cross them bottom-up: start at the oldest one still above this
|
|
|
207
207
|
deployment's pin and work back up the page, because each boundary assumes the
|
|
208
208
|
older ones are already done.
|
|
209
209
|
|
|
210
|
+
### 0.18.1 → 0.18.2
|
|
211
|
+
|
|
212
|
+
Nothing throws for an existing deployment, and the version bump alone crosses
|
|
213
|
+
it. The release adds a third `auth` shape to `remoteMcp()` and every maintained
|
|
214
|
+
hosted connection — `{ type: "credential" }` — under which the connector
|
|
215
|
+
declares an operator slot on `/credentials` and reads the pasted value on each
|
|
216
|
+
request. A deployment carrying a static key as a runtime secret
|
|
217
|
+
(`auth: { type: "headers", headers: { Authorization: env.KEY } }`) keeps
|
|
218
|
+
working unchanged; moving it behind `/credentials` is an edit to the connector's
|
|
219
|
+
`auth` and one paste on the operator page, and needs `credentials.encryptionKey`
|
|
220
|
+
configured — a deployment without a vault gets a startup warning and
|
|
221
|
+
`recovery: "unavailable"` at use for that connector, not a boot failure. Two
|
|
222
|
+
Linear notes: the `headers` example in `documentation/linear.md` now shows
|
|
223
|
+
`Bearer ${key}` (Linear's MCP server documents that framing), and the credential
|
|
224
|
+
shape sends `Bearer` by default; a `headers` connector already sending a bare
|
|
225
|
+
key is untouched.
|
|
226
|
+
|
|
210
227
|
### 0.18.0 → 0.18.1
|
|
211
228
|
|
|
212
229
|
Nothing throws, no option moves, and every deployment crosses this on the
|
|
@@ -123,6 +123,11 @@ function build(env: Env) {
|
|
|
123
123
|
auth: {
|
|
124
124
|
type: "headers",
|
|
125
125
|
headers: { Authorization: `Bearer ${env.DOWNSTREAM_TOKEN}` },
|
|
126
|
+
// The vault-backed alternative for a downstream that authenticates
|
|
127
|
+
// with a static key: the operator pastes it at /credentials and
|
|
128
|
+
// rotates it there, so no Worker secret holds it.
|
|
129
|
+
// type: "credential",
|
|
130
|
+
// credential: { label: "Notion internal integration token" },
|
|
126
131
|
},
|
|
127
132
|
}),
|
|
128
133
|
api("echo", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
|