@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/version.mjs +1 -1
  3. package/dist/auth/downstream-oauth.js +31 -12
  4. package/dist/auth/static-oauth.js +3 -2
  5. package/dist/auth/stored-observation.js +8 -0
  6. package/dist/call-admission.js +27 -14
  7. package/dist/connectors/api-connector.js +4 -0
  8. package/dist/connectors/option-shapes.js +5 -1
  9. package/dist/connectors/remote-mcp.d.ts +29 -3
  10. package/dist/connectors/remote-mcp.js +142 -22
  11. package/dist/execute.js +5 -5
  12. package/dist/executor-admission.js +27 -13
  13. package/dist/executors/quickjs.js +4 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/invocation.js +6 -7
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/model.d.ts +2 -0
  18. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  19. package/dist/request-principal.js +34 -0
  20. package/dist/request-token.d.ts +46 -0
  21. package/dist/routes/mcp.js +6 -6
  22. package/dist/routes/ui-config.js +38 -9
  23. package/dist/routes/ui-data.js +30 -5
  24. package/dist/routes/ui.js +10 -2
  25. package/dist/runtime/settlement.js +59 -0
  26. package/dist/version.d.ts +1 -1
  27. package/dist/version.js +1 -1
  28. package/dist/worker.js +4 -1
  29. package/documentation/architecture.md +68 -25
  30. package/documentation/auth.md +129 -11
  31. package/documentation/code-mode.md +25 -5
  32. package/documentation/integrating.md +18 -4
  33. package/documentation/operator-tests.md +19 -0
  34. package/documentation/operator-ui.md +57 -46
  35. package/examples/worker/README.md +5 -1
  36. package/guides/connecting-agents.md +6 -0
  37. package/guides/safety-and-trust.md +1 -0
  38. package/package.json +1 -1
  39. 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,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.36.2";
3
+ export const CONNECTA_VERSION = "0.37.0";
@@ -1,11 +1,12 @@
1
1
  import { UnauthorizedError } from "@modelcontextprotocol/client";
2
- import { Deferred, Effect } from "effect";
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
- * This wait is a Promise edge of its own, and the caller's storage reads stay
193
- * outside it, on purpose. The owner settles the Deferred from its own request,
194
- * and Deferred resumes a waiting fiber synchronously inside that call — on
195
- * Workers, inside the owner's I/O context. Resolving this caller's promise
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 = Deferred.await(flight.outcome);
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
- Deferred.doneUnsafe(flight.outcome, Effect.succeed({ status: "retired" }));
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: Deferred.makeUnsafe() };
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, because joined fibers resume inside this call.
440
- Deferred.doneUnsafe(flight.outcome, Effect.succeed(outcome));
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.
@@ -0,0 +1,8 @@
1
+ const observations = new WeakMap();
2
+ export function withStoredAuth(target, observe) {
3
+ observations.set(target, observe);
4
+ return target;
5
+ }
6
+ export function storedAuthObservation(target) {
7
+ return observations.get(target);
8
+ }
@@ -1,8 +1,9 @@
1
- import { Deferred, Duration, Effect } from "effect";
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
- Deferred.doneUnsafe(waiter.outcome, Effect.fail(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission is closed.`)));
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
- Deferred.doneUnsafe(waiter.outcome, Effect.fail(this.budgetLimited(retryAfterMs)));
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
- Deferred.doneUnsafe(waiter.outcome, Effect.succeed(this.admit(state, now, waitMs)));
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
- if (!Deferred.isDoneUnsafe(waiter.outcome))
263
- return Effect.void;
264
- return Deferred.await(waiter.outcome).pipe(Effect.tap((permit) => Effect.sync(() => permit.release())), Effect.ignore);
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 Deferred per waiter.
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: Deferred.makeUnsafe(),
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 in the same tick,
357
- // and the outcome they recorded is the answer.
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 Deferred.await(waiter.outcome);
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
- Deferred.await(waiter.outcome),
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
- return Effect.raceAllFirst(contenders).pipe(Effect.onInterrupt(() => controller.cleanupWaiter(key, state, waiter)));
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()({ ...keys("type", "token"), headers: strings() }).shape,
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
- export type RemoteMcpAuth = {
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; echoed values are redacted at eight characters or more.
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