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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -4
- package/dist/auth.js +4 -4
- package/dist/bundle.js +1199 -180
- package/dist/client.js +4 -1
- package/dist/config.js +37 -0
- package/dist/index.js +13 -9
- package/dist/tool-surface.js +14 -0
- package/dist/tools/_shared.js +5 -1
- package/dist/tools/delivery.js +42 -2
- package/dist/tools/expenses.js +563 -56
- package/dist/tools/healthcheck.js +2 -2
- package/dist/tools/messages.js +2 -1
- package/package.json +3 -3
- package/server.json +14 -2
- package/skills/ofw/SKILL.md +11 -4
- package/skills/ofw-fpx/SKILL.md +5 -2
- package/skills/ofw-fpx/references/requests.md +58 -4
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-
|
|
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-
|
|
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
|
-
|
|
24543
|
-
|
|
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
|
-
|
|
24555
|
-
|
|
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
|
|
25777
|
-
if (!Array.isArray(
|
|
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:
|
|
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
|
|
27009
|
-
if (method === "tools/list" && Array.isArray(
|
|
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 (
|
|
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)
|
|
28707
|
-
|
|
28708
|
-
|
|
28709
|
-
|
|
28710
|
-
|
|
28711
|
-
|
|
28712
|
-
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
28717
|
-
|
|
28718
|
-
|
|
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 &&
|
|
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-
|
|
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)
|
|
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
|
|
37310
|
-
|
|
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.
|
|
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 =
|
|
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*)([
|
|
38126
|
-
var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
38736
|
+
function readConfirmTtl(env) {
|
|
38637
38737
|
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
38638
|
-
|
|
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, ...
|
|
38769
|
+
const { tool, account, confirmToken, subject, instruction, spent, args, env = process.env, ...rest } = options;
|
|
38652
38770
|
const mode = readConfirmMode(env);
|
|
38653
|
-
|
|
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} ${
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
41700
|
-
|
|
41701
|
-
|
|
41702
|
-
|
|
41703
|
-
|
|
41704
|
-
|
|
41705
|
-
|
|
41706
|
-
|
|
41707
|
-
|
|
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,
|
|
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
|
|
41816
|
-
const
|
|
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({
|
|
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({
|
|
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 = `
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 {
|
|
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 {
|
|
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.
|
|
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.
|
|
45143
|
-
"@fetchproxy/bootstrap": "^3.
|
|
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
|
|
45163
|
-
var BRIDGE_DOWN_PREFIX = "OFW auth:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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}
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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 =
|
|
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
|
-
|
|
49764
|
-
|
|
49765
|
-
|
|
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("
|
|
49773
|
-
description: "List OurFamilyWizard
|
|
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
|
-
|
|
49777
|
-
|
|
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
|
|
49781
|
-
const
|
|
49782
|
-
const data = await client2.request("GET", `/pub/v2/expense/expenses?
|
|
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
|
|
49785
|
-
|
|
49786
|
-
|
|
49787
|
-
|
|
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
|
-
|
|
49791
|
-
|
|
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:
|
|
49797
|
-
// Not a harmless local write:
|
|
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
|
-
|
|
49803
|
-
|
|
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
|
|
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
|
-
|
|
49817
|
-
|
|
49818
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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-
|
|
51552
|
+
@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs:
|
|
50534
51553
|
(*!
|
|
50535
51554
|
* content-type
|
|
50536
51555
|
* Copyright(c) 2015 Douglas Christopher Wilson
|