ofw-mcp 2.19.3 → 2.19.5

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/dist/bundle.js CHANGED
@@ -2279,7 +2279,7 @@ var require_websocket = __commonJS({
2279
2279
  var http = __require("http");
2280
2280
  var net = __require("net");
2281
2281
  var tls = __require("tls");
2282
- var { randomBytes: randomBytes4, createHash: createHash2 } = __require("crypto");
2282
+ var { randomBytes: randomBytes6, createHash: createHash6 } = __require("crypto");
2283
2283
  var { Duplex, Readable } = __require("stream");
2284
2284
  var { URL: URL2 } = __require("url");
2285
2285
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2817,7 +2817,7 @@ var require_websocket = __commonJS({
2817
2817
  }
2818
2818
  }
2819
2819
  const defaultPort = isSecure ? 443 : 80;
2820
- const key = randomBytes4(16).toString("base64");
2820
+ const key = randomBytes6(16).toString("base64");
2821
2821
  const request = isSecure ? https.request : http.request;
2822
2822
  const protocolSet = /* @__PURE__ */ new Set();
2823
2823
  let perMessageDeflate;
@@ -2947,7 +2947,7 @@ var require_websocket = __commonJS({
2947
2947
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2948
2948
  return;
2949
2949
  }
2950
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
2950
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
2951
2951
  if (res.headers["sec-websocket-accept"] !== digest) {
2952
2952
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2953
2953
  return;
@@ -3316,7 +3316,7 @@ var require_websocket_server = __commonJS({
3316
3316
  var EventEmitter = __require("events");
3317
3317
  var http = __require("http");
3318
3318
  var { Duplex } = __require("stream");
3319
- var { createHash: createHash2 } = __require("crypto");
3319
+ var { createHash: createHash6 } = __require("crypto");
3320
3320
  var extension2 = require_extension();
3321
3321
  var PerMessageDeflate2 = require_permessage_deflate();
3322
3322
  var subprotocol2 = require_subprotocol();
@@ -3623,7 +3623,7 @@ var require_websocket_server = __commonJS({
3623
3623
  );
3624
3624
  }
3625
3625
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3626
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
3626
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
3627
3627
  const headers = [
3628
3628
  "HTTP/1.1 101 Switching Protocols",
3629
3629
  "Upgrade: websocket",
@@ -3734,6 +3734,33 @@ var storage2 = new AsyncLocalStorage2();
3734
3734
  function withCallerCapabilities(capabilities, fn) {
3735
3735
  return capabilities ? storage2.run(capabilities, fn) : fn();
3736
3736
  }
3737
+ function currentCallerCapabilities() {
3738
+ return storage2.getStore();
3739
+ }
3740
+ var ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
3741
+ var ELICITATION_MODES = ["form", "url"];
3742
+ function isRecord(value) {
3743
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3744
+ }
3745
+ function callerCapabilities(ctx) {
3746
+ const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
3747
+ if (isRecord(envelope)) {
3748
+ const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
3749
+ if (isRecord(declared))
3750
+ return declared;
3751
+ }
3752
+ return currentCallerCapabilities();
3753
+ }
3754
+ function callerAcceptsFormElicitation(ctx) {
3755
+ const capabilities = callerCapabilities(ctx);
3756
+ if (!capabilities)
3757
+ return void 0;
3758
+ const elicitation = capabilities.elicitation;
3759
+ if (!isRecord(elicitation))
3760
+ return false;
3761
+ const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
3762
+ return namedModes.length === 0 || namedModes.includes("form");
3763
+ }
3737
3764
 
3738
3765
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
3739
3766
  var __create2 = Object.create;
@@ -23460,7 +23487,7 @@ function date4(params) {
23460
23487
  return _coercedDate(ZodDate, params);
23461
23488
  }
23462
23489
 
23463
- // node_modules/@modelcontextprotocol/core/dist/auth-CUe6YdwF.mjs
23490
+ // node_modules/@modelcontextprotocol/core/dist/auth-CGP0BDVq.mjs
23464
23491
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
23465
23492
  var SUPPORTED_PROTOCOL_VERSIONS = [
23466
23493
  LATEST_PROTOCOL_VERSION,
@@ -24297,7 +24324,8 @@ var OAuthMetadataSchema = looseObject({
24297
24324
  introspection_endpoint_auth_signing_alg_values_supported: array(string2()).optional(),
24298
24325
  code_challenge_methods_supported: array(string2()).optional(),
24299
24326
  client_id_metadata_document_supported: boolean2().optional(),
24300
- authorization_response_iss_parameter_supported: boolean2().optional().catch(void 0)
24327
+ authorization_response_iss_parameter_supported: boolean2().optional().catch(void 0),
24328
+ dpop_signing_alg_values_supported: array(string2()).optional()
24301
24329
  });
24302
24330
  var OpenIdProviderMetadataSchema = looseObject({
24303
24331
  issuer: string2(),
@@ -24398,7 +24426,7 @@ var OAuthTokenRevocationRequestSchema = object({
24398
24426
  token_type_hint: string2().optional()
24399
24427
  }).strip();
24400
24428
 
24401
- // node_modules/@modelcontextprotocol/server/dist/src-CX2iR2pK.mjs
24429
+ // node_modules/@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs
24402
24430
  var BRANDS = /* @__PURE__ */ Symbol.for("mcp.sdk.errorBrands");
24403
24431
  function stampErrorBrands(instance, ctor) {
24404
24432
  const brands = /* @__PURE__ */ new Set();
@@ -24512,8 +24540,18 @@ var SdkError = class extends Error {
24512
24540
  if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SdkError.isInstance(value)`); for callbacks use `v => SdkError.isInstance(v)`");
24513
24541
  return brandedHasInstance(this, value);
24514
24542
  }
24515
- constructor(code, message, data) {
24516
- super(message);
24543
+ /**
24544
+ * @param code - Stable string code identifying the failure ({@linkcode SdkErrorCode}).
24545
+ * @param message - Human-readable description.
24546
+ * @param data - Optional structured payload (for example the HTTP status carried by
24547
+ * {@linkcode SdkHttpError}). Opaque to the SDK: a `cause` key inside `data` is not
24548
+ * promoted to `Error.cause`.
24549
+ * @param options - Standard `ErrorOptions`, forwarded to `Error`. Pass the underlying
24550
+ * failure as `{ cause }` so it is reachable through the `Error.cause` chain that
24551
+ * loggers and error trackers walk.
24552
+ */
24553
+ constructor(code, message, data, options) {
24554
+ super(message, options);
24517
24555
  this.code = code;
24518
24556
  this.data = data;
24519
24557
  this.name = "SdkError";
@@ -24524,8 +24562,11 @@ var SdkHttpError = class extends SdkError {
24524
24562
  static {
24525
24563
  Object.defineProperty(this, "mcpBrand", { value: "mcp.SdkHttpError" });
24526
24564
  }
24527
- constructor(code, message, data) {
24528
- super(code, message, data);
24565
+ /**
24566
+ * @param options - Standard `ErrorOptions`, forwarded to `Error` (see {@linkcode SdkError}).
24567
+ */
24568
+ constructor(code, message, data, options) {
24569
+ super(code, message, data, options);
24529
24570
  this.name = "SdkHttpError";
24530
24571
  }
24531
24572
  get status() {
@@ -27476,7 +27517,7 @@ var INBOUND_VALIDATION_LADDER = [
27476
27517
  evaluatedAt: "pre-dispatch",
27477
27518
  codes: [HEADER_MISMATCH_ERROR_CODE],
27478
27519
  conformance: ["http-header-validation"],
27479
- rationale: "SEP-2243 standard `Mcp-Method` / `Mcp-Name` headers \u2014 presence, sentinel decoding, and `Mcp-Name` \u2194 body cross-check \u2014 are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before dispatch. The classifier\u2019s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge `era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half. Evaluated before the capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung is answered here before the dispatch rungs (5\u20136) are consulted."
27520
+ rationale: "SEP-2243 standard `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers \u2014 presence, sentinel decoding, and `Mcp-Name` \u2194 body cross-check \u2014 are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before dispatch. The spec requires `MCP-Protocol-Version` and `Mcp-Method` on every modern *request* POST (`Mcp-Name` only for the methods that mirror `params.name` / `params.uri` / `params.taskId`, see `MCP_NAME_HEADER_SOURCE`) and names them in that order, so a request missing several is answered by the earliest. Notification POSTs are exempt: the presence half runs on requests only, so a modern-enveloped notification is dispatched even with no standard headers at all. The classifier\u2019s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge `era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half \u2014 including the missing `MCP-Protocol-Version` cell, which cannot live on the edge rung without breaking body-primary classification. Evaluated before the capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung is answered here before the dispatch rungs (5\u20136) are consulted."
27480
27521
  },
27481
27522
  {
27482
27523
  rung: "client-capabilities",
@@ -27798,6 +27839,37 @@ var inputRequired = Object.assign(buildInputRequired, {
27798
27839
  return { method: "roots/list" };
27799
27840
  }
27800
27841
  });
27842
+ function acceptedContent(responses, key, schema) {
27843
+ const view = inputResponse(responses, key);
27844
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
27845
+ if (schema === void 0) return view.content;
27846
+ const outcome = schema["~standard"].validate(view.content);
27847
+ if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
27848
+ return outcome.issues === void 0 ? outcome.value : void 0;
27849
+ }
27850
+ function inputResponse(responses, key) {
27851
+ if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
27852
+ const entry = responses[key];
27853
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
27854
+ const candidate = entry;
27855
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
27856
+ const content = candidate["content"];
27857
+ return {
27858
+ kind: "elicit",
27859
+ action: candidate["action"],
27860
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
27861
+ };
27862
+ }
27863
+ if (Array.isArray(candidate["roots"])) return {
27864
+ kind: "roots",
27865
+ roots: candidate["roots"]
27866
+ };
27867
+ if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
27868
+ kind: "sampling",
27869
+ result: candidate
27870
+ };
27871
+ return { kind: "missing" };
27872
+ }
27801
27873
  var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
27802
27874
  function inputRequiredRoundsExceededMessage(method, maxRounds) {
27803
27875
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
@@ -28246,7 +28318,7 @@ var Protocol = class {
28246
28318
  return this._requestHandlers.get(method);
28247
28319
  }
28248
28320
  async _oncancel(notification) {
28249
- if (!notification.params.requestId) return;
28321
+ if (notification.params.requestId === void 0) return;
28250
28322
  this._requestHandlerAbortControllers.get(notification.params.requestId)?.abort(notification.params.reason);
28251
28323
  }
28252
28324
  _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) {
@@ -28645,19 +28717,20 @@ var Protocol = class {
28645
28717
  const cancel = (reason) => {
28646
28718
  if (responseReceived) return;
28647
28719
  this._progressHandlers.delete(messageId);
28648
- if (requestAbort === void 0) this._transport?.send(this._envelopeOutbound({
28649
- jsonrpc: "2.0",
28650
- method: "notifications/cancelled",
28651
- params: {
28652
- requestId: messageId,
28653
- reason: String(reason)
28654
- }
28655
- }), {
28656
- relatedRequestId,
28657
- resumptionToken,
28658
- onresumptiontoken
28659
- }).catch((error62) => this._onerror(/* @__PURE__ */ new Error(`Failed to send cancellation: ${error62}`)));
28660
- else requestAbort.abort();
28720
+ if (requestAbort === void 0) {
28721
+ if (request.method !== "initialize") this._transport?.send(this._envelopeOutbound({
28722
+ jsonrpc: "2.0",
28723
+ method: "notifications/cancelled",
28724
+ params: {
28725
+ requestId: messageId,
28726
+ reason: String(reason)
28727
+ }
28728
+ }), {
28729
+ relatedRequestId,
28730
+ resumptionToken,
28731
+ onresumptiontoken
28732
+ }).catch((error62) => this._onerror(/* @__PURE__ */ new Error(`Failed to send cancellation: ${error62}`)));
28733
+ } else requestAbort.abort();
28661
28734
  reject(reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason)));
28662
28735
  };
28663
28736
  this._responseHandlers.set(messageId, (response) => {
@@ -28737,7 +28810,7 @@ var Protocol = class {
28737
28810
  jsonrpc: "2.0",
28738
28811
  ...notification
28739
28812
  });
28740
- if ((this._options?.debouncedNotificationMethods ?? []).includes(notification.method) && !notification.params && !options?.relatedRequestId) {
28813
+ if ((this._options?.debouncedNotificationMethods ?? []).includes(notification.method) && !notification.params && options?.relatedRequestId === void 0) {
28741
28814
  if (this._pendingDebouncedNotifications.has(notification.method)) return;
28742
28815
  this._pendingDebouncedNotifications.add(notification.method);
28743
28816
  Promise.resolve().then(() => {
@@ -36040,7 +36113,7 @@ var Ajv = import_ajv.Ajv;
36040
36113
  // node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs
36041
36114
  import process2 from "node:process";
36042
36115
 
36043
- // node_modules/@modelcontextprotocol/server/dist/mcp-DXXb3Vv3.mjs
36116
+ // node_modules/@modelcontextprotocol/server/dist/mcp-Dw2OlZ1f.mjs
36044
36117
  var COMPLETABLE_SYMBOL = /* @__PURE__ */ Symbol.for("mcp.completable");
36045
36118
  function isCompletable(schema) {
36046
36119
  return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
@@ -36233,6 +36306,9 @@ function notificationToServerEvent(method, uri) {
36233
36306
  };
36234
36307
  }
36235
36308
  }
36309
+ function supportsScopeChallengeResolver(transport) {
36310
+ return typeof transport === "object" && transport !== null && "setScopeChallengeResolver" in transport && typeof transport.setScopeChallengeResolver === "function";
36311
+ }
36236
36312
  var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
36237
36313
  var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
36238
36314
  function resolveLegacyShimOptions(options) {
@@ -37043,6 +37119,7 @@ var McpServer = class {
37043
37119
  * ```
37044
37120
  */
37045
37121
  async connect(transport) {
37122
+ if (supportsScopeChallengeResolver(transport)) transport.setScopeChallengeResolver((context) => this.resolveScopeChallenge(context));
37046
37123
  return await this.server.connect(transport);
37047
37124
  }
37048
37125
  /**
@@ -37051,6 +37128,41 @@ var McpServer = class {
37051
37128
  async close() {
37052
37129
  await this.server.close();
37053
37130
  }
37131
+ /** @internal */
37132
+ resolveScopeChallenge = (context) => {
37133
+ switch (context.request.method) {
37134
+ case "tools/call": {
37135
+ const toolName = context.request.params?.name;
37136
+ if (typeof toolName !== "string") return;
37137
+ const tool = this._registeredTools[toolName];
37138
+ if (tool === void 0 || !tool.enabled) return;
37139
+ return tool.scopeChallenge?.(context);
37140
+ }
37141
+ case "resources/read": {
37142
+ const resourceUri = context.request.params?.uri;
37143
+ if (typeof resourceUri !== "string") return;
37144
+ let uri;
37145
+ try {
37146
+ uri = new URL(resourceUri);
37147
+ } catch {
37148
+ return;
37149
+ }
37150
+ const resource = this._registeredResources[uri.toString()];
37151
+ if (resource !== void 0) return resource.enabled ? resource.scopeChallenge?.(context) : void 0;
37152
+ for (const template of Object.values(this._registeredResourceTemplates)) if (template.resourceTemplate.uriTemplate.match(uri.toString())) return template.enabled ? template.scopeChallenge?.(context) : void 0;
37153
+ return;
37154
+ }
37155
+ case "prompts/get": {
37156
+ const promptName = context.request.params?.name;
37157
+ if (typeof promptName !== "string") return;
37158
+ const prompt = this._registeredPrompts[promptName];
37159
+ if (prompt === void 0 || !prompt.enabled) return;
37160
+ return prompt.scopeChallenge?.(context);
37161
+ }
37162
+ default:
37163
+ return;
37164
+ }
37165
+ };
37054
37166
  _toolHandlersInitialized = false;
37055
37167
  setToolRequestHandlers() {
37056
37168
  if (this._toolHandlersInitialized) return;
@@ -37217,7 +37329,10 @@ var McpServer = class {
37217
37329
  }
37218
37330
  for (const template of Object.values(this._registeredResourceTemplates)) {
37219
37331
  const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
37220
- if (variables) return attachCacheHintFallback(await template.readCallback(uri, variables, ctx), template.cacheHint);
37332
+ if (variables) {
37333
+ if (!template.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${template.resourceTemplate.uriTemplate} disabled`);
37334
+ return attachCacheHintFallback(await template.readCallback(uri, variables, ctx), template.cacheHint);
37335
+ }
37221
37336
  }
37222
37337
  throw new ResourceNotFoundError(request.params.uri);
37223
37338
  });
@@ -37248,36 +37363,32 @@ var McpServer = class {
37248
37363
  this._promptHandlersInitialized = true;
37249
37364
  }
37250
37365
  registerResource(name, uriOrTemplate, config2, readCallback) {
37251
- const cacheHint = config2.cacheHint;
37252
- let metadata = config2;
37253
- if (cacheHint !== void 0) {
37254
- assertValidCacheHint(cacheHint, `resource ${name}`);
37255
- const rest = { ...config2 };
37256
- delete rest.cacheHint;
37257
- metadata = rest;
37258
- }
37366
+ const { cacheHint, scopeChallenge, ...resourceMetadata } = config2;
37367
+ const metadata = resourceMetadata;
37368
+ if (cacheHint !== void 0) assertValidCacheHint(cacheHint, `resource ${name}`);
37259
37369
  if (typeof uriOrTemplate === "string") {
37260
37370
  if (this._registeredResources[uriOrTemplate]) throw new Error(`Resource ${uriOrTemplate} is already registered`);
37261
- const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, readCallback);
37371
+ const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
37262
37372
  if (cacheHint !== void 0) registeredResource.cacheHint = cacheHint;
37263
37373
  this.setResourceRequestHandlers();
37264
37374
  this.sendResourceListChanged();
37265
37375
  return registeredResource;
37266
37376
  } else {
37267
37377
  if (this._registeredResourceTemplates[name]) throw new Error(`Resource template ${name} is already registered`);
37268
- const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, readCallback);
37378
+ const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
37269
37379
  if (cacheHint !== void 0) registeredResourceTemplate.cacheHint = cacheHint;
37270
37380
  this.setResourceRequestHandlers();
37271
37381
  this.sendResourceListChanged();
37272
37382
  return registeredResourceTemplate;
37273
37383
  }
37274
37384
  }
37275
- _createRegisteredResource(name, title, uri, metadata, readCallback) {
37385
+ _createRegisteredResource(name, title, uri, metadata, scopeChallenge, readCallback) {
37276
37386
  const registeredResource = {
37277
37387
  name,
37278
37388
  title,
37279
37389
  metadata,
37280
37390
  readCallback,
37391
+ scopeChallenge,
37281
37392
  enabled: true,
37282
37393
  disable: () => registeredResource.update({ enabled: false }),
37283
37394
  enable: () => registeredResource.update({ enabled: true }),
@@ -37291,6 +37402,7 @@ var McpServer = class {
37291
37402
  if (updates.title !== void 0) registeredResource.title = updates.title;
37292
37403
  if (updates.metadata !== void 0) registeredResource.metadata = updates.metadata;
37293
37404
  if (updates.callback !== void 0) registeredResource.readCallback = updates.callback;
37405
+ if (updates.scopeChallenge !== void 0) registeredResource.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
37294
37406
  if (updates.enabled !== void 0) registeredResource.enabled = updates.enabled;
37295
37407
  this.sendResourceListChanged();
37296
37408
  }
@@ -37298,12 +37410,13 @@ var McpServer = class {
37298
37410
  this._registeredResources[uri] = registeredResource;
37299
37411
  return registeredResource;
37300
37412
  }
37301
- _createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
37413
+ _createRegisteredResourceTemplate(name, title, template, metadata, scopeChallenge, readCallback) {
37302
37414
  const registeredResourceTemplate = {
37303
37415
  resourceTemplate: template,
37304
37416
  title,
37305
37417
  metadata,
37306
37418
  readCallback,
37419
+ scopeChallenge,
37307
37420
  enabled: true,
37308
37421
  disable: () => registeredResourceTemplate.update({ enabled: false }),
37309
37422
  enable: () => registeredResourceTemplate.update({ enabled: true }),
@@ -37317,6 +37430,7 @@ var McpServer = class {
37317
37430
  if (updates.template !== void 0) registeredResourceTemplate.resourceTemplate = updates.template;
37318
37431
  if (updates.metadata !== void 0) registeredResourceTemplate.metadata = updates.metadata;
37319
37432
  if (updates.callback !== void 0) registeredResourceTemplate.readCallback = updates.callback;
37433
+ if (updates.scopeChallenge !== void 0) registeredResourceTemplate.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
37320
37434
  if (updates.enabled !== void 0) registeredResourceTemplate.enabled = updates.enabled;
37321
37435
  this.sendResourceListChanged();
37322
37436
  }
@@ -37326,7 +37440,7 @@ var McpServer = class {
37326
37440
  if (Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v))) this.setCompletionRequestHandler();
37327
37441
  return registeredResourceTemplate;
37328
37442
  }
37329
- _createRegisteredPrompt(name, title, description, argsSchema, callback, icons, _meta) {
37443
+ _createRegisteredPrompt(name, title, description, argsSchema, callback, icons, scopeChallenge, _meta) {
37330
37444
  let currentArgsSchema = argsSchema;
37331
37445
  let currentCallback = callback;
37332
37446
  const registeredPrompt = {
@@ -37334,6 +37448,7 @@ var McpServer = class {
37334
37448
  description,
37335
37449
  argsSchema,
37336
37450
  icons,
37451
+ scopeChallenge,
37337
37452
  _meta,
37338
37453
  handler: createPromptHandler(name, argsSchema, callback),
37339
37454
  enabled: true,
@@ -37348,6 +37463,7 @@ var McpServer = class {
37348
37463
  if (updates.title !== void 0) registeredPrompt.title = updates.title;
37349
37464
  if (updates.description !== void 0) registeredPrompt.description = updates.description;
37350
37465
  if (updates.icons !== void 0) registeredPrompt.icons = updates.icons;
37466
+ if (updates.scopeChallenge !== void 0) registeredPrompt.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
37351
37467
  if (updates._meta !== void 0) registeredPrompt._meta = updates._meta;
37352
37468
  let needsHandlerRegen = false;
37353
37469
  if (updates.argsSchema !== void 0) {
@@ -37375,7 +37491,7 @@ var McpServer = class {
37375
37491
  }
37376
37492
  return registeredPrompt;
37377
37493
  }
37378
- _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, _meta, handler) {
37494
+ _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, scopeChallenge, _meta, handler) {
37379
37495
  validateAndWarnToolName(name);
37380
37496
  if (inputSchema !== void 0) try {
37381
37497
  const json2 = standardSchemaToJsonSchema(inputSchema, "input");
@@ -37394,6 +37510,7 @@ var McpServer = class {
37394
37510
  annotations,
37395
37511
  icons,
37396
37512
  execution,
37513
+ scopeChallenge,
37397
37514
  _meta,
37398
37515
  handler,
37399
37516
  executor: createToolExecutor(inputSchema, handler),
@@ -37432,6 +37549,7 @@ var McpServer = class {
37432
37549
  }
37433
37550
  if (updates.annotations !== void 0) registeredTool.annotations = updates.annotations;
37434
37551
  if (updates.icons !== void 0) registeredTool.icons = updates.icons;
37552
+ if (updates.scopeChallenge !== void 0) registeredTool.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
37435
37553
  if (updates._meta !== void 0) registeredTool._meta = updates._meta;
37436
37554
  if (updates.enabled !== void 0) registeredTool.enabled = updates.enabled;
37437
37555
  this.sendToolListChanged();
@@ -37444,13 +37562,13 @@ var McpServer = class {
37444
37562
  }
37445
37563
  registerTool(name, config2, cb) {
37446
37564
  if (this._registeredTools[name]) throw new Error(`Tool ${name} is already registered`);
37447
- const { title, description, inputSchema, outputSchema, annotations, icons, _meta } = config2;
37448
- return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, _meta, cb);
37565
+ const { title, description, inputSchema, outputSchema, annotations, icons, scopeChallenge, _meta } = config2;
37566
+ return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, scopeChallenge, _meta, cb);
37449
37567
  }
37450
37568
  registerPrompt(name, config2, cb) {
37451
37569
  if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
37452
- const { title, description, argsSchema, icons, _meta } = config2;
37453
- const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, _meta);
37570
+ const { title, description, argsSchema, icons, scopeChallenge, _meta } = config2;
37571
+ const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, scopeChallenge, _meta);
37454
37572
  this.setPromptRequestHandlers();
37455
37573
  this.sendPromptListChanged();
37456
37574
  return registeredPrompt;
@@ -37562,6 +37680,7 @@ function unwrapOptionalSchema(schema) {
37562
37680
  }
37563
37681
 
37564
37682
  // node_modules/@modelcontextprotocol/server/dist/stdio.mjs
37683
+ var swallowsErrorsAfterClose = /* @__PURE__ */ Symbol("swallowsErrorsAfterClose");
37565
37684
  var StdioServerTransport = class {
37566
37685
  _readBuffer;
37567
37686
  _started = false;
@@ -37588,18 +37707,27 @@ var StdioServerTransport = class {
37588
37707
  this.onerror?.(error62);
37589
37708
  };
37590
37709
  _onstdouterror = (error62) => {
37710
+ if (this._closed) return;
37591
37711
  this.onerror?.(error62);
37592
37712
  this.close().catch(() => {
37593
37713
  });
37594
37714
  };
37715
+ _onstdinclose = () => {
37716
+ this.close().catch(() => {
37717
+ });
37718
+ };
37595
37719
  /**
37596
37720
  * Starts listening for messages on `stdin`.
37597
37721
  */
37598
37722
  async start() {
37599
37723
  if (this._started) throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
37600
37724
  this._started = true;
37725
+ for (const listener of this._stdout.listeners("error")) if (listener[swallowsErrorsAfterClose]) this._stdout.off("error", listener);
37726
+ if (this._stdin.readableEnded || this._stdin.destroyed) setImmediate(this._onstdinclose);
37601
37727
  this._stdin.on("data", this._ondata);
37602
37728
  this._stdin.on("error", this._onerror);
37729
+ this._stdin.on("end", this._onstdinclose);
37730
+ this._stdin.on("close", this._onstdinclose);
37603
37731
  this._stdout.on("error", this._onstdouterror);
37604
37732
  }
37605
37733
  processReadBuffer() {
@@ -37616,7 +37744,9 @@ var StdioServerTransport = class {
37616
37744
  this._closed = true;
37617
37745
  this._stdin.off("data", this._ondata);
37618
37746
  this._stdin.off("error", this._onerror);
37619
- this._stdout.off("error", this._onstdouterror);
37747
+ this._stdin.off("end", this._onstdinclose);
37748
+ this._stdin.off("close", this._onstdinclose);
37749
+ this._onstdouterror[swallowsErrorsAfterClose] = true;
37620
37750
  if (this._stdin.listenerCount("data") === 0) this._stdin.pause();
37621
37751
  this._readBuffer.clear();
37622
37752
  this.onclose?.();
@@ -37845,14 +37975,14 @@ function serveStdio(factory, options = {}) {
37845
37975
  return false;
37846
37976
  };
37847
37977
  const answerLegacyRejection = (request, reason, requestedVersion) => {
37848
- const rejection2 = modernOnlyStrictRejection({
37978
+ const rejection3 = modernOnlyStrictRejection({
37849
37979
  kind: "legacy",
37850
37980
  reason,
37851
37981
  ...requestedVersion !== void 0 && { requestedVersion }
37852
37982
  }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
37853
- if (rejection2 === void 0) return Promise.resolve();
37854
- reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
37855
- return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
37983
+ if (rejection3 === void 0) return Promise.resolve();
37984
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
37985
+ return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
37856
37986
  };
37857
37987
  const onInstanceClosed = (channel) => {
37858
37988
  if (closing || channel === discarding) return;
@@ -37893,6 +38023,7 @@ function serveStdio(factory, options = {}) {
37893
38023
  return;
37894
38024
  }
37895
38025
  if (state.era === "modern" && await tryServeListen(message)) return;
38026
+ if (isTornDown()) return;
37896
38027
  state.instance.channel.deliver(message);
37897
38028
  return;
37898
38029
  }
@@ -37959,6 +38090,7 @@ function serveStdio(factory, options = {}) {
37959
38090
  };
37960
38091
  }
37961
38092
  if (await tryServeListen(message)) return;
38093
+ if (isTornDown()) return;
37962
38094
  state.instance.channel.deliver(message, { classification: opening.classification });
37963
38095
  return;
37964
38096
  case "legacy": {
@@ -38045,6 +38177,9 @@ function toError(value) {
38045
38177
  return value instanceof Error ? value : new Error(String(value));
38046
38178
  }
38047
38179
 
38180
+ // node_modules/@modelcontextprotocol/server/dist/index.mjs
38181
+ var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
38182
+
38048
38183
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
38049
38184
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
38050
38185
  var McpToolError = class extends Error {
@@ -38061,11 +38196,12 @@ var McpToolError = class extends Error {
38061
38196
  function createHelpfulError(message, opts) {
38062
38197
  return new McpToolError(message, opts);
38063
38198
  }
38199
+ var HEADER_NAME_RUN = "[a-z0-9_-]{0,40}";
38064
38200
  var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
38065
- var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
38201
+ var JWT_RE = /(?<![A-Za-z0-9_-])[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}(?![A-Za-z0-9_-])/g;
38066
38202
  var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
38067
- var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
38068
- var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
38203
+ var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,:\s]+)=[^;,\s]*/gi;
38204
+ var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,:\s]+=[^;,\s]*)(?:;\s*[^=;,:\s]+=[^;,\s]*)*)/gi;
38069
38205
  var API_KEY_RE = new RegExp([
38070
38206
  "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
38071
38207
  // OpenAI / Anthropic (incl. sk-ant-…)
@@ -38080,10 +38216,10 @@ var API_KEY_RE = new RegExp([
38080
38216
  "whsec_[A-Za-z0-9]{16,}\\b"
38081
38217
  // webhook signing secret (Stripe-style)
38082
38218
  ].map((p) => `\\b${p}`).join("|"), "g");
38083
- var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
38219
+ var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
38084
38220
  var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
38085
38221
  var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
38086
- var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
38222
+ var HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
38087
38223
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
38088
38224
  var JSON_SECRET_KEYS = [
38089
38225
  "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
@@ -38091,7 +38227,8 @@ var JSON_SECRET_KEYS = [
38091
38227
  "api[_-]?secret",
38092
38228
  "(?:x[_-])?api[_-]?key",
38093
38229
  "private[_-]?key",
38094
- "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
38230
+ `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
38231
+ // bounded like HEADER_SECRET_RE
38095
38232
  "(?:proxy-)?authorization",
38096
38233
  "password",
38097
38234
  "passwd",
@@ -38100,12 +38237,30 @@ var JSON_SECRET_KEYS = [
38100
38237
  ].join("|");
38101
38238
  var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
38102
38239
  var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
38240
+ var JSON_SECRET_NUM_RE = new RegExp(`(["'](?:${JSON_SECRET_KEYS})["']\\s*:\\s*)-?\\d+(?:\\.\\d+)?(?![\\d.])`, "gi");
38241
+ var JSON_COOKIE_DQ_RE = /("((?:set-)?cookie)"\s*:\s*)("(?:[^"\\]|\\.)*"|\[\s*"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*)/gi;
38242
+ var JSON_COOKIE_SQ_RE = /('((?:set-)?cookie)'\s*:\s*)('(?:[^'\\]|\\.)*'|\[\s*'(?:[^'\\]|\\.)*'(?:\s*,\s*'(?:[^'\\]|\\.)*')*)/gi;
38243
+ var DQ_STRING_RE = /"((?:[^"\\]|\\.)*)"/g;
38244
+ var SQ_STRING_RE = /'((?:[^'\\]|\\.)*)'/g;
38245
+ function redactJsonCookie(quote) {
38246
+ const stringRe = quote === '"' ? DQ_STRING_RE : SQ_STRING_RE;
38247
+ return (_m, prefix, key, value) => {
38248
+ const setCookie = key.toLowerCase() === "set-cookie";
38249
+ const redacted = value.replace(stringRe, (_s, body) => {
38250
+ const out = setCookie ? body.replace(/^(\s*[^=;]+)=[^;]*/, "$1=[REDACTED]") : body.replace(/=[^;]*/g, "=[REDACTED]");
38251
+ return `${quote}${out}${quote}`;
38252
+ });
38253
+ return `${prefix}${redacted}`;
38254
+ };
38255
+ }
38103
38256
  function redactSecrets(text) {
38104
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
38257
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_COOKIE_DQ_RE, redactJsonCookie('"')).replace(JSON_COOKIE_SQ_RE, redactJsonCookie("'")).replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_NUM_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
38105
38258
  }
38259
+ var ERROR_REDACTION_INPUT_MAX = 64 * 1024;
38106
38260
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
38107
38261
  const str = text === null || text === void 0 ? "" : String(text);
38108
- const redacted = redactSecrets(str);
38262
+ const cap = Math.max(ERROR_REDACTION_INPUT_MAX, max);
38263
+ const redacted = redactSecrets(str.length > cap ? str.slice(0, cap) : str);
38109
38264
  if (redacted.length <= max)
38110
38265
  return redacted;
38111
38266
  return `${redacted.slice(0, max)}\u2026 [truncated]`;
@@ -38164,6 +38319,11 @@ var MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|mast
38164
38319
  var MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
38165
38320
 
38166
38321
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
38322
+ function textResult(data) {
38323
+ return {
38324
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
38325
+ };
38326
+ }
38167
38327
  function rawTextResult(text) {
38168
38328
  return { content: [{ type: "text", text }] };
38169
38329
  }
@@ -38182,6 +38342,463 @@ function pruneUndefined(obj) {
38182
38342
  return out;
38183
38343
  }
38184
38344
 
38345
+ // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
38346
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
38347
+
38348
+ // node_modules/@chrischall/mcp-utils/dist/server/canonical.js
38349
+ function canonicalJson(value) {
38350
+ if (value === null || typeof value === "string" || typeof value === "boolean")
38351
+ return JSON.stringify(value);
38352
+ if (value === void 0)
38353
+ return "null";
38354
+ if (typeof value === "number")
38355
+ return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
38356
+ if (typeof value === "bigint")
38357
+ return `{"$bigint":"${value.toString()}"}`;
38358
+ if (typeof value === "function" || typeof value === "symbol") {
38359
+ throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
38360
+ }
38361
+ if (Array.isArray(value))
38362
+ return `[${value.map(canonicalJson).join(",")}]`;
38363
+ if (value instanceof Date) {
38364
+ return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
38365
+ }
38366
+ if (ArrayBuffer.isView(value)) {
38367
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
38368
+ return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
38369
+ }
38370
+ if (value instanceof ArrayBuffer)
38371
+ return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
38372
+ if (value instanceof Map) {
38373
+ const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
38374
+ return `{"$map":[${entries2.join(",")}]}`;
38375
+ }
38376
+ if (value instanceof Set) {
38377
+ return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
38378
+ }
38379
+ const proto = Object.getPrototypeOf(value);
38380
+ if (proto !== Object.prototype && proto !== null) {
38381
+ throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
38382
+ }
38383
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
38384
+ return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
38385
+ }
38386
+ function escapeKey(key) {
38387
+ return key.startsWith("$") ? `$${key}` : key;
38388
+ }
38389
+
38390
+ // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
38391
+ var DEFAULT_REQUEST_KEY = "confirmation";
38392
+ var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
38393
+ var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
38394
+ var STATE_PREFIX = "mcpu.confirm.v1.";
38395
+ function bindingKey(binding) {
38396
+ if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
38397
+ throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
38398
+ }
38399
+ const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
38400
+ if (key.length < 32) {
38401
+ throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
38402
+ }
38403
+ return key;
38404
+ }
38405
+ function commitment(action, args) {
38406
+ return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
38407
+ }
38408
+ function mintState(key, action, binding) {
38409
+ const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
38410
+ const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
38411
+ const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
38412
+ return `${STATE_PREFIX}${body}.${mac3}`;
38413
+ }
38414
+ function verifyState(key, action, binding, state) {
38415
+ if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
38416
+ return false;
38417
+ const rest = state.slice(STATE_PREFIX.length);
38418
+ const dot = rest.indexOf(".");
38419
+ if (dot < 0)
38420
+ return false;
38421
+ const body = rest.slice(0, dot);
38422
+ const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
38423
+ const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
38424
+ if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
38425
+ return false;
38426
+ let payload;
38427
+ try {
38428
+ payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
38429
+ } catch {
38430
+ return false;
38431
+ }
38432
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
38433
+ return false;
38434
+ const want = Buffer.from(commitment(action, binding.args));
38435
+ const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
38436
+ return got.length === want.length && timingSafeEqual(got, want);
38437
+ }
38438
+ function echoedState(ctx) {
38439
+ const accessor = ctx.mcpReq.requestState;
38440
+ return typeof accessor === "function" ? accessor() : void 0;
38441
+ }
38442
+ function requireConfirmation(ctx, options) {
38443
+ const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
38444
+ const key = options.binding ? bindingKey(options.binding) : void 0;
38445
+ const confirmationSchema = external_exports.object({
38446
+ confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
38447
+ });
38448
+ const ask = () => {
38449
+ const preview = {
38450
+ action: options.action,
38451
+ ...options.details === void 0 ? {} : { details: options.details }
38452
+ };
38453
+ return inputRequired({
38454
+ inputRequests: {
38455
+ [requestKey]: inputRequired.elicit({
38456
+ message: `${options.message}
38457
+ ${JSON.stringify(preview, null, 2)}`,
38458
+ requestedSchema: confirmationSchema
38459
+ })
38460
+ },
38461
+ ...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
38462
+ });
38463
+ };
38464
+ const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
38465
+ const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
38466
+ if (response.kind === "missing") {
38467
+ if (callerAcceptsFormElicitation(ctx) === false) {
38468
+ return textResult({
38469
+ confirmed: false,
38470
+ dispatched: false,
38471
+ action: options.action,
38472
+ reason: "confirmation-unsupported",
38473
+ note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
38474
+ });
38475
+ }
38476
+ return ask();
38477
+ }
38478
+ if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
38479
+ if (key && options.binding) {
38480
+ const state = echoedState(ctx);
38481
+ if (state === void 0 || state === null) {
38482
+ return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
38483
+ }
38484
+ if (!verifyState(key, options.action, options.binding, state))
38485
+ return ask();
38486
+ }
38487
+ return void 0;
38488
+ }
38489
+ return textResult({
38490
+ confirmed: false,
38491
+ cancelled: true,
38492
+ action: options.action,
38493
+ note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
38494
+ });
38495
+ }
38496
+
38497
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
38498
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
38499
+ function createSpentTokenStore() {
38500
+ const spent = /* @__PURE__ */ new Map();
38501
+ return {
38502
+ has: (nonce) => spent.has(nonce),
38503
+ add: (nonce, exp) => {
38504
+ spent.set(nonce, exp);
38505
+ },
38506
+ prune: (now) => {
38507
+ for (const [nonce, exp] of spent)
38508
+ if (exp < now)
38509
+ spent.delete(nonce);
38510
+ },
38511
+ clear: () => spent.clear(),
38512
+ get size() {
38513
+ return spent.size;
38514
+ }
38515
+ };
38516
+ }
38517
+ var PROCESS_SPENT = createSpentTokenStore();
38518
+ var PREFIX = "mcpu.token.v1.";
38519
+ var DEFAULT_TTL_SECONDS = 600;
38520
+ function tokenKey(key) {
38521
+ const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
38522
+ if (bytes.length < 32)
38523
+ throw new RangeError("confirm token: key must be at least 32 bytes.");
38524
+ return bytes;
38525
+ }
38526
+ function ttlOf(ttlSeconds) {
38527
+ if (ttlSeconds === void 0)
38528
+ return DEFAULT_TTL_SECONDS;
38529
+ if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
38530
+ throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
38531
+ }
38532
+ return ttlSeconds;
38533
+ }
38534
+ function sign(key, body) {
38535
+ return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
38536
+ }
38537
+ function hashConfirmPayload(payload) {
38538
+ return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
38539
+ }
38540
+ function issueConfirmToken(key, binding, options = {}) {
38541
+ const k = tokenKey(key);
38542
+ const now = options.now ?? Date.now();
38543
+ const exp = now + ttlOf(options.ttlSeconds) * 1e3;
38544
+ const claims = {
38545
+ t: binding.tool,
38546
+ ...binding.account === void 0 ? {} : { a: binding.account },
38547
+ g: binding.target,
38548
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
38549
+ h: binding.payloadHash,
38550
+ exp,
38551
+ n: randomBytes(16).toString("base64url")
38552
+ };
38553
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
38554
+ return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
38555
+ }
38556
+ function parseClaims(body) {
38557
+ try {
38558
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
38559
+ return claims && typeof claims === "object" ? claims : void 0;
38560
+ } catch {
38561
+ return void 0;
38562
+ }
38563
+ }
38564
+ function verifyConfirmToken(key, token, binding, options = {}) {
38565
+ const k = tokenKey(key);
38566
+ const now = options.now ?? Date.now();
38567
+ const spent = options.spent ?? PROCESS_SPENT;
38568
+ spent.prune(now);
38569
+ if (!token.startsWith(PREFIX))
38570
+ return { ok: false, error: "TOKEN_INVALID" };
38571
+ const rest = token.slice(PREFIX.length);
38572
+ const dot = rest.indexOf(".");
38573
+ const body = dot < 0 ? "" : rest.slice(0, dot);
38574
+ const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
38575
+ const expected = sign(k, body);
38576
+ if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
38577
+ return { ok: false, error: "TOKEN_INVALID" };
38578
+ const claims = parseClaims(body);
38579
+ if (!claims)
38580
+ return { ok: false, error: "TOKEN_INVALID" };
38581
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
38582
+ return { ok: false, error: "TOKEN_INVALID" };
38583
+ }
38584
+ if (spent.has(claims.n))
38585
+ return { ok: false, error: "TOKEN_REUSED" };
38586
+ if (now > claims.exp)
38587
+ return { ok: false, error: "TOKEN_EXPIRED" };
38588
+ if (claims.r !== binding.revision)
38589
+ return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
38590
+ if (claims.h !== binding.payloadHash)
38591
+ return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
38592
+ spent.add(claims.n, claims.exp);
38593
+ return { ok: true };
38594
+ }
38595
+ var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
38596
+ var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
38597
+ var TOKEN_ERROR_NOTE = {
38598
+ TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
38599
+ TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
38600
+ TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
38601
+ };
38602
+ var CHANGED_NOTE = {
38603
+ "revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
38604
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
38605
+ };
38606
+ function isToolResult(value) {
38607
+ return Array.isArray(value.content);
38608
+ }
38609
+ function rejection2(data) {
38610
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
38611
+ }
38612
+ async function tokenConfirmation(action, fb) {
38613
+ const subject = await fb.subject();
38614
+ if (isToolResult(subject))
38615
+ return subject;
38616
+ if (subject.payload === void 0) {
38617
+ throw new TypeError(`confirm token: ${fb.tool}'s subject() returned no payload. Bind what the call will do (the arguments or the request body), or pass \`args\` to confirmationFromEnv.`);
38618
+ }
38619
+ const binding = {
38620
+ tool: fb.tool,
38621
+ ...fb.account === void 0 ? {} : { account: fb.account },
38622
+ target: subject.target,
38623
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
38624
+ payloadHash: hashConfirmPayload(subject.payload)
38625
+ };
38626
+ const phaseOne = () => {
38627
+ const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
38628
+ return {
38629
+ action,
38630
+ preview: subject.preview,
38631
+ confirmToken: token,
38632
+ expiresAt,
38633
+ ttlSeconds: ttlOf(fb.ttlSeconds),
38634
+ instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
38635
+ };
38636
+ };
38637
+ if (!fb.confirmToken) {
38638
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
38639
+ }
38640
+ const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
38641
+ if (verdict.ok)
38642
+ return void 0;
38643
+ if (verdict.error === "DRAFT_CHANGED") {
38644
+ return rejection2({
38645
+ error: "DRAFT_CHANGED",
38646
+ reason: verdict.reason,
38647
+ note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
38648
+ ...phaseOne()
38649
+ });
38650
+ }
38651
+ return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
38652
+ }
38653
+ async function requireConfirmationWithFallback(ctx, options) {
38654
+ const { tokenFallback, ...confirmation } = options;
38655
+ if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
38656
+ return tokenConfirmation(options.action, tokenFallback);
38657
+ }
38658
+ return requireConfirmation(ctx, confirmation);
38659
+ }
38660
+
38661
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
38662
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
38663
+
38664
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
38665
+ import { homedir } from "node:os";
38666
+ import { isAbsolute, join, resolve } from "node:path";
38667
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
38668
+ function readEnvVar(key, opts = {}) {
38669
+ const env = opts.env ?? process.env;
38670
+ const raw = env[key];
38671
+ if (typeof raw === "string") {
38672
+ const trimmed = raw.trim();
38673
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
38674
+ return trimmed;
38675
+ }
38676
+ }
38677
+ return opts.default;
38678
+ }
38679
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
38680
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
38681
+ function parseBoolEnv(key, opts = {}) {
38682
+ const fallback = opts.default ?? false;
38683
+ const raw = readEnvVar(key, { env: opts.env });
38684
+ if (raw === void 0)
38685
+ return fallback;
38686
+ const token = raw.toLowerCase();
38687
+ if (TRUE_TOKENS.has(token))
38688
+ return true;
38689
+ if (FALSE_TOKENS.has(token))
38690
+ return false;
38691
+ return fallback;
38692
+ }
38693
+ function expandPath(p) {
38694
+ let expanded = p;
38695
+ if (p === "~") {
38696
+ expanded = homedir();
38697
+ } else if (p.startsWith("~/")) {
38698
+ expanded = join(homedir(), p.slice(2));
38699
+ }
38700
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
38701
+ }
38702
+ async function loadDotenvSafely(opts = {}) {
38703
+ try {
38704
+ const mod = await import(
38705
+ /* @vite-ignore */
38706
+ "dotenv"
38707
+ );
38708
+ const result = mod.config({
38709
+ ...opts.path !== void 0 ? { path: opts.path } : {},
38710
+ override: opts.override ?? false,
38711
+ quiet: true
38712
+ });
38713
+ return result.error === void 0;
38714
+ } catch {
38715
+ return false;
38716
+ }
38717
+ }
38718
+
38719
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
38720
+ var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
38721
+ var DEFAULT_TTL_SECONDS2 = 600;
38722
+ var warned = /* @__PURE__ */ new Set();
38723
+ function readConfirmMode(env = process.env) {
38724
+ const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
38725
+ if (!raw)
38726
+ return "ask-user";
38727
+ if (MODES.has(raw))
38728
+ return raw;
38729
+ if (!warned.has(raw)) {
38730
+ warned.add(raw);
38731
+ process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
38732
+ `);
38733
+ }
38734
+ return "refuse";
38735
+ }
38736
+ function readConfirmTtl(env) {
38737
+ const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
38738
+ if (!raw)
38739
+ return DEFAULT_TTL_SECONDS2;
38740
+ if (/^[1-9]\d*$/.test(raw))
38741
+ return Number(raw);
38742
+ const key = `ttl:${raw}`;
38743
+ if (!warned.has(key)) {
38744
+ warned.add(key);
38745
+ process.stderr.write(`MCP_CONFIRM_TTL_SECONDS="${raw}" is not a positive whole number of seconds; gated writes will refuse on clients that cannot show a confirmation prompt until it is fixed.
38746
+ `);
38747
+ }
38748
+ return void 0;
38749
+ }
38750
+ var processKey;
38751
+ function confirmKeyFromEnv(env = process.env) {
38752
+ const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
38753
+ if (secret)
38754
+ return createHash3("sha256").update(secret, "utf8").digest();
38755
+ processKey ??= randomBytes2(32);
38756
+ return processKey;
38757
+ }
38758
+ var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
38759
+ var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
38760
+ var BAD_TTL_HINT = "MCP_CONFIRM_TTL_SECONDS on the server is not a positive whole number of seconds; fix it to allow two-step confirmation.";
38761
+ function boundArgs(args) {
38762
+ if (args === null || typeof args !== "object" || Array.isArray(args) || !("confirmToken" in args))
38763
+ return args;
38764
+ const { confirmToken, ...rest } = args;
38765
+ void confirmToken;
38766
+ return rest;
38767
+ }
38768
+ function confirmationFromEnv(options) {
38769
+ const { tool, account, confirmToken, subject, instruction, spent, args, env = process.env, ...rest } = options;
38770
+ const mode = readConfirmMode(env);
38771
+ const ttlSeconds = readConfirmTtl(env);
38772
+ const bound = args === void 0 ? void 0 : boundArgs(args);
38773
+ const confirmation = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
38774
+ if (mode === "refuse" || ttlSeconds === void 0) {
38775
+ const hint = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
38776
+ return {
38777
+ ...confirmation,
38778
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
38779
+ };
38780
+ }
38781
+ const boundSubject = bound === void 0 ? subject : async () => {
38782
+ const read = await subject();
38783
+ if ("content" in read)
38784
+ return read;
38785
+ return { ...read, payload: { payload: read.payload, args: bound } };
38786
+ };
38787
+ return {
38788
+ ...confirmation,
38789
+ tokenFallback: {
38790
+ key: confirmKeyFromEnv(env),
38791
+ tool,
38792
+ ...account === void 0 ? {} : { account },
38793
+ ...confirmToken === void 0 ? {} : { confirmToken },
38794
+ subject: boundSubject,
38795
+ ttlSeconds,
38796
+ instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
38797
+ ...spent === void 0 ? {} : { spent }
38798
+ }
38799
+ };
38800
+ }
38801
+
38185
38802
  // node_modules/@chrischall/mcp-utils/dist/server/index.js
38186
38803
  var SERVER_PROTOCOL_VERSIONS = Object.freeze([
38187
38804
  "2026-07-28",
@@ -38299,6 +38916,8 @@ function withGracefulShutdown(target, opts = {}) {
38299
38916
  };
38300
38917
  const handler = (signal) => {
38301
38918
  if (shuttingDown) {
38919
+ if (signal === "stdin-eof")
38920
+ return;
38302
38921
  if (Date.now() - firstSignalAt < repeatGraceMs)
38303
38922
  return;
38304
38923
  if (shouldExit) {
@@ -38334,6 +38953,10 @@ function withGracefulShutdown(target, opts = {}) {
38334
38953
  };
38335
38954
  process.on("SIGINT", () => handler("SIGINT"));
38336
38955
  process.on("SIGTERM", () => handler("SIGTERM"));
38956
+ if (opts.stdin) {
38957
+ opts.stdin.once("end", () => handler("stdin-eof"));
38958
+ opts.stdin.once("close", () => handler("stdin-eof"));
38959
+ }
38337
38960
  }
38338
38961
  function runMcp(opts) {
38339
38962
  if (opts.banner !== void 0) {
@@ -38352,66 +38975,12 @@ function runMcp(opts) {
38352
38975
  });
38353
38976
  const shutdown = opts.shutdown ?? true;
38354
38977
  if (shutdown !== false) {
38355
- withGracefulShutdown(handle, shutdown === true ? {} : shutdown);
38978
+ const stdinDefault = spec === "stdio" ? { stdin: process.stdin } : {};
38979
+ withGracefulShutdown(handle, { ...stdinDefault, ...shutdown === true ? {} : shutdown });
38356
38980
  }
38357
38981
  return handle;
38358
38982
  }
38359
38983
 
38360
- // node_modules/@chrischall/mcp-utils/dist/config/index.js
38361
- import { homedir } from "node:os";
38362
- import { isAbsolute, join, resolve } from "node:path";
38363
- var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
38364
- function readEnvVar(key, opts = {}) {
38365
- const env = opts.env ?? process.env;
38366
- const raw = env[key];
38367
- if (typeof raw === "string") {
38368
- const trimmed = raw.trim();
38369
- if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
38370
- return trimmed;
38371
- }
38372
- }
38373
- return opts.default;
38374
- }
38375
- var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
38376
- var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
38377
- function parseBoolEnv(key, opts = {}) {
38378
- const fallback = opts.default ?? false;
38379
- const raw = readEnvVar(key, { env: opts.env });
38380
- if (raw === void 0)
38381
- return fallback;
38382
- const token = raw.toLowerCase();
38383
- if (TRUE_TOKENS.has(token))
38384
- return true;
38385
- if (FALSE_TOKENS.has(token))
38386
- return false;
38387
- return fallback;
38388
- }
38389
- function expandPath(p) {
38390
- let expanded = p;
38391
- if (p === "~") {
38392
- expanded = homedir();
38393
- } else if (p.startsWith("~/")) {
38394
- expanded = join(homedir(), p.slice(2));
38395
- }
38396
- return isAbsolute(expanded) ? expanded : resolve(expanded);
38397
- }
38398
- async function loadDotenvSafely(opts = {}) {
38399
- try {
38400
- const mod = await import(
38401
- /* @vite-ignore */
38402
- "dotenv"
38403
- );
38404
- const result = mod.config({
38405
- ...opts.path !== void 0 ? { path: opts.path } : {},
38406
- override: opts.override ?? false,
38407
- quiet: true
38408
- });
38409
- return result.error === void 0;
38410
- } catch {
38411
- return false;
38412
- }
38413
- }
38414
-
38415
38984
  // node_modules/@chrischall/mcp-utils/dist/fs/index.js
38416
38985
  import { openAsBlob } from "node:fs";
38417
38986
 
@@ -38539,16 +39108,16 @@ async function resolveAuthPattern(pattern) {
38539
39108
  if (resolver)
38540
39109
  return resolver();
38541
39110
  }
38542
- throw createHelpfulError("No auth configured. Provide a token, OAuth credentials, login credentials, or sign in in your browser (fetchproxy fallback).", { hint: "Set one of the supported credential env vars, or sign in in your browser." });
39111
+ throw createHelpfulError("No auth configured. Provide a token, OAuth credentials, login credentials, or sign in in your browser (read via ContextMint Bridge).", { hint: "Set one of the supported credential env vars, or sign in in your browser." });
38543
39112
  }
38544
39113
 
38545
39114
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
38546
39115
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
38547
39116
  import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
38548
39117
  import { homedir as homedir2 } from "node:os";
38549
- import { randomBytes, createHmac } from "node:crypto";
39118
+ import { randomBytes as randomBytes3, createHmac as createHmac3 } from "node:crypto";
38550
39119
  function bindingDigest(secret, salt) {
38551
- return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
39120
+ return createHmac3("sha256", salt).update(secret).digest("hex").slice(0, 32);
38552
39121
  }
38553
39122
  function isEnvelope(raw) {
38554
39123
  return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
@@ -38598,7 +39167,7 @@ function createFileStatePersistence(opts) {
38598
39167
  },
38599
39168
  save(state) {
38600
39169
  const dir = dirname2(filePath);
38601
- const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
39170
+ const tmp = `${filePath}.tmp-${randomBytes3(6).toString("hex")}`;
38602
39171
  const dirExisted = existsSync2(dir);
38603
39172
  try {
38604
39173
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -38606,7 +39175,7 @@ function createFileStatePersistence(opts) {
38606
39175
  chmodSync(dir, 448);
38607
39176
  let boundTo;
38608
39177
  if (secret !== void 0) {
38609
- const salt = randomBytes(16).toString("hex");
39178
+ const salt = randomBytes3(16).toString("hex");
38610
39179
  boundTo = { salt, digest: bindingDigest(secret, salt) };
38611
39180
  }
38612
39181
  const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
@@ -38919,7 +39488,7 @@ var TokenManager = class {
38919
39488
  };
38920
39489
 
38921
39490
  // src/client.ts
38922
- import { dirname as dirname3, join as join5 } from "path";
39491
+ import { dirname as dirname3, join as join6 } from "path";
38923
39492
  import { fileURLToPath } from "url";
38924
39493
 
38925
39494
  // node_modules/@fetchproxy/protocol/dist/crypto.js
@@ -39125,6 +39694,30 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
39125
39694
  "write_cookies"
39126
39695
  ]);
39127
39696
 
39697
+ // node_modules/@fetchproxy/protocol/dist/capability-availability.js
39698
+ var CAPABILITY_UNAVAILABLE_CODE = "capability_unavailable";
39699
+ var MAX_UNAVAILABLE_CAPABILITIES = 32;
39700
+ var MAX_CAPABILITY_NAME_LENGTH = 64;
39701
+ var UNSUPPORTED_CAPABILITY_REASON_PREFIX = "unsupported-capability:";
39702
+ function capabilityUnavailableMessage(capability, platform) {
39703
+ return `capability ${JSON.stringify(capability)} is not available in this browser (${platform})`;
39704
+ }
39705
+ var UNAVAILABLE_RE = /^capability "([^"]{1,64})" is not available in this browser(?: \(([^)]{1,32})\))?/;
39706
+ function parseCapabilityUnavailable(error62) {
39707
+ const m = UNAVAILABLE_RE.exec(error62);
39708
+ if (!m)
39709
+ return null;
39710
+ return { capability: m[1], platform: m[2] ?? null };
39711
+ }
39712
+ function parseUnsupportedCapabilityReason(reason) {
39713
+ if (!reason.startsWith(UNSUPPORTED_CAPABILITY_REASON_PREFIX))
39714
+ return null;
39715
+ const rest = reason.slice(UNSUPPORTED_CAPABILITY_REASON_PREFIX.length).replace(/\s*\(not available in this browser\)\s*$/, "").trim();
39716
+ if (rest === "")
39717
+ return [];
39718
+ return rest.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
39719
+ }
39720
+
39128
39721
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
39129
39722
  var ID_RE = /^([^:]+):([^:]+):([0-9a-f]{16})$/;
39130
39723
  function generateMcpId(serverName, version2) {
@@ -39839,6 +40432,9 @@ function validateHello(raw) {
39839
40432
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
39840
40433
  if (raw.accepts !== void 0)
39841
40434
  assertAcceptsList(raw.accepts);
40435
+ if (raw.unavailableCapabilities !== void 0) {
40436
+ assertUnavailableCapabilities(raw.unavailableCapabilities);
40437
+ }
39842
40438
  return raw;
39843
40439
  }
39844
40440
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -39904,6 +40500,22 @@ function assertAcceptsList(accepts) {
39904
40500
  }
39905
40501
  }
39906
40502
  }
40503
+ function assertUnavailableCapabilities(list) {
40504
+ const label = "hello.unavailableCapabilities";
40505
+ if (!Array.isArray(list))
40506
+ throw new ProtocolError3(`${label}: expected array`);
40507
+ if (list.length > MAX_UNAVAILABLE_CAPABILITIES) {
40508
+ throw new ProtocolError3(`${label}: at most ${MAX_UNAVAILABLE_CAPABILITIES} entries, got ${list.length}`);
40509
+ }
40510
+ for (const c of list) {
40511
+ if (typeof c !== "string") {
40512
+ throw new ProtocolError3(`${label}: entry must be string, got ${typeof c}`);
40513
+ }
40514
+ if (c.length < 1 || c.length > MAX_CAPABILITY_NAME_LENGTH) {
40515
+ throw new ProtocolError3(`${label}: entry must be 1-${MAX_CAPABILITY_NAME_LENGTH} characters, got ${c.length}`);
40516
+ }
40517
+ }
40518
+ }
39907
40519
  function validatePeerGone(raw) {
39908
40520
  assertString(raw.mcpId, "peer-gone.mcpId");
39909
40521
  if (!isValidMcpId(raw.mcpId))
@@ -40379,6 +40991,8 @@ function validateInnerResponse(raw) {
40379
40991
  }
40380
40992
  if (raw.ok === false) {
40381
40993
  assertString(raw.error, "inner.error");
40994
+ if (raw.code !== void 0)
40995
+ assertString(raw.code, "inner.code");
40382
40996
  if (raw.op !== void 0) {
40383
40997
  if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
40384
40998
  throw new ProtocolError3(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
@@ -40474,6 +41088,38 @@ async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
40474
41088
  }
40475
41089
  }
40476
41090
 
41091
+ // node_modules/@fetchproxy/server/dist/session-info.js
41092
+ function sessionInfoFromHello(hello) {
41093
+ const raw = hello.unavailableCapabilities;
41094
+ const known = /* @__PURE__ */ new Set();
41095
+ if (Array.isArray(raw)) {
41096
+ for (const c of raw) {
41097
+ if (typeof c === "string" && KNOWN_CAPABILITIES.has(c))
41098
+ known.add(c);
41099
+ }
41100
+ }
41101
+ return { platform: hello.platform, unavailableCapabilities: [...known].sort() };
41102
+ }
41103
+ function capabilitiesForRequest(inner) {
41104
+ if (inner.type !== "request")
41105
+ return [];
41106
+ if (inner.op === "graphql_query")
41107
+ return ["graphql"];
41108
+ if (inner.op === "fetch") {
41109
+ return inner.init.inPage === true ? ["fetch", "fetch_in_page"] : ["fetch"];
41110
+ }
41111
+ return [inner.op];
41112
+ }
41113
+ function unavailableCapabilityFor(inner, info) {
41114
+ if (!info || info.unavailableCapabilities.length === 0)
41115
+ return null;
41116
+ for (const c of capabilitiesForRequest(inner)) {
41117
+ if (info.unavailableCapabilities.includes(c))
41118
+ return c;
41119
+ }
41120
+ return null;
41121
+ }
41122
+
40477
41123
  // node_modules/@fetchproxy/server/dist/election.js
40478
41124
  import { createServer, Server as HttpServer } from "node:http";
40479
41125
  var DEFAULT_BIND_TIMEOUT_MS = 5e3;
@@ -40719,7 +41365,7 @@ var FetchproxySessionNotReadyError = class extends Error {
40719
41365
  hint;
40720
41366
  constructor(info) {
40721
41367
  const pairing = info.pairCode !== null && info.pairCode !== "";
40722
- const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
41368
+ const hint = pairing ? `Open the ContextMint Bridge extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
40723
41369
  super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
40724
41370
  this.name = "FetchproxySessionNotReadyError";
40725
41371
  this.reason = pairing ? "pair-required" : "not-ready";
@@ -40732,15 +41378,35 @@ var FetchproxySessionNotReadyError = class extends Error {
40732
41378
  var FetchproxyHelloRejectedError = class extends Error {
40733
41379
  mcpId;
40734
41380
  reason;
41381
+ /**
41382
+ * #418: when the reason is `unsupported-capability: a, b (not available in
41383
+ * this browser)` — every capability this MCP declared is one the browser
41384
+ * cannot serve — those capabilities, parsed out of it. Empty otherwise.
41385
+ */
41386
+ unavailableCapabilities;
41387
+ /** #418: the refusing browser's platform, when this bridge knew it. */
41388
+ platform;
41389
+ /**
41390
+ * #418: the remedy, when the reason is one this package knows how to
41391
+ * explain — today only `unsupported-capability`, which blames the browser
41392
+ * rather than the MCP. `null` otherwise.
41393
+ */
41394
+ hint;
40735
41395
  constructor(info) {
40736
- super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`);
41396
+ const unavailable = parseUnsupportedCapabilityReason(info.reason);
41397
+ const platform = info.platform ?? null;
41398
+ const hint = unavailable !== null ? `${platform ? `this browser (${platform})` : "this browser"} cannot serve any of the capabilities this MCP needs (${unavailable.join(", ")}) \u2014 ContextMint Bridge checked for the browser APIs and they are missing. Nothing is wrong with the MCP or your pairing, and updating will not change it: use this MCP from a browser that provides them (for example Chrome).` : null;
41399
+ super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. ` + (hint !== null ? hint : `This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`));
40737
41400
  this.name = "FetchproxyHelloRejectedError";
40738
41401
  this.mcpId = info.mcpId;
40739
41402
  this.reason = info.reason;
41403
+ this.unavailableCapabilities = unavailable ?? [];
41404
+ this.platform = platform;
41405
+ this.hint = hint;
40740
41406
  Object.setPrototypeOf(this, new.target.prototype);
40741
41407
  }
40742
41408
  };
40743
- var MIN_VERSION = "3.0.0";
41409
+ var MIN_SERVER_VERSION = "3.0.0";
40744
41410
  var FetchproxyProtocolVersionError = class extends Error {
40745
41411
  /** The protocol version this process speaks. */
40746
41412
  ourVersion;
@@ -40748,7 +41414,7 @@ var FetchproxyProtocolVersionError = class extends Error {
40748
41414
  theirVersion;
40749
41415
  peer;
40750
41416
  constructor(info) {
40751
- const far = info.peer === "extension" ? `the attached browser extension speaks ${info.theirVersion} \u2014 update Transporter (the fetchproxy extension) to ${MIN_VERSION} or later` : `the MCP holding the bridge port speaks ${info.theirVersion} \u2014 upgrade @fetchproxy/server to ${MIN_VERSION} or later in that MCP`;
41417
+ const far = info.peer === "extension" ? `the attached browser extension speaks ${info.theirVersion} \u2014 update ContextMint Bridge to a release that speaks fetchproxy protocol ${info.ourVersion}` : `the MCP holding the bridge port speaks ${info.theirVersion} \u2014 upgrade @fetchproxy/server to ${MIN_SERVER_VERSION} or later in that MCP`;
40752
41418
  super(`protocol version mismatch: this MCP speaks fetchproxy protocol ${info.ourVersion}, ${far}`);
40753
41419
  this.name = "FetchproxyProtocolVersionError";
40754
41420
  this.ourVersion = info.ourVersion;
@@ -40781,13 +41447,13 @@ async function awaitSessionReady(ready, opts) {
40781
41447
  }
40782
41448
 
40783
41449
  // node_modules/@fetchproxy/server/dist/extension-trust.js
40784
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
40785
- import { randomBytes as randomBytes3 } from "node:crypto";
40786
- import { isAbsolute as isAbsolute4, join as join4 } from "node:path";
41450
+ import { readFile as readFile3, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
41451
+ import { randomBytes as randomBytes5 } from "node:crypto";
41452
+ import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
40787
41453
 
40788
41454
  // node_modules/@fetchproxy/server/dist/identity.js
40789
41455
  import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
40790
- import { randomBytes as randomBytes2 } from "node:crypto";
41456
+ import { randomBytes as randomBytes4 } from "node:crypto";
40791
41457
  import { isAbsolute as isAbsolute3, join as join3 } from "node:path";
40792
41458
  import { homedir as homedir3 } from "node:os";
40793
41459
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
@@ -40884,6 +41550,7 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
40884
41550
  throw e;
40885
41551
  }
40886
41552
  let unreadable = null;
41553
+ let recovered = false;
40887
41554
  for (let attempt = 0; attempt < 5; attempt++) {
40888
41555
  const id = await generateIdentity();
40889
41556
  if (await publishIdentityIfAbsent(dir, serverName, id))
@@ -40897,6 +41564,11 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
40897
41564
  if (!(e instanceof SyntaxError))
40898
41565
  throw e;
40899
41566
  unreadable = e;
41567
+ if (!recovered && await removeAbandonedIdentity(path)) {
41568
+ recovered = true;
41569
+ attempt--;
41570
+ continue;
41571
+ }
40900
41572
  await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
40901
41573
  }
40902
41574
  }
@@ -40908,7 +41580,7 @@ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "
40908
41580
  async function publishIdentityIfAbsent(dir, serverName, id) {
40909
41581
  const path = identityFilePath(dir, serverName);
40910
41582
  await sweepStagingFiles(dir, serverName, { olderThanMs: 6e4 });
40911
- const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
41583
+ const tmp = `${path}.${randomBytes4(STAGING_BYTES).toString("hex")}.tmp`;
40912
41584
  try {
40913
41585
  await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
40914
41586
  await chmod(tmp, 384);
@@ -40938,6 +41610,192 @@ async function publishIdentityIfAbsent(dir, serverName, id) {
40938
41610
  await rm(tmp, { force: true });
40939
41611
  }
40940
41612
  }
41613
+ var ABANDONED_IDENTITY_MS = 5e3;
41614
+ var RECOVERY_LOCK_STALE_MS = 3e4;
41615
+ function looksAbandoned(text) {
41616
+ const t = text.trim();
41617
+ return t === "" || t.startsWith("{") && !t.endsWith("}");
41618
+ }
41619
+ function sameFile(a, b) {
41620
+ return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
41621
+ }
41622
+ async function takeRecoveryLock(lock) {
41623
+ for (let attempt = 0; attempt < 2; attempt++) {
41624
+ try {
41625
+ await writeFile(lock, `${process.pid}
41626
+ `, { mode: 384, flag: "wx" });
41627
+ return await lstat(lock);
41628
+ } catch (e) {
41629
+ if (e.code !== "EEXIST")
41630
+ return null;
41631
+ const st = await lstat(lock).catch(() => null);
41632
+ if (st === null)
41633
+ continue;
41634
+ if (Date.now() - st.mtimeMs < RECOVERY_LOCK_STALE_MS)
41635
+ return null;
41636
+ const again = await lstat(lock).catch(() => null);
41637
+ if (again === null || !sameFile(st, again))
41638
+ continue;
41639
+ await unlink(lock).catch(() => void 0);
41640
+ }
41641
+ }
41642
+ return null;
41643
+ }
41644
+ async function removeAbandonedIdentity(path) {
41645
+ const lock = `${path}.recover.lock`;
41646
+ const held = await takeRecoveryLock(lock);
41647
+ if (held === null)
41648
+ return false;
41649
+ try {
41650
+ const before = await lstat(path);
41651
+ if (!before.isFile() || Date.now() - before.mtimeMs < ABANDONED_IDENTITY_MS)
41652
+ return false;
41653
+ if (!looksAbandoned(await readFile(path, "utf8")))
41654
+ return false;
41655
+ const now = await lstat(path);
41656
+ if (!sameFile(before, now))
41657
+ return false;
41658
+ await unlink(path);
41659
+ return true;
41660
+ } catch {
41661
+ return false;
41662
+ } finally {
41663
+ const st = await lstat(lock).catch(() => null);
41664
+ if (st !== null && sameFile(held, st))
41665
+ await unlink(lock).catch(() => void 0);
41666
+ }
41667
+ }
41668
+
41669
+ // node_modules/@fetchproxy/server/dist/extension-pins.js
41670
+ import { readFile as readFile2 } from "node:fs/promises";
41671
+ import { join as join4 } from "node:path";
41672
+ var EXTENSION_PINS_FILE_VERSION = 1;
41673
+ var EXTENSION_PINS_ENV = "FETCHPROXY_EXTENSION_PINS";
41674
+ var ExtensionPinsError = class extends Error {
41675
+ code;
41676
+ constructor(code, message) {
41677
+ super(message);
41678
+ this.name = "ExtensionPinsError";
41679
+ this.code = code;
41680
+ }
41681
+ };
41682
+ function malformed(why) {
41683
+ return new ExtensionPinsError("EXTENSION_PINS_MALFORMED", `malformed extension pin set: ${why}`);
41684
+ }
41685
+ function isKey(x) {
41686
+ if (typeof x !== "string" || x === "")
41687
+ return false;
41688
+ let raw;
41689
+ try {
41690
+ raw = fromB64(x);
41691
+ } catch {
41692
+ return false;
41693
+ }
41694
+ return raw.length === 32 && toB64(raw) === x;
41695
+ }
41696
+ function parseExtensionPins(text) {
41697
+ let parsed;
41698
+ try {
41699
+ parsed = JSON.parse(text);
41700
+ } catch {
41701
+ throw malformed("not JSON");
41702
+ }
41703
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
41704
+ throw malformed("not an object");
41705
+ }
41706
+ const r = parsed;
41707
+ if (r.v !== EXTENSION_PINS_FILE_VERSION) {
41708
+ throw malformed(`"v" is not ${EXTENSION_PINS_FILE_VERSION}`);
41709
+ }
41710
+ if (r.managed !== true)
41711
+ throw malformed('"managed" is not true');
41712
+ if (r.managedBy !== void 0 && typeof r.managedBy !== "string") {
41713
+ throw malformed('"managedBy" is not a string');
41714
+ }
41715
+ if (!Array.isArray(r.extensions))
41716
+ throw malformed('"extensions" is not an array');
41717
+ const extensions = r.extensions.map((e, i) => {
41718
+ if (!e || typeof e !== "object" || Array.isArray(e)) {
41719
+ throw malformed(`extensions[${i}] is not an object`);
41720
+ }
41721
+ const x = e;
41722
+ if (!isKey(x.x25519Pub)) {
41723
+ throw malformed(`extensions[${i}].x25519Pub is not canonical base64 of 32 bytes`);
41724
+ }
41725
+ if (!isKey(x.ed25519Pub)) {
41726
+ throw malformed(`extensions[${i}].ed25519Pub is not canonical base64 of 32 bytes`);
41727
+ }
41728
+ if (x.label !== void 0 && typeof x.label !== "string") {
41729
+ throw malformed(`extensions[${i}].label is not a string`);
41730
+ }
41731
+ return {
41732
+ x25519Pub: x.x25519Pub,
41733
+ ed25519Pub: x.ed25519Pub,
41734
+ ...x.label !== void 0 ? { label: x.label } : {}
41735
+ };
41736
+ });
41737
+ return {
41738
+ v: EXTENSION_PINS_FILE_VERSION,
41739
+ managed: true,
41740
+ ...r.managedBy !== void 0 ? { managedBy: r.managedBy } : {},
41741
+ extensions
41742
+ };
41743
+ }
41744
+ function extensionPinsPath(serverName, trustDir) {
41745
+ return join4(trustDir, `${safeIdentityFileBase(serverName)}.extension-pins.json`);
41746
+ }
41747
+ async function readExtensionPinsAt(path) {
41748
+ let text;
41749
+ try {
41750
+ text = await readFile2(path, "utf8");
41751
+ } catch (e) {
41752
+ const code = e.code ?? "read failed";
41753
+ throw new ExtensionPinsError("EXTENSION_PINS_MISSING", `no managed extension pin set at ${path} (${code})`);
41754
+ }
41755
+ try {
41756
+ return parseExtensionPins(text);
41757
+ } catch (e) {
41758
+ const why = e instanceof ExtensionPinsError ? e.message : "malformed extension pin set";
41759
+ throw new ExtensionPinsError("EXTENSION_PINS_MALFORMED", `${why} at ${path}`);
41760
+ }
41761
+ }
41762
+ function decideManagedExtensionTrust(args) {
41763
+ const { set: set2, hello, serverName } = args;
41764
+ const listed = set2.extensions.some((e) => e.x25519Pub === hello.identityX25519Pub && e.ed25519Pub === hello.identityEd25519Pub);
41765
+ if (listed)
41766
+ return { decision: "pinned" };
41767
+ return {
41768
+ decision: "refused",
41769
+ code: "EXTENSION_NOT_PINNED",
41770
+ message: `[fetchproxy] ${serverName}: refusing an extension that is not in the host-managed pin set${args.location ? ` (${args.location})` : ""}. The host that runs this MCP decides which browsers it accepts; confirm this browser there.`
41771
+ };
41772
+ }
41773
+ async function evaluateManagedExtensionTrust(port, hello, serverName) {
41774
+ let set2;
41775
+ try {
41776
+ set2 = await port.read();
41777
+ } catch (e) {
41778
+ const code = e instanceof ExtensionPinsError ? e.code : "EXTENSION_PINS_MISSING";
41779
+ const detail = e instanceof ExtensionPinsError ? e.message : `could not read ${port.location}`;
41780
+ return {
41781
+ decision: "refused",
41782
+ code,
41783
+ message: `[fetchproxy] ${serverName}: ${code}: ${detail} \u2014 refusing every extension until the host that manages this MCP's pins writes a valid set.`
41784
+ };
41785
+ }
41786
+ return decideManagedExtensionTrust({ set: set2, hello, serverName, location: port.location });
41787
+ }
41788
+ function resolveExtensionPinsMode(explicit, env = process.env) {
41789
+ if (explicit === "managed")
41790
+ return "managed";
41791
+ const raw = env[EXTENSION_PINS_ENV];
41792
+ if (raw === void 0 || raw.trim() === "")
41793
+ return "first-use";
41794
+ if (raw === "managed")
41795
+ return "managed";
41796
+ console.warn(`[fetchproxy] ${EXTENSION_PINS_ENV}=${JSON.stringify(raw)} is not "managed" \u2014 treating it as managed anyway, because a set variable means a host manages this MCP's extension pins. Unset it for trust on first use.`);
41797
+ return "managed";
41798
+ }
40941
41799
 
40942
41800
  // node_modules/@fetchproxy/server/dist/extension-trust.js
40943
41801
  var TRUST_DIR_ENV = "FETCHPROXY_TRUST_DIR";
@@ -40961,6 +41819,22 @@ function defaultTrustDir() {
40961
41819
  }
40962
41820
  function fileExtensionTrust(args) {
40963
41821
  const dir = resolveTrustDir(args.trustDir, args.dir);
41822
+ if (args.extensionPins === "managed") {
41823
+ const location = extensionPinsPath(args.serverName, dir);
41824
+ return {
41825
+ // Managed mode has no first-use pin: nothing to read, nothing to write,
41826
+ // and no operator override. `write` REJECTS rather than no-ops, so a
41827
+ // path that ever reached it would log the attempt instead of silently
41828
+ // pretending to have pinned.
41829
+ allowNew: false,
41830
+ location,
41831
+ read: async () => null,
41832
+ write: async () => {
41833
+ throw new Error(`refusing to write an extension pin: ${location} is host-managed and this MCP never pins on first use`);
41834
+ },
41835
+ managed: { location, read: () => readExtensionPinsAt(location) }
41836
+ };
41837
+ }
40964
41838
  return {
40965
41839
  allowNew: args.allowNew,
40966
41840
  location: extensionTrustPath(args.serverName, dir),
@@ -40994,13 +41868,13 @@ function decideExtensionTrust(args) {
40994
41868
  };
40995
41869
  }
40996
41870
  function extensionTrustPath(serverName, dir = defaultTrustDir()) {
40997
- return join4(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
41871
+ return join5(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
40998
41872
  }
40999
41873
  function extensionTrustPathHint(serverName) {
41000
41874
  try {
41001
41875
  return extensionTrustPath(serverName);
41002
41876
  } catch {
41003
- return join4(defaultTrustDir(), "<server-name>.extension-trust.json");
41877
+ return join5(defaultTrustDir(), "<server-name>.extension-trust.json");
41004
41878
  }
41005
41879
  }
41006
41880
  function isPin(x) {
@@ -41013,7 +41887,7 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
41013
41887
  const path = extensionTrustPath(serverName, dir);
41014
41888
  let raw;
41015
41889
  try {
41016
- raw = await readFile2(path, "utf8");
41890
+ raw = await readFile3(path, "utf8");
41017
41891
  } catch (e) {
41018
41892
  if (e.code === "ENOENT")
41019
41893
  return null;
@@ -41037,7 +41911,7 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
41037
41911
  async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
41038
41912
  const path = extensionTrustPath(serverName, dir);
41039
41913
  await mkdir2(dir, { recursive: true, mode: 448 });
41040
- const tmp = `${path}.${randomBytes3(6).toString("hex")}.tmp`;
41914
+ const tmp = `${path}.${randomBytes5(6).toString("hex")}.tmp`;
41041
41915
  try {
41042
41916
  await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384, flag: "wx" });
41043
41917
  await chmod2(tmp, 384);
@@ -41186,6 +42060,7 @@ async function startHost(opts) {
41186
42060
  return true;
41187
42061
  }
41188
42062
  let extensionHello = null;
42063
+ let ownSessionInfo = null;
41189
42064
  let extensionClaim = null;
41190
42065
  const pairCodeFor = async (hello, mint) => {
41191
42066
  if (!mint)
@@ -41204,6 +42079,7 @@ async function startHost(opts) {
41204
42079
  return;
41205
42080
  console.warn(`[fetchproxy] ${opts.ownServerName}: ${OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE} frames in a row from the extension failed authentication under this MCP's session key \u2014 re-handshaking this MCP's session (the extension stays connected).`);
41206
42081
  ownSession = dropOwnSessionAndEphemeral();
42082
+ ownSessionInfo = null;
41207
42083
  resetSessionPromise();
41208
42084
  const mintedKeypair = await generateSessionKeypair();
41209
42085
  const mintedHello = await buildServerHello({
@@ -41262,28 +42138,39 @@ async function startHost(opts) {
41262
42138
  return;
41263
42139
  }
41264
42140
  extensionClaim = ws;
41265
- let pin;
41266
- try {
41267
- pin = await opts.extensionTrust.read();
41268
- } catch (e) {
41269
- console.error(`[fetchproxy] ${String(e)}`);
41270
- if (extensionClaim === ws)
41271
- extensionClaim = null;
41272
- ws.close(1008, "extension pin unreadable");
41273
- return;
42141
+ const managed = opts.extensionTrust.managed;
42142
+ let outcome;
42143
+ let refusalReason = "extension identity is not the pinned one";
42144
+ if (managed) {
42145
+ const decided = await evaluateManagedExtensionTrust(managed, frame, opts.ownServerName);
42146
+ outcome = decided;
42147
+ if (decided.decision === "refused") {
42148
+ refusalReason = decided.code === "EXTENSION_NOT_PINNED" ? "extension identity is not in the managed pin set" : "extension pin set unavailable";
42149
+ }
42150
+ } else {
42151
+ let pin;
42152
+ try {
42153
+ pin = await opts.extensionTrust.read();
42154
+ } catch (e) {
42155
+ console.error(`[fetchproxy] ${String(e)}`);
42156
+ if (extensionClaim === ws)
42157
+ extensionClaim = null;
42158
+ ws.close(1008, "extension pin unreadable");
42159
+ return;
42160
+ }
42161
+ outcome = decideExtensionTrust({
42162
+ pin,
42163
+ hello: frame,
42164
+ allowNew: opts.extensionTrust.allowNew,
42165
+ serverName: opts.ownServerName,
42166
+ location: opts.extensionTrust.location
42167
+ });
41274
42168
  }
41275
- const outcome = decideExtensionTrust({
41276
- pin,
41277
- hello: frame,
41278
- allowNew: opts.extensionTrust.allowNew,
41279
- serverName: opts.ownServerName,
41280
- location: opts.extensionTrust.location
41281
- });
41282
42169
  if (outcome.decision === "refused") {
41283
42170
  console.warn(outcome.message);
41284
42171
  if (extensionClaim === ws)
41285
42172
  extensionClaim = null;
41286
- ws.close(1008, "extension identity is not the pinned one");
42173
+ ws.close(1008, refusalReason);
41287
42174
  return;
41288
42175
  }
41289
42176
  if (outcome.decision === "replace")
@@ -41378,8 +42265,9 @@ async function startHost(opts) {
41378
42265
  console.warn("[fetchproxy] discarding a ready for a session ephemeral this host no longer holds (the extension reconnected while a hello was in flight)");
41379
42266
  return;
41380
42267
  }
41381
- const extEdPub = fromB64(extensionHello.identityEd25519Pub);
41382
- const extNonce = fromB64(extensionHello.sessionNonce);
42268
+ const verifiedHello = extensionHello;
42269
+ const extEdPub = fromB64(verifiedHello.identityEd25519Pub);
42270
+ const extNonce = fromB64(verifiedHello.sessionNonce);
41383
42271
  const msg = readySignaturePayload(held.nonce, extNonce, fromB64(frame.extensionSessionPub), held.pub);
41384
42272
  const sig = fromB64(frame.sessionSig);
41385
42273
  let sigOk = false;
@@ -41412,6 +42300,7 @@ async function startHost(opts) {
41412
42300
  if (extensionWs !== ws)
41413
42301
  return;
41414
42302
  ownSession = new SessionState(key);
42303
+ ownSessionInfo = sessionInfoFromHello(verifiedHello);
41415
42304
  ownDecryptFailures = 0;
41416
42305
  ownPendingPairCode = null;
41417
42306
  resolveOwnSession(ownSession);
@@ -41482,7 +42371,11 @@ async function startHost(opts) {
41482
42371
  }
41483
42372
  if (frame.type === "hello-rejected" && identified === "extension") {
41484
42373
  if (frame.mcpId === opts.ownMcpId) {
41485
- rejectOwnSession(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
42374
+ rejectOwnSession(new FetchproxyHelloRejectedError({
42375
+ mcpId: frame.mcpId,
42376
+ reason: frame.reason,
42377
+ platform: extensionHello?.platform ?? null
42378
+ }));
41486
42379
  } else {
41487
42380
  const slot = peers.get(frame.mcpId);
41488
42381
  if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
@@ -41528,6 +42421,7 @@ async function startHost(opts) {
41528
42421
  rejectOwnSession(new Error("extension disconnected before ready"));
41529
42422
  }
41530
42423
  ownSession = dropOwnSessionAndEphemeral();
42424
+ ownSessionInfo = null;
41531
42425
  ownDecryptFailures = 0;
41532
42426
  ownPendingPairCode = null;
41533
42427
  resetSessionPromise();
@@ -41574,6 +42468,7 @@ async function startHost(opts) {
41574
42468
  });
41575
42469
  if (!extensionWs)
41576
42470
  throw new Error("host: no extension connected");
42471
+ opts.refuseOutbound?.(inner, ownSessionInfo);
41577
42472
  const plaintext = encodeOutboundInnerFrame(opts.ownMcpId, inner);
41578
42473
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), plaintext, "s2e");
41579
42474
  extensionWs.send(JSON.stringify(sealed));
@@ -41589,7 +42484,8 @@ async function startHost(opts) {
41589
42484
  },
41590
42485
  pendingPairCode: () => ownPendingPairCode,
41591
42486
  extensionConnected: () => extensionWs !== null,
41592
- sessionLinked: () => ownSession !== null
42487
+ sessionLinked: () => ownSession !== null,
42488
+ sessionInfo: () => ownSession !== null ? ownSessionInfo : null
41593
42489
  };
41594
42490
  }
41595
42491
 
@@ -41674,6 +42570,7 @@ async function startPeer(opts) {
41674
42570
  const closeListeners = [];
41675
42571
  const extensionDisconnectListeners = [];
41676
42572
  let session = null;
42573
+ let sessionInfo = null;
41677
42574
  let sessionEphemeral = null;
41678
42575
  function dropSessionEphemeral() {
41679
42576
  if (sessionEphemeral)
@@ -41739,6 +42636,15 @@ async function startPeer(opts) {
41739
42636
  console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
41740
42637
  return null;
41741
42638
  }
42639
+ const managed = opts.extensionTrust.managed;
42640
+ if (managed) {
42641
+ const decided = await evaluateManagedExtensionTrust(managed, hello2, opts.serverName);
42642
+ if (decided.decision === "refused") {
42643
+ console.warn(decided.message);
42644
+ return null;
42645
+ }
42646
+ return hello2;
42647
+ }
41742
42648
  if (cachedPin === void 0) {
41743
42649
  try {
41744
42650
  cachedPin = await opts.extensionTrust.read();
@@ -41806,6 +42712,7 @@ async function startPeer(opts) {
41806
42712
  extensionGone = true;
41807
42713
  dropSessionEphemeral();
41808
42714
  pendingPairCode = null;
42715
+ sessionInfo = null;
41809
42716
  if (session !== null) {
41810
42717
  session = null;
41811
42718
  if (sessionPromiseSettled)
@@ -41838,6 +42745,7 @@ async function startPeer(opts) {
41838
42745
  }
41839
42746
  const isRenegotiation = session !== null;
41840
42747
  session = new SessionState(sessionKey);
42748
+ sessionInfo = sessionInfoFromHello(authenticated);
41841
42749
  extensionGone = false;
41842
42750
  pendingPairCode = null;
41843
42751
  if (isRenegotiation) {
@@ -41848,7 +42756,11 @@ async function startPeer(opts) {
41848
42756
  return;
41849
42757
  }
41850
42758
  if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
41851
- rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
42759
+ rejectFirstReady(new FetchproxyHelloRejectedError({
42760
+ mcpId: frame.mcpId,
42761
+ reason: frame.reason,
42762
+ platform: extensionHello?.platform ?? null
42763
+ }));
41852
42764
  }
41853
42765
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
41854
42766
  const hello2 = extensionHello;
@@ -41948,6 +42860,7 @@ async function startPeer(opts) {
41948
42860
  const s = session;
41949
42861
  if (s === null)
41950
42862
  throw new Error("peer: extension disconnected");
42863
+ opts.refuseOutbound?.(inner, sessionInfo);
41951
42864
  const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
41952
42865
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
41953
42866
  if (ws.readyState !== import_websocket.default.OPEN)
@@ -41966,6 +42879,7 @@ async function startPeer(opts) {
41966
42879
  pendingPairCode: () => pendingPairCode,
41967
42880
  extensionConnected: () => extensionHello !== null,
41968
42881
  sessionLinked: () => session !== null && !extensionGone,
42882
+ sessionInfo: () => session !== null && !extensionGone ? sessionInfo : null,
41969
42883
  onClose: (cb) => {
41970
42884
  closeListeners.push(cb);
41971
42885
  },
@@ -41978,7 +42892,9 @@ async function startPeer(opts) {
41978
42892
  }
41979
42893
 
41980
42894
  // node_modules/@fetchproxy/server/dist/error-kind.js
41981
- function classifyFetchError(error62) {
42895
+ function classifyFetchError(error62, code) {
42896
+ if (code === CAPABILITY_UNAVAILABLE_CODE)
42897
+ return "capability_unavailable";
41982
42898
  if (/^(in-page )?fetch threw:/.test(error62)) {
41983
42899
  return "tab_fetch_failed";
41984
42900
  }
@@ -41994,6 +42910,9 @@ function classifyFetchError(error62) {
41994
42910
  if (/not in domains \[/.test(error62)) {
41995
42911
  return "domain_denied";
41996
42912
  }
42913
+ if (parseCapabilityUnavailable(error62) !== null) {
42914
+ return "capability_unavailable";
42915
+ }
41997
42916
  if (/^capability .+ not granted/.test(error62)) {
41998
42917
  return "capability_denied";
41999
42918
  }
@@ -42072,7 +42991,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
42072
42991
  const retryAttempted = args.retryAttempted ?? false;
42073
42992
  const op = args.op ?? "fetch";
42074
42993
  const retryClause = retryAttempted ? `Server already burned a one-shot lazy-revive retry; SW is still down. ` : `Server lazy-revive retry was disabled (bridgeReviveDelayMs unset/0). `;
42075
- const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
42994
+ const hint = `ContextMint Bridge's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
42076
42995
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
42077
42996
  this.name = "FetchproxyBridgeDownError";
42078
42997
  this.originalError = args.originalError;
@@ -42099,7 +43018,7 @@ var FetchproxyHintedError = class extends FetchproxyProtocolError {
42099
43018
  };
42100
43019
  var FetchproxyScopeError = class extends FetchproxyHintedError {
42101
43020
  constructor(originalError) {
42102
- super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
43021
+ super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the ContextMint Bridge extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
42103
43022
  this.name = "FetchproxyScopeError";
42104
43023
  }
42105
43024
  };
@@ -42121,6 +43040,19 @@ var FetchproxyWaitedError = class extends FetchproxyHintedError {
42121
43040
  this.name = "FetchproxyWaitedError";
42122
43041
  }
42123
43042
  };
43043
+ var FetchproxyCapabilityUnavailableError = class extends FetchproxyHintedError {
43044
+ /** The capability the browser cannot serve (`graphql` for a `graphql_query`). */
43045
+ capability;
43046
+ /** The browser, when known (`'safari'`); `null` when the extension did not say. */
43047
+ platform;
43048
+ constructor(originalError, info) {
43049
+ const where = info.platform ? `this browser (${info.platform})` : "this browser";
43050
+ super(originalError, `${where} cannot serve the "${info.capability}" capability \u2014 ContextMint Bridge checked for the browser API it needs and it is missing. The rest of this MCP still works here. Nothing is wrong with the MCP's code or your pairing, and neither updating nor re-approving will change it: use this tool from a browser that provides the API (for example Chrome).`);
43051
+ this.name = "FetchproxyCapabilityUnavailableError";
43052
+ this.capability = info.capability;
43053
+ this.platform = info.platform;
43054
+ }
43055
+ };
42124
43056
  function waitedHint(op) {
42125
43057
  const tail = " This is not a version problem and does not need an update.";
42126
43058
  if (op === "capture" || op === "capture_redirect") {
@@ -42134,7 +43066,15 @@ function waitedHint(op) {
42134
43066
  var SCOPE_REJECTION = /not in declared/;
42135
43067
  var NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
42136
43068
  var TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
42137
- function protocolErrorFrom(error62, op) {
43069
+ function protocolErrorFrom(error62, op, response) {
43070
+ const unavailable = parseCapabilityUnavailable(error62);
43071
+ if (unavailable !== null || response?.code === CAPABILITY_UNAVAILABLE_CODE) {
43072
+ const fromOp = response?.op === "graphql_query" ? "graphql" : response?.op ?? "unknown";
43073
+ return new FetchproxyCapabilityUnavailableError(error62, {
43074
+ capability: unavailable?.capability ?? fromOp,
43075
+ platform: unavailable?.platform ?? null
43076
+ });
43077
+ }
42138
43078
  if (error62 === "timeout")
42139
43079
  return new FetchproxyWaitedError(error62, op);
42140
43080
  if (SCOPE_REJECTION.test(error62))
@@ -42193,7 +43133,8 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
42193
43133
  constructor(args) {
42194
43134
  const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
42195
43135
  const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
42196
- super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (callerChose ? ` \u2014 the ${args.requestedTimeoutMs}ms this call asked for, plus ${graceMs}ms for the reply to reach this server. The extension runs its own timer on the value you passed, so a window that is genuinely too short reports what did not happen rather than this.` : ""));
43136
+ const base = `fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (callerChose ? ` \u2014 the ${args.requestedTimeoutMs}ms this call asked for, plus ${graceMs}ms for the reply to reach this server. The extension runs its own timer on the value you passed, so a window that is genuinely too short reports what did not happen rather than this.` : "");
43137
+ super(args.retrySafe === false ? withNotResentNote(base) : base);
42197
43138
  this.name = "FetchproxyTimeoutError";
42198
43139
  this.url = args.url;
42199
43140
  this.timeoutMs = args.timeoutMs;
@@ -42205,6 +43146,10 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
42205
43146
  this.retrySafe = args.retrySafe ?? true;
42206
43147
  }
42207
43148
  };
43149
+ var TIMEOUT_NOT_RESENT_NOTE = "It was not re-sent because it may already have run in the browser \u2014 check before retrying.";
43150
+ function withNotResentNote(message) {
43151
+ return `${message}${message.endsWith(".") ? " " : ". "}${TIMEOUT_NOT_RESENT_NOTE}`;
43152
+ }
42208
43153
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
42209
43154
  function normalizeCookiePath(path) {
42210
43155
  if (path === void 0 || path === "")
@@ -42464,6 +43409,7 @@ var FetchproxyServer = class {
42464
43409
  identityDir: opts.identityDir,
42465
43410
  trustDir: opts.trustDir,
42466
43411
  allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
43412
+ extensionPins: opts.extensionPins,
42467
43413
  requireExtensionIdentity: opts.requireExtensionIdentity,
42468
43414
  onPairCode: opts.onPairCode
42469
43415
  };
@@ -42549,6 +43495,7 @@ var FetchproxyServer = class {
42549
43495
  this.role = "host";
42550
43496
  this.hostHandle = await startHost({
42551
43497
  httpServer: el.server,
43498
+ refuseOutbound: (inner, info) => this.refuseIfUnavailable(inner, info),
42552
43499
  ownIdentity: identity,
42553
43500
  ownMcpId: mcpId,
42554
43501
  ownServerName: this.opts.serverName,
@@ -42579,6 +43526,7 @@ var FetchproxyServer = class {
42579
43526
  } else {
42580
43527
  this.role = "peer";
42581
43528
  this.peerHandle = await startPeer({
43529
+ refuseOutbound: (inner, info) => this.refuseIfUnavailable(inner, info),
42582
43530
  host: this.opts.host,
42583
43531
  port: this.opts.port,
42584
43532
  identity,
@@ -42627,7 +43575,7 @@ var FetchproxyServer = class {
42627
43575
  }
42628
43576
  }
42629
43577
  pairingErrorMessage(code) {
42630
- return `fetchproxy transport error: pairing required for ${this.opts.serverName}. Tell the user to open the Transporter browser extension popup and approve the pair request. The pair code is: ${code} \u2014 display this code to the user so they can verify it matches.`;
43578
+ return `fetchproxy transport error: pairing required for ${this.opts.serverName}. Tell the user to open the ContextMint Bridge browser extension popup and approve the pair request. The pair code is: ${code} \u2014 display this code to the user so they can verify it matches.`;
42631
43579
  }
42632
43580
  /**
42633
43581
  * Raw single-shot fetch through the bridge. Most callers should prefer
@@ -42757,13 +43705,28 @@ var FetchproxyServer = class {
42757
43705
  * every one of them.
42758
43706
  */
42759
43707
  extensionTrust() {
43708
+ const allowNew = allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity);
43709
+ if (resolveExtensionPinsMode(this.opts.extensionPins) === "managed") {
43710
+ if (allowNew && !this.warnedAllowNewIgnored) {
43711
+ this.warnedAllowNewIgnored = true;
43712
+ console.warn(`[fetchproxy] ${this.opts.serverName}: ignoring ${TRUST_NEW_EXTENSION_ENV} / allowNewExtensionIdentity \u2014 this MCP's extension pins are host-managed (${EXTENSION_PINS_ENV}=managed), so only the host's pin set decides which browsers it accepts.`);
43713
+ }
43714
+ return fileExtensionTrust({
43715
+ serverName: this.opts.serverName,
43716
+ dir: this.opts.identityDir,
43717
+ trustDir: this.opts.trustDir,
43718
+ allowNew: false,
43719
+ extensionPins: "managed"
43720
+ });
43721
+ }
42760
43722
  return fileExtensionTrust({
42761
43723
  serverName: this.opts.serverName,
42762
43724
  dir: this.opts.identityDir,
42763
43725
  trustDir: this.opts.trustDir,
42764
- allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
43726
+ allowNew
42765
43727
  });
42766
43728
  }
43729
+ warnedAllowNewIgnored = false;
42767
43730
  noteActivityForKeepalive() {
42768
43731
  const intervalMs = this.opts.keepAliveIntervalMs;
42769
43732
  if (intervalMs <= 0)
@@ -42857,6 +43820,15 @@ var FetchproxyServer = class {
42857
43820
  resendable: isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout)
42858
43821
  });
42859
43822
  } catch (err) {
43823
+ if (err instanceof FetchproxyCapabilityUnavailableError) {
43824
+ return {
43825
+ ok: false,
43826
+ error: err.originalError,
43827
+ kind: "capability_unavailable",
43828
+ retryAttempted: false,
43829
+ cause: err
43830
+ };
43831
+ }
42860
43832
  return {
42861
43833
  ok: false,
42862
43834
  error: err instanceof Error ? err.message : String(err),
@@ -42877,7 +43849,8 @@ var FetchproxyServer = class {
42877
43849
  timer = setTimeout(() => {
42878
43850
  this.pending.delete(id);
42879
43851
  const elapsedMs = Date.now() - start;
42880
- const error62 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
43852
+ const base = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
43853
+ const error62 = isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout) ? base : withNotResentNote(base);
42881
43854
  resolve6({
42882
43855
  ok: false,
42883
43856
  error: error62,
@@ -42999,6 +43972,9 @@ var FetchproxyServer = class {
42999
43972
  port: this.opts.port
43000
43973
  });
43001
43974
  }
43975
+ if (result.kind === "capability_unavailable") {
43976
+ return protocolErrorFrom(result.error, void 0, { code: CAPABILITY_UNAVAILABLE_CODE, op });
43977
+ }
43002
43978
  return protocolErrorFrom(result.error);
43003
43979
  }
43004
43980
  /**
@@ -43065,6 +44041,9 @@ var FetchproxyServer = class {
43065
44041
  if (result.kind === "session_not_ready" && result.cause instanceof Error) {
43066
44042
  throw result.cause;
43067
44043
  }
44044
+ if (result.kind === "capability_unavailable" && result.cause instanceof FetchproxyCapabilityUnavailableError) {
44045
+ throw result.cause;
44046
+ }
43068
44047
  throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
43069
44048
  }
43070
44049
  const response = {
@@ -43247,7 +44226,9 @@ var FetchproxyServer = class {
43247
44226
  session_state: health.session.state,
43248
44227
  pending_pair_code: health.session.pairCode,
43249
44228
  extension_connected: health.session.extensionConnected,
43250
- last_extension_message_at: health.lastExtensionMessageAt
44229
+ last_extension_message_at: health.lastExtensionMessageAt,
44230
+ unavailable_capabilities: health.session.unavailableCapabilities,
44231
+ platform: health.session.platform
43251
44232
  },
43252
44233
  ...error62 ? { error: error62 } : {}
43253
44234
  };
@@ -43893,7 +44874,7 @@ var FetchproxyServer = class {
43893
44874
  fetchCb({
43894
44875
  ok: false,
43895
44876
  error: inner.error,
43896
- kind: classifyFetchError(inner.error),
44877
+ kind: classifyFetchError(inner.error, inner.code),
43897
44878
  retryAttempted: false
43898
44879
  });
43899
44880
  }
@@ -43909,7 +44890,7 @@ var FetchproxyServer = class {
43909
44890
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
43910
44891
  }
43911
44892
  } else {
43912
- storageCb.reject(protocolErrorFrom(inner.error));
44893
+ storageCb.reject(protocolErrorFrom(inner.error, void 0, inner));
43913
44894
  }
43914
44895
  return;
43915
44896
  }
@@ -43923,7 +44904,7 @@ var FetchproxyServer = class {
43923
44904
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
43924
44905
  }
43925
44906
  } else {
43926
- captureCb.reject(protocolErrorFrom(inner.error, "capture"));
44907
+ captureCb.reject(protocolErrorFrom(inner.error, "capture", inner));
43927
44908
  }
43928
44909
  return;
43929
44910
  }
@@ -43937,7 +44918,7 @@ var FetchproxyServer = class {
43937
44918
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
43938
44919
  }
43939
44920
  } else {
43940
- redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect"));
44921
+ redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect", inner));
43941
44922
  }
43942
44923
  return;
43943
44924
  }
@@ -43951,7 +44932,7 @@ var FetchproxyServer = class {
43951
44932
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
43952
44933
  }
43953
44934
  } else {
43954
- idbCb.reject(protocolErrorFrom(inner.error));
44935
+ idbCb.reject(protocolErrorFrom(inner.error, void 0, inner));
43955
44936
  }
43956
44937
  return;
43957
44938
  }
@@ -43965,7 +44946,7 @@ var FetchproxyServer = class {
43965
44946
  domListCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_dom_list awaiter`));
43966
44947
  }
43967
44948
  } else {
43968
- domListCb.reject(protocolErrorFrom(inner.error));
44949
+ domListCb.reject(protocolErrorFrom(inner.error, void 0, inner));
43969
44950
  }
43970
44951
  return;
43971
44952
  }
@@ -43979,7 +44960,7 @@ var FetchproxyServer = class {
43979
44960
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
43980
44961
  }
43981
44962
  } else {
43982
- downloadCb.reject(protocolErrorFrom(inner.error, "download"));
44963
+ downloadCb.reject(protocolErrorFrom(inner.error, "download", inner));
43983
44964
  }
43984
44965
  return;
43985
44966
  }
@@ -43993,7 +44974,7 @@ var FetchproxyServer = class {
43993
44974
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
43994
44975
  }
43995
44976
  } else {
43996
- graphqlCb.reject(protocolErrorFrom(inner.error));
44977
+ graphqlCb.reject(protocolErrorFrom(inner.error, void 0, inner));
43997
44978
  }
43998
44979
  return;
43999
44980
  }
@@ -44003,7 +44984,7 @@ var FetchproxyServer = class {
44003
44984
  if (inner.ok && inner.op === "write_cookies") {
44004
44985
  writeCookiesCb.resolve([...inner.written]);
44005
44986
  } else {
44006
- writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
44987
+ writeCookiesCb.reject(inner.ok ? protocolErrorFrom("write_cookies response had the wrong op") : protocolErrorFrom(inner.error, void 0, inner));
44007
44988
  }
44008
44989
  return;
44009
44990
  }
@@ -44193,11 +45174,38 @@ var FetchproxyServer = class {
44193
45174
  const handle = this.hostHandle ?? this.peerHandle;
44194
45175
  const pairCode = this.currentPendingPairCode();
44195
45176
  if (!handle || this.role === null) {
44196
- return { state: "not_listening", pairCode, extensionConnected: false };
45177
+ return {
45178
+ state: "not_listening",
45179
+ pairCode,
45180
+ extensionConnected: false,
45181
+ unavailableCapabilities: [],
45182
+ platform: null
45183
+ };
44197
45184
  }
45185
+ const info = handle.sessionInfo?.() ?? null;
44198
45186
  const extensionConnected = handle.extensionConnected();
44199
45187
  const state = handle.sessionLinked() ? "linked" : pairCode !== null ? "pair_pending" : !extensionConnected ? "extension_disconnected" : "no_session";
44200
- return { state, pairCode, extensionConnected };
45188
+ return {
45189
+ state,
45190
+ pairCode,
45191
+ extensionConnected,
45192
+ unavailableCapabilities: info ? [...info.unavailableCapabilities] : [],
45193
+ platform: info?.platform ?? null
45194
+ };
45195
+ }
45196
+ /**
45197
+ * #418: refuse, before a seq is claimed, a request whose capability the
45198
+ * extension said this browser cannot serve. Called by the host / peer
45199
+ * handle once the session is ready, so it judges against the list from the
45200
+ * hello that session was verified against. Advisory: the list is unsigned,
45201
+ * so it may only ever make this MCP refuse MORE — the extension still
45202
+ * enforces from its own runtime check for anything that gets through.
45203
+ */
45204
+ refuseIfUnavailable(inner, info) {
45205
+ const cap = unavailableCapabilityFor(inner, info);
45206
+ if (cap === null || info === null)
45207
+ return;
45208
+ throw new FetchproxyCapabilityUnavailableError(capabilityUnavailableMessage(cap, info.platform), { capability: cap, platform: info.platform });
44201
45209
  }
44202
45210
  /**
44203
45211
  * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
@@ -44574,6 +45582,9 @@ function registerCredentialHealthcheckTool(args) {
44574
45582
  }, async () => runCredentialHealthcheck(args));
44575
45583
  }
44576
45584
 
45585
+ // src/auth-password.ts
45586
+ import { createHash as createHash4 } from "node:crypto";
45587
+
44577
45588
  // src/protocol.ts
44578
45589
  var BASE_URL = "https://ofw.ourfamilywizard.com";
44579
45590
  var OFW_PROTOCOL_HEADERS = {
@@ -44598,7 +45609,23 @@ function assertOfwUrl(rawUrl) {
44598
45609
  }
44599
45610
 
44600
45611
  // src/auth-password.ts
45612
+ var CredentialsRejectedError = class extends Error {
45613
+ constructor(message) {
45614
+ super(message);
45615
+ this.name = "CredentialsRejectedError";
45616
+ }
45617
+ };
45618
+ var rejectedPairs = /* @__PURE__ */ new Set();
45619
+ function pairDigest(username, password) {
45620
+ return createHash4("sha256").update(`${username.length}:${username}\0${password}`).digest("hex");
45621
+ }
44601
45622
  async function loginWithPassword(username, password) {
45623
+ const digest = pairDigest(username, password);
45624
+ if (rejectedPairs.has(digest)) {
45625
+ throw new CredentialsRejectedError(
45626
+ "OFW login not attempted \u2014 this OurFamilyWizard email and password were already rejected by OFW earlier in this session, and OFW counts failed sign-ins against the account, so they are not re-sent. Update OFW_USERNAME / OFW_PASSWORD to the current values (or restart the server) and try again."
45627
+ );
45628
+ }
44602
45629
  const initUrl = `${BASE_URL}/ofw/login.form`;
44603
45630
  assertOfwUrl(initUrl);
44604
45631
  const initResponse = await fetch(initUrl, {
@@ -44634,8 +45661,9 @@ async function loginWithPassword(username, password) {
44634
45661
  const contentType = response.headers.get("content-type") ?? "";
44635
45662
  if (!contentType.includes("application/json")) {
44636
45663
  if (contentType.includes("text/html")) {
44637
- throw new Error(
44638
- "OFW login failed \u2014 your OurFamilyWizard email or password was not accepted. Check them and try again."
45664
+ rejectedPairs.add(digest);
45665
+ throw new CredentialsRejectedError(
45666
+ "OFW login failed \u2014 your OurFamilyWizard email or password was not accepted. Check them and try again. They will not be re-sent until OFW_USERNAME / OFW_PASSWORD change or the server restarts."
44639
45667
  );
44640
45668
  }
44641
45669
  const body = await response.text();
@@ -44651,7 +45679,7 @@ async function loginWithPassword(username, password) {
44651
45679
  // package.json
44652
45680
  var package_default = {
44653
45681
  name: "ofw-mcp",
44654
- version: "2.19.3",
45682
+ version: "2.19.5",
44655
45683
  license: "MIT",
44656
45684
  mcpName: "io.github.chrischall/ofw-mcp",
44657
45685
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -44685,8 +45713,8 @@ var package_default = {
44685
45713
  typecheck: "tsc -p tsconfig.json --noEmit"
44686
45714
  },
44687
45715
  dependencies: {
44688
- "@chrischall/mcp-utils": "^2.4.0",
44689
- "@fetchproxy/bootstrap": "^3.2.0",
45716
+ "@chrischall/mcp-utils": "^2.8.0",
45717
+ "@fetchproxy/bootstrap": "^3.4.1",
44690
45718
  "@modelcontextprotocol/server": "^2.0.0",
44691
45719
  dotenv: "^18.0.0",
44692
45720
  zod: "^4.6.5"
@@ -44705,8 +45733,8 @@ var package_default = {
44705
45733
  function fetchproxyDisabled() {
44706
45734
  return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
44707
45735
  }
44708
- var NO_AUTH_CONFIGURED = "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set).";
44709
- var BRIDGE_DOWN_PREFIX = "OFW auth: fetchproxy bridge is down";
45736
+ var NO_AUTH_CONFIGURED = "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install ContextMint Bridge and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set).";
45737
+ var BRIDGE_DOWN_PREFIX = "OFW auth: ContextMint Bridge is down";
44710
45738
  function isBridgeDown(e) {
44711
45739
  return e instanceof Error && e.message.startsWith(BRIDGE_DOWN_PREFIX);
44712
45740
  }
@@ -44748,7 +45776,7 @@ async function resolveAuth() {
44748
45776
  const expiryRaw = session.localStorage["tokenExpiry"];
44749
45777
  if (!token) {
44750
45778
  throw new Error(
44751
- 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with the fetchproxy extension installed) and retry.'
45779
+ 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with ContextMint Bridge installed) and retry.'
44752
45780
  );
44753
45781
  }
44754
45782
  return {
@@ -44823,7 +45851,7 @@ function reportCacheWriteFailure(err) {
44823
45851
  // src/client.ts
44824
45852
  try {
44825
45853
  const dir = dirname3(fileURLToPath(import.meta.url));
44826
- await loadDotenvSafely({ path: join5(dir, "..", ".env") });
45854
+ await loadDotenvSafely({ path: join6(dir, "..", ".env") });
44827
45855
  } catch {
44828
45856
  }
44829
45857
  function parseContentDispositionFilename(cd) {
@@ -45019,9 +46047,9 @@ function offsetAt(instant, tz) {
45019
46047
  const w = wallPartsIn(instant, tz);
45020
46048
  const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
45021
46049
  const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
45022
- const sign = minutes < 0 ? "-" : "+";
46050
+ const sign2 = minutes < 0 ? "-" : "+";
45023
46051
  const abs = Math.abs(minutes);
45024
- return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
46052
+ return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
45025
46053
  }
45026
46054
  var wallPartsFormatters = /* @__PURE__ */ new Map();
45027
46055
  var displayFormatters = /* @__PURE__ */ new Map();
@@ -45312,6 +46340,21 @@ function isDefinitiveRejection(e) {
45312
46340
  const m = /OFW API error: (4\d\d)\b/.exec(e.message);
45313
46341
  return m !== null && m[1] !== "408";
45314
46342
  }
46343
+ async function requestWrite(client2, method, path, body) {
46344
+ try {
46345
+ return await client2.request(method, path, body);
46346
+ } catch (e) {
46347
+ throw isDefinitiveRejection(e) ? e : new UnconfirmedWriteError(null, e);
46348
+ }
46349
+ }
46350
+ function unconfirmedWriteResponse(e, opts) {
46351
+ return jsonErrorResponse({
46352
+ result: opts.result,
46353
+ mayHaveLanded: true,
46354
+ reason: `The request to ${opts.what} failed without a definitive answer from OFW: ${e.message}. It MAY HAVE BEEN APPLIED on OurFamilyWizard.`,
46355
+ remedy: `Do NOT retry blindly \u2014 a second attempt can put a duplicate on the co-parent-visible record. First check with ${opts.checkWith} (or on ourfamilywizard.com), and retry only once you have confirmed it did not land.`
46356
+ });
46357
+ }
45315
46358
  async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
45316
46359
  let posted;
45317
46360
  try {
@@ -45392,12 +46435,12 @@ function registerHealthcheckTools(server, client2, resolve6 = resolveAuth) {
45392
46435
  kind: "transport",
45393
46436
  // The upstream `.hint` rides along in `error.message` — it carries
45394
46437
  // the actionable "click the toolbar icon" copy this cannot know.
45395
- hint: "The fetchproxy bridge is down, so the browser path could not be tried. This is not a credential problem: OFW_USERNAME/OFW_PASSWORD, if set, were not reached either. See error.message for the extension-specific fix."
46438
+ hint: "ContextMint Bridge is down, so the browser path could not be tried. This is not a credential problem: OFW_USERNAME/OFW_PASSWORD, if set, were not reached either. See error.message for the extension-specific fix."
45396
46439
  } : void 0,
45397
46440
  hints: {
45398
46441
  // Now means exactly what it says: nothing is set up. A configured path
45399
46442
  // that was tried and failed no longer lands here.
45400
- no_credential: "No OFW credential is configured. Either set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign in to ourfamilywizard.com in a tab (unsetting OFW_DISABLE_FETCHPROXY if you set it).",
46443
+ no_credential: "No OFW credential is configured. Either set OFW_USERNAME + OFW_PASSWORD, or install ContextMint Bridge and sign in to ourfamilywizard.com in a tab (unsetting OFW_DISABLE_FETCHPROXY if you set it).",
45401
46444
  credential_rejected: "OurFamilyWizard rejected the credential. If it came from `env`, the password changed or the account is locked; if from `fetchproxy`, the browser session expired \u2014 sign in again in the tab. Retrying will not fix either."
45402
46445
  }
45403
46446
  });
@@ -45822,26 +46865,29 @@ async function syncAll(client2, opts, store) {
45822
46865
  }
45823
46866
 
45824
46867
  // src/config.ts
45825
- import { createHash } from "node:crypto";
46868
+ import { createHash as createHash5 } from "node:crypto";
45826
46869
  import { homedir as homedir4 } from "node:os";
45827
- import { join as join6 } from "node:path";
46870
+ import { join as join7 } from "node:path";
45828
46871
  function readCacheIdentity() {
45829
46872
  return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
45830
46873
  }
45831
46874
  function getCacheDir() {
45832
46875
  const override = process.env.OFW_CACHE_DIR;
45833
46876
  if (override && override.trim().length > 0) return override.trim();
45834
- return join6(homedir4(), ".cache", "ofw-mcp");
46877
+ return join7(homedir4(), ".cache", "ofw-mcp");
45835
46878
  }
45836
46879
  function getCacheDbPath() {
45837
46880
  const identity = readCacheIdentity();
45838
- const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
45839
- return join6(getCacheDir(), `${hash2}.db`);
46881
+ const hash2 = createHash5("sha256").update(identity).digest("hex").slice(0, 16);
46882
+ return join7(getCacheDir(), `${hash2}.db`);
45840
46883
  }
45841
46884
  function getAttachmentsDir() {
45842
46885
  const override = process.env.OFW_ATTACHMENTS_DIR;
45843
46886
  if (override && override.trim().length > 0) return override.trim();
45844
- return join6(homedir4(), "Downloads", "ofw-mcp");
46887
+ return getDefaultAttachmentsDir();
46888
+ }
46889
+ function getDefaultAttachmentsDir() {
46890
+ return join7(homedir4(), "Downloads", "ofw-mcp");
45845
46891
  }
45846
46892
  function getUploadDir() {
45847
46893
  const override = process.env.OFW_UPLOAD_DIR;
@@ -46349,6 +47395,31 @@ function newDraftKey() {
46349
47395
  return `dk_${crypto.randomUUID()}`;
46350
47396
  }
46351
47397
 
47398
+ // src/tools/_confirm.ts
47399
+ var CONFIRM_NOTE = "Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call performs NO write and returns a preview of exactly what would happen plus a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).";
47400
+ function confirmWrite(ctx, opts) {
47401
+ return requireConfirmationWithFallback(
47402
+ ctx,
47403
+ confirmationFromEnv({
47404
+ action: opts.action,
47405
+ message: opts.message,
47406
+ details: opts.preview,
47407
+ unsupportedNote: "Complete this action on ourfamilywizard.com instead.",
47408
+ tool: opts.tool,
47409
+ confirmToken: opts.confirmToken,
47410
+ subject: () => ({
47411
+ target: opts.target,
47412
+ ...opts.revision !== void 0 ? { revision: opts.revision } : {},
47413
+ payload: opts.payload,
47414
+ preview: opts.preview
47415
+ })
47416
+ })
47417
+ );
47418
+ }
47419
+ function stateRevision(value) {
47420
+ return `s1:${fnv1a64(JSON.stringify(value))}`;
47421
+ }
47422
+
46352
47423
  // src/extract/inflate.ts
46353
47424
  var MAX_DECOMPRESSED_BYTES = 32 * 1024 * 1024;
46354
47425
  var DecompressionLimitError = class extends Error {
@@ -47229,7 +48300,7 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
47229
48300
  }
47230
48301
 
47231
48302
  // src/tools/attachments.ts
47232
- import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
48303
+ import { chmodSync as chmodSync2, existsSync as existsSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
47233
48304
  import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute5, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
47234
48305
  function isWithin(root, candidate) {
47235
48306
  const rel = relative2(root, candidate);
@@ -47321,7 +48392,7 @@ var NodeAttachmentIO = class {
47321
48392
  }
47322
48393
  const fileName = basename(abs);
47323
48394
  const mimeType = mimeFromName(fileName);
47324
- const blob = await fileBlob(real, { type: mimeType });
48395
+ const blob = await fileBlob(real, { type: mimeType, allowedRoots: [realRoot] });
47325
48396
  return { blob, fileName, mimeType, sizeBytes: stat.size };
47326
48397
  }
47327
48398
  readDownloaded(path) {
@@ -47332,14 +48403,15 @@ var NodeAttachmentIO = class {
47332
48403
  }
47333
48404
  }
47334
48405
  writeDownload(dest, bytes, { root, overwrite }) {
47335
- mkdirSync2(root, { recursive: true });
48406
+ mkdirSync2(root, { recursive: true, mode: 448 });
48407
+ if (resolve4(root) === resolve4(getDefaultAttachmentsDir())) chmodSync2(root, 448);
47336
48408
  const realRoot = realpathSync2(root);
47337
48409
  const parent = dirname4(dest);
47338
48410
  const anchor2 = realpathSync2(deepestExisting(parent));
47339
48411
  if (anchor2 !== realRoot && !isWithin(realRoot, anchor2)) {
47340
48412
  throw new Error(`Refusing to write ${dest}: it resolves outside the attachments directory (${root}).`);
47341
48413
  }
47342
- mkdirSync2(parent, { recursive: true });
48414
+ mkdirSync2(parent, { recursive: true, mode: 448 });
47343
48415
  if (overwrite) {
47344
48416
  try {
47345
48417
  unlinkSync2(dest);
@@ -47358,6 +48430,7 @@ var NodeAttachmentIO = class {
47358
48430
  };
47359
48431
 
47360
48432
  // src/tools/delivery.ts
48433
+ var MAX_INLINE_BYTES = 10 * 1024 * 1024;
47361
48434
  async function tryExtract(bytes, mimeType, fileName, opts) {
47362
48435
  try {
47363
48436
  const extracted = await extractAttachment(bytes, mimeType, fileName, {
@@ -47372,6 +48445,17 @@ async function tryExtract(bytes, mimeType, fileName, opts) {
47372
48445
  return { reason: `extraction failed: ${err instanceof Error ? err.message : String(err)}` };
47373
48446
  }
47374
48447
  }
48448
+ function assertInlineSize(input2, rawRequested) {
48449
+ const { bytes, fileName, fileId, diskAvailable } = input2;
48450
+ if (bytes.length <= MAX_INLINE_BYTES) return;
48451
+ const hints = [];
48452
+ if (rawRequested) hints.push("omit extract:false to get the file's extracted text instead, if it is a readable document type");
48453
+ if (diskAvailable) hints.push("pass inline:false to save it to disk and get the path");
48454
+ hints.push("or open it directly in OurFamilyWizard");
48455
+ throw new Error(
48456
+ `Attachment ${fileId} (${fileName}) is ${bytes.length} bytes, over the ${MAX_INLINE_BYTES / (1024 * 1024)} MiB limit for returning a file inline (a larger result would exceed the host's response size limit). To get it: ${hints.join("; ")}.`
48457
+ );
48458
+ }
47375
48459
  async function buildInlineDelivery(input2) {
47376
48460
  const { fileId, fileName, mimeType, bytes, forcedInline, options } = input2;
47377
48461
  const meta3 = {
@@ -47384,6 +48468,7 @@ async function buildInlineDelivery(input2) {
47384
48468
  if (forcedInline) meta3.forcedInline = true;
47385
48469
  const block = () => ({ type: "text", text: JSON.stringify(meta3, null, 2) });
47386
48470
  if (isHostRenderableImage(mimeType)) {
48471
+ assertInlineSize(input2, false);
47387
48472
  meta3.deliveredVia = "image";
47388
48473
  return { content: [block(), { type: "image", data: bytes.toString("base64"), mimeType }] };
47389
48474
  }
@@ -47401,6 +48486,7 @@ async function buildInlineDelivery(input2) {
47401
48486
  }
47402
48487
  attempts.push(outcome.reason ?? "extraction produced no content");
47403
48488
  }
48489
+ assertInlineSize(input2, options.extract === false);
47404
48490
  meta3.deliveredVia = "blob";
47405
48491
  meta3.deliveryAttempts = attempts;
47406
48492
  meta3.note = "Returned as raw bytes. Some hosts cannot render an embedded resource of this type; if it came back unreadable, the file has no text extractor here (see deliveryAttempts).";
@@ -47417,7 +48503,7 @@ async function buildInlineDelivery(input2) {
47417
48503
  }
47418
48504
 
47419
48505
  // src/tools/messages.ts
47420
- import { basename as basename2, join as join7, resolve as resolve5 } from "node:path";
48506
+ import { basename as basename2, join as join8, resolve as resolve5 } from "node:path";
47421
48507
 
47422
48508
  // src/tools/pagination.ts
47423
48509
  function pageState(input2) {
@@ -47682,6 +48768,19 @@ function markReadVerdict(cached2, requested) {
47682
48768
  ...cached2 === null ? { hint: "Run ofw_sync_messages first: it walks list pages, not bodies, so it can tell you what this id is without stamping anything." } : { subject: cached2.subject, fromUser: cached2.fromUser, sentAt: cached2.sentAt }
47683
48769
  });
47684
48770
  }
48771
+ async function describeRecipients(cache, recipientIds, known) {
48772
+ const names = /* @__PURE__ */ new Map();
48773
+ const learn = (rs) => {
48774
+ for (const r of rs ?? []) {
48775
+ if (r.userId !== 0 && r.name && !names.has(r.userId)) names.set(r.userId, r.name);
48776
+ }
48777
+ };
48778
+ known.forEach(learn);
48779
+ if (recipientIds.some((id) => !names.has(id))) {
48780
+ for (const row of await cache.listMessages({ page: 1, size: 200 })) learn(row.recipients);
48781
+ }
48782
+ return recipientIds.map((userId) => ({ userId, name: names.get(userId) ?? null }));
48783
+ }
47685
48784
  function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47686
48785
  const writeMode = getWriteMode();
47687
48786
  const allowSend = writeMode === "all";
@@ -47938,8 +49037,8 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47938
49037
  return jsonResponse({ ...viewOne(view, withReadState(row)), attachments, freshness });
47939
49038
  });
47940
49039
  if (allowSend) server.registerTool("ofw_send_message", {
47941
- description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. If the send request times out or drops without a definitive answer, the result is SEND_UNCONFIRMED: the message may already have been delivered, so do NOT retry until a sent-folder sync (or ourfamilywizard.com) shows it did not go out. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted.",
47942
- annotations: { destructiveHint: true },
49040
+ description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. If the send request times out or drops without a definitive answer, the result is SEND_UNCONFIRMED: the message may already have been delivered, so do NOT retry until a sent-folder sync (or ourfamilywizard.com) shows it did not go out. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted. " + CONFIRM_NOTE,
49041
+ annotations: { readOnlyHint: false, destructiveHint: true },
47943
49042
  inputSchema: external_exports.object({
47944
49043
  subject: external_exports.string().describe("Message subject. Required unless draftId/messageId is given (then it overrides the server draft's subject).").optional(),
47945
49044
  body: external_exports.string().describe("Message body text. Required unless draftId/messageId is given (then it overrides the server draft's body \u2014 omit it to send exactly what is on OurFamilyWizard).").optional(),
@@ -47950,9 +49049,10 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47950
49049
  expectedRevision: external_exports.string().describe('With draftId: the `revision` from ofw_list_drafts / ofw_get_message / ofw_check_freshness for that draft. Asserts you are sending THAT version; if the draft changed on OFW since, the send is refused and the current server content returned. Omit and the tool compares the server against the local cache instead \u2014 omitting never means "send whatever is there now".').optional(),
47951
49050
  deleteDraftOnSuccess: external_exports.boolean().describe("Default true. Delete the source draft after \u2014 and ONLY after \u2014 the send is confirmed (new message id returned and the re-fetched sent record checks out). Set false to keep the draft. On a failed or unverifiable send the draft is ALWAYS kept, regardless of this flag.").optional(),
47952
49051
  force: external_exports.boolean().describe("Default false. Send even when the draft changed on OurFamilyWizard since you read it, or its current state could not be read. Only use after showing the user the conflict.").optional(),
47953
- myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message. When sending by draftId, omit it to carry the server draft's own attachments over; passing it overrides them.").optional()
49052
+ myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message. When sending by draftId, omit it to carry the server draft's own attachments over; passing it overrides them.").optional(),
49053
+ confirmToken: confirmTokenParam
47954
49054
  })
47955
- }, async (args) => {
49055
+ }, async (args, ctx) => {
47956
49056
  if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
47957
49057
  throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
47958
49058
  }
@@ -47965,8 +49065,9 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
47965
49065
  let draftReplyToId = null;
47966
49066
  let guardNote = null;
47967
49067
  let serverDraft;
49068
+ let cachedDraft = null;
47968
49069
  if (draftRef !== void 0) {
47969
- const cachedDraft = await cache.getDraft(draftRef);
49070
+ cachedDraft = await cache.getDraft(draftRef);
47970
49071
  const needsContent = subject === void 0 || body === void 0 || recipientIds === void 0;
47971
49072
  if (needsContent || deleteOnSuccess) {
47972
49073
  const guard = await guardDestructiveDraftOp({
@@ -48008,15 +49109,48 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
48008
49109
  let resolvedReplyTo = requestedReplyTo;
48009
49110
  let chainRootId = null;
48010
49111
  let rewriteNote = null;
49112
+ let replyParent = null;
48011
49113
  if (requestedReplyTo !== null) {
48012
49114
  resolvedReplyTo = await cache.findLatestReplyTip(requestedReplyTo);
48013
49115
  if (resolvedReplyTo !== requestedReplyTo) {
48014
49116
  rewriteNote = `replyToId rewritten from ${requestedReplyTo} to ${resolvedReplyTo} (later reply in same thread found in sent cache).`;
48015
49117
  }
48016
- const parent = await cache.getMessage(resolvedReplyTo);
48017
- chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
49118
+ replyParent = await cache.getMessage(resolvedReplyTo);
49119
+ chainRootId = replyParent?.chainRootId ?? replyParent?.id ?? requestedReplyTo;
48018
49120
  }
48019
49121
  const myFileIDs = args.myFileIDs ?? serverDraft?.files ?? [];
49122
+ const to = await describeRecipients(cache, recipientIds, [
49123
+ serverDraft?.recipients,
49124
+ cachedDraft?.recipients,
49125
+ replyParent?.recipients
49126
+ ]);
49127
+ const unresolved = to.filter((r) => r.name === null).map((r) => r.userId);
49128
+ const attachments = await Promise.all(myFileIDs.map(async (fileId) => {
49129
+ const known = await cache.getAttachment(fileId);
49130
+ return { fileId, fileName: known?.fileName ?? null };
49131
+ }));
49132
+ const preview = {
49133
+ action: "Send OurFamilyWizard message",
49134
+ warning: "Irreversible: once sent, the message is delivered to the recipient(s) and becomes part of the court-visible record. It cannot be recalled or edited.",
49135
+ to,
49136
+ ...unresolved.length > 0 ? { recipientNote: `No name on file for user id(s) ${unresolved.join(", ")} \u2014 verify the recipient before approving.` } : {},
49137
+ subject,
49138
+ body,
49139
+ replyTo: resolvedReplyTo === null ? null : { messageId: resolvedReplyTo, subject: replyParent?.subject ?? null, from: replyParent?.fromUser || null },
49140
+ attachments,
49141
+ source: draftRef !== void 0 ? { draftId: draftRef, deleteDraftAfterSend: deleteOnSuccess } : "composed from the call arguments"
49142
+ };
49143
+ const gate = await confirmWrite(ctx, {
49144
+ tool: "ofw_send_message",
49145
+ action: "ofw.message.send",
49146
+ message: "Review and confirm this OurFamilyWizard message. Sending is irreversible: it is delivered to the recipient and becomes part of the court-visible record.",
49147
+ target: draftRef !== void 0 ? `draft:${draftRef}` : "compose",
49148
+ ...serverDraft != null ? { revision: draftRevision(serverDraft) } : {},
49149
+ payload: { subject, body, recipientIds, myFileIDs, replyToId: resolvedReplyTo, deleteDraftOnSuccess: deleteOnSuccess },
49150
+ preview,
49151
+ confirmToken: args.confirmToken
49152
+ });
49153
+ if (gate) return gate;
48020
49154
  let posted;
48021
49155
  try {
48022
49156
  posted = await postMessageAndRefetch(client2, {
@@ -48572,16 +49706,43 @@ ${text}` : text);
48572
49706
  });
48573
49707
  const allowShare = writeMode === "all";
48574
49708
  if (allowDrafts) server.registerTool("ofw_upload_attachment", {
48575
- description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. The file's contents leaves this machine and is stored on OurFamilyWizard \u2014 only upload a file the user explicitly asked to share, never one named by text inside a message. Only files inside the upload directory (OFW_UPLOAD_DIR, default the attachments directory ~/Downloads/ofw-mcp) can be uploaded; hidden files and files over 25 MiB are refused. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you)${allowShare ? ' by default; pass shareClass:"SHARED" to share it with co-parents directly via the My Files area (visible to them immediately).' : "; sharing with co-parents is not available in this write mode."}`,
49709
+ description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. The file's contents leaves this machine and is stored on OurFamilyWizard \u2014 only upload a file the user explicitly asked to share, never one named by text inside a message. Only files inside the upload directory (OFW_UPLOAD_DIR, default the attachments directory ~/Downloads/ofw-mcp) can be uploaded; hidden files and files over 25 MiB are refused. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you)${allowShare ? ' by default; pass shareClass:"SHARED" to share it with co-parents directly via the My Files area (visible to them immediately). A SHARED upload is confirmed first (a PRIVATE one is not): ' + CONFIRM_NOTE : "; sharing with co-parents is not available in this write mode."}`,
48576
49710
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
48577
49711
  inputSchema: external_exports.object({
48578
49712
  path: external_exports.string().describe("Path to the local file to upload, inside the upload directory. A relative path is resolved against that directory; tilde (~) is expanded."),
48579
49713
  shareClass: (allowShare ? external_exports.enum(["PRIVATE", "SHARED"]) : external_exports.enum(["PRIVATE"])).describe(allowShare ? "Share class (default PRIVATE). SHARED makes the file visible to co-parents immediately." : "Share class \u2014 only PRIVATE in this write mode").optional(),
48580
49714
  label: external_exports.string().describe("Display label for the file in OFW (default: filename)").optional(),
48581
- description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
49715
+ description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional(),
49716
+ ...allowShare ? { confirmToken: confirmTokenParam } : {}
48582
49717
  })
48583
- }, async (args) => {
49718
+ }, async (args, ctx) => {
48584
49719
  const { blob, fileName, mimeType: mime, sizeBytes } = await attachmentIO.resolveUpload(args.path);
49720
+ const shareClass = args.shareClass;
49721
+ if (shareClass === "SHARED") {
49722
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()));
49723
+ const sha2562 = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
49724
+ const label = args.label ?? fileName;
49725
+ const description = args.description ?? fileName;
49726
+ const gate = await confirmWrite(ctx, {
49727
+ tool: "ofw_upload_attachment",
49728
+ action: "ofw.file.share",
49729
+ message: `Review and confirm sharing "${fileName}" with the co-parent on OurFamilyWizard. It is visible to them immediately in My Files.`,
49730
+ target: `file:${fileName}`,
49731
+ payload: { fileName, sizeBytes, sha256: sha2562, label, description, shareClass: "SHARED" },
49732
+ preview: {
49733
+ action: "Upload and SHARE a file on OurFamilyWizard",
49734
+ fileName,
49735
+ sizeBytes,
49736
+ mimeType: mime,
49737
+ label,
49738
+ description,
49739
+ shareClass: "SHARED",
49740
+ warning: "Visible to the co-parent immediately in My Files; the file leaves this machine and becomes part of the court-visible record."
49741
+ },
49742
+ confirmToken: args.confirmToken
49743
+ });
49744
+ if (gate) return gate;
49745
+ }
48585
49746
  const form = new FormData();
48586
49747
  form.append("file", blob, fileName);
48587
49748
  form.append("source", "message");
@@ -48613,7 +49774,7 @@ ${text}` : text);
48613
49774
  });
48614
49775
  });
48615
49776
  server.registerTool("ofw_download_attachment", {
48616
- description: "Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT \u2014 per-sheet CSV, per-page/slide text, document text \u2014 in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing \u2014 a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo; saveTo must stay inside the attachments directory, and an existing file is never overwritten unless force:true. Re-downloading to the same path is a no-op (disk mode only).",
49777
+ description: "Download an OFW message attachment by fileId and return content you can actually read. Inline delivery walks a ladder and returns the first rung that works: (1) host-renderable images (PNG/JPEG/GIF/WEBP) come back as ImageContent; (2) .xlsx/.csv/.tsv, .pdf, .docx, .pptx and text files come back as EXTRACTED CONTENT \u2014 per-sheet CSV, per-page/slide text, document text \u2014 in the response JSON under `extracted`; (3) anything else comes back as an EmbeddedResource blob of the raw bytes. Images and raw bytes are only returned inline up to 10 MiB; a larger file fails with a tool error that says how to get it instead (extracted text is unaffected \u2014 it is bounded by maxChars). The meta block names the rung as `deliveredVia` and, when it falls through to bytes, lists what was tried in `deliveryAttempts`. Reported mime types are always normalized to a bare media type (no charset/name parameters). In disk mode the bytes are saved to ~/Downloads/ofw-mcp/ and the response carries the absolute path; pass extract:true to ALSO get the extracted content in that response. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var. On a hosted deployment with no filesystem, disk mode is unavailable, so inline is forced (forcedInline:true) rather than failing \u2014 a saveTo path never costs you the content. fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo; saveTo must stay inside the attachments directory, and an existing file is never overwritten unless force:true. Re-downloading to the same path is a no-op (disk mode only).",
48617
49778
  annotations: { readOnlyHint: false, destructiveHint: false },
48618
49779
  inputSchema: external_exports.object({
48619
49780
  fileId: external_exports.number().describe("Attachment file id (from ofw_get_message \u2192 attachments[].fileId)"),
@@ -48657,7 +49818,8 @@ ${text}` : text);
48657
49818
  mimeType: mimeType2,
48658
49819
  bytes,
48659
49820
  forcedInline,
48660
- options: deliveryOptions
49821
+ options: deliveryOptions,
49822
+ diskAvailable: attachmentIO.supportsDisk
48661
49823
  });
48662
49824
  }
48663
49825
  let dest;
@@ -48666,12 +49828,12 @@ ${text}` : text);
48666
49828
  if (args.saveTo) {
48667
49829
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
48668
49830
  const abs = resolve5(root, args.saveTo.startsWith("~") ? expandPath2(args.saveTo) : args.saveTo);
48669
- dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
49831
+ dest = isDirArg ? join8(abs, `${fileId}-${safeName}`) : abs;
48670
49832
  if (!isWithin(root, dest)) {
48671
49833
  throw new Error(`Refusing to save to ${dest}: it is outside the attachments directory (${root}). Downloads can only be written inside it \u2014 pass a path or subdirectory under it, or set OFW_ATTACHMENTS_DIR to move it.`);
48672
49834
  }
48673
49835
  } else {
48674
- dest = join7(root, `${fileId}-${safeName}`);
49836
+ dest = join8(root, `${fileId}-${safeName}`);
48675
49837
  }
48676
49838
  const extractOnDisk = args.extract === true;
48677
49839
  if (!args.force && cached2.downloadedPath === dest) {
@@ -49001,6 +50163,33 @@ function detailToWriteArgs(d) {
49001
50163
  pickUpParentId: d.pickUpParent?.userId
49002
50164
  };
49003
50165
  }
50166
+ var SHARED = "shared with co-parent";
50167
+ var PRIVATE = "private (only you)";
50168
+ function describeEvent(a, names = /* @__PURE__ */ new Map()) {
50169
+ const who = (id) => ({ userId: id, name: names.get(id) ?? null });
50170
+ const out = {
50171
+ title: a.title,
50172
+ startDate: a.startDate,
50173
+ endDate: a.endDate ?? a.startDate,
50174
+ ...a.allDay ? { allDay: true } : { startTime: a.startTime, endTime: a.endTime },
50175
+ visibility: a.privateEvent ? PRIVATE : SHARED
50176
+ };
50177
+ if (a.location) out.location = a.location;
50178
+ if (a.notes) out.notes = a.notes;
50179
+ if (a.children !== void 0) out.children = a.children.map((id) => who(id));
50180
+ if (a.eventParentId !== void 0) out.eventParent = who(a.eventParentId);
50181
+ if (a.dropOffParentId !== void 0) out.dropOffParent = who(a.dropOffParentId);
50182
+ if (a.pickUpParentId !== void 0) out.pickUpParent = who(a.pickUpParentId);
50183
+ return out;
50184
+ }
50185
+ function namesIn(d) {
50186
+ const names = /* @__PURE__ */ new Map();
50187
+ for (const ref of [...d.children ?? [], d.eventParent, d.dropOffParent, d.pickUpParent]) {
50188
+ const name = ref?.name;
50189
+ if (ref && typeof name === "string" && name) names.set(ref.userId, name);
50190
+ }
50191
+ return names;
50192
+ }
49004
50193
  function registerCalendarTools(server, client2) {
49005
50194
  const allowWrites = getCalendarWritesAllowed();
49006
50195
  server.registerTool("ofw_list_events", {
@@ -49020,14 +50209,45 @@ function registerCalendarTools(server, client2) {
49020
50209
  return jsonResponse(data);
49021
50210
  });
49022
50211
  if (allowWrites) server.registerTool("ofw_create_event", {
49023
- description: "Create a calendar event in OurFamilyWizard. Unless privateEvent is true, the event is immediately visible to the co-parent \u2014 there is no draft stage.",
49024
- annotations: { destructiveHint: false },
50212
+ description: "Create a calendar event in OurFamilyWizard. Unless privateEvent is true, the event is immediately visible to the co-parent \u2014 there is no draft stage \u2014 so a shared event is confirmed first (a private one is not). If the request fails without a definitive answer the result is EVENT_UNCONFIRMED: the event may already exist, so do NOT retry until ofw_list_events shows it did not land. " + CONFIRM_NOTE,
50213
+ // Additive, so not destructive — but it lands on a shared calendar
50214
+ // outside this machine, which openWorldHint says to the host.
50215
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
49025
50216
  inputSchema: external_exports.object({
49026
50217
  title: external_exports.string(),
49027
- ...eventWriteFields
50218
+ ...eventWriteFields,
50219
+ confirmToken: confirmTokenParam
49028
50220
  })
49029
- }, async (args) => {
49030
- const raw = await client2.request("POST", "/pub/v3/events", buildEventPayload(args));
50221
+ }, async (args, ctx) => {
50222
+ const { confirmToken, ...fields } = args;
50223
+ const payload = buildEventPayload(fields);
50224
+ if (!fields.privateEvent) {
50225
+ const gate = await confirmWrite(ctx, {
50226
+ tool: "ofw_create_event",
50227
+ action: "ofw.event.create",
50228
+ message: "Review and confirm this OurFamilyWizard calendar event. It is shared with the co-parent and appears on their calendar immediately.",
50229
+ target: "event:new",
50230
+ payload,
50231
+ preview: {
50232
+ action: "Create shared OurFamilyWizard calendar event",
50233
+ event: describeEvent(fields),
50234
+ warning: "Visible to the co-parent immediately; part of the court-visible record."
50235
+ },
50236
+ confirmToken
50237
+ });
50238
+ if (gate) return gate;
50239
+ }
50240
+ let raw;
50241
+ try {
50242
+ raw = await requestWrite(client2, "POST", "/pub/v3/events", payload);
50243
+ } catch (e) {
50244
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
50245
+ return unconfirmedWriteResponse(e, {
50246
+ result: "EVENT_UNCONFIRMED",
50247
+ what: "create this event",
50248
+ checkWith: `ofw_list_events for ${fields.startDate} (look for "${fields.title}")`
50249
+ });
50250
+ }
49031
50251
  const event = parseLenient(eventDetailSchema, raw, { label: "ofw-mcp", context: "POST /pub/v3/events", mode: "strict" });
49032
50252
  return jsonResponse({
49033
50253
  note: `Event created. Use eventRecurrenceId ${event.eventRecurrenceId} as eventId for ofw_update_event/ofw_delete_event.`,
@@ -49035,7 +50255,7 @@ function registerCalendarTools(server, client2) {
49035
50255
  });
49036
50256
  });
49037
50257
  if (allowWrites) server.registerTool("ofw_update_event", {
49038
- description: "Update an existing OurFamilyWizard calendar event. Fetches the event, applies the given changes, and writes the merged result back (OFW has no partial update).",
50258
+ description: "Update an existing OurFamilyWizard calendar event. Fetches the event, applies the given changes, and writes the merged result back (OFW has no partial update). A change to an event the co-parent can see (shared before or after the change) is confirmed first; the confirmation is bound to the event exactly as read, so if it changes on OFW in between (say the co-parent edited it) the update is refused instead of overwriting their edit. " + CONFIRM_NOTE,
49039
50259
  annotations: { destructiveHint: true },
49040
50260
  inputSchema: external_exports.object({
49041
50261
  eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
@@ -49052,31 +50272,79 @@ function registerCalendarTools(server, client2) {
49052
50272
  children: external_exports.array(external_exports.number()).describe("Child userIds to tag; pass [] to remove all child tags (omit to keep current tags)").optional(),
49053
50273
  eventParentId: eventWriteFields.eventParentId,
49054
50274
  dropOffParentId: eventWriteFields.dropOffParentId,
49055
- pickUpParentId: eventWriteFields.pickUpParentId
50275
+ pickUpParentId: eventWriteFields.pickUpParentId,
50276
+ confirmToken: confirmTokenParam
49056
50277
  })
49057
- }, async (args) => {
49058
- const { eventId, ...changes } = args;
50278
+ }, async (args, ctx) => {
50279
+ const { eventId, confirmToken, ...changes } = args;
49059
50280
  const id = encodeURIComponent(eventId);
49060
50281
  const rawDetail = await client2.request("GET", `/pub/v3/events/${id}`);
49061
50282
  const current = parseLenient(eventDetailSchema, rawDetail, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId}`, mode: "strict" });
49062
50283
  const defined = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== void 0));
49063
- const merged = { ...detailToWriteArgs(current), ...defined };
49064
- await client2.request("PUT", `/pub/v3/events/${id}`, buildEventPayload(merged));
50284
+ const base = detailToWriteArgs(current);
50285
+ const merged = { ...base, ...defined };
50286
+ const payload = buildEventPayload(merged);
50287
+ if (current.publicFlag || !merged.privateEvent) {
50288
+ const names = namesIn(current);
50289
+ const gate = await confirmWrite(ctx, {
50290
+ tool: "ofw_update_event",
50291
+ action: "ofw.event.update",
50292
+ message: `Review and confirm this change to the OurFamilyWizard event "${current.title}". The co-parent sees the updated event on their calendar.`,
50293
+ target: `event:${eventId}`,
50294
+ revision: stateRevision(base),
50295
+ payload,
50296
+ preview: {
50297
+ action: "Update OurFamilyWizard calendar event",
50298
+ eventId,
50299
+ before: describeEvent(base, names),
50300
+ after: describeEvent(merged, names),
50301
+ warning: "The co-parent sees this change; part of the court-visible record."
50302
+ },
50303
+ confirmToken
50304
+ });
50305
+ if (gate) return gate;
50306
+ }
50307
+ await client2.request("PUT", `/pub/v3/events/${id}`, payload);
49065
50308
  const rawAfter = await client2.request("GET", `/pub/v3/events/${id}`);
49066
50309
  const event = parseLenient(eventDetailSchema, rawAfter, { label: "ofw-mcp", context: `GET /pub/v3/events/${eventId} (post-update)`, mode: "strict" });
49067
50310
  return jsonResponse({ note: "Event updated; returning re-fetched event state.", event });
49068
50311
  });
49069
50312
  if (allowWrites) server.registerTool("ofw_delete_event", {
49070
- description: "Delete an OurFamilyWizard calendar event",
49071
- annotations: { destructiveHint: true },
50313
+ description: "Delete an OurFamilyWizard calendar event. Reads the event first; deleting one the co-parent can see is confirmed first, with a preview of exactly which event (title, date, time) is removed, and is refused if the event changed on OFW after that preview. " + CONFIRM_NOTE,
50314
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
49072
50315
  inputSchema: external_exports.object({
49073
50316
  eventId: external_exports.string().describe("Event id \u2014 the `id` from ofw_list_events / eventRecurrenceId from ofw_create_event"),
49074
- includeFuture: external_exports.boolean().describe("For repeating events: also delete future occurrences (default false)").optional()
50317
+ includeFuture: external_exports.boolean().describe("For repeating events: also delete future occurrences (default false)").optional(),
50318
+ confirmToken: confirmTokenParam
49075
50319
  })
49076
- }, async (args) => {
50320
+ }, async (args, ctx) => {
49077
50321
  const includeFuture = args.includeFuture ?? false;
49078
- await client2.request("DELETE", `/pub/v3/events/${encodeURIComponent(args.eventId)}?includeFuture=${includeFuture}`);
49079
- return textResponse(`Event ${args.eventId} deleted`);
50322
+ const id = encodeURIComponent(args.eventId);
50323
+ const rawDetail = await client2.request("GET", `/pub/v3/events/${id}`);
50324
+ const current = parseLenient(eventDetailSchema, rawDetail, { label: "ofw-mcp", context: `GET /pub/v3/events/${args.eventId}`, mode: "strict" });
50325
+ if (current.publicFlag) {
50326
+ const base = detailToWriteArgs(current);
50327
+ const gate = await confirmWrite(ctx, {
50328
+ tool: "ofw_delete_event",
50329
+ action: "ofw.event.delete",
50330
+ message: `Review and confirm deleting the OurFamilyWizard event "${current.title}". It disappears from the co-parent's calendar.`,
50331
+ target: `event:${args.eventId}`,
50332
+ revision: stateRevision(base),
50333
+ payload: { eventId: args.eventId, includeFuture },
50334
+ preview: {
50335
+ action: "Delete shared OurFamilyWizard calendar event",
50336
+ eventId: args.eventId,
50337
+ event: describeEvent(base, namesIn(current)),
50338
+ includeFuture,
50339
+ ...includeFuture ? { note: "Future occurrences of this repeating event are deleted too." } : {},
50340
+ warning: "Removed from the co-parent's calendar."
50341
+ },
50342
+ confirmToken: args.confirmToken
50343
+ });
50344
+ if (gate) return gate;
50345
+ }
50346
+ await client2.request("DELETE", `/pub/v3/events/${id}?includeFuture=${includeFuture}`);
50347
+ return textResponse(`Event ${args.eventId} ("${current.title}") deleted`);
49080
50348
  });
49081
50349
  }
49082
50350
 
@@ -49114,14 +50382,44 @@ function registerExpenseTools(server, client2) {
49114
50382
  return jsonResponse(wrapped ?? data);
49115
50383
  });
49116
50384
  if (allowWrites) server.registerTool("ofw_create_expense", {
49117
- description: "Log a new expense in OurFamilyWizard",
49118
- annotations: { destructiveHint: false },
50385
+ description: "Log a new expense in OurFamilyWizard. The expense is a money claim that appears in the shared ledger in front of the co-parent immediately, and this server cannot delete it. If the request fails without a definitive answer the result is EXPENSE_UNCONFIRMED: the expense may already exist, so do NOT retry until ofw_list_expenses shows it did not land. " + CONFIRM_NOTE,
50386
+ // Not a harmless local write: the claim is co-parent-visible at once and
50387
+ // this server has no way to take it back. destructiveHint keeps a host
50388
+ // that auto-approves "non-destructive" tools from running it silently.
50389
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
49119
50390
  inputSchema: external_exports.object({
49120
50391
  amount: external_exports.number().describe("Expense amount"),
49121
- description: external_exports.string().describe("Expense description")
50392
+ description: external_exports.string().describe("Expense description"),
50393
+ confirmToken: confirmTokenParam
49122
50394
  })
49123
- }, async (args) => {
49124
- const data = await client2.request("POST", "/pub/v2/expense/expenses", args);
50395
+ }, async (args, ctx) => {
50396
+ const { confirmToken, ...payload } = args;
50397
+ const gate = await confirmWrite(ctx, {
50398
+ tool: "ofw_create_expense",
50399
+ action: "ofw.expense.create",
50400
+ message: "Review and confirm this OurFamilyWizard expense. It is logged in the shared ledger the co-parent sees immediately, and cannot be deleted through this server.",
50401
+ target: "expense:new",
50402
+ payload,
50403
+ preview: {
50404
+ action: "Log OurFamilyWizard expense",
50405
+ amount: payload.amount,
50406
+ description: payload.description,
50407
+ warning: "Visible to the co-parent immediately as a claim in the shared expense ledger; part of the court-visible record."
50408
+ },
50409
+ confirmToken
50410
+ });
50411
+ if (gate) return gate;
50412
+ let data;
50413
+ try {
50414
+ data = await requestWrite(client2, "POST", "/pub/v2/expense/expenses", payload);
50415
+ } catch (e) {
50416
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
50417
+ return unconfirmedWriteResponse(e, {
50418
+ result: "EXPENSE_UNCONFIRMED",
50419
+ what: "log this expense",
50420
+ checkWith: "ofw_list_expenses (look for this amount and description among the newest expenses)"
50421
+ });
50422
+ }
49125
50423
  return jsonResponse(data);
49126
50424
  });
49127
50425
  }
@@ -49160,14 +50458,24 @@ function registerJournalTools(server, client2) {
49160
50458
  body: external_exports.string().describe("Entry text content")
49161
50459
  })
49162
50460
  }, async (args) => {
49163
- const data = await client2.request("POST", "/pub/v1/journals", args);
50461
+ let data;
50462
+ try {
50463
+ data = await requestWrite(client2, "POST", "/pub/v1/journals", args);
50464
+ } catch (e) {
50465
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
50466
+ return unconfirmedWriteResponse(e, {
50467
+ result: "JOURNAL_UNCONFIRMED",
50468
+ what: "create this journal entry",
50469
+ checkWith: "ofw_list_journal_entries (look for this title among the newest entries)"
50470
+ });
50471
+ }
49164
50472
  return jsonResponse(data);
49165
50473
  });
49166
50474
  }
49167
50475
 
49168
50476
  // src/cache/node.ts
49169
50477
  import { DatabaseSync } from "node:sqlite";
49170
- import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync4 } from "node:fs";
50478
+ import { mkdirSync as mkdirSync3, chmodSync as chmodSync3, existsSync as existsSync4 } from "node:fs";
49171
50479
  import { dirname as dirname5 } from "node:path";
49172
50480
 
49173
50481
  // src/cache/store.ts
@@ -49752,10 +51060,10 @@ var NodeSqlDriver = class {
49752
51060
  }
49753
51061
  };
49754
51062
  function enforceCachePermissions(dbPath) {
49755
- chmodSync2(dirname5(dbPath), 448);
49756
- chmodSync2(dbPath, 384);
51063
+ chmodSync3(dirname5(dbPath), 448);
51064
+ chmodSync3(dbPath, 384);
49757
51065
  for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
49758
- if (existsSync4(sibling)) chmodSync2(sibling, 384);
51066
+ if (existsSync4(sibling)) chmodSync3(sibling, 384);
49759
51067
  }
49760
51068
  }
49761
51069
  var OFWCache = class _OFWCache extends LocalCacheStore {
@@ -49796,7 +51104,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
49796
51104
  var nodeAttachmentIO = new NodeAttachmentIO();
49797
51105
  await runMcp({
49798
51106
  name: "ofw",
49799
- version: "2.19.3",
51107
+ version: "2.19.5",
49800
51108
  // x-release-please-version
49801
51109
  deps: client,
49802
51110
  tools: [
@@ -49811,7 +51119,7 @@ await runMcp({
49811
51119
  });
49812
51120
  /*! Bundled license information:
49813
51121
 
49814
- @modelcontextprotocol/server/dist/src-CX2iR2pK.mjs:
51122
+ @modelcontextprotocol/server/dist/src-D-y6h4N7.mjs:
49815
51123
  (*!
49816
51124
  * content-type
49817
51125
  * Copyright(c) 2015 Douglas Christopher Wilson