ofw-mcp 2.19.4 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -23487,7 +23487,7 @@ function date4(params) {
23487
23487
  return _coercedDate(ZodDate, params);
23488
23488
  }
23489
23489
 
23490
- // node_modules/@modelcontextprotocol/core/dist/auth-CUe6YdwF.mjs
23490
+ // node_modules/@modelcontextprotocol/core/dist/auth-CGP0BDVq.mjs
23491
23491
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
23492
23492
  var SUPPORTED_PROTOCOL_VERSIONS = [
23493
23493
  LATEST_PROTOCOL_VERSION,
@@ -24324,7 +24324,8 @@ var OAuthMetadataSchema = looseObject({
24324
24324
  introspection_endpoint_auth_signing_alg_values_supported: array(string2()).optional(),
24325
24325
  code_challenge_methods_supported: array(string2()).optional(),
24326
24326
  client_id_metadata_document_supported: boolean2().optional(),
24327
- 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()
24328
24329
  });
24329
24330
  var OpenIdProviderMetadataSchema = looseObject({
24330
24331
  issuer: string2(),
@@ -24425,7 +24426,7 @@ var OAuthTokenRevocationRequestSchema = object({
24425
24426
  token_type_hint: string2().optional()
24426
24427
  }).strip();
24427
24428
 
24428
- // node_modules/@modelcontextprotocol/server/dist/src-CX2iR2pK.mjs
24429
+ // node_modules/@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs
24429
24430
  var BRANDS = /* @__PURE__ */ Symbol.for("mcp.sdk.errorBrands");
24430
24431
  function stampErrorBrands(instance, ctor) {
24431
24432
  const brands = /* @__PURE__ */ new Set();
@@ -24539,8 +24540,18 @@ var SdkError = class extends Error {
24539
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)`");
24540
24541
  return brandedHasInstance(this, value);
24541
24542
  }
24542
- constructor(code, message, data) {
24543
- 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);
24544
24555
  this.code = code;
24545
24556
  this.data = data;
24546
24557
  this.name = "SdkError";
@@ -24551,8 +24562,11 @@ var SdkHttpError = class extends SdkError {
24551
24562
  static {
24552
24563
  Object.defineProperty(this, "mcpBrand", { value: "mcp.SdkHttpError" });
24553
24564
  }
24554
- constructor(code, message, data) {
24555
- 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);
24556
24570
  this.name = "SdkHttpError";
24557
24571
  }
24558
24572
  get status() {
@@ -25773,11 +25787,11 @@ var rev2025Codec = {
25773
25787
  },
25774
25788
  encodeResult(method, result) {
25775
25789
  if (method !== "tools/list") return result;
25776
- const tools = result.tools;
25777
- if (!Array.isArray(tools) || !tools.some((t) => toolNeedsLegacyWrap(t))) return result;
25790
+ const tools2 = result.tools;
25791
+ if (!Array.isArray(tools2) || !tools2.some((t) => toolNeedsLegacyWrap(t))) return result;
25778
25792
  return {
25779
25793
  ...result,
25780
- tools: tools.map((t) => toolNeedsLegacyWrap(t) ? {
25794
+ tools: tools2.map((t) => toolNeedsLegacyWrap(t) ? {
25781
25795
  ...t,
25782
25796
  outputSchema: wrapOutputSchemaForLegacy(t.outputSchema)
25783
25797
  } : t)
@@ -27005,8 +27019,8 @@ function enforceDeletedFields(method, result) {
27005
27019
  }
27006
27020
  return next;
27007
27021
  };
27008
- const tools = result.tools;
27009
- if (method === "tools/list" && Array.isArray(tools) && tools.some((tool) => isPlainObject$4(tool) && "execution" in tool)) copy().tools = tools.map((tool) => {
27022
+ const tools2 = result.tools;
27023
+ if (method === "tools/list" && Array.isArray(tools2) && tools2.some((tool) => isPlainObject$4(tool) && "execution" in tool)) copy().tools = tools2.map((tool) => {
27010
27024
  if (!isPlainObject$4(tool) || !("execution" in tool)) return tool;
27011
27025
  const rest = { ...tool };
27012
27026
  delete rest["execution"];
@@ -27503,7 +27517,7 @@ var INBOUND_VALIDATION_LADDER = [
27503
27517
  evaluatedAt: "pre-dispatch",
27504
27518
  codes: [HEADER_MISMATCH_ERROR_CODE],
27505
27519
  conformance: ["http-header-validation"],
27506
- 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."
27507
27521
  },
27508
27522
  {
27509
27523
  rung: "client-capabilities",
@@ -28304,7 +28318,7 @@ var Protocol = class {
28304
28318
  return this._requestHandlers.get(method);
28305
28319
  }
28306
28320
  async _oncancel(notification) {
28307
- if (!notification.params.requestId) return;
28321
+ if (notification.params.requestId === void 0) return;
28308
28322
  this._requestHandlerAbortControllers.get(notification.params.requestId)?.abort(notification.params.reason);
28309
28323
  }
28310
28324
  _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) {
@@ -28703,19 +28717,20 @@ var Protocol = class {
28703
28717
  const cancel = (reason) => {
28704
28718
  if (responseReceived) return;
28705
28719
  this._progressHandlers.delete(messageId);
28706
- if (requestAbort === void 0) this._transport?.send(this._envelopeOutbound({
28707
- jsonrpc: "2.0",
28708
- method: "notifications/cancelled",
28709
- params: {
28710
- requestId: messageId,
28711
- reason: String(reason)
28712
- }
28713
- }), {
28714
- relatedRequestId,
28715
- resumptionToken,
28716
- onresumptiontoken
28717
- }).catch((error62) => this._onerror(/* @__PURE__ */ new Error(`Failed to send cancellation: ${error62}`)));
28718
- 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();
28719
28734
  reject(reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason)));
28720
28735
  };
28721
28736
  this._responseHandlers.set(messageId, (response) => {
@@ -28795,7 +28810,7 @@ var Protocol = class {
28795
28810
  jsonrpc: "2.0",
28796
28811
  ...notification
28797
28812
  });
28798
- 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) {
28799
28814
  if (this._pendingDebouncedNotifications.has(notification.method)) return;
28800
28815
  this._pendingDebouncedNotifications.add(notification.method);
28801
28816
  Promise.resolve().then(() => {
@@ -36098,7 +36113,7 @@ var Ajv = import_ajv.Ajv;
36098
36113
  // node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs
36099
36114
  import process2 from "node:process";
36100
36115
 
36101
- // node_modules/@modelcontextprotocol/server/dist/mcp-DXXb3Vv3.mjs
36116
+ // node_modules/@modelcontextprotocol/server/dist/mcp-Dw2OlZ1f.mjs
36102
36117
  var COMPLETABLE_SYMBOL = /* @__PURE__ */ Symbol.for("mcp.completable");
36103
36118
  function isCompletable(schema) {
36104
36119
  return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
@@ -36291,6 +36306,9 @@ function notificationToServerEvent(method, uri) {
36291
36306
  };
36292
36307
  }
36293
36308
  }
36309
+ function supportsScopeChallengeResolver(transport) {
36310
+ return typeof transport === "object" && transport !== null && "setScopeChallengeResolver" in transport && typeof transport.setScopeChallengeResolver === "function";
36311
+ }
36294
36312
  var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
36295
36313
  var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
36296
36314
  function resolveLegacyShimOptions(options) {
@@ -37101,6 +37119,7 @@ var McpServer = class {
37101
37119
  * ```
37102
37120
  */
37103
37121
  async connect(transport) {
37122
+ if (supportsScopeChallengeResolver(transport)) transport.setScopeChallengeResolver((context) => this.resolveScopeChallenge(context));
37104
37123
  return await this.server.connect(transport);
37105
37124
  }
37106
37125
  /**
@@ -37109,6 +37128,41 @@ var McpServer = class {
37109
37128
  async close() {
37110
37129
  await this.server.close();
37111
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
+ };
37112
37166
  _toolHandlersInitialized = false;
37113
37167
  setToolRequestHandlers() {
37114
37168
  if (this._toolHandlersInitialized) return;
@@ -37275,7 +37329,10 @@ var McpServer = class {
37275
37329
  }
37276
37330
  for (const template of Object.values(this._registeredResourceTemplates)) {
37277
37331
  const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
37278
- 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
+ }
37279
37336
  }
37280
37337
  throw new ResourceNotFoundError(request.params.uri);
37281
37338
  });
@@ -37306,36 +37363,32 @@ var McpServer = class {
37306
37363
  this._promptHandlersInitialized = true;
37307
37364
  }
37308
37365
  registerResource(name, uriOrTemplate, config2, readCallback) {
37309
- const cacheHint = config2.cacheHint;
37310
- let metadata = config2;
37311
- if (cacheHint !== void 0) {
37312
- assertValidCacheHint(cacheHint, `resource ${name}`);
37313
- const rest = { ...config2 };
37314
- delete rest.cacheHint;
37315
- metadata = rest;
37316
- }
37366
+ const { cacheHint, scopeChallenge, ...resourceMetadata } = config2;
37367
+ const metadata = resourceMetadata;
37368
+ if (cacheHint !== void 0) assertValidCacheHint(cacheHint, `resource ${name}`);
37317
37369
  if (typeof uriOrTemplate === "string") {
37318
37370
  if (this._registeredResources[uriOrTemplate]) throw new Error(`Resource ${uriOrTemplate} is already registered`);
37319
- const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, readCallback);
37371
+ const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
37320
37372
  if (cacheHint !== void 0) registeredResource.cacheHint = cacheHint;
37321
37373
  this.setResourceRequestHandlers();
37322
37374
  this.sendResourceListChanged();
37323
37375
  return registeredResource;
37324
37376
  } else {
37325
37377
  if (this._registeredResourceTemplates[name]) throw new Error(`Resource template ${name} is already registered`);
37326
- const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, readCallback);
37378
+ const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
37327
37379
  if (cacheHint !== void 0) registeredResourceTemplate.cacheHint = cacheHint;
37328
37380
  this.setResourceRequestHandlers();
37329
37381
  this.sendResourceListChanged();
37330
37382
  return registeredResourceTemplate;
37331
37383
  }
37332
37384
  }
37333
- _createRegisteredResource(name, title, uri, metadata, readCallback) {
37385
+ _createRegisteredResource(name, title, uri, metadata, scopeChallenge, readCallback) {
37334
37386
  const registeredResource = {
37335
37387
  name,
37336
37388
  title,
37337
37389
  metadata,
37338
37390
  readCallback,
37391
+ scopeChallenge,
37339
37392
  enabled: true,
37340
37393
  disable: () => registeredResource.update({ enabled: false }),
37341
37394
  enable: () => registeredResource.update({ enabled: true }),
@@ -37349,6 +37402,7 @@ var McpServer = class {
37349
37402
  if (updates.title !== void 0) registeredResource.title = updates.title;
37350
37403
  if (updates.metadata !== void 0) registeredResource.metadata = updates.metadata;
37351
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;
37352
37406
  if (updates.enabled !== void 0) registeredResource.enabled = updates.enabled;
37353
37407
  this.sendResourceListChanged();
37354
37408
  }
@@ -37356,12 +37410,13 @@ var McpServer = class {
37356
37410
  this._registeredResources[uri] = registeredResource;
37357
37411
  return registeredResource;
37358
37412
  }
37359
- _createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
37413
+ _createRegisteredResourceTemplate(name, title, template, metadata, scopeChallenge, readCallback) {
37360
37414
  const registeredResourceTemplate = {
37361
37415
  resourceTemplate: template,
37362
37416
  title,
37363
37417
  metadata,
37364
37418
  readCallback,
37419
+ scopeChallenge,
37365
37420
  enabled: true,
37366
37421
  disable: () => registeredResourceTemplate.update({ enabled: false }),
37367
37422
  enable: () => registeredResourceTemplate.update({ enabled: true }),
@@ -37375,6 +37430,7 @@ var McpServer = class {
37375
37430
  if (updates.template !== void 0) registeredResourceTemplate.resourceTemplate = updates.template;
37376
37431
  if (updates.metadata !== void 0) registeredResourceTemplate.metadata = updates.metadata;
37377
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;
37378
37434
  if (updates.enabled !== void 0) registeredResourceTemplate.enabled = updates.enabled;
37379
37435
  this.sendResourceListChanged();
37380
37436
  }
@@ -37384,7 +37440,7 @@ var McpServer = class {
37384
37440
  if (Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v))) this.setCompletionRequestHandler();
37385
37441
  return registeredResourceTemplate;
37386
37442
  }
37387
- _createRegisteredPrompt(name, title, description, argsSchema, callback, icons, _meta) {
37443
+ _createRegisteredPrompt(name, title, description, argsSchema, callback, icons, scopeChallenge, _meta) {
37388
37444
  let currentArgsSchema = argsSchema;
37389
37445
  let currentCallback = callback;
37390
37446
  const registeredPrompt = {
@@ -37392,6 +37448,7 @@ var McpServer = class {
37392
37448
  description,
37393
37449
  argsSchema,
37394
37450
  icons,
37451
+ scopeChallenge,
37395
37452
  _meta,
37396
37453
  handler: createPromptHandler(name, argsSchema, callback),
37397
37454
  enabled: true,
@@ -37406,6 +37463,7 @@ var McpServer = class {
37406
37463
  if (updates.title !== void 0) registeredPrompt.title = updates.title;
37407
37464
  if (updates.description !== void 0) registeredPrompt.description = updates.description;
37408
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;
37409
37467
  if (updates._meta !== void 0) registeredPrompt._meta = updates._meta;
37410
37468
  let needsHandlerRegen = false;
37411
37469
  if (updates.argsSchema !== void 0) {
@@ -37433,7 +37491,7 @@ var McpServer = class {
37433
37491
  }
37434
37492
  return registeredPrompt;
37435
37493
  }
37436
- _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, _meta, handler) {
37494
+ _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, scopeChallenge, _meta, handler) {
37437
37495
  validateAndWarnToolName(name);
37438
37496
  if (inputSchema !== void 0) try {
37439
37497
  const json2 = standardSchemaToJsonSchema(inputSchema, "input");
@@ -37452,6 +37510,7 @@ var McpServer = class {
37452
37510
  annotations,
37453
37511
  icons,
37454
37512
  execution,
37513
+ scopeChallenge,
37455
37514
  _meta,
37456
37515
  handler,
37457
37516
  executor: createToolExecutor(inputSchema, handler),
@@ -37490,6 +37549,7 @@ var McpServer = class {
37490
37549
  }
37491
37550
  if (updates.annotations !== void 0) registeredTool.annotations = updates.annotations;
37492
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;
37493
37553
  if (updates._meta !== void 0) registeredTool._meta = updates._meta;
37494
37554
  if (updates.enabled !== void 0) registeredTool.enabled = updates.enabled;
37495
37555
  this.sendToolListChanged();
@@ -37502,13 +37562,13 @@ var McpServer = class {
37502
37562
  }
37503
37563
  registerTool(name, config2, cb) {
37504
37564
  if (this._registeredTools[name]) throw new Error(`Tool ${name} is already registered`);
37505
- const { title, description, inputSchema, outputSchema, annotations, icons, _meta } = config2;
37506
- 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);
37507
37567
  }
37508
37568
  registerPrompt(name, config2, cb) {
37509
37569
  if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
37510
- const { title, description, argsSchema, icons, _meta } = config2;
37511
- 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);
37512
37572
  this.setPromptRequestHandlers();
37513
37573
  this.sendPromptListChanged();
37514
37574
  return registeredPrompt;
@@ -37620,6 +37680,7 @@ function unwrapOptionalSchema(schema) {
37620
37680
  }
37621
37681
 
37622
37682
  // node_modules/@modelcontextprotocol/server/dist/stdio.mjs
37683
+ var swallowsErrorsAfterClose = /* @__PURE__ */ Symbol("swallowsErrorsAfterClose");
37623
37684
  var StdioServerTransport = class {
37624
37685
  _readBuffer;
37625
37686
  _started = false;
@@ -37646,18 +37707,27 @@ var StdioServerTransport = class {
37646
37707
  this.onerror?.(error62);
37647
37708
  };
37648
37709
  _onstdouterror = (error62) => {
37710
+ if (this._closed) return;
37649
37711
  this.onerror?.(error62);
37650
37712
  this.close().catch(() => {
37651
37713
  });
37652
37714
  };
37715
+ _onstdinclose = () => {
37716
+ this.close().catch(() => {
37717
+ });
37718
+ };
37653
37719
  /**
37654
37720
  * Starts listening for messages on `stdin`.
37655
37721
  */
37656
37722
  async start() {
37657
37723
  if (this._started) throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
37658
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);
37659
37727
  this._stdin.on("data", this._ondata);
37660
37728
  this._stdin.on("error", this._onerror);
37729
+ this._stdin.on("end", this._onstdinclose);
37730
+ this._stdin.on("close", this._onstdinclose);
37661
37731
  this._stdout.on("error", this._onstdouterror);
37662
37732
  }
37663
37733
  processReadBuffer() {
@@ -37674,7 +37744,9 @@ var StdioServerTransport = class {
37674
37744
  this._closed = true;
37675
37745
  this._stdin.off("data", this._ondata);
37676
37746
  this._stdin.off("error", this._onerror);
37677
- 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;
37678
37750
  if (this._stdin.listenerCount("data") === 0) this._stdin.pause();
37679
37751
  this._readBuffer.clear();
37680
37752
  this.onclose?.();
@@ -37951,6 +38023,7 @@ function serveStdio(factory, options = {}) {
37951
38023
  return;
37952
38024
  }
37953
38025
  if (state.era === "modern" && await tryServeListen(message)) return;
38026
+ if (isTornDown()) return;
37954
38027
  state.instance.channel.deliver(message);
37955
38028
  return;
37956
38029
  }
@@ -38017,6 +38090,7 @@ function serveStdio(factory, options = {}) {
38017
38090
  };
38018
38091
  }
38019
38092
  if (await tryServeListen(message)) return;
38093
+ if (isTornDown()) return;
38020
38094
  state.instance.channel.deliver(message, { classification: opening.classification });
38021
38095
  return;
38022
38096
  case "legacy": {
@@ -38103,6 +38177,9 @@ function toError(value) {
38103
38177
  return value instanceof Error ? value : new Error(String(value));
38104
38178
  }
38105
38179
 
38180
+ // node_modules/@modelcontextprotocol/server/dist/index.mjs
38181
+ var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
38182
+
38106
38183
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
38107
38184
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
38108
38185
  var McpToolError = class extends Error {
@@ -38119,11 +38196,12 @@ var McpToolError = class extends Error {
38119
38196
  function createHelpfulError(message, opts) {
38120
38197
  return new McpToolError(message, opts);
38121
38198
  }
38199
+ var HEADER_NAME_RUN = "[a-z0-9_-]{0,40}";
38122
38200
  var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
38123
- 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;
38124
38202
  var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
38125
- var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
38126
- 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;
38127
38205
  var API_KEY_RE = new RegExp([
38128
38206
  "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
38129
38207
  // OpenAI / Anthropic (incl. sk-ant-…)
@@ -38138,10 +38216,10 @@ var API_KEY_RE = new RegExp([
38138
38216
  "whsec_[A-Za-z0-9]{16,}\\b"
38139
38217
  // webhook signing secret (Stripe-style)
38140
38218
  ].map((p) => `\\b${p}`).join("|"), "g");
38141
- 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");
38142
38220
  var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
38143
38221
  var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
38144
- 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");
38145
38223
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
38146
38224
  var JSON_SECRET_KEYS = [
38147
38225
  "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
@@ -38149,7 +38227,8 @@ var JSON_SECRET_KEYS = [
38149
38227
  "api[_-]?secret",
38150
38228
  "(?:x[_-])?api[_-]?key",
38151
38229
  "private[_-]?key",
38152
- "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
38153
38232
  "(?:proxy-)?authorization",
38154
38233
  "password",
38155
38234
  "passwd",
@@ -38158,12 +38237,30 @@ var JSON_SECRET_KEYS = [
38158
38237
  ].join("|");
38159
38238
  var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
38160
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
+ }
38161
38256
  function redactSecrets(text) {
38162
- 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]");
38163
38258
  }
38259
+ var ERROR_REDACTION_INPUT_MAX = 64 * 1024;
38164
38260
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
38165
38261
  const str = text === null || text === void 0 ? "" : String(text);
38166
- 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);
38167
38264
  if (redacted.length <= max)
38168
38265
  return redacted;
38169
38266
  return `${redacted.slice(0, max)}\u2026 [truncated]`;
@@ -38516,6 +38613,9 @@ async function tokenConfirmation(action, fb) {
38516
38613
  const subject = await fb.subject();
38517
38614
  if (isToolResult(subject))
38518
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
+ }
38519
38619
  const binding = {
38520
38620
  tool: fb.tool,
38521
38621
  ...fb.account === void 0 ? {} : { account: fb.account },
@@ -38633,9 +38733,19 @@ function readConfirmMode(env = process.env) {
38633
38733
  }
38634
38734
  return "refuse";
38635
38735
  }
38636
- function confirmTtlFromEnv(env = process.env) {
38736
+ function readConfirmTtl(env) {
38637
38737
  const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
38638
- return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
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;
38639
38749
  }
38640
38750
  var processKey;
38641
38751
  function confirmKeyFromEnv(env = process.env) {
@@ -38647,15 +38757,33 @@ function confirmKeyFromEnv(env = process.env) {
38647
38757
  }
38648
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.)";
38649
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
+ }
38650
38768
  function confirmationFromEnv(options) {
38651
- const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
38769
+ const { tool, account, confirmToken, subject, instruction, spent, args, env = process.env, ...rest } = options;
38652
38770
  const mode = readConfirmMode(env);
38653
- if (mode === "refuse") {
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;
38654
38776
  return {
38655
38777
  ...confirmation,
38656
- unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
38778
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
38657
38779
  };
38658
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
+ };
38659
38787
  return {
38660
38788
  ...confirmation,
38661
38789
  tokenFallback: {
@@ -38663,8 +38791,8 @@ function confirmationFromEnv(options) {
38663
38791
  tool,
38664
38792
  ...account === void 0 ? {} : { account },
38665
38793
  ...confirmToken === void 0 ? {} : { confirmToken },
38666
- subject,
38667
- ttlSeconds: confirmTtlFromEnv(env),
38794
+ subject: boundSubject,
38795
+ ttlSeconds,
38668
38796
  instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
38669
38797
  ...spent === void 0 ? {} : { spent }
38670
38798
  }
@@ -38788,6 +38916,8 @@ function withGracefulShutdown(target, opts = {}) {
38788
38916
  };
38789
38917
  const handler = (signal) => {
38790
38918
  if (shuttingDown) {
38919
+ if (signal === "stdin-eof")
38920
+ return;
38791
38921
  if (Date.now() - firstSignalAt < repeatGraceMs)
38792
38922
  return;
38793
38923
  if (shouldExit) {
@@ -38823,6 +38953,10 @@ function withGracefulShutdown(target, opts = {}) {
38823
38953
  };
38824
38954
  process.on("SIGINT", () => handler("SIGINT"));
38825
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
+ }
38826
38960
  }
38827
38961
  function runMcp(opts) {
38828
38962
  if (opts.banner !== void 0) {
@@ -38841,7 +38975,8 @@ function runMcp(opts) {
38841
38975
  });
38842
38976
  const shutdown = opts.shutdown ?? true;
38843
38977
  if (shutdown !== false) {
38844
- withGracefulShutdown(handle, shutdown === true ? {} : shutdown);
38978
+ const stdinDefault = spec === "stdio" ? { stdin: process.stdin } : {};
38979
+ withGracefulShutdown(handle, { ...stdinDefault, ...shutdown === true ? {} : shutdown });
38845
38980
  }
38846
38981
  return handle;
38847
38982
  }
@@ -38973,7 +39108,7 @@ async function resolveAuthPattern(pattern) {
38973
39108
  if (resolver)
38974
39109
  return resolver();
38975
39110
  }
38976
- 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." });
38977
39112
  }
38978
39113
 
38979
39114
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
@@ -39353,7 +39488,7 @@ var TokenManager = class {
39353
39488
  };
39354
39489
 
39355
39490
  // src/client.ts
39356
- import { dirname as dirname3, join as join5 } from "path";
39491
+ import { dirname as dirname3, join as join6 } from "path";
39357
39492
  import { fileURLToPath } from "url";
39358
39493
 
39359
39494
  // node_modules/@fetchproxy/protocol/dist/crypto.js
@@ -39559,6 +39694,30 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
39559
39694
  "write_cookies"
39560
39695
  ]);
39561
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
+
39562
39721
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
39563
39722
  var ID_RE = /^([^:]+):([^:]+):([0-9a-f]{16})$/;
39564
39723
  function generateMcpId(serverName, version2) {
@@ -40273,6 +40432,9 @@ function validateHello(raw) {
40273
40432
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
40274
40433
  if (raw.accepts !== void 0)
40275
40434
  assertAcceptsList(raw.accepts);
40435
+ if (raw.unavailableCapabilities !== void 0) {
40436
+ assertUnavailableCapabilities(raw.unavailableCapabilities);
40437
+ }
40276
40438
  return raw;
40277
40439
  }
40278
40440
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -40338,6 +40500,22 @@ function assertAcceptsList(accepts) {
40338
40500
  }
40339
40501
  }
40340
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
+ }
40341
40519
  function validatePeerGone(raw) {
40342
40520
  assertString(raw.mcpId, "peer-gone.mcpId");
40343
40521
  if (!isValidMcpId(raw.mcpId))
@@ -40813,6 +40991,8 @@ function validateInnerResponse(raw) {
40813
40991
  }
40814
40992
  if (raw.ok === false) {
40815
40993
  assertString(raw.error, "inner.error");
40994
+ if (raw.code !== void 0)
40995
+ assertString(raw.code, "inner.code");
40816
40996
  if (raw.op !== void 0) {
40817
40997
  if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
40818
40998
  throw new ProtocolError3(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
@@ -40908,6 +41088,38 @@ async function openEncryptedFrameDetailed(sessionKey, frame, direction) {
40908
41088
  }
40909
41089
  }
40910
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
+
40911
41123
  // node_modules/@fetchproxy/server/dist/election.js
40912
41124
  import { createServer, Server as HttpServer } from "node:http";
40913
41125
  var DEFAULT_BIND_TIMEOUT_MS = 5e3;
@@ -41153,7 +41365,7 @@ var FetchproxySessionNotReadyError = class extends Error {
41153
41365
  hint;
41154
41366
  constructor(info) {
41155
41367
  const pairing = info.pairCode !== null && info.pairCode !== "";
41156
- 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.`;
41157
41369
  super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
41158
41370
  this.name = "FetchproxySessionNotReadyError";
41159
41371
  this.reason = pairing ? "pair-required" : "not-ready";
@@ -41166,15 +41378,35 @@ var FetchproxySessionNotReadyError = class extends Error {
41166
41378
  var FetchproxyHelloRejectedError = class extends Error {
41167
41379
  mcpId;
41168
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;
41169
41395
  constructor(info) {
41170
- 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.`));
41171
41400
  this.name = "FetchproxyHelloRejectedError";
41172
41401
  this.mcpId = info.mcpId;
41173
41402
  this.reason = info.reason;
41403
+ this.unavailableCapabilities = unavailable ?? [];
41404
+ this.platform = platform;
41405
+ this.hint = hint;
41174
41406
  Object.setPrototypeOf(this, new.target.prototype);
41175
41407
  }
41176
41408
  };
41177
- var MIN_VERSION = "3.0.0";
41409
+ var MIN_SERVER_VERSION = "3.0.0";
41178
41410
  var FetchproxyProtocolVersionError = class extends Error {
41179
41411
  /** The protocol version this process speaks. */
41180
41412
  ourVersion;
@@ -41182,7 +41414,7 @@ var FetchproxyProtocolVersionError = class extends Error {
41182
41414
  theirVersion;
41183
41415
  peer;
41184
41416
  constructor(info) {
41185
- 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`;
41186
41418
  super(`protocol version mismatch: this MCP speaks fetchproxy protocol ${info.ourVersion}, ${far}`);
41187
41419
  this.name = "FetchproxyProtocolVersionError";
41188
41420
  this.ourVersion = info.ourVersion;
@@ -41215,9 +41447,9 @@ async function awaitSessionReady(ready, opts) {
41215
41447
  }
41216
41448
 
41217
41449
  // node_modules/@fetchproxy/server/dist/extension-trust.js
41218
- 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";
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";
41219
41451
  import { randomBytes as randomBytes5 } from "node:crypto";
41220
- import { isAbsolute as isAbsolute4, join as join4 } from "node:path";
41452
+ import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
41221
41453
 
41222
41454
  // node_modules/@fetchproxy/server/dist/identity.js
41223
41455
  import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
@@ -41318,6 +41550,7 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
41318
41550
  throw e;
41319
41551
  }
41320
41552
  let unreadable = null;
41553
+ let recovered = false;
41321
41554
  for (let attempt = 0; attempt < 5; attempt++) {
41322
41555
  const id = await generateIdentity();
41323
41556
  if (await publishIdentityIfAbsent(dir, serverName, id))
@@ -41331,6 +41564,11 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
41331
41564
  if (!(e instanceof SyntaxError))
41332
41565
  throw e;
41333
41566
  unreadable = e;
41567
+ if (!recovered && await removeAbandonedIdentity(path)) {
41568
+ recovered = true;
41569
+ attempt--;
41570
+ continue;
41571
+ }
41334
41572
  await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
41335
41573
  }
41336
41574
  }
@@ -41372,6 +41610,192 @@ async function publishIdentityIfAbsent(dir, serverName, id) {
41372
41610
  await rm(tmp, { force: true });
41373
41611
  }
41374
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
+ }
41375
41799
 
41376
41800
  // node_modules/@fetchproxy/server/dist/extension-trust.js
41377
41801
  var TRUST_DIR_ENV = "FETCHPROXY_TRUST_DIR";
@@ -41395,6 +41819,22 @@ function defaultTrustDir() {
41395
41819
  }
41396
41820
  function fileExtensionTrust(args) {
41397
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
+ }
41398
41838
  return {
41399
41839
  allowNew: args.allowNew,
41400
41840
  location: extensionTrustPath(args.serverName, dir),
@@ -41428,13 +41868,13 @@ function decideExtensionTrust(args) {
41428
41868
  };
41429
41869
  }
41430
41870
  function extensionTrustPath(serverName, dir = defaultTrustDir()) {
41431
- return join4(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
41871
+ return join5(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
41432
41872
  }
41433
41873
  function extensionTrustPathHint(serverName) {
41434
41874
  try {
41435
41875
  return extensionTrustPath(serverName);
41436
41876
  } catch {
41437
- return join4(defaultTrustDir(), "<server-name>.extension-trust.json");
41877
+ return join5(defaultTrustDir(), "<server-name>.extension-trust.json");
41438
41878
  }
41439
41879
  }
41440
41880
  function isPin(x) {
@@ -41447,7 +41887,7 @@ async function readExtensionPin(serverName, dir = defaultTrustDir()) {
41447
41887
  const path = extensionTrustPath(serverName, dir);
41448
41888
  let raw;
41449
41889
  try {
41450
- raw = await readFile2(path, "utf8");
41890
+ raw = await readFile3(path, "utf8");
41451
41891
  } catch (e) {
41452
41892
  if (e.code === "ENOENT")
41453
41893
  return null;
@@ -41620,6 +42060,7 @@ async function startHost(opts) {
41620
42060
  return true;
41621
42061
  }
41622
42062
  let extensionHello = null;
42063
+ let ownSessionInfo = null;
41623
42064
  let extensionClaim = null;
41624
42065
  const pairCodeFor = async (hello, mint) => {
41625
42066
  if (!mint)
@@ -41638,6 +42079,7 @@ async function startHost(opts) {
41638
42079
  return;
41639
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).`);
41640
42081
  ownSession = dropOwnSessionAndEphemeral();
42082
+ ownSessionInfo = null;
41641
42083
  resetSessionPromise();
41642
42084
  const mintedKeypair = await generateSessionKeypair();
41643
42085
  const mintedHello = await buildServerHello({
@@ -41696,28 +42138,39 @@ async function startHost(opts) {
41696
42138
  return;
41697
42139
  }
41698
42140
  extensionClaim = ws;
41699
- let pin;
41700
- try {
41701
- pin = await opts.extensionTrust.read();
41702
- } catch (e) {
41703
- console.error(`[fetchproxy] ${String(e)}`);
41704
- if (extensionClaim === ws)
41705
- extensionClaim = null;
41706
- ws.close(1008, "extension pin unreadable");
41707
- 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
+ });
41708
42168
  }
41709
- const outcome = decideExtensionTrust({
41710
- pin,
41711
- hello: frame,
41712
- allowNew: opts.extensionTrust.allowNew,
41713
- serverName: opts.ownServerName,
41714
- location: opts.extensionTrust.location
41715
- });
41716
42169
  if (outcome.decision === "refused") {
41717
42170
  console.warn(outcome.message);
41718
42171
  if (extensionClaim === ws)
41719
42172
  extensionClaim = null;
41720
- ws.close(1008, "extension identity is not the pinned one");
42173
+ ws.close(1008, refusalReason);
41721
42174
  return;
41722
42175
  }
41723
42176
  if (outcome.decision === "replace")
@@ -41812,8 +42265,9 @@ async function startHost(opts) {
41812
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)");
41813
42266
  return;
41814
42267
  }
41815
- const extEdPub = fromB64(extensionHello.identityEd25519Pub);
41816
- const extNonce = fromB64(extensionHello.sessionNonce);
42268
+ const verifiedHello = extensionHello;
42269
+ const extEdPub = fromB64(verifiedHello.identityEd25519Pub);
42270
+ const extNonce = fromB64(verifiedHello.sessionNonce);
41817
42271
  const msg = readySignaturePayload(held.nonce, extNonce, fromB64(frame.extensionSessionPub), held.pub);
41818
42272
  const sig = fromB64(frame.sessionSig);
41819
42273
  let sigOk = false;
@@ -41846,6 +42300,7 @@ async function startHost(opts) {
41846
42300
  if (extensionWs !== ws)
41847
42301
  return;
41848
42302
  ownSession = new SessionState(key);
42303
+ ownSessionInfo = sessionInfoFromHello(verifiedHello);
41849
42304
  ownDecryptFailures = 0;
41850
42305
  ownPendingPairCode = null;
41851
42306
  resolveOwnSession(ownSession);
@@ -41916,7 +42371,11 @@ async function startHost(opts) {
41916
42371
  }
41917
42372
  if (frame.type === "hello-rejected" && identified === "extension") {
41918
42373
  if (frame.mcpId === opts.ownMcpId) {
41919
- 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
+ }));
41920
42379
  } else {
41921
42380
  const slot = peers.get(frame.mcpId);
41922
42381
  if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
@@ -41962,6 +42421,7 @@ async function startHost(opts) {
41962
42421
  rejectOwnSession(new Error("extension disconnected before ready"));
41963
42422
  }
41964
42423
  ownSession = dropOwnSessionAndEphemeral();
42424
+ ownSessionInfo = null;
41965
42425
  ownDecryptFailures = 0;
41966
42426
  ownPendingPairCode = null;
41967
42427
  resetSessionPromise();
@@ -42008,6 +42468,7 @@ async function startHost(opts) {
42008
42468
  });
42009
42469
  if (!extensionWs)
42010
42470
  throw new Error("host: no extension connected");
42471
+ opts.refuseOutbound?.(inner, ownSessionInfo);
42011
42472
  const plaintext = encodeOutboundInnerFrame(opts.ownMcpId, inner);
42012
42473
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), plaintext, "s2e");
42013
42474
  extensionWs.send(JSON.stringify(sealed));
@@ -42023,7 +42484,8 @@ async function startHost(opts) {
42023
42484
  },
42024
42485
  pendingPairCode: () => ownPendingPairCode,
42025
42486
  extensionConnected: () => extensionWs !== null,
42026
- sessionLinked: () => ownSession !== null
42487
+ sessionLinked: () => ownSession !== null,
42488
+ sessionInfo: () => ownSession !== null ? ownSessionInfo : null
42027
42489
  };
42028
42490
  }
42029
42491
 
@@ -42108,6 +42570,7 @@ async function startPeer(opts) {
42108
42570
  const closeListeners = [];
42109
42571
  const extensionDisconnectListeners = [];
42110
42572
  let session = null;
42573
+ let sessionInfo = null;
42111
42574
  let sessionEphemeral = null;
42112
42575
  function dropSessionEphemeral() {
42113
42576
  if (sessionEphemeral)
@@ -42173,6 +42636,15 @@ async function startPeer(opts) {
42173
42636
  console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
42174
42637
  return null;
42175
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
+ }
42176
42648
  if (cachedPin === void 0) {
42177
42649
  try {
42178
42650
  cachedPin = await opts.extensionTrust.read();
@@ -42240,6 +42712,7 @@ async function startPeer(opts) {
42240
42712
  extensionGone = true;
42241
42713
  dropSessionEphemeral();
42242
42714
  pendingPairCode = null;
42715
+ sessionInfo = null;
42243
42716
  if (session !== null) {
42244
42717
  session = null;
42245
42718
  if (sessionPromiseSettled)
@@ -42272,6 +42745,7 @@ async function startPeer(opts) {
42272
42745
  }
42273
42746
  const isRenegotiation = session !== null;
42274
42747
  session = new SessionState(sessionKey);
42748
+ sessionInfo = sessionInfoFromHello(authenticated);
42275
42749
  extensionGone = false;
42276
42750
  pendingPairCode = null;
42277
42751
  if (isRenegotiation) {
@@ -42282,7 +42756,11 @@ async function startPeer(opts) {
42282
42756
  return;
42283
42757
  }
42284
42758
  if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
42285
- 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
+ }));
42286
42764
  }
42287
42765
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
42288
42766
  const hello2 = extensionHello;
@@ -42382,6 +42860,7 @@ async function startPeer(opts) {
42382
42860
  const s = session;
42383
42861
  if (s === null)
42384
42862
  throw new Error("peer: extension disconnected");
42863
+ opts.refuseOutbound?.(inner, sessionInfo);
42385
42864
  const plaintext = encodeOutboundInnerFrame(opts.mcpId, inner);
42386
42865
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), plaintext, "s2e");
42387
42866
  if (ws.readyState !== import_websocket.default.OPEN)
@@ -42400,6 +42879,7 @@ async function startPeer(opts) {
42400
42879
  pendingPairCode: () => pendingPairCode,
42401
42880
  extensionConnected: () => extensionHello !== null,
42402
42881
  sessionLinked: () => session !== null && !extensionGone,
42882
+ sessionInfo: () => session !== null && !extensionGone ? sessionInfo : null,
42403
42883
  onClose: (cb) => {
42404
42884
  closeListeners.push(cb);
42405
42885
  },
@@ -42412,7 +42892,9 @@ async function startPeer(opts) {
42412
42892
  }
42413
42893
 
42414
42894
  // node_modules/@fetchproxy/server/dist/error-kind.js
42415
- function classifyFetchError(error62) {
42895
+ function classifyFetchError(error62, code) {
42896
+ if (code === CAPABILITY_UNAVAILABLE_CODE)
42897
+ return "capability_unavailable";
42416
42898
  if (/^(in-page )?fetch threw:/.test(error62)) {
42417
42899
  return "tab_fetch_failed";
42418
42900
  }
@@ -42428,6 +42910,9 @@ function classifyFetchError(error62) {
42428
42910
  if (/not in domains \[/.test(error62)) {
42429
42911
  return "domain_denied";
42430
42912
  }
42913
+ if (parseCapabilityUnavailable(error62) !== null) {
42914
+ return "capability_unavailable";
42915
+ }
42431
42916
  if (/^capability .+ not granted/.test(error62)) {
42432
42917
  return "capability_denied";
42433
42918
  }
@@ -42506,7 +42991,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
42506
42991
  const retryAttempted = args.retryAttempted ?? false;
42507
42992
  const op = args.op ?? "fetch";
42508
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). `;
42509
- 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.`;
42510
42995
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
42511
42996
  this.name = "FetchproxyBridgeDownError";
42512
42997
  this.originalError = args.originalError;
@@ -42533,7 +43018,7 @@ var FetchproxyHintedError = class extends FetchproxyProtocolError {
42533
43018
  };
42534
43019
  var FetchproxyScopeError = class extends FetchproxyHintedError {
42535
43020
  constructor(originalError) {
42536
- 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.");
42537
43022
  this.name = "FetchproxyScopeError";
42538
43023
  }
42539
43024
  };
@@ -42555,6 +43040,19 @@ var FetchproxyWaitedError = class extends FetchproxyHintedError {
42555
43040
  this.name = "FetchproxyWaitedError";
42556
43041
  }
42557
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
+ };
42558
43056
  function waitedHint(op) {
42559
43057
  const tail = " This is not a version problem and does not need an update.";
42560
43058
  if (op === "capture" || op === "capture_redirect") {
@@ -42568,7 +43066,15 @@ function waitedHint(op) {
42568
43066
  var SCOPE_REJECTION = /not in declared/;
42569
43067
  var NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
42570
43068
  var TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
42571
- 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
+ }
42572
43078
  if (error62 === "timeout")
42573
43079
  return new FetchproxyWaitedError(error62, op);
42574
43080
  if (SCOPE_REJECTION.test(error62))
@@ -42627,7 +43133,8 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
42627
43133
  constructor(args) {
42628
43134
  const graceMs = args.graceMs ?? VERB_DEADLINE_GRACE_MS;
42629
43135
  const callerChose = args.requestedTimeoutMs !== void 0 && args.timeoutMs === args.requestedTimeoutMs + graceMs;
42630
- 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);
42631
43138
  this.name = "FetchproxyTimeoutError";
42632
43139
  this.url = args.url;
42633
43140
  this.timeoutMs = args.timeoutMs;
@@ -42639,6 +43146,10 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
42639
43146
  this.retrySafe = args.retrySafe ?? true;
42640
43147
  }
42641
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
+ }
42642
43153
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
42643
43154
  function normalizeCookiePath(path) {
42644
43155
  if (path === void 0 || path === "")
@@ -42898,6 +43409,7 @@ var FetchproxyServer = class {
42898
43409
  identityDir: opts.identityDir,
42899
43410
  trustDir: opts.trustDir,
42900
43411
  allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
43412
+ extensionPins: opts.extensionPins,
42901
43413
  requireExtensionIdentity: opts.requireExtensionIdentity,
42902
43414
  onPairCode: opts.onPairCode
42903
43415
  };
@@ -42983,6 +43495,7 @@ var FetchproxyServer = class {
42983
43495
  this.role = "host";
42984
43496
  this.hostHandle = await startHost({
42985
43497
  httpServer: el.server,
43498
+ refuseOutbound: (inner, info) => this.refuseIfUnavailable(inner, info),
42986
43499
  ownIdentity: identity,
42987
43500
  ownMcpId: mcpId,
42988
43501
  ownServerName: this.opts.serverName,
@@ -43013,6 +43526,7 @@ var FetchproxyServer = class {
43013
43526
  } else {
43014
43527
  this.role = "peer";
43015
43528
  this.peerHandle = await startPeer({
43529
+ refuseOutbound: (inner, info) => this.refuseIfUnavailable(inner, info),
43016
43530
  host: this.opts.host,
43017
43531
  port: this.opts.port,
43018
43532
  identity,
@@ -43061,7 +43575,7 @@ var FetchproxyServer = class {
43061
43575
  }
43062
43576
  }
43063
43577
  pairingErrorMessage(code) {
43064
- 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.`;
43065
43579
  }
43066
43580
  /**
43067
43581
  * Raw single-shot fetch through the bridge. Most callers should prefer
@@ -43191,13 +43705,28 @@ var FetchproxyServer = class {
43191
43705
  * every one of them.
43192
43706
  */
43193
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
+ }
43194
43722
  return fileExtensionTrust({
43195
43723
  serverName: this.opts.serverName,
43196
43724
  dir: this.opts.identityDir,
43197
43725
  trustDir: this.opts.trustDir,
43198
- allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
43726
+ allowNew
43199
43727
  });
43200
43728
  }
43729
+ warnedAllowNewIgnored = false;
43201
43730
  noteActivityForKeepalive() {
43202
43731
  const intervalMs = this.opts.keepAliveIntervalMs;
43203
43732
  if (intervalMs <= 0)
@@ -43291,6 +43820,15 @@ var FetchproxyServer = class {
43291
43820
  resendable: isRetrySafeOnTimeout(init.method, fetchOpts.retryOnTimeout)
43292
43821
  });
43293
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
+ }
43294
43832
  return {
43295
43833
  ok: false,
43296
43834
  error: err instanceof Error ? err.message : String(err),
@@ -43311,7 +43849,8 @@ var FetchproxyServer = class {
43311
43849
  timer = setTimeout(() => {
43312
43850
  this.pending.delete(id);
43313
43851
  const elapsedMs = Date.now() - start;
43314
- 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);
43315
43854
  resolve6({
43316
43855
  ok: false,
43317
43856
  error: error62,
@@ -43433,6 +43972,9 @@ var FetchproxyServer = class {
43433
43972
  port: this.opts.port
43434
43973
  });
43435
43974
  }
43975
+ if (result.kind === "capability_unavailable") {
43976
+ return protocolErrorFrom(result.error, void 0, { code: CAPABILITY_UNAVAILABLE_CODE, op });
43977
+ }
43436
43978
  return protocolErrorFrom(result.error);
43437
43979
  }
43438
43980
  /**
@@ -43499,6 +44041,9 @@ var FetchproxyServer = class {
43499
44041
  if (result.kind === "session_not_ready" && result.cause instanceof Error) {
43500
44042
  throw result.cause;
43501
44043
  }
44044
+ if (result.kind === "capability_unavailable" && result.cause instanceof FetchproxyCapabilityUnavailableError) {
44045
+ throw result.cause;
44046
+ }
43502
44047
  throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
43503
44048
  }
43504
44049
  const response = {
@@ -43681,7 +44226,9 @@ var FetchproxyServer = class {
43681
44226
  session_state: health.session.state,
43682
44227
  pending_pair_code: health.session.pairCode,
43683
44228
  extension_connected: health.session.extensionConnected,
43684
- last_extension_message_at: health.lastExtensionMessageAt
44229
+ last_extension_message_at: health.lastExtensionMessageAt,
44230
+ unavailable_capabilities: health.session.unavailableCapabilities,
44231
+ platform: health.session.platform
43685
44232
  },
43686
44233
  ...error62 ? { error: error62 } : {}
43687
44234
  };
@@ -44327,7 +44874,7 @@ var FetchproxyServer = class {
44327
44874
  fetchCb({
44328
44875
  ok: false,
44329
44876
  error: inner.error,
44330
- kind: classifyFetchError(inner.error),
44877
+ kind: classifyFetchError(inner.error, inner.code),
44331
44878
  retryAttempted: false
44332
44879
  });
44333
44880
  }
@@ -44343,7 +44890,7 @@ var FetchproxyServer = class {
44343
44890
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
44344
44891
  }
44345
44892
  } else {
44346
- storageCb.reject(protocolErrorFrom(inner.error));
44893
+ storageCb.reject(protocolErrorFrom(inner.error, void 0, inner));
44347
44894
  }
44348
44895
  return;
44349
44896
  }
@@ -44357,7 +44904,7 @@ var FetchproxyServer = class {
44357
44904
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
44358
44905
  }
44359
44906
  } else {
44360
- captureCb.reject(protocolErrorFrom(inner.error, "capture"));
44907
+ captureCb.reject(protocolErrorFrom(inner.error, "capture", inner));
44361
44908
  }
44362
44909
  return;
44363
44910
  }
@@ -44371,7 +44918,7 @@ var FetchproxyServer = class {
44371
44918
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
44372
44919
  }
44373
44920
  } else {
44374
- redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect"));
44921
+ redirectCb.reject(protocolErrorFrom(inner.error, "capture_redirect", inner));
44375
44922
  }
44376
44923
  return;
44377
44924
  }
@@ -44385,7 +44932,7 @@ var FetchproxyServer = class {
44385
44932
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
44386
44933
  }
44387
44934
  } else {
44388
- idbCb.reject(protocolErrorFrom(inner.error));
44935
+ idbCb.reject(protocolErrorFrom(inner.error, void 0, inner));
44389
44936
  }
44390
44937
  return;
44391
44938
  }
@@ -44399,7 +44946,7 @@ var FetchproxyServer = class {
44399
44946
  domListCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_dom_list awaiter`));
44400
44947
  }
44401
44948
  } else {
44402
- domListCb.reject(protocolErrorFrom(inner.error));
44949
+ domListCb.reject(protocolErrorFrom(inner.error, void 0, inner));
44403
44950
  }
44404
44951
  return;
44405
44952
  }
@@ -44413,7 +44960,7 @@ var FetchproxyServer = class {
44413
44960
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
44414
44961
  }
44415
44962
  } else {
44416
- downloadCb.reject(protocolErrorFrom(inner.error, "download"));
44963
+ downloadCb.reject(protocolErrorFrom(inner.error, "download", inner));
44417
44964
  }
44418
44965
  return;
44419
44966
  }
@@ -44427,7 +44974,7 @@ var FetchproxyServer = class {
44427
44974
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
44428
44975
  }
44429
44976
  } else {
44430
- graphqlCb.reject(protocolErrorFrom(inner.error));
44977
+ graphqlCb.reject(protocolErrorFrom(inner.error, void 0, inner));
44431
44978
  }
44432
44979
  return;
44433
44980
  }
@@ -44437,7 +44984,7 @@ var FetchproxyServer = class {
44437
44984
  if (inner.ok && inner.op === "write_cookies") {
44438
44985
  writeCookiesCb.resolve([...inner.written]);
44439
44986
  } else {
44440
- 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));
44441
44988
  }
44442
44989
  return;
44443
44990
  }
@@ -44627,11 +45174,38 @@ var FetchproxyServer = class {
44627
45174
  const handle = this.hostHandle ?? this.peerHandle;
44628
45175
  const pairCode = this.currentPendingPairCode();
44629
45176
  if (!handle || this.role === null) {
44630
- return { state: "not_listening", pairCode, extensionConnected: false };
45177
+ return {
45178
+ state: "not_listening",
45179
+ pairCode,
45180
+ extensionConnected: false,
45181
+ unavailableCapabilities: [],
45182
+ platform: null
45183
+ };
44631
45184
  }
45185
+ const info = handle.sessionInfo?.() ?? null;
44632
45186
  const extensionConnected = handle.extensionConnected();
44633
45187
  const state = handle.sessionLinked() ? "linked" : pairCode !== null ? "pair_pending" : !extensionConnected ? "extension_disconnected" : "no_session";
44634
- 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 });
44635
45209
  }
44636
45210
  /**
44637
45211
  * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
@@ -45105,7 +45679,7 @@ async function loginWithPassword(username, password) {
45105
45679
  // package.json
45106
45680
  var package_default = {
45107
45681
  name: "ofw-mcp",
45108
- version: "2.19.4",
45682
+ version: "2.20.0",
45109
45683
  license: "MIT",
45110
45684
  mcpName: "io.github.chrischall/ofw-mcp",
45111
45685
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -45139,8 +45713,8 @@ var package_default = {
45139
45713
  typecheck: "tsc -p tsconfig.json --noEmit"
45140
45714
  },
45141
45715
  dependencies: {
45142
- "@chrischall/mcp-utils": "^2.6.0",
45143
- "@fetchproxy/bootstrap": "^3.2.0",
45716
+ "@chrischall/mcp-utils": "^2.8.0",
45717
+ "@fetchproxy/bootstrap": "^3.4.1",
45144
45718
  "@modelcontextprotocol/server": "^2.0.0",
45145
45719
  dotenv: "^18.0.0",
45146
45720
  zod: "^4.6.5"
@@ -45159,8 +45733,8 @@ var package_default = {
45159
45733
  function fetchproxyDisabled() {
45160
45734
  return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
45161
45735
  }
45162
- 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).";
45163
- 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";
45164
45738
  function isBridgeDown(e) {
45165
45739
  return e instanceof Error && e.message.startsWith(BRIDGE_DOWN_PREFIX);
45166
45740
  }
@@ -45202,7 +45776,7 @@ async function resolveAuth() {
45202
45776
  const expiryRaw = session.localStorage["tokenExpiry"];
45203
45777
  if (!token) {
45204
45778
  throw new Error(
45205
- '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.'
45206
45780
  );
45207
45781
  }
45208
45782
  return {
@@ -45277,7 +45851,7 @@ function reportCacheWriteFailure(err) {
45277
45851
  // src/client.ts
45278
45852
  try {
45279
45853
  const dir = dirname3(fileURLToPath(import.meta.url));
45280
- await loadDotenvSafely({ path: join5(dir, "..", ".env") });
45854
+ await loadDotenvSafely({ path: join6(dir, "..", ".env") });
45281
45855
  } catch {
45282
45856
  }
45283
45857
  function parseContentDispositionFilename(cd) {
@@ -45384,7 +45958,11 @@ var OFWClient = class {
45384
45958
  if (response.status === 429) throw new Error("Rate limited by OFW API");
45385
45959
  }
45386
45960
  if (!response.ok) {
45387
- throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
45961
+ const errorBody = await response.text();
45962
+ const safeBody = redactSecrets(errorBody).replace(/\s+/g, " ").trim().slice(0, 4e3);
45963
+ throw new Error(
45964
+ `OFW API error: ${response.status} ${response.statusText} for ${method} ${path}` + (safeBody ? ` \u2014 ${safeBody}` : "")
45965
+ );
45388
45966
  }
45389
45967
  return response;
45390
45968
  }
@@ -45774,11 +46352,12 @@ async function requestWrite(client2, method, path, body) {
45774
46352
  }
45775
46353
  }
45776
46354
  function unconfirmedWriteResponse(e, opts) {
46355
+ const fallback = /ourfamilywizard\.com/i.test(opts.checkWith) ? "" : " (or on ourfamilywizard.com)";
45777
46356
  return jsonErrorResponse({
45778
46357
  result: opts.result,
45779
46358
  mayHaveLanded: true,
45780
46359
  reason: `The request to ${opts.what} failed without a definitive answer from OFW: ${e.message}. It MAY HAVE BEEN APPLIED on OurFamilyWizard.`,
45781
- 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.`
46360
+ remedy: `Do NOT retry blindly \u2014 a second attempt can put a duplicate on the co-parent-visible record. First check with ${opts.checkWith}${fallback}, and retry only once you have confirmed it did not land.`
45782
46361
  });
45783
46362
  }
45784
46363
  async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
@@ -45861,12 +46440,12 @@ function registerHealthcheckTools(server, client2, resolve6 = resolveAuth) {
45861
46440
  kind: "transport",
45862
46441
  // The upstream `.hint` rides along in `error.message` — it carries
45863
46442
  // the actionable "click the toolbar icon" copy this cannot know.
45864
- 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."
46443
+ 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."
45865
46444
  } : void 0,
45866
46445
  hints: {
45867
46446
  // Now means exactly what it says: nothing is set up. A configured path
45868
46447
  // that was tried and failed no longer lands here.
45869
- 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).",
46448
+ 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).",
45870
46449
  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."
45871
46450
  }
45872
46451
  });
@@ -46293,19 +46872,19 @@ async function syncAll(client2, opts, store) {
46293
46872
  // src/config.ts
46294
46873
  import { createHash as createHash5 } from "node:crypto";
46295
46874
  import { homedir as homedir4 } from "node:os";
46296
- import { join as join6 } from "node:path";
46875
+ import { join as join7 } from "node:path";
46297
46876
  function readCacheIdentity() {
46298
46877
  return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
46299
46878
  }
46300
46879
  function getCacheDir() {
46301
46880
  const override = process.env.OFW_CACHE_DIR;
46302
46881
  if (override && override.trim().length > 0) return override.trim();
46303
- return join6(homedir4(), ".cache", "ofw-mcp");
46882
+ return join7(homedir4(), ".cache", "ofw-mcp");
46304
46883
  }
46305
46884
  function getCacheDbPath() {
46306
46885
  const identity = readCacheIdentity();
46307
46886
  const hash2 = createHash5("sha256").update(identity).digest("hex").slice(0, 16);
46308
- return join6(getCacheDir(), `${hash2}.db`);
46887
+ return join7(getCacheDir(), `${hash2}.db`);
46309
46888
  }
46310
46889
  function getAttachmentsDir() {
46311
46890
  const override = process.env.OFW_ATTACHMENTS_DIR;
@@ -46313,13 +46892,30 @@ function getAttachmentsDir() {
46313
46892
  return getDefaultAttachmentsDir();
46314
46893
  }
46315
46894
  function getDefaultAttachmentsDir() {
46316
- return join6(homedir4(), "Downloads", "ofw-mcp");
46895
+ return join7(homedir4(), "Downloads", "ofw-mcp");
46317
46896
  }
46318
46897
  function getUploadDir() {
46319
46898
  const override = process.env.OFW_UPLOAD_DIR;
46320
46899
  if (override && override.trim().length > 0) return override.trim();
46321
46900
  return getAttachmentsDir();
46322
46901
  }
46902
+ function restrictiveBoolEnv(name) {
46903
+ const raw = process.env[name];
46904
+ if (typeof raw !== "string" || raw.trim().length === 0) return false;
46905
+ const value = raw.trim().toLowerCase();
46906
+ if (["1", "true", "yes", "on"].includes(value)) return true;
46907
+ if (["0", "false", "no", "off"].includes(value)) return false;
46908
+ console.error(
46909
+ `[ofw-mcp] Unrecognized ${name} "${raw.trim()}" \u2014 failing closed to "true" (restriction enabled). Valid values: true, false.`
46910
+ );
46911
+ return true;
46912
+ }
46913
+ function getExpenseUploadOnly() {
46914
+ return restrictiveBoolEnv("OFW_EXPENSE_UPLOAD_ONLY");
46915
+ }
46916
+ function getExpenseOnly() {
46917
+ return getExpenseUploadOnly() || restrictiveBoolEnv("OFW_EXPENSE_ONLY");
46918
+ }
46323
46919
  function getWriteMode() {
46324
46920
  const raw = process.env.OFW_WRITE_MODE;
46325
46921
  if (typeof raw !== "string" || raw.trim().length === 0) return "all";
@@ -47856,6 +48452,7 @@ var NodeAttachmentIO = class {
47856
48452
  };
47857
48453
 
47858
48454
  // src/tools/delivery.ts
48455
+ var MAX_INLINE_BYTES = 10 * 1024 * 1024;
47859
48456
  async function tryExtract(bytes, mimeType, fileName, opts) {
47860
48457
  try {
47861
48458
  const extracted = await extractAttachment(bytes, mimeType, fileName, {
@@ -47870,6 +48467,17 @@ async function tryExtract(bytes, mimeType, fileName, opts) {
47870
48467
  return { reason: `extraction failed: ${err instanceof Error ? err.message : String(err)}` };
47871
48468
  }
47872
48469
  }
48470
+ function assertInlineSize(input2, rawRequested) {
48471
+ const { bytes, fileName, fileId, diskAvailable } = input2;
48472
+ if (bytes.length <= MAX_INLINE_BYTES) return;
48473
+ const hints = [];
48474
+ if (rawRequested) hints.push("omit extract:false to get the file's extracted text instead, if it is a readable document type");
48475
+ if (diskAvailable) hints.push("pass inline:false to save it to disk and get the path");
48476
+ hints.push("or open it directly in OurFamilyWizard");
48477
+ throw new Error(
48478
+ `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("; ")}.`
48479
+ );
48480
+ }
47873
48481
  async function buildInlineDelivery(input2) {
47874
48482
  const { fileId, fileName, mimeType, bytes, forcedInline, options } = input2;
47875
48483
  const meta3 = {
@@ -47882,6 +48490,7 @@ async function buildInlineDelivery(input2) {
47882
48490
  if (forcedInline) meta3.forcedInline = true;
47883
48491
  const block = () => ({ type: "text", text: JSON.stringify(meta3, null, 2) });
47884
48492
  if (isHostRenderableImage(mimeType)) {
48493
+ assertInlineSize(input2, false);
47885
48494
  meta3.deliveredVia = "image";
47886
48495
  return { content: [block(), { type: "image", data: bytes.toString("base64"), mimeType }] };
47887
48496
  }
@@ -47899,6 +48508,7 @@ async function buildInlineDelivery(input2) {
47899
48508
  }
47900
48509
  attempts.push(outcome.reason ?? "extraction produced no content");
47901
48510
  }
48511
+ assertInlineSize(input2, options.extract === false);
47902
48512
  meta3.deliveredVia = "blob";
47903
48513
  meta3.deliveryAttempts = attempts;
47904
48514
  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).";
@@ -47915,7 +48525,7 @@ async function buildInlineDelivery(input2) {
47915
48525
  }
47916
48526
 
47917
48527
  // src/tools/messages.ts
47918
- import { basename as basename2, join as join7, resolve as resolve5 } from "node:path";
48528
+ import { basename as basename2, join as join8, resolve as resolve5 } from "node:path";
47919
48529
 
47920
48530
  // src/tools/pagination.ts
47921
48531
  function pageState(input2) {
@@ -49186,7 +49796,7 @@ ${text}` : text);
49186
49796
  });
49187
49797
  });
49188
49798
  server.registerTool("ofw_download_attachment", {
49189
- 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).",
49799
+ 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).",
49190
49800
  annotations: { readOnlyHint: false, destructiveHint: false },
49191
49801
  inputSchema: external_exports.object({
49192
49802
  fileId: external_exports.number().describe("Attachment file id (from ofw_get_message \u2192 attachments[].fileId)"),
@@ -49230,7 +49840,8 @@ ${text}` : text);
49230
49840
  mimeType: mimeType2,
49231
49841
  bytes,
49232
49842
  forcedInline,
49233
- options: deliveryOptions
49843
+ options: deliveryOptions,
49844
+ diskAvailable: attachmentIO.supportsDisk
49234
49845
  });
49235
49846
  }
49236
49847
  let dest;
@@ -49239,12 +49850,12 @@ ${text}` : text);
49239
49850
  if (args.saveTo) {
49240
49851
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
49241
49852
  const abs = resolve5(root, args.saveTo.startsWith("~") ? expandPath2(args.saveTo) : args.saveTo);
49242
- dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
49853
+ dest = isDirArg ? join8(abs, `${fileId}-${safeName}`) : abs;
49243
49854
  if (!isWithin(root, dest)) {
49244
49855
  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.`);
49245
49856
  }
49246
49857
  } else {
49247
- dest = join7(root, `${fileId}-${safeName}`);
49858
+ dest = join8(root, `${fileId}-${safeName}`);
49248
49859
  }
49249
49860
  const extractOnDisk = args.extract === true;
49250
49861
  if (!args.force && cached2.downloadedPath === dest) {
@@ -49760,75 +50371,474 @@ function registerCalendarTools(server, client2) {
49760
50371
  }
49761
50372
 
49762
50373
  // src/tools/expenses.ts
49763
- function registerExpenseTools(server, client2) {
49764
- const allowWrites = getWriteMode() === "all";
49765
- server.registerTool("ofw_get_expense_totals", {
50374
+ var UploadedExpenseFileSchema = external_exports.looseObject({
50375
+ fileId: external_exports.number(),
50376
+ fileName: external_exports.string().optional(),
50377
+ label: external_exports.string().optional(),
50378
+ fileType: external_exports.string().optional(),
50379
+ sizeInBytes: external_exports.number().optional(),
50380
+ shareClass: external_exports.string().optional()
50381
+ });
50382
+ var PDF_MIME = "application/pdf";
50383
+ var MAX_REMOTE_PDF_BYTES = MAX_UPLOAD_BYTES;
50384
+ var MAX_REMOTE_REDIRECTS = 5;
50385
+ var REMOTE_FETCH_TIMEOUT_MS = 3e4;
50386
+ function isPdf(bytes) {
50387
+ return bytes.byteLength >= 5 && bytes[0] === 37 && bytes[1] === 80 && bytes[2] === 68 && bytes[3] === 70 && bytes[4] === 45;
50388
+ }
50389
+ function assertReceiptUrl(url2) {
50390
+ if (url2.protocol !== "https:") {
50391
+ throw new Error("Remote expense receipt URLs must use HTTPS.");
50392
+ }
50393
+ if (!url2.hostname.toLowerCase().endsWith(".oaiusercontent.com")) {
50394
+ throw new Error("Remote expense receipt URLs must be signed oaiusercontent.com file URLs.");
50395
+ }
50396
+ }
50397
+ async function readCapped(response, cap) {
50398
+ const tooLarge = () => new Error(`Expense receipt exceeds ${cap} bytes.`);
50399
+ const declared = Number(response.headers.get("content-length") ?? 0);
50400
+ if (Number.isFinite(declared) && declared > cap) throw tooLarge();
50401
+ const chunks = [];
50402
+ let total = 0;
50403
+ if (response.body) {
50404
+ const reader = response.body.getReader();
50405
+ for (; ; ) {
50406
+ const { done, value } = await reader.read();
50407
+ if (done) break;
50408
+ total += value.byteLength;
50409
+ if (total > cap) {
50410
+ await reader.cancel();
50411
+ throw tooLarge();
50412
+ }
50413
+ chunks.push(value);
50414
+ }
50415
+ }
50416
+ const bytes = new Uint8Array(total);
50417
+ let offset = 0;
50418
+ for (const chunk2 of chunks) {
50419
+ bytes.set(chunk2, offset);
50420
+ offset += chunk2.byteLength;
50421
+ }
50422
+ return bytes;
50423
+ }
50424
+ async function resolveRemotePdf(urlValue, fileNameValue) {
50425
+ const fileName = fileNameValue?.trim() || "receipt.pdf";
50426
+ if (!fileName.toLowerCase().endsWith(".pdf")) {
50427
+ throw new Error(`Expense receipts must use a .pdf filename; received ${fileName}`);
50428
+ }
50429
+ let url2 = new URL(urlValue);
50430
+ let response;
50431
+ for (let hop = 0; ; hop++) {
50432
+ assertReceiptUrl(url2);
50433
+ response = await fetch(url2, { redirect: "manual", signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS) });
50434
+ const location = response.headers.get("location");
50435
+ if (response.status < 300 || response.status >= 400 || location === null) break;
50436
+ if (hop >= MAX_REMOTE_REDIRECTS) {
50437
+ throw new Error(`Remote expense receipt redirected more than ${MAX_REMOTE_REDIRECTS} times.`);
50438
+ }
50439
+ url2 = new URL(location, url2);
50440
+ }
50441
+ if (!response.ok) {
50442
+ throw new Error(`Unable to fetch remote expense receipt: HTTP ${response.status}`);
50443
+ }
50444
+ const bytes = await readCapped(response, MAX_REMOTE_PDF_BYTES);
50445
+ if (!isPdf(bytes)) {
50446
+ throw new Error("Remote expense receipt is not a valid PDF file.");
50447
+ }
50448
+ return {
50449
+ blob: new Blob([bytes], { type: PDF_MIME }),
50450
+ fileName,
50451
+ mimeType: PDF_MIME,
50452
+ sizeBytes: bytes.byteLength
50453
+ };
50454
+ }
50455
+ function expenseParties(args) {
50456
+ return {
50457
+ payerUserId: args.payerId,
50458
+ categoryId: args.categoryId,
50459
+ childUserIds: args.children,
50460
+ idsNote: "payerUserId is the parent this claim is billed to; confirm it names the right parent before approving (ofw_get_profile, where registered, or the OFW web app). categoryId is from ofw_list_expense_categories."
50461
+ };
50462
+ }
50463
+ var EXPENSE_FIELDS = [
50464
+ "title",
50465
+ "amount",
50466
+ "purchaseDate",
50467
+ "categoryId",
50468
+ "payerId",
50469
+ "children",
50470
+ "isPrivate",
50471
+ "description",
50472
+ "fileIds"
50473
+ ];
50474
+ var ARG_FOR = {
50475
+ title: "title",
50476
+ amount: "amount",
50477
+ purchaseDate: "purchaseDate",
50478
+ categoryId: "categoryId",
50479
+ payerId: "payerId",
50480
+ children: "children",
50481
+ isPrivate: "privateExpense",
50482
+ description: "description",
50483
+ fileIds: "receiptFileId"
50484
+ };
50485
+ var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
50486
+ var positiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0 ? v : void 0;
50487
+ var refId = (v, key) => positiveInt(v) ?? (isRecord2(v) ? positiveInt(v[key]) ?? positiveInt(v.id) : void 0);
50488
+ var idList = (v, key) => {
50489
+ if (!Array.isArray(v)) return void 0;
50490
+ const ids = v.map((x) => refId(x, key));
50491
+ return ids.every((id) => id !== void 0) ? ids : void 0;
50492
+ };
50493
+ function readExpenseState(raw) {
50494
+ const d = isRecord2(raw) && isRecord2(raw.data) ? raw.data : raw;
50495
+ if (!isRecord2(d)) return {};
50496
+ const out = {};
50497
+ if (typeof d.title === "string" && d.title.trim()) out.title = d.title;
50498
+ const amount = typeof d.amount === "string" ? Number(d.amount) : d.amount;
50499
+ if (typeof amount === "number" && Number.isFinite(amount) && amount > 0) out.amount = amount;
50500
+ const dateRaw = isRecord2(d.purchaseDate) ? d.purchaseDate.dateTime : d.purchaseDate;
50501
+ if (typeof dateRaw === "string" && /^\d{4}-\d{2}-\d{2}/.test(dateRaw)) out.purchaseDate = dateRaw.slice(0, 10);
50502
+ const categoryId = positiveInt(d.categoryId) ?? (isRecord2(d.category) ? positiveInt(d.category.id) : void 0);
50503
+ if (categoryId !== void 0) out.categoryId = categoryId;
50504
+ const payerId = positiveInt(d.payerId) ?? refId(d.payer, "userId");
50505
+ if (payerId !== void 0) out.payerId = payerId;
50506
+ const children = idList(d.children, "userId");
50507
+ if (children !== void 0 && children.length > 0) out.children = children;
50508
+ const isPrivate = typeof d.isPrivate === "boolean" ? d.isPrivate : d.private;
50509
+ if (typeof isPrivate === "boolean") out.isPrivate = isPrivate;
50510
+ if ("description" in d) {
50511
+ if (d.description === null || d.description === "") out.description = null;
50512
+ else if (typeof d.description === "string") out.description = d.description;
50513
+ }
50514
+ for (const key of ["fileIds", "files"]) {
50515
+ if (!(key in d)) continue;
50516
+ const ids = d[key] === null ? [] : idList(d[key], "fileId");
50517
+ if (ids !== void 0) out.fileIds = ids;
50518
+ break;
50519
+ }
50520
+ return out;
50521
+ }
50522
+ function registerExpenseTools(server, client2, attachmentIO) {
50523
+ const writeMode = getWriteMode();
50524
+ const uploadOnly = getExpenseUploadOnly();
50525
+ const checkIn = uploadOnly ? "the Expenses log on ourfamilywizard.com (ofw_list_expenses is not available in this deployment)" : "ofw_list_expenses";
50526
+ const allowWrites = writeMode === "all";
50527
+ const allowReceiptUploads = allowWrites && attachmentIO !== void 0;
50528
+ if (!uploadOnly) server.registerTool("ofw_get_expense_totals", {
49766
50529
  description: "Get OurFamilyWizard expense summary totals (owed/paid)",
49767
50530
  annotations: { readOnlyHint: true }
49768
50531
  }, async () => {
49769
50532
  const data = await client2.request("GET", "/pub/v2/expense/expenses/totals");
49770
50533
  return jsonResponse(data);
49771
50534
  });
49772
- server.registerTool("ofw_list_expenses", {
49773
- description: "List OurFamilyWizard expenses. Offset-paged via start/max. The response leads with its paging state \u2014 `hasMore` and `nextStart` (null when the list is exhausted) \u2014 BEFORE the records, so a truncated or partially-read response still says whether more remain. Never state an expense total or an absence from one page.",
50535
+ if (!uploadOnly) server.registerTool("ofw_list_expense_categories", {
50536
+ description: "List OurFamilyWizard expense categories, including preset and custom categories with their responsibility split metadata. Read-only.",
50537
+ annotations: { readOnlyHint: true }
50538
+ }, async () => {
50539
+ const data = await client2.request("GET", "/pub/v2/expense/categories");
50540
+ return jsonResponse(data);
50541
+ });
50542
+ if (!uploadOnly) server.registerTool("ofw_list_expenses", {
50543
+ description: "List OurFamilyWizard expenses. OFW pages this endpoint with 1-based page/size parameters; its older start/max parameters are ignored and repeatedly return page 1. The response leads with hasMore and nextPage (null when exhausted) before the records. Continue by passing nextPage.",
49774
50544
  annotations: { readOnlyHint: true },
49775
50545
  inputSchema: external_exports.object({
49776
- start: external_exports.number().int().min(0).describe("Start offset, 0-based (default 0). To continue a listing, pass the `nextStart` from the previous response.").optional(),
49777
- max: external_exports.number().int().min(1).describe("Max results (default 20)").optional()
50546
+ page: external_exports.number().int().min(1).describe("1-based page number (default 1). To continue, pass the nextPage returned by the previous response.").optional(),
50547
+ size: external_exports.number().int().min(1).max(100).describe("Requested page size (default 20). OFW may cap or normalize this value.").optional()
49778
50548
  })
49779
50549
  }, async (args) => {
49780
- const start = args.start ?? 0;
49781
- const max = args.max ?? 20;
49782
- const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
50550
+ const page = args.page ?? 1;
50551
+ const size = args.size ?? 20;
50552
+ const data = await client2.request("GET", `/pub/v2/expense/expenses?page=${page}&size=${size}`);
49783
50553
  const { returned, total, last } = readUpstreamPaging(data);
49784
- const wrapped = withPaginationFirst({
49785
- state: offsetState({ start, max, returned, total, last, base: 0 }),
49786
- start,
49787
- max,
50554
+ const body = typeof data === "object" && data !== null && !Array.isArray(data) ? data : null;
50555
+ if (body === null) return jsonResponse(data);
50556
+ const metadata = typeof body.metadata === "object" && body.metadata !== null && !Array.isArray(body.metadata) ? body.metadata : null;
50557
+ const upstreamPage = metadata !== null && typeof metadata.currentPage === "number" ? metadata.currentPage : metadata !== null && typeof metadata.page === "number" ? metadata.page : page;
50558
+ const upstreamSize = metadata !== null && typeof metadata.perPage === "number" ? metadata.perPage : size;
50559
+ const hasMore = last !== null ? !last : total !== null ? upstreamPage * upstreamSize < total : returned >= upstreamSize;
50560
+ const nextPage = hasMore ? upstreamPage + 1 : null;
50561
+ const scope = total !== null ? ` of ${total}` : "";
50562
+ const head = {
50563
+ hasMore,
50564
+ nextPage,
50565
+ page: upstreamPage,
50566
+ size: upstreamSize,
49788
50567
  returned,
49789
- total,
49790
- hint: `Re-call ofw_list_expenses with start:${start + max}.`,
49791
- payload: data
50568
+ ...total !== null ? { total } : {},
50569
+ paginationNote: hasMore ? `PARTIAL: this response holds ${returned} record(s) on page ${upstreamPage}${scope}. Re-call ofw_list_expenses with page:${nextPage}. Do not state a total or an absence from this response alone.` : `This response reaches the end of the expense list${scope === "" ? "" : ` (${total} record(s) in total)`}.`
50570
+ };
50571
+ return jsonResponse({ ...head, ...body, ...head });
50572
+ });
50573
+ if (allowReceiptUploads) server.registerTool("ofw_upload_expense_pdf", {
50574
+ description: "Upload a PDF to OurFamilyWizard My Files for later attachment to an expense. Accepts either a local path or a signed ChatGPT/oaiusercontent HTTPS URL plus fileName. Exactly one of path or url must be supplied. This tool accepts PDF files only and uploads them using the same SHARED file metadata as the OFW expense form so the returned fileId can be attached to an expense. A SHARED file is visible to the co-parent in My Files immediately, whatever the visibility of the expense it is later attached to (that is set separately by ofw_create_expense privateExpense). " + CONFIRM_NOTE,
50575
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
50576
+ inputSchema: external_exports.object({
50577
+ path: external_exports.string().describe("Path to a local PDF file inside the upload directory (OFW_UPLOAD_DIR). Tilde (~) is expanded by the configured attachment I/O implementation. Mutually exclusive with url.").optional(),
50578
+ url: external_exports.string().describe("Signed HTTPS oaiusercontent.com URL for a PDF supplied by the ChatGPT host. Mutually exclusive with path.").optional(),
50579
+ fileName: external_exports.string().describe("Filename to use for a remote URL upload. Must end in .pdf. Defaults to receipt.pdf.").optional(),
50580
+ label: external_exports.string().describe("Display label for the file in OFW (default: filename)").optional(),
50581
+ description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional(),
50582
+ confirmToken: confirmTokenParam
50583
+ })
50584
+ }, async (args, ctx) => {
50585
+ const io = attachmentIO;
50586
+ if ((args.path ? 1 : 0) + (args.url ? 1 : 0) !== 1) {
50587
+ throw new Error("Pass exactly one of path or url to ofw_upload_expense_pdf.");
50588
+ }
50589
+ const { blob, fileName, mimeType, sizeBytes } = args.url ? await resolveRemotePdf(args.url, args.fileName) : await io.resolveUpload(args.path);
50590
+ if (!fileName.toLowerCase().endsWith(".pdf") || mimeType !== PDF_MIME) {
50591
+ throw new Error(`Expense receipts must be PDF files; received ${fileName} (${mimeType})`);
50592
+ }
50593
+ const bytes = new Uint8Array(await blob.arrayBuffer());
50594
+ if (!isPdf(bytes)) {
50595
+ throw new Error(`Expense receipts must be PDF files; ${fileName} does not start with a PDF header.`);
50596
+ }
50597
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
50598
+ const sha2562 = Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
50599
+ const label = args.label ?? fileName;
50600
+ const description = args.description ?? fileName;
50601
+ const gate = await confirmWrite(ctx, {
50602
+ tool: "ofw_upload_expense_pdf",
50603
+ action: "ofw.file.share",
50604
+ message: `Review and confirm uploading the receipt "${fileName}" to OurFamilyWizard. It is uploaded SHARED, so the co-parent can see it in My Files immediately \u2014 even if the expense it is attached to is private.`,
50605
+ target: `file:${fileName}`,
50606
+ payload: { fileName, sizeBytes, sha256: sha2562, label, description, shareClass: "SHARED", source: "expense" },
50607
+ preview: {
50608
+ action: "Upload and SHARE an expense receipt on OurFamilyWizard",
50609
+ fileName,
50610
+ sizeBytes,
50611
+ mimeType,
50612
+ label,
50613
+ description,
50614
+ shareClass: "SHARED",
50615
+ from: args.url ? `hosted file (${new URL(args.url).hostname})` : "local file",
50616
+ warning: "Visible to the co-parent immediately in My Files, whatever the visibility of the expense it is attached to; the file becomes part of the court-visible record."
50617
+ },
50618
+ confirmToken: args.confirmToken
50619
+ });
50620
+ if (gate) return gate;
50621
+ const form = new FormData();
50622
+ form.append("file", blob, fileName);
50623
+ form.append("source", "expense");
50624
+ form.append("description", args.description ?? fileName);
50625
+ form.append("label", args.label ?? fileName);
50626
+ form.append("fileName", fileName);
50627
+ form.append("shared", "true");
50628
+ form.append("shareClass", "SHARED");
50629
+ const meta3 = parseLenient(
50630
+ UploadedExpenseFileSchema,
50631
+ await client2.request("POST", "/pub/v3/myfiles/multipart", form),
50632
+ { label: "ofw-mcp", context: "POST /pub/v3/myfiles/multipart (ofw_upload_expense_pdf)", mode: "strict" }
50633
+ );
50634
+ return jsonResponse({
50635
+ fileId: meta3.fileId,
50636
+ fileName: meta3.fileName ?? fileName,
50637
+ mimeType: meta3.fileType ?? mimeType,
50638
+ sizeBytes: meta3.sizeInBytes ?? sizeBytes,
50639
+ shareClass: meta3.shareClass ?? "SHARED",
50640
+ note: "Pass fileId to ofw_create_expense as receiptFileId. Expense visibility is controlled separately by privateExpense."
50641
+ });
50642
+ });
50643
+ if (allowWrites) server.registerTool("ofw_update_expense", {
50644
+ description: `Change an existing OurFamilyWizard expense. Pass expenseId plus ONLY the fields you want to change; every field you omit keeps its current value. The tool reads the expense from OFW first and sends the merged result, because OFW's update replaces the whole expense and a field left out would otherwise be erased. If a field you did not pass cannot be read back from OFW, the update is refused as EXPENSE_FIELDS_UNREADABLE and names the field; pass it explicitly (description:null or receiptFileId:null to send none). receiptFileId REPLACES all current receipts; omit it to keep them. Set privateExpense=false to publish a private expense to the co-parent. The confirmation is bound to the expense exactly as read, so if it changes on OFW in between, the update is refused instead of overwriting that change. If the request fails without a definitive answer the result is EXPENSE_UNCONFIRMED: the update may already have been applied, so check ${checkIn} before retrying. ` + CONFIRM_NOTE,
50645
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
50646
+ inputSchema: external_exports.object({
50647
+ expenseId: external_exports.number().int().positive().describe("Existing OFW expense entity id"),
50648
+ title: external_exports.string().trim().min(1).describe("New title (omit to keep)").optional(),
50649
+ amount: external_exports.number().positive().describe("New full expense amount (omit to keep)").optional(),
50650
+ purchaseDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("New expense date, YYYY-MM-DD (omit to keep)").optional(),
50651
+ categoryId: external_exports.number().int().positive().describe("New OFW expense category id (omit to keep)").optional(),
50652
+ payerId: external_exports.number().int().positive().describe("New payer: the userId of the parent who owes (omit to keep)").optional(),
50653
+ children: external_exports.array(external_exports.number().int().positive()).min(1).describe("New child userIds, replacing the current list (omit to keep)").optional(),
50654
+ description: external_exports.string().trim().min(1).nullable().describe("New description; null removes it (omit to keep)").optional(),
50655
+ privateExpense: external_exports.boolean().describe("true = visible only to you; false = shared with the co-parent (omit to keep)").optional(),
50656
+ receiptFileId: external_exports.number().int().positive().nullable().describe("Receipt fileId that REPLACES all current receipts; null removes them (omit to keep)").optional(),
50657
+ confirmToken: confirmTokenParam
50658
+ })
50659
+ }, async (args, ctx) => {
50660
+ const { expenseId, confirmToken } = args;
50661
+ const path = `/pub/v2/expense/expenses/${expenseId}`;
50662
+ const supplied = {};
50663
+ if (args.title !== void 0) supplied.title = args.title;
50664
+ if (args.amount !== void 0) supplied.amount = args.amount;
50665
+ if (args.purchaseDate !== void 0) supplied.purchaseDate = args.purchaseDate;
50666
+ if (args.categoryId !== void 0) supplied.categoryId = args.categoryId;
50667
+ if (args.payerId !== void 0) supplied.payerId = args.payerId;
50668
+ if (args.children !== void 0) supplied.children = args.children;
50669
+ if (args.privateExpense !== void 0) supplied.isPrivate = args.privateExpense;
50670
+ if (args.description !== void 0) supplied.description = args.description;
50671
+ if (args.receiptFileId !== void 0) supplied.fileIds = args.receiptFileId === null ? [] : [args.receiptFileId];
50672
+ if (Object.keys(supplied).length === 0) {
50673
+ return jsonErrorResponse({
50674
+ result: "NO_CHANGES",
50675
+ expenseId,
50676
+ remedy: "Pass the fields to change alongside expenseId (for example privateExpense:false to publish it). Nothing was sent."
50677
+ });
50678
+ }
50679
+ let current = {};
50680
+ let readError;
50681
+ try {
50682
+ const raw = await client2.request("GET", path);
50683
+ current = readExpenseState(parseLenient(external_exports.looseObject({}), raw, { label: "ofw-mcp", context: `GET ${path}` }));
50684
+ } catch (e) {
50685
+ readError = e instanceof Error ? e.message : String(e);
50686
+ }
50687
+ const merged = { ...current, ...supplied };
50688
+ const missing = EXPENSE_FIELDS.filter((f) => merged[f] === void 0);
50689
+ if (missing.length > 0) {
50690
+ return jsonErrorResponse({
50691
+ result: "EXPENSE_FIELDS_UNREADABLE",
50692
+ expenseId,
50693
+ missing: missing.map((f) => ARG_FOR[f]),
50694
+ reason: readError !== void 0 ? `Could not read expense ${expenseId} from OFW (${readError}), so the fields you did not pass have no known value.` : `OFW's copy of expense ${expenseId} did not include a readable value for these fields.`,
50695
+ remedy: `OFW replaces the whole expense on update, so sending without these would erase them. Pass them explicitly (description:null or receiptFileId:null if the expense should have none). Nothing was sent.`
50696
+ });
50697
+ }
50698
+ const next = merged;
50699
+ const payload = {
50700
+ title: next.title,
50701
+ amount: next.amount,
50702
+ purchaseDate: next.purchaseDate,
50703
+ categoryId: next.categoryId,
50704
+ payerId: next.payerId,
50705
+ children: next.children,
50706
+ isPrivate: next.isPrivate
50707
+ };
50708
+ if (next.description !== null) payload.description = next.description;
50709
+ if (next.fileIds.length > 0) payload.fileIds = next.fileIds;
50710
+ const changes = {};
50711
+ for (const f of Object.keys(supplied)) {
50712
+ const from = current[f];
50713
+ if (JSON.stringify(from) !== JSON.stringify(next[f])) {
50714
+ changes[ARG_FOR[f]] = { from: from === void 0 ? "unknown (not readable from OFW)" : from, to: next[f] };
50715
+ }
50716
+ }
50717
+ const baseRead = readError === void 0;
50718
+ if (baseRead && Object.keys(changes).length === 0) {
50719
+ return jsonErrorResponse({
50720
+ result: "NO_CHANGES",
50721
+ expenseId,
50722
+ remedy: "Every field you passed already has that value on OFW, so there is nothing to update. Nothing was sent."
50723
+ });
50724
+ }
50725
+ const gate = await confirmWrite(ctx, {
50726
+ tool: "ofw_update_expense",
50727
+ action: "ofw.expense.update",
50728
+ message: next.isPrivate ? `Review and confirm this update to the private OurFamilyWizard expense "${next.title}".` : `Review and confirm this update to the OurFamilyWizard expense "${next.title}". The expense will be SHARED: it appears in the ledger the co-parent sees immediately, and cannot be deleted through this server.`,
50729
+ target: `expense:${expenseId}`,
50730
+ ...baseRead ? { revision: stateRevision(current) } : {},
50731
+ payload,
50732
+ preview: {
50733
+ action: next.isPrivate ? "Update private OurFamilyWizard expense" : "Update shared OurFamilyWizard expense",
50734
+ expenseId,
50735
+ changes,
50736
+ after: {
50737
+ title: next.title,
50738
+ amount: next.amount,
50739
+ purchaseDate: next.purchaseDate,
50740
+ ...expenseParties(next),
50741
+ description: next.description,
50742
+ visibility: next.isPrivate ? "private (only you)" : "shared with the co-parent",
50743
+ receiptFileIds: next.fileIds
50744
+ },
50745
+ ...baseRead ? {} : { baseNote: `The current expense could not be read from OFW (${readError}); every field comes from this call, and nothing is carried over.` },
50746
+ ...next.isPrivate ? {} : { warning: "Visible to the co-parent immediately as a claim in the shared expense ledger; part of the court-visible record." }
50747
+ },
50748
+ confirmToken
50749
+ });
50750
+ if (gate) return gate;
50751
+ let data;
50752
+ try {
50753
+ data = await requestWrite(client2, "PUT", path, payload);
50754
+ } catch (e) {
50755
+ if (!(e instanceof UnconfirmedWriteError)) throw e;
50756
+ return unconfirmedWriteResponse(e, {
50757
+ result: "EXPENSE_UNCONFIRMED",
50758
+ what: `update expense ${expenseId}`,
50759
+ checkWith: `${checkIn} (compare this expense against the values you sent)`
50760
+ });
50761
+ }
50762
+ const warnings = [];
50763
+ try {
50764
+ const after = readExpenseState(await client2.request("GET", path));
50765
+ for (const f of EXPENSE_FIELDS) {
50766
+ if (after[f] !== void 0 && JSON.stringify(after[f]) !== JSON.stringify(next[f])) {
50767
+ warnings.push(`OFW reports ${ARG_FOR[f]} as ${JSON.stringify(after[f])} after the update, not the ${JSON.stringify(next[f])} that was sent.`);
50768
+ }
50769
+ }
50770
+ } catch (e) {
50771
+ warnings.push(`The update was accepted but could not be read back to verify it (${e instanceof Error ? e.message : String(e)}); check ${checkIn}.`);
50772
+ }
50773
+ return jsonResponse({
50774
+ result: "EXPENSE_UPDATED",
50775
+ expenseId,
50776
+ changed: Object.keys(changes),
50777
+ kept: EXPENSE_FIELDS.filter((f) => supplied[f] === void 0).map((f) => ARG_FOR[f]),
50778
+ ...warnings.length > 0 ? { warnings } : {},
50779
+ response: data
49792
50780
  });
49793
- return jsonResponse(wrapped ?? data);
49794
50781
  });
49795
50782
  if (allowWrites) server.registerTool("ofw_create_expense", {
49796
- 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,
49797
- // Not a harmless local write: the claim is co-parent-visible at once and
49798
- // this server has no way to take it back. destructiveHint keeps a host
50783
+ description: `Log a new expense in OurFamilyWizard using the current web-app expense contract. Required fields are title, amount, purchaseDate, categoryId, payerId (the parent who owes), and at least one child user id. Supports one previously-uploaded receipt PDF and private entries. privateExpense=true creates an expense visible only to you; false/default creates the normal shared expense. receiptFileId should come from ofw_upload_expense_pdf. A shared expense is a money claim that appears in the 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 ${checkIn} shows it did not land. ` + CONFIRM_NOTE,
50784
+ // Not a harmless local write: a shared claim is co-parent-visible at once
50785
+ // and this server has no way to take it back. destructiveHint keeps a host
49799
50786
  // that auto-approves "non-destructive" tools from running it silently.
49800
50787
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
49801
50788
  inputSchema: external_exports.object({
49802
- amount: external_exports.number().describe("Expense amount"),
49803
- description: external_exports.string().describe("Expense description"),
50789
+ title: external_exports.string().trim().min(1).describe("Expense title/name shown in the OFW expense log"),
50790
+ amount: external_exports.number().positive().describe("Full expense amount before OFW applies the category split"),
50791
+ purchaseDate: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Date the expense was incurred, YYYY-MM-DD"),
50792
+ categoryId: external_exports.number().int().positive().describe("OFW expense category id (for example General is commonly id 1; use the id from OFW, not the category display name)"),
50793
+ payerId: external_exports.number().int().positive().describe("OFW userId of the parent who owes/reimburses this expense"),
50794
+ children: external_exports.array(external_exports.number().int().positive()).min(1).describe("One or more OFW child userIds associated with the expense"),
50795
+ description: external_exports.string().trim().min(1).describe("Optional supporting description/details for the expense").optional(),
50796
+ privateExpense: external_exports.boolean().describe("true = visible only to you; false/default = shared with co-parent").optional(),
50797
+ receiptFileId: external_exports.number().int().positive().describe("Single OFW My Files fileId to attach as the receipt, normally returned by ofw_upload_expense_pdf").optional(),
49804
50798
  confirmToken: confirmTokenParam
49805
50799
  })
49806
50800
  }, async (args, ctx) => {
49807
- const { confirmToken, ...payload } = args;
50801
+ const payload = {
50802
+ title: args.title,
50803
+ amount: args.amount,
50804
+ purchaseDate: args.purchaseDate,
50805
+ categoryId: args.categoryId,
50806
+ payerId: args.payerId,
50807
+ children: args.children
50808
+ };
50809
+ if (args.description !== void 0) payload.description = args.description;
50810
+ if (args.privateExpense !== void 0) payload.isPrivate = args.privateExpense;
50811
+ if (args.receiptFileId !== void 0) payload.fileIds = [args.receiptFileId];
50812
+ const isPrivate = args.privateExpense === true;
49808
50813
  const gate = await confirmWrite(ctx, {
49809
50814
  tool: "ofw_create_expense",
49810
50815
  action: "ofw.expense.create",
49811
- 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.",
50816
+ message: isPrivate ? "Review and confirm this private OurFamilyWizard expense (visible only to you)." : "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.",
49812
50817
  target: "expense:new",
49813
50818
  payload,
49814
50819
  preview: {
49815
- action: "Log OurFamilyWizard expense",
49816
- amount: payload.amount,
49817
- description: payload.description,
49818
- warning: "Visible to the co-parent immediately as a claim in the shared expense ledger; part of the court-visible record."
50820
+ action: isPrivate ? "Log private OurFamilyWizard expense" : "Log OurFamilyWizard expense",
50821
+ title: args.title,
50822
+ amount: args.amount,
50823
+ purchaseDate: args.purchaseDate,
50824
+ ...expenseParties(args),
50825
+ ...args.description !== void 0 ? { description: args.description } : {},
50826
+ visibility: isPrivate ? "private (only you)" : "shared with the co-parent",
50827
+ ...args.receiptFileId !== void 0 ? { receiptFileId: args.receiptFileId } : {},
50828
+ ...isPrivate ? {} : { warning: "Visible to the co-parent immediately as a claim in the shared expense ledger; part of the court-visible record." }
49819
50829
  },
49820
- confirmToken
50830
+ confirmToken: args.confirmToken
49821
50831
  });
49822
50832
  if (gate) return gate;
49823
50833
  let data;
49824
50834
  try {
49825
- data = await requestWrite(client2, "POST", "/pub/v2/expense/expenses", payload);
50835
+ data = await requestWrite(client2, "POST", "/pub/v2/expense", payload);
49826
50836
  } catch (e) {
49827
50837
  if (!(e instanceof UnconfirmedWriteError)) throw e;
49828
50838
  return unconfirmedWriteResponse(e, {
49829
50839
  result: "EXPENSE_UNCONFIRMED",
49830
50840
  what: "log this expense",
49831
- checkWith: "ofw_list_expenses (look for this amount and description among the newest expenses)"
50841
+ checkWith: `${checkIn} (look for this title and amount among the newest expenses)`
49832
50842
  });
49833
50843
  }
49834
50844
  return jsonResponse(data);
@@ -50499,6 +51509,12 @@ var OFWCache = class _OFWCache extends LocalCacheStore {
50499
51509
  }
50500
51510
  };
50501
51511
 
51512
+ // src/tool-surface.ts
51513
+ function selectToolRegistrars(r) {
51514
+ if (getExpenseOnly()) return [r.healthcheck, r.expenses];
51515
+ return [r.healthcheck, r.user, r.messages, r.calendar, r.expenses, r.journal];
51516
+ }
51517
+
50502
51518
  // src/index.ts
50503
51519
  var originalEmit = process.emit.bind(process);
50504
51520
  process.emit = function(event, ...args) {
@@ -50513,24 +51529,27 @@ process.emit = function(event, ...args) {
50513
51529
  var nodeCache;
50514
51530
  var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
50515
51531
  var nodeAttachmentIO = new NodeAttachmentIO();
51532
+ var expenseRegistrar = (server, deps) => registerExpenseTools(server, deps, nodeAttachmentIO);
51533
+ var messageRegistrar = (server, deps) => registerMessageTools(server, deps, nodeCacheProvider, nodeAttachmentIO);
51534
+ var tools = selectToolRegistrars({
51535
+ healthcheck: registerHealthcheckTools,
51536
+ user: registerUserTools,
51537
+ messages: messageRegistrar,
51538
+ calendar: registerCalendarTools,
51539
+ expenses: expenseRegistrar,
51540
+ journal: registerJournalTools
51541
+ });
50516
51542
  await runMcp({
50517
51543
  name: "ofw",
50518
- version: "2.19.4",
51544
+ version: "2.20.0",
50519
51545
  // x-release-please-version
50520
51546
  deps: client,
50521
- tools: [
50522
- registerHealthcheckTools,
50523
- registerUserTools,
50524
- (server, deps) => registerMessageTools(server, deps, nodeCacheProvider, nodeAttachmentIO),
50525
- registerCalendarTools,
50526
- registerExpenseTools,
50527
- registerJournalTools
50528
- ],
51547
+ tools,
50529
51548
  banner: "[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion."
50530
51549
  });
50531
51550
  /*! Bundled license information:
50532
51551
 
50533
- @modelcontextprotocol/server/dist/src-CX2iR2pK.mjs:
51552
+ @modelcontextprotocol/server/dist/src-D-y6h4N7.mjs:
50534
51553
  (*!
50535
51554
  * content-type
50536
51555
  * Copyright(c) 2015 Douglas Christopher Wilson