@zackbart/connecta 0.36.2 → 0.37.0
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 +23 -0
- package/bin/version.mjs +1 -1
- package/dist/auth/downstream-oauth.js +31 -12
- package/dist/auth/static-oauth.js +3 -2
- package/dist/auth/stored-observation.js +8 -0
- package/dist/call-admission.js +27 -14
- package/dist/connectors/api-connector.js +4 -0
- package/dist/connectors/option-shapes.js +5 -1
- package/dist/connectors/remote-mcp.d.ts +29 -3
- package/dist/connectors/remote-mcp.js +142 -22
- package/dist/execute.js +5 -5
- package/dist/executor-admission.js +27 -13
- package/dist/executors/quickjs.js +4 -1
- package/dist/index.d.ts +2 -1
- package/dist/invocation.js +6 -7
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +2 -0
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/request-principal.js +34 -0
- package/dist/request-token.d.ts +46 -0
- package/dist/routes/mcp.js +6 -6
- package/dist/routes/ui-config.js +38 -9
- package/dist/routes/ui-data.js +30 -5
- package/dist/routes/ui.js +10 -2
- package/dist/runtime/settlement.js +59 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +4 -1
- package/documentation/architecture.md +68 -25
- package/documentation/auth.md +129 -11
- package/documentation/code-mode.md +25 -5
- package/documentation/integrating.md +18 -4
- package/documentation/operator-tests.md +19 -0
- package/documentation/operator-ui.md +57 -46
- package/examples/worker/README.md +5 -1
- package/guides/connecting-agents.md +6 -0
- package/guides/safety-and-trust.md +1 -0
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.37.0 — 2026-10-27
|
|
6
|
+
|
|
7
|
+
Connecta 0.37.0 lets a `remoteMcp()` connection reach a trusted first-party downstream as the caller Connecta admitted, with no per-person OAuth. Request auth's new `principal` option gives the token resolver that caller's reference: people by default, and declared agents only with `accept: "human-or-agent"`. The new optional `fetch` carries all of the connector's MCP traffic, for example through a Workers service binding to a sibling Worker. The downstream must accept a forwarded identity only on its binding entrypoint or behind a verified signature. Operator status checks carry no caller, so a principal-forwarding connector reports `auth_required` on the operator pages even when calls succeed.
|
|
8
|
+
|
|
9
|
+
Concurrent programs on Workers no longer fail with "Cannot call this AsyncLocalStorage bound function outside of the request in which it was created." Queued `execute_code` programs, MCP requests, connector calls and OAuth refresh waiters now resume in their own request. `workerExecutor()`'s default queue deadline rises from 5 to 60 seconds, so a queued run can wait through one default guest deadline. Queue plus run time can approach two minutes, so clients should allow deadlines to match. The pool is per isolate, as before.
|
|
10
|
+
|
|
11
|
+
The operator UI no longer probes every connector twice on each page load. Maintainer admission now reads stored observations only. Each connector is then probed once, independently, and refreshes and recovery touch only the connectors involved. A slow refresh keeps the last snapshot visible as stale instead of blanking the page. The default `GET /ui/api/config` response is unchanged for other consumers.
|
|
12
|
+
|
|
13
|
+
Remote MCP OAuth now decides status and consent from the stored grant. Top-level `headers` values are no longer redacted as sent secrets. Nothing here is breaking. Deployments that use neither principal forwarding nor custom `fetch` need no configuration change, but should review the longer queue deadline. The root bundle cap rose to 404,500 B gzip in #1110.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Let `remoteMcp()` reach a trusted first-party downstream as the admitted caller. Request auth's new `principal: { accept, token }` gives the resolver the admitted person's `{ kind: "human", namespace, id }`, and declared agents only with `accept: "human-or-agent"`. Every other caller is refused with `auth_required` before resolving. Arguments, programs and headers cannot change the principal, and a different principal never reuses a client. An optional `fetch`, such as a Workers service binding, carries all of a connector's MCP traffic under the configured URL's HTTPS, redirect and origin rules; it cannot be combined with OAuth. Existing auth modes are unchanged.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `remoteMcp()` top-level `headers` values are no longer treated as sent secrets. Every call in a code-mode program now reaches the downstream with arguments equal to a header value intact, instead of `[redacted]` after the first call, and results, catalogs and errors that echo a header value return it unchanged. `auth` headers and other credentials are still registered and redacted as before.
|
|
22
|
+
- `remoteMcp()` OAuth now decides on the owner's stored grant rather than on a healthy handshake. A downstream that answers `initialize` and `tools/list` without credentials and challenges only `tools/call` (Google's hosted BigQuery MCP, for one) no longer reports "Already authorized": with no grant, Connect, Restart, a start after Disconnect, and `authorize_connector` start consent from the resource's RFC 9728 metadata and return its URL, and the operator status and UI report `auth_required` until a grant exists. An existing grant still reports `ok` without starting a new flow.
|
|
23
|
+
- Resume a queued `execute_code` program, MCP request, connector call or OAuth refresh waiter in its own request, whichever request released the slot or finished the refresh. On Workers with compatibility date 2025-06-16 or later, workerd's September 2026 releases refused that cross-request resume: the releasing program failed with `Executor failed: Cannot call this AsyncLocalStorage bound function outside of the request in which it was created.`, and the queued one later hit `Executor admission timed out`. A slot granted just before the executor or call admission closes is given back and refused with `executor_closed` (or the call-admission `closed` refusal), so no program or call starts after shutdown. QuickJS host calls now also run in the async context of the request running the program, not the request that spawned its warm child. `workerExecutor()`'s default queue deadline rises from 5 to 60 seconds, so a queued run can wait out one default guest deadline; the pool remains two runs and eight queued per isolate, and `admission` still replaces the defaults. Queue refusals remain `executor_overloaded` with `retryable: true` and `retryAfterMs`.
|
|
24
|
+
- Admit maintainer pages without live connector probes, load each connector once with its complete catalog and auth controls, scope refresh and recovery to affected connectors, and keep the last maintainer snapshot visible when admission becomes stale.
|
|
25
|
+
|
|
26
|
+
Discard queued reads withheld by stale admission without showing sign-in, and fence connector reads again when credential or OAuth mutations settle so follow-ups observe post-write state.
|
|
27
|
+
|
|
5
28
|
## 0.36.2 — 2026-10-27
|
|
6
29
|
|
|
7
30
|
Connecta 0.36.2 makes Google's hosted MCP servers, such as BigQuery, usable through generic `remoteMcp()` connections. The new `@zackbart/connecta/google` entry mints Google service-account tokens for request-local auth, so agent pools can share a service-account connection without a browser consent. The service account acts as itself, or as a Workspace user when a delegation subject is given. A rejected token is dropped and re-minted for the next request; dispatched writes are never replayed.
|
package/bin/version.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect, Exit } from "effect";
|
|
3
3
|
import { byteReadResponse } from "../byte-read-response.js";
|
|
4
4
|
import { deriveContext } from "../derived-context.js";
|
|
5
5
|
import { ConnectorCallError, resendAdvice } from "../errors.js";
|
|
6
6
|
import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
|
|
7
7
|
import { keepErrorName } from "../redaction-identity.js";
|
|
8
8
|
import { fromSignal, runEdge } from "../runtime/run.js";
|
|
9
|
+
import { Settlement } from "../runtime/settlement.js";
|
|
9
10
|
import { SentSecrets } from "../sent-secrets.js";
|
|
10
11
|
import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
|
|
11
12
|
import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
|
|
@@ -189,15 +190,13 @@ function aborted(signal) {
|
|
|
189
190
|
* The outcome of a flight another request owns, failures included. Rejects
|
|
190
191
|
* only with this caller's own abort.
|
|
191
192
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* instead hands the rest of its work back to its own request, as workerd does
|
|
197
|
-
* for any promise resolved from another one.
|
|
193
|
+
* The owner settles the flight from its own request. Settlement wakes this
|
|
194
|
+
* waiter through a Promise this request created, so the wait — and the
|
|
195
|
+
* caller's storage reads after it — resume here, never inside the owner's
|
|
196
|
+
* call (src/runtime/settlement.ts).
|
|
198
197
|
*/
|
|
199
198
|
function waitForRefreshFlight(flight, signal, deadline) {
|
|
200
|
-
const settled =
|
|
199
|
+
const settled = flight.outcome.wait();
|
|
201
200
|
// The abort goes first, so a caller that has already left never joins.
|
|
202
201
|
return runEdge(Effect.raceAllFirst([
|
|
203
202
|
...(signal ? [fromSignal(signal)] : []),
|
|
@@ -341,7 +340,7 @@ export class OAuthRefreshCoordinator {
|
|
|
341
340
|
if (!flight)
|
|
342
341
|
return;
|
|
343
342
|
this.flights.delete(epoch);
|
|
344
|
-
|
|
343
|
+
flight.outcome.settle(Exit.succeed({ status: "retired" }));
|
|
345
344
|
}
|
|
346
345
|
static {
|
|
347
346
|
registerAuthMethod(OAuthRefreshCoordinator.prototype, "refresh", function (provider, input, init, baseFetch, signal, defer) {
|
|
@@ -428,7 +427,7 @@ export class OAuthRefreshCoordinator {
|
|
|
428
427
|
}, OAuthRefreshCoordinator.prototype.refresh);
|
|
429
428
|
registerAuthMethod(OAuthRefreshCoordinator.prototype, "redeem", function (provider, epoch, requested, input, init, baseFetch, waiting) {
|
|
430
429
|
return authFlow(function* () {
|
|
431
|
-
const flight = { outcome:
|
|
430
|
+
const flight = { outcome: new Settlement() };
|
|
432
431
|
this.flights.set(epoch, flight);
|
|
433
432
|
// Pin the owner partition until the flight settles, whoever is waiting.
|
|
434
433
|
const release = this.retainPartition();
|
|
@@ -436,8 +435,8 @@ export class OAuthRefreshCoordinator {
|
|
|
436
435
|
if (this.flights.get(epoch) === flight)
|
|
437
436
|
this.flights.delete(epoch);
|
|
438
437
|
release();
|
|
439
|
-
// Last
|
|
440
|
-
|
|
438
|
+
// Last: joined waiters wake in their own requests, after this.
|
|
439
|
+
flight.outcome.settle(Exit.succeed(outcome));
|
|
441
440
|
};
|
|
442
441
|
const fail = (error, verdict) => {
|
|
443
442
|
if (verdict)
|
|
@@ -783,6 +782,10 @@ export class KvOAuthProvider {
|
|
|
783
782
|
if (client)
|
|
784
783
|
this.addClientAuthentication = (headers, params) => authenticateRemoteClient(client, headers, params);
|
|
785
784
|
}
|
|
785
|
+
/** Stored facts only: no migration, sealing writes, refresh or discovery. */
|
|
786
|
+
storedAuthorization(issuer) {
|
|
787
|
+
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedAuthorization", [issuer]));
|
|
788
|
+
}
|
|
786
789
|
get redirectUrl() {
|
|
787
790
|
return this.redirectUri;
|
|
788
791
|
}
|
|
@@ -1432,6 +1435,22 @@ export class KvOAuthProvider {
|
|
|
1432
1435
|
};
|
|
1433
1436
|
}.bind(this));
|
|
1434
1437
|
}, KvOAuthProvider.prototype.openGrant);
|
|
1438
|
+
registerAuthMethod(KvOAuthProvider.prototype, "storedAuthorization", function (issuer) {
|
|
1439
|
+
return authFlow(function* () {
|
|
1440
|
+
const raw = (yield authStorage(this.storage, "get", [GRANT]));
|
|
1441
|
+
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
1442
|
+
if (isDisconnectedEpoch(grant.epoch))
|
|
1443
|
+
return false;
|
|
1444
|
+
const body = grant.body;
|
|
1445
|
+
const configured = issuer ?? this.clientOptions?.client?.issuer;
|
|
1446
|
+
const discovered = body.discovery;
|
|
1447
|
+
if (!configured &&
|
|
1448
|
+
(!discovered || discovered.authorizationServerMetadata?.issuer !== discovered.authorizationServerUrl))
|
|
1449
|
+
return false;
|
|
1450
|
+
const expected = configured ?? discoveryIssuer(discovered);
|
|
1451
|
+
return Boolean(expected && body.issuer === expected && body.tokens?.access_token);
|
|
1452
|
+
}.bind(this)).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
1453
|
+
}, KvOAuthProvider.prototype.storedAuthorization);
|
|
1435
1454
|
registerAuthMethod(KvOAuthProvider.prototype, "readGrant", function (signal) {
|
|
1436
1455
|
signal = signal === undefined ? this.signal : signal;
|
|
1437
1456
|
return authFlow(function* () {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withStoredAuth } from "./stored-observation.js";
|
|
1
2
|
import { auth, OAuthError, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
3
|
import { Effect } from "effect";
|
|
3
4
|
import { byteReadResponse } from "../byte-read-response.js";
|
|
@@ -946,7 +947,7 @@ export function staticOAuth(id, config) {
|
|
|
946
947
|
});
|
|
947
948
|
}), true);
|
|
948
949
|
});
|
|
949
|
-
return {
|
|
950
|
+
return withStoredAuth({
|
|
950
951
|
status: retainAuthPartition(status, 0),
|
|
951
952
|
startAuth: retainAuthPartition(startAuth, 0),
|
|
952
953
|
disconnectAuth: retainAuthPartition(disconnectAuth, 0),
|
|
@@ -976,7 +977,7 @@ export function staticOAuth(id, config) {
|
|
|
976
977
|
return authCall(authorizedFetch, [ctx, input, init]);
|
|
977
978
|
})),
|
|
978
979
|
}, grantFor(ctx)),
|
|
979
|
-
};
|
|
980
|
+
}, (ctx) => providerFor(ctx).storedAuthorization(settings.identity));
|
|
980
981
|
}
|
|
981
982
|
// Hook calls keep the existing owner partition pinned while their native
|
|
982
983
|
// program settles. Each Promise shell enters its own registered program.
|
package/dist/call-admission.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Duration, Effect, Exit } from "effect";
|
|
2
2
|
import { CALL_ADMISSION } from "./provider.js";
|
|
3
3
|
import { ConnectorCallError } from "./errors.js";
|
|
4
4
|
import { provideCallAdmissionProgram, provideCallBudgetCharge, startCallAdmission } from "./runtime/call-admission.js";
|
|
5
5
|
import { fromSignal, runEdge } from "./runtime/run.js";
|
|
6
|
+
import { Settlement } from "./runtime/settlement.js";
|
|
6
7
|
import { keepErrorName } from "./redaction-identity.js";
|
|
7
8
|
const DEFAULT_MAX_QUEUE_SIZE = 32;
|
|
8
9
|
const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
|
|
@@ -133,7 +134,7 @@ export class ConnectorCallAdmissionController {
|
|
|
133
134
|
this.closed = true;
|
|
134
135
|
for (const state of this.partitions.values()) {
|
|
135
136
|
for (const waiter of state.waiters.splice(0)) {
|
|
136
|
-
|
|
137
|
+
waiter.outcome.settle(Exit.fail(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission is closed.`)));
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -198,11 +199,11 @@ export class ConnectorCallAdmissionController {
|
|
|
198
199
|
const retryAfterMs = this.budgetRetryAfterMs(state, now);
|
|
199
200
|
if (retryAfterMs !== undefined) {
|
|
200
201
|
this.rateLimitedTotal++;
|
|
201
|
-
|
|
202
|
+
waiter.outcome.settle(Exit.fail(this.budgetLimited(retryAfterMs)));
|
|
202
203
|
continue;
|
|
203
204
|
}
|
|
204
205
|
const waitMs = Math.max(0, now - waiter.queuedAt);
|
|
205
|
-
|
|
206
|
+
waiter.outcome.settle(Exit.succeed(this.admit(state, now, waitMs)));
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
pruneBudget(state, now) {
|
|
@@ -259,9 +260,11 @@ export class ConnectorCallAdmissionController {
|
|
|
259
260
|
this.maybeDeletePartition(key, state, waiter.now());
|
|
260
261
|
return Effect.void;
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
return Effect.sync(() => {
|
|
264
|
+
const outcome = waiter.outcome.outcome;
|
|
265
|
+
if (outcome && Exit.isSuccess(outcome))
|
|
266
|
+
outcome.value.release();
|
|
267
|
+
});
|
|
265
268
|
}
|
|
266
269
|
concurrencyLimited(reason) {
|
|
267
270
|
return new CallAdmissionError("concurrency", "rate_limited", `Connector "${this.connectorId}" call concurrency ${reason}.`, { retryAfterMs: this.retryAfterMs });
|
|
@@ -283,7 +286,7 @@ export class ConnectorCallAdmissionController {
|
|
|
283
286
|
// for a fiber. Semaphore does not fit that wait — permits here are
|
|
284
287
|
// per-partition, the queue is bounded and counted, and each waiter carries
|
|
285
288
|
// its own timeout and budget recheck — so the queue stays explicit, as in
|
|
286
|
-
// AdmissionController, with a
|
|
289
|
+
// AdmissionController, with a Settlement per waiter.
|
|
287
290
|
static {
|
|
288
291
|
provideCallAdmissionProgram((controller, input, now) => {
|
|
289
292
|
// Copy the signal out before building the wait: the effect returned
|
|
@@ -348,16 +351,16 @@ export class ConnectorCallAdmissionController {
|
|
|
348
351
|
const waiter = {
|
|
349
352
|
queuedAt,
|
|
350
353
|
now,
|
|
351
|
-
outcome:
|
|
354
|
+
outcome: new Settlement(),
|
|
352
355
|
};
|
|
353
356
|
state.waiters.push(waiter);
|
|
354
357
|
controller.queuedTotal++;
|
|
355
358
|
// A timeout or abort settles the waiter only if it takes it out of the
|
|
356
|
-
// queue first. Otherwise the pump or close() got there
|
|
357
|
-
//
|
|
359
|
+
// queue first. Otherwise the pump or close() got there first, and the
|
|
360
|
+
// outcome they recorded is the answer, rechecked once heard.
|
|
358
361
|
const giveUp = (count, error) => Effect.suspend(() => {
|
|
359
362
|
if (!controller.removeWaiter(state, waiter)) {
|
|
360
|
-
return
|
|
363
|
+
return waiter.outcome.wait();
|
|
361
364
|
}
|
|
362
365
|
count();
|
|
363
366
|
const refusal = error();
|
|
@@ -368,13 +371,23 @@ export class ConnectorCallAdmissionController {
|
|
|
368
371
|
// checks the signal as it starts, which closes the check-to-listener
|
|
369
372
|
// race an AbortSignal would not replay.
|
|
370
373
|
const contenders = [
|
|
371
|
-
|
|
374
|
+
waiter.outcome.wait(),
|
|
372
375
|
Effect.sleep(Duration.millis(controller.queueTimeoutMs)).pipe(Effect.andThen(giveUp(() => controller.rejectedTotal++, () => controller.concurrencyLimited(`queue wait exceeded ${controller.queueTimeoutMs}ms`)))),
|
|
373
376
|
];
|
|
374
377
|
if (signal) {
|
|
375
378
|
contenders.push(fromSignal(signal).pipe(Effect.catch(() => giveUp(() => controller.cancelledTotal++, () => controller.cancelled(signal)))));
|
|
376
379
|
}
|
|
377
|
-
|
|
380
|
+
// A grant is recorded in the releasing request and heard here a turn
|
|
381
|
+
// later. A shutdown in that gap wins, so no call dispatches after
|
|
382
|
+
// close(): give the permit back. A grant that beat the caller's abort
|
|
383
|
+
// stays the caller's to release, as it always has.
|
|
384
|
+
const received = (permit) => Effect.suspend(() => {
|
|
385
|
+
if (!controller.closed)
|
|
386
|
+
return Effect.succeed(permit);
|
|
387
|
+
permit.release();
|
|
388
|
+
return Effect.fail(new CallAdmissionError("closed", "unavailable", `Connector "${controller.connectorId}" call admission is closed.`));
|
|
389
|
+
});
|
|
390
|
+
return Effect.raceAllFirst(contenders).pipe(Effect.flatMap(received), Effect.onInterrupt(() => controller.cleanupWaiter(key, state, waiter)));
|
|
378
391
|
});
|
|
379
392
|
}
|
|
380
393
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { storedAuthObservation, withStoredAuth } from "../auth/stored-observation.js";
|
|
1
2
|
import { Effect, Exit } from "effect";
|
|
2
3
|
import { registerConnectorVocabulary } from "../operation-vocabulary.js";
|
|
3
4
|
import { causeError, runEdge } from "../runtime/run.js";
|
|
@@ -304,5 +305,8 @@ export function apiConnector(id, opts, oauth) {
|
|
|
304
305
|
}
|
|
305
306
|
});
|
|
306
307
|
registerConnectorVocabulary(connector, opts.tools);
|
|
308
|
+
const storedAuth = oauth && storedAuthObservation(oauth);
|
|
309
|
+
if (storedAuth)
|
|
310
|
+
withStoredAuth(connector, storedAuth);
|
|
307
311
|
return connector;
|
|
308
312
|
}
|
|
@@ -14,7 +14,11 @@ export const CREDENTIAL = optionsOf()({
|
|
|
14
14
|
export const USAGE_GUIDE = optionsOf()(keys("content", "summary", "required"));
|
|
15
15
|
/** `remoteMcp()`'s `auth`, closed per `type`. */
|
|
16
16
|
export const REMOTE_MCP_AUTH = variants("type", {
|
|
17
|
-
request: optionsOf()({
|
|
17
|
+
request: optionsOf()({
|
|
18
|
+
...keys("type", "token"),
|
|
19
|
+
headers: strings(),
|
|
20
|
+
principal: optionsOf()(keys("accept", "token")),
|
|
21
|
+
}).shape,
|
|
18
22
|
headers: optionsOf()({ ...keys("type"), headers: strings() }).shape,
|
|
19
23
|
credential: optionsOf()({
|
|
20
24
|
...keys("type", "header", "scheme"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RemoteOAuthClient } from "../auth/downstream-client-metadata.js";
|
|
2
2
|
import type { FetchLike, Transport } from "@modelcontextprotocol/client";
|
|
3
3
|
import { ConnectorCallError } from "../errors.js";
|
|
4
|
-
import type { RequestToken } from "../request-token.js";
|
|
4
|
+
import type { PrincipalForwarding, RequestToken } from "../request-token.js";
|
|
5
5
|
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
|
|
6
6
|
/**
|
|
7
7
|
* A static downstream credential supplied through the connection in the
|
|
@@ -35,13 +35,29 @@ interface RemoteMcpCredentialAuth {
|
|
|
35
35
|
*/
|
|
36
36
|
scheme?: string | null;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Request-local Bearer auth. With `principal`, the resolver also receives the
|
|
40
|
+
* caller inbound auth admitted, for a trusted first-party downstream.
|
|
41
|
+
*/
|
|
42
|
+
export type RemoteMcpRequestAuth = {
|
|
39
43
|
type: "request";
|
|
40
44
|
/** Resolve a Bearer token inside this request. Never persisted or described. */
|
|
41
45
|
token: RequestToken;
|
|
46
|
+
principal?: never;
|
|
42
47
|
/** Static protocol/catalog headers, never Authorization. */
|
|
43
48
|
headers?: Record<string, string>;
|
|
44
49
|
} | {
|
|
50
|
+
type: "request";
|
|
51
|
+
token?: never;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the Bearer token for the admitted caller instead. Callers the
|
|
54
|
+
* policy does not accept are refused before the resolver runs.
|
|
55
|
+
*/
|
|
56
|
+
principal: PrincipalForwarding;
|
|
57
|
+
/** Static protocol/catalog headers, never Authorization. */
|
|
58
|
+
headers?: Record<string, string>;
|
|
59
|
+
};
|
|
60
|
+
export type RemoteMcpAuth = RemoteMcpRequestAuth | {
|
|
45
61
|
type: "headers";
|
|
46
62
|
headers: Record<string, string>;
|
|
47
63
|
} | RemoteMcpCredentialAuth | {
|
|
@@ -88,7 +104,8 @@ export interface RemoteMcpOptions {
|
|
|
88
104
|
* Non-secret resource context headers. Never credentials: use auth for those.
|
|
89
105
|
* Auth-owned, HTTP transport/session and OAuth client-auth headers are
|
|
90
106
|
* reserved. Cannot be combined with inline headers auth. Sent only at the
|
|
91
|
-
* resource URL path
|
|
107
|
+
* resource URL path. Values are never treated as sent secrets: arguments and
|
|
108
|
+
* echoes equal to them pass unchanged.
|
|
92
109
|
*/
|
|
93
110
|
headers?: Record<string, string>;
|
|
94
111
|
/**
|
|
@@ -122,6 +139,15 @@ export interface RemoteMcpOptions {
|
|
|
122
139
|
* Default console.
|
|
123
140
|
*/
|
|
124
141
|
logger?: Logger;
|
|
142
|
+
/**
|
|
143
|
+
* Send every MCP request through this fetch instead of the global one, such
|
|
144
|
+
* as a Workers service binding: `(input, init) => env.NOTES.fetch(input, init)`.
|
|
145
|
+
* `url` still names the logical origin, and the HTTPS, redirect and
|
|
146
|
+
* same-origin rules apply to it. Each request, writes included, is handed
|
|
147
|
+
* over once with `redirect: "manual"`; the function must not retry or follow
|
|
148
|
+
* redirects. Cannot be combined with `auth: { type: "oauth" }`.
|
|
149
|
+
*/
|
|
150
|
+
fetch?: FetchLike;
|
|
125
151
|
/**
|
|
126
152
|
* @internal Testing seam. When set, this transport is used instead of the
|
|
127
153
|
* HTTP transport, letting tests point the connector at an in-process MCP
|