ofw-mcp 2.0.19 → 2.2.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/dist/auth.js +6 -0
- package/dist/bundle.js +766 -135
- package/dist/client.js +46 -6
- package/dist/index.js +1 -1
- package/dist/tools/messages.js +55 -14
- package/package.json +9 -8
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -3112,6 +3112,9 @@ var require_utils = __commonJS({
|
|
|
3112
3112
|
"use strict";
|
|
3113
3113
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3114
3114
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3115
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3116
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3117
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3115
3118
|
function stringArrayToHexStripped(input) {
|
|
3116
3119
|
let acc = "";
|
|
3117
3120
|
let code = 0;
|
|
@@ -3304,27 +3307,77 @@ var require_utils = __commonJS({
|
|
|
3304
3307
|
}
|
|
3305
3308
|
return output.join("");
|
|
3306
3309
|
}
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3310
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3311
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3312
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3313
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3314
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3315
|
+
re.lastIndex = 0;
|
|
3316
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3317
|
+
}
|
|
3318
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3319
|
+
if (input.indexOf("%") === -1) {
|
|
3320
|
+
return input;
|
|
3317
3321
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3322
|
+
let output = "";
|
|
3323
|
+
for (let i = 0; i < input.length; i++) {
|
|
3324
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3325
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3326
|
+
if (isHexPair(hex3)) {
|
|
3327
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3328
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3329
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3330
|
+
output += decoded;
|
|
3331
|
+
} else {
|
|
3332
|
+
output += "%" + normalizedHex;
|
|
3333
|
+
}
|
|
3334
|
+
i += 2;
|
|
3335
|
+
continue;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
output += input[i];
|
|
3320
3339
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3340
|
+
return output;
|
|
3341
|
+
}
|
|
3342
|
+
function normalizePathEncoding(input) {
|
|
3343
|
+
let output = "";
|
|
3344
|
+
for (let i = 0; i < input.length; i++) {
|
|
3345
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3346
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3347
|
+
if (isHexPair(hex3)) {
|
|
3348
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3349
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3350
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3351
|
+
output += decoded;
|
|
3352
|
+
} else {
|
|
3353
|
+
output += "%" + normalizedHex;
|
|
3354
|
+
}
|
|
3355
|
+
i += 2;
|
|
3356
|
+
continue;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
if (isPathCharacter(input[i])) {
|
|
3360
|
+
output += input[i];
|
|
3361
|
+
} else {
|
|
3362
|
+
output += escape(input[i]);
|
|
3363
|
+
}
|
|
3323
3364
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3365
|
+
return output;
|
|
3366
|
+
}
|
|
3367
|
+
function escapePreservingEscapes(input) {
|
|
3368
|
+
let output = "";
|
|
3369
|
+
for (let i = 0; i < input.length; i++) {
|
|
3370
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3371
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3372
|
+
if (isHexPair(hex3)) {
|
|
3373
|
+
output += "%" + hex3.toUpperCase();
|
|
3374
|
+
i += 2;
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
output += escape(input[i]);
|
|
3326
3379
|
}
|
|
3327
|
-
return
|
|
3380
|
+
return output;
|
|
3328
3381
|
}
|
|
3329
3382
|
function recomposeAuthority(component) {
|
|
3330
3383
|
const uriTokens = [];
|
|
@@ -3339,7 +3392,7 @@ var require_utils = __commonJS({
|
|
|
3339
3392
|
if (ipV6res.isIPV6 === true) {
|
|
3340
3393
|
host = `[${ipV6res.escapedHost}]`;
|
|
3341
3394
|
} else {
|
|
3342
|
-
host =
|
|
3395
|
+
host = reescapeHostDelimiters(host, false);
|
|
3343
3396
|
}
|
|
3344
3397
|
}
|
|
3345
3398
|
uriTokens.push(host);
|
|
@@ -3353,7 +3406,10 @@ var require_utils = __commonJS({
|
|
|
3353
3406
|
module.exports = {
|
|
3354
3407
|
nonSimpleDomain,
|
|
3355
3408
|
recomposeAuthority,
|
|
3356
|
-
|
|
3409
|
+
reescapeHostDelimiters,
|
|
3410
|
+
normalizePercentEncoding,
|
|
3411
|
+
normalizePathEncoding,
|
|
3412
|
+
escapePreservingEscapes,
|
|
3357
3413
|
removeDotSegments,
|
|
3358
3414
|
isIPv4,
|
|
3359
3415
|
isUUID,
|
|
@@ -3577,12 +3633,12 @@ var require_schemes = __commonJS({
|
|
|
3577
3633
|
var require_fast_uri = __commonJS({
|
|
3578
3634
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
3579
3635
|
"use strict";
|
|
3580
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3636
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3581
3637
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3582
3638
|
function normalize(uri, options) {
|
|
3583
3639
|
if (typeof uri === "string") {
|
|
3584
3640
|
uri = /** @type {T} */
|
|
3585
|
-
|
|
3641
|
+
normalizeString(uri, options);
|
|
3586
3642
|
} else if (typeof uri === "object") {
|
|
3587
3643
|
uri = /** @type {T} */
|
|
3588
3644
|
parse3(serialize(uri, options), options);
|
|
@@ -3649,19 +3705,9 @@ var require_fast_uri = __commonJS({
|
|
|
3649
3705
|
return target;
|
|
3650
3706
|
}
|
|
3651
3707
|
function equal(uriA, uriB, options) {
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
} else if (typeof uriA === "object") {
|
|
3656
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3657
|
-
}
|
|
3658
|
-
if (typeof uriB === "string") {
|
|
3659
|
-
uriB = unescape(uriB);
|
|
3660
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3661
|
-
} else if (typeof uriB === "object") {
|
|
3662
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3663
|
-
}
|
|
3664
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3708
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3709
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3710
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3665
3711
|
}
|
|
3666
3712
|
function serialize(cmpts, opts) {
|
|
3667
3713
|
const component = {
|
|
@@ -3686,12 +3732,12 @@ var require_fast_uri = __commonJS({
|
|
|
3686
3732
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3687
3733
|
if (component.path !== void 0) {
|
|
3688
3734
|
if (!options.skipEscape) {
|
|
3689
|
-
component.path =
|
|
3735
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3690
3736
|
if (component.scheme !== void 0) {
|
|
3691
3737
|
component.path = component.path.split("%3A").join(":");
|
|
3692
3738
|
}
|
|
3693
3739
|
} else {
|
|
3694
|
-
component.path =
|
|
3740
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3695
3741
|
}
|
|
3696
3742
|
}
|
|
3697
3743
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3726,7 +3772,16 @@ var require_fast_uri = __commonJS({
|
|
|
3726
3772
|
return uriTokens.join("");
|
|
3727
3773
|
}
|
|
3728
3774
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3729
|
-
function
|
|
3775
|
+
function getParseError(parsed, matches) {
|
|
3776
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3777
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3778
|
+
}
|
|
3779
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3780
|
+
return "URI port is malformed.";
|
|
3781
|
+
}
|
|
3782
|
+
return void 0;
|
|
3783
|
+
}
|
|
3784
|
+
function parseWithStatus(uri, opts) {
|
|
3730
3785
|
const options = Object.assign({}, opts);
|
|
3731
3786
|
const parsed = {
|
|
3732
3787
|
scheme: void 0,
|
|
@@ -3737,6 +3792,7 @@ var require_fast_uri = __commonJS({
|
|
|
3737
3792
|
query: void 0,
|
|
3738
3793
|
fragment: void 0
|
|
3739
3794
|
};
|
|
3795
|
+
let malformedAuthorityOrPort = false;
|
|
3740
3796
|
let isIP = false;
|
|
3741
3797
|
if (options.reference === "suffix") {
|
|
3742
3798
|
if (options.scheme) {
|
|
@@ -3757,6 +3813,11 @@ var require_fast_uri = __commonJS({
|
|
|
3757
3813
|
if (isNaN(parsed.port)) {
|
|
3758
3814
|
parsed.port = matches[5];
|
|
3759
3815
|
}
|
|
3816
|
+
const parseError = getParseError(parsed, matches);
|
|
3817
|
+
if (parseError !== void 0) {
|
|
3818
|
+
parsed.error = parsed.error || parseError;
|
|
3819
|
+
malformedAuthorityOrPort = true;
|
|
3820
|
+
}
|
|
3760
3821
|
if (parsed.host) {
|
|
3761
3822
|
const ipv4result = isIPv4(parsed.host);
|
|
3762
3823
|
if (ipv4result === false) {
|
|
@@ -3795,14 +3856,18 @@ var require_fast_uri = __commonJS({
|
|
|
3795
3856
|
parsed.scheme = unescape(parsed.scheme);
|
|
3796
3857
|
}
|
|
3797
3858
|
if (parsed.host !== void 0) {
|
|
3798
|
-
parsed.host = unescape(parsed.host);
|
|
3859
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3799
3860
|
}
|
|
3800
3861
|
}
|
|
3801
3862
|
if (parsed.path) {
|
|
3802
|
-
parsed.path =
|
|
3863
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3803
3864
|
}
|
|
3804
3865
|
if (parsed.fragment) {
|
|
3805
|
-
|
|
3866
|
+
try {
|
|
3867
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3868
|
+
} catch {
|
|
3869
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3870
|
+
}
|
|
3806
3871
|
}
|
|
3807
3872
|
}
|
|
3808
3873
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3811,7 +3876,29 @@ var require_fast_uri = __commonJS({
|
|
|
3811
3876
|
} else {
|
|
3812
3877
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3813
3878
|
}
|
|
3814
|
-
return parsed;
|
|
3879
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3880
|
+
}
|
|
3881
|
+
function parse3(uri, opts) {
|
|
3882
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3883
|
+
}
|
|
3884
|
+
function normalizeString(uri, opts) {
|
|
3885
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3886
|
+
}
|
|
3887
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3888
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3889
|
+
return {
|
|
3890
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3891
|
+
malformedAuthorityOrPort
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
function normalizeComparableURI(uri, opts) {
|
|
3895
|
+
if (typeof uri === "string") {
|
|
3896
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3897
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3898
|
+
}
|
|
3899
|
+
if (typeof uri === "object") {
|
|
3900
|
+
return serialize(uri, opts);
|
|
3901
|
+
}
|
|
3815
3902
|
}
|
|
3816
3903
|
var fastUri = {
|
|
3817
3904
|
SCHEMES,
|
|
@@ -34856,6 +34943,8 @@ function validateFrame(raw) {
|
|
|
34856
34943
|
return validateReady(raw);
|
|
34857
34944
|
if (t === "frame")
|
|
34858
34945
|
return validateEncrypted(raw);
|
|
34946
|
+
if (t === "pair-pending")
|
|
34947
|
+
return validatePairPending(raw);
|
|
34859
34948
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
34860
34949
|
}
|
|
34861
34950
|
function validateHello(raw) {
|
|
@@ -34957,6 +35046,17 @@ function validateEncrypted(raw) {
|
|
|
34957
35046
|
assertBase64(raw.ciphertext, "frame.ciphertext");
|
|
34958
35047
|
return raw;
|
|
34959
35048
|
}
|
|
35049
|
+
var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
|
|
35050
|
+
function validatePairPending(raw) {
|
|
35051
|
+
assertString(raw.mcpId, "pair-pending.mcpId");
|
|
35052
|
+
if (!isValidMcpId(raw.mcpId))
|
|
35053
|
+
throw new ProtocolError("pair-pending.mcpId: invalid format");
|
|
35054
|
+
assertString(raw.pairCode, "pair-pending.pairCode");
|
|
35055
|
+
if (!PAIR_CODE_RE.test(raw.pairCode)) {
|
|
35056
|
+
throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
|
|
35057
|
+
}
|
|
35058
|
+
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
35059
|
+
}
|
|
34960
35060
|
function validateInnerFrame(raw) {
|
|
34961
35061
|
assertObject(raw, "inner");
|
|
34962
35062
|
const t = raw.type;
|
|
@@ -35540,7 +35640,9 @@ async function startHost(opts) {
|
|
|
35540
35640
|
const peers = /* @__PURE__ */ new Map();
|
|
35541
35641
|
const ownInnerListeners = [];
|
|
35542
35642
|
const disconnectListeners = [];
|
|
35643
|
+
const pendingPairListeners = [];
|
|
35543
35644
|
let ownSession = null;
|
|
35645
|
+
let ownPendingPairCode = null;
|
|
35544
35646
|
let resolveOwnSession;
|
|
35545
35647
|
let rejectOwnSession;
|
|
35546
35648
|
let ownSessionReady;
|
|
@@ -35625,6 +35727,7 @@ async function startHost(opts) {
|
|
|
35625
35727
|
if (extensionWs !== ws)
|
|
35626
35728
|
return;
|
|
35627
35729
|
ownSession = new SessionState(key);
|
|
35730
|
+
ownPendingPairCode = null;
|
|
35628
35731
|
resolveOwnSession(ownSession);
|
|
35629
35732
|
} else {
|
|
35630
35733
|
const slot = peers.get(frame.mcpId);
|
|
@@ -35652,6 +35755,16 @@ async function startHost(opts) {
|
|
|
35652
35755
|
extensionWs.send(JSON.stringify(frame));
|
|
35653
35756
|
}
|
|
35654
35757
|
}
|
|
35758
|
+
if (frame.type === "pair-pending" && identified === "extension") {
|
|
35759
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
35760
|
+
ownPendingPairCode = frame.pairCode;
|
|
35761
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35762
|
+
} else {
|
|
35763
|
+
const slot = peers.get(frame.mcpId);
|
|
35764
|
+
if (slot)
|
|
35765
|
+
slot.ws.send(JSON.stringify(frame));
|
|
35766
|
+
}
|
|
35767
|
+
}
|
|
35655
35768
|
} catch (e) {
|
|
35656
35769
|
console.error("[fetchproxy] host: message handler error:", e);
|
|
35657
35770
|
try {
|
|
@@ -35697,7 +35810,11 @@ async function startHost(opts) {
|
|
|
35697
35810
|
},
|
|
35698
35811
|
onExtensionDisconnect: (cb) => {
|
|
35699
35812
|
disconnectListeners.push(cb);
|
|
35700
|
-
}
|
|
35813
|
+
},
|
|
35814
|
+
onPendingPair: (cb) => {
|
|
35815
|
+
pendingPairListeners.push(cb);
|
|
35816
|
+
},
|
|
35817
|
+
pendingPairCode: () => ownPendingPairCode
|
|
35701
35818
|
};
|
|
35702
35819
|
}
|
|
35703
35820
|
|
|
@@ -35727,36 +35844,57 @@ async function startPeer(opts) {
|
|
|
35727
35844
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
35728
35845
|
ws.send(JSON.stringify(hello));
|
|
35729
35846
|
const innerListeners = [];
|
|
35847
|
+
const renegotiateListeners = [];
|
|
35848
|
+
const pendingPairListeners = [];
|
|
35730
35849
|
let session = null;
|
|
35850
|
+
let pendingPairCode = null;
|
|
35851
|
+
let resolveFirstReady;
|
|
35852
|
+
let rejectFirstReady;
|
|
35731
35853
|
const sessionPromise = new Promise((resolve2, reject) => {
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
35742
|
-
|
|
35854
|
+
resolveFirstReady = resolve2;
|
|
35855
|
+
rejectFirstReady = reject;
|
|
35856
|
+
});
|
|
35857
|
+
const onMessage = async (data) => {
|
|
35858
|
+
try {
|
|
35859
|
+
const raw = JSON.parse(data.toString());
|
|
35860
|
+
const frame = validateFrame(raw);
|
|
35861
|
+
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
35862
|
+
const extPub = fromB64(frame.extensionSessionPub);
|
|
35863
|
+
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
35864
|
+
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
35865
|
+
const isRenegotiation = session !== null;
|
|
35866
|
+
session = new SessionState(sessionKey);
|
|
35867
|
+
pendingPairCode = null;
|
|
35868
|
+
if (isRenegotiation) {
|
|
35869
|
+
renegotiateListeners.forEach((cb) => cb());
|
|
35870
|
+
} else {
|
|
35871
|
+
resolveFirstReady(session);
|
|
35743
35872
|
}
|
|
35744
|
-
|
|
35745
|
-
|
|
35746
|
-
|
|
35747
|
-
|
|
35748
|
-
|
|
35873
|
+
return;
|
|
35874
|
+
}
|
|
35875
|
+
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
35876
|
+
pendingPairCode = frame.pairCode;
|
|
35877
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35878
|
+
return;
|
|
35879
|
+
}
|
|
35880
|
+
if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
|
|
35881
|
+
if (!session)
|
|
35882
|
+
return;
|
|
35883
|
+
if (!session.acceptInboundSeq(frame.seq))
|
|
35884
|
+
return;
|
|
35885
|
+
try {
|
|
35749
35886
|
const inner = await openEncryptedFrame(session.sessionKey, frame);
|
|
35750
35887
|
innerListeners.forEach((cb) => cb(inner));
|
|
35888
|
+
} catch {
|
|
35751
35889
|
}
|
|
35752
|
-
} catch (e) {
|
|
35753
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
35754
35890
|
}
|
|
35755
|
-
}
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35891
|
+
} catch (e) {
|
|
35892
|
+
rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
|
|
35893
|
+
}
|
|
35894
|
+
};
|
|
35895
|
+
ws.on("message", onMessage);
|
|
35896
|
+
ws.once("close", () => {
|
|
35897
|
+
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
35760
35898
|
});
|
|
35761
35899
|
sessionPromise.catch(() => {
|
|
35762
35900
|
});
|
|
@@ -35764,13 +35902,21 @@ async function startPeer(opts) {
|
|
|
35764
35902
|
ws,
|
|
35765
35903
|
session: sessionPromise,
|
|
35766
35904
|
sendInner: async (inner) => {
|
|
35767
|
-
|
|
35905
|
+
await sessionPromise;
|
|
35906
|
+
const s = session;
|
|
35768
35907
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
35769
35908
|
ws.send(JSON.stringify(sealed));
|
|
35770
35909
|
},
|
|
35771
35910
|
onInner: (cb) => {
|
|
35772
35911
|
innerListeners.push(cb);
|
|
35773
35912
|
},
|
|
35913
|
+
onRenegotiate: (cb) => {
|
|
35914
|
+
renegotiateListeners.push(cb);
|
|
35915
|
+
},
|
|
35916
|
+
onPendingPair: (cb) => {
|
|
35917
|
+
pendingPairListeners.push(cb);
|
|
35918
|
+
},
|
|
35919
|
+
pendingPairCode: () => pendingPairCode,
|
|
35774
35920
|
close: () => ws.close()
|
|
35775
35921
|
};
|
|
35776
35922
|
return handle;
|
|
@@ -35868,6 +36014,52 @@ var FetchproxyHttpError = class extends Error {
|
|
|
35868
36014
|
this.name = "FetchproxyHttpError";
|
|
35869
36015
|
}
|
|
35870
36016
|
};
|
|
36017
|
+
var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
36018
|
+
originalError;
|
|
36019
|
+
retryAttempted;
|
|
36020
|
+
op;
|
|
36021
|
+
url;
|
|
36022
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
36023
|
+
role;
|
|
36024
|
+
/** 0.8.0+: bridge port at throw time (the same port `listen()` bound to). */
|
|
36025
|
+
port;
|
|
36026
|
+
hint;
|
|
36027
|
+
constructor(args) {
|
|
36028
|
+
const retryAttempted = args.retryAttempted ?? false;
|
|
36029
|
+
const op = args.op ?? "fetch";
|
|
36030
|
+
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). `;
|
|
36031
|
+
const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Wake it by clicking the fetchproxy extension toolbar icon, then retry. If it keeps happening, reload the extension from chrome://extensions.`;
|
|
36032
|
+
super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
|
|
36033
|
+
this.name = "FetchproxyBridgeDownError";
|
|
36034
|
+
this.originalError = args.originalError;
|
|
36035
|
+
this.retryAttempted = retryAttempted;
|
|
36036
|
+
this.op = op;
|
|
36037
|
+
if (args.url !== void 0)
|
|
36038
|
+
this.url = args.url;
|
|
36039
|
+
this.role = args.role ?? null;
|
|
36040
|
+
this.port = args.port ?? 0;
|
|
36041
|
+
this.hint = hint;
|
|
36042
|
+
}
|
|
36043
|
+
};
|
|
36044
|
+
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36045
|
+
url;
|
|
36046
|
+
timeoutMs;
|
|
36047
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
36048
|
+
role;
|
|
36049
|
+
/** 0.8.0+: bridge port at throw time. */
|
|
36050
|
+
port;
|
|
36051
|
+
/** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
|
|
36052
|
+
elapsedMs;
|
|
36053
|
+
constructor(args) {
|
|
36054
|
+
super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
|
|
36055
|
+
this.name = "FetchproxyTimeoutError";
|
|
36056
|
+
this.url = args.url;
|
|
36057
|
+
this.timeoutMs = args.timeoutMs;
|
|
36058
|
+
this.role = args.role ?? null;
|
|
36059
|
+
this.port = args.port ?? 0;
|
|
36060
|
+
this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
|
|
36061
|
+
}
|
|
36062
|
+
};
|
|
35871
36063
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
35872
36064
|
function assertSubdomainLabel(label) {
|
|
35873
36065
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
@@ -35895,12 +36087,28 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
35895
36087
|
}
|
|
35896
36088
|
var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
|
|
35897
36089
|
var FetchproxyServer = class {
|
|
35898
|
-
/**
|
|
36090
|
+
/**
|
|
36091
|
+
* Bridge role. `null` until the first verb call (or an explicit
|
|
36092
|
+
* `connect()`) — `listen()` no longer triggers the role election
|
|
36093
|
+
* as of 0.5.3+. Reset to `null` on `close()`.
|
|
36094
|
+
*/
|
|
35899
36095
|
role = null;
|
|
35900
36096
|
opts;
|
|
35901
36097
|
hostHandle = null;
|
|
35902
36098
|
peerHandle = null;
|
|
35903
36099
|
nextRequestId = 1;
|
|
36100
|
+
// 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
|
|
36101
|
+
// Replaces the local copies every downstream MCP was rolling on top
|
|
36102
|
+
// of its own transport adapter — see realty-mcp cohort drift notes.
|
|
36103
|
+
// Updated by recordSuccess / recordFailure from fetch + capture paths.
|
|
36104
|
+
// `lastExtensionMessageAt` (#23 ask 4) is updated whenever any inner
|
|
36105
|
+
// frame from the extension arrives — gives extension-side liveness
|
|
36106
|
+
// distinct from per-call success/failure.
|
|
36107
|
+
lastSuccessAt = null;
|
|
36108
|
+
lastFailureAt = null;
|
|
36109
|
+
lastFailureReason = null;
|
|
36110
|
+
consecutiveFailures = 0;
|
|
36111
|
+
lastExtensionMessageAt = null;
|
|
35904
36112
|
pending = /* @__PURE__ */ new Map();
|
|
35905
36113
|
// Separate pending map for read_cookies so the response shape (cookies
|
|
35906
36114
|
// string vs status/body) doesn't have to share a union type with fetch.
|
|
@@ -35917,6 +36125,12 @@ var FetchproxyServer = class {
|
|
|
35917
36125
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
35918
36126
|
mcpId = null;
|
|
35919
36127
|
identity = null;
|
|
36128
|
+
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
36129
|
+
// first time a verb call runs `ensureConnected`, awaited by concurrent
|
|
36130
|
+
// callers, cleared once the connection is up. Single source of truth
|
|
36131
|
+
// for "we're connecting right now" so two parallel first-calls don't
|
|
36132
|
+
// race the port bind.
|
|
36133
|
+
connectingPromise = null;
|
|
35920
36134
|
constructor(opts) {
|
|
35921
36135
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
35922
36136
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -35963,28 +36177,98 @@ var FetchproxyServer = class {
|
|
|
35963
36177
|
key: d.key,
|
|
35964
36178
|
jsonPointer: d.jsonPointer
|
|
35965
36179
|
})),
|
|
36180
|
+
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36181
|
+
// adapter was about to set these to the same numbers anyway; the
|
|
36182
|
+
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
36183
|
+
// the legacy hang-forever / fail-once-on-SW-eviction behavior.
|
|
36184
|
+
fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
|
|
36185
|
+
bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
|
|
35966
36186
|
identityDir: opts.identityDir,
|
|
35967
36187
|
onPairCode: opts.onPairCode
|
|
35968
36188
|
};
|
|
35969
36189
|
}
|
|
35970
36190
|
/**
|
|
35971
|
-
*
|
|
35972
|
-
* from disk (creating it on first call)
|
|
35973
|
-
*
|
|
35974
|
-
*
|
|
35975
|
-
* `
|
|
35976
|
-
*
|
|
36191
|
+
* Prepare the bridge for use. Loads the long-term identity keypair
|
|
36192
|
+
* from disk (creating it on first call) and computes this instance's
|
|
36193
|
+
* `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
|
|
36194
|
+
* connection is established lazily on the first verb call (see
|
|
36195
|
+
* `ensureConnected` / `getOrConnect`).
|
|
36196
|
+
*
|
|
36197
|
+
* Pre-0.5.3 behavior: `listen()` also did role election and started
|
|
36198
|
+
* the host/peer immediately, which meant every configured-but-unused
|
|
36199
|
+
* MCP claimed bridge resources at MCP-client boot. Several MCPs
|
|
36200
|
+
* starting in parallel under Claude Desktop also produced noisy
|
|
36201
|
+
* `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
|
|
36202
|
+
* of the first MCP's port bind. Deferring keeps boot quiet and
|
|
36203
|
+
* leaves the port unowned until something actually needs it.
|
|
36204
|
+
*
|
|
36205
|
+
* Calling `listen()` twice without an intervening `close()` is a
|
|
36206
|
+
* no-op (the second call's identity load is idempotent).
|
|
35977
36207
|
*/
|
|
35978
36208
|
async listen() {
|
|
35979
|
-
|
|
35980
|
-
|
|
36209
|
+
if (!this.identity) {
|
|
36210
|
+
this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
|
|
36211
|
+
}
|
|
36212
|
+
if (!this.mcpId) {
|
|
36213
|
+
this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
|
|
36214
|
+
}
|
|
36215
|
+
}
|
|
36216
|
+
/**
|
|
36217
|
+
* Force an eager bridge connection (role-election + host/peer handle
|
|
36218
|
+
* start + listener wiring) without waiting for the first verb call.
|
|
36219
|
+
* Useful for callers that want to surface the role / connection
|
|
36220
|
+
* outcome at boot, or for tests whose harness dials a mock extension
|
|
36221
|
+
* immediately after server construction. Production MCPs that just
|
|
36222
|
+
* answer tool calls should NOT call this — the lazy connect via
|
|
36223
|
+
* `ensureConnected` will do the right thing on first use, keeping
|
|
36224
|
+
* boot cheap and avoiding port-bind contention for MCPs that never
|
|
36225
|
+
* actually get invoked.
|
|
36226
|
+
*
|
|
36227
|
+
* Idempotent: a second call after the first has resolved is a no-op
|
|
36228
|
+
* (the existing handle is reused). Throws if `listen()` was never
|
|
36229
|
+
* called.
|
|
36230
|
+
*/
|
|
36231
|
+
async connect() {
|
|
36232
|
+
await this.ensureConnected();
|
|
36233
|
+
}
|
|
36234
|
+
/**
|
|
36235
|
+
* Establish the bridge connection (role-election + host/peer handle
|
|
36236
|
+
* start + listener wiring) the first time a verb is invoked.
|
|
36237
|
+
* Idempotent after the connection is up; concurrent first-callers
|
|
36238
|
+
* share the same in-flight promise so only one election happens.
|
|
36239
|
+
*
|
|
36240
|
+
* Throws if `listen()` was never called — the contract is that the
|
|
36241
|
+
* MCP author still must wire `transport.start()` at boot to load
|
|
36242
|
+
* identity / set mcpId, even though the WS doesn't open until a
|
|
36243
|
+
* verb runs.
|
|
36244
|
+
*/
|
|
36245
|
+
async ensureConnected() {
|
|
36246
|
+
if (this.hostHandle || this.peerHandle)
|
|
36247
|
+
return;
|
|
36248
|
+
if (this.connectingPromise) {
|
|
36249
|
+
await this.connectingPromise;
|
|
36250
|
+
return;
|
|
36251
|
+
}
|
|
36252
|
+
if (!this.identity || !this.mcpId) {
|
|
36253
|
+
throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
|
|
36254
|
+
}
|
|
36255
|
+
this.connectingPromise = this.doConnect();
|
|
36256
|
+
try {
|
|
36257
|
+
await this.connectingPromise;
|
|
36258
|
+
} finally {
|
|
36259
|
+
this.connectingPromise = null;
|
|
36260
|
+
}
|
|
36261
|
+
}
|
|
36262
|
+
async doConnect() {
|
|
36263
|
+
const identity = this.identity;
|
|
36264
|
+
const mcpId = this.mcpId;
|
|
35981
36265
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
35982
36266
|
if (el.role === "host") {
|
|
35983
36267
|
this.role = "host";
|
|
35984
36268
|
this.hostHandle = await startHost({
|
|
35985
36269
|
httpServer: el.server,
|
|
35986
|
-
ownIdentity:
|
|
35987
|
-
ownMcpId:
|
|
36270
|
+
ownIdentity: identity,
|
|
36271
|
+
ownMcpId: mcpId,
|
|
35988
36272
|
ownServerName: this.opts.serverName,
|
|
35989
36273
|
ownVersion: this.opts.version,
|
|
35990
36274
|
ownDomains: this.opts.domains,
|
|
@@ -36000,13 +36284,16 @@ var FetchproxyServer = class {
|
|
|
36000
36284
|
});
|
|
36001
36285
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36002
36286
|
this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
|
|
36287
|
+
this.hostHandle.onPendingPair((code) => {
|
|
36288
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36289
|
+
});
|
|
36003
36290
|
} else {
|
|
36004
36291
|
this.role = "peer";
|
|
36005
36292
|
this.peerHandle = await startPeer({
|
|
36006
36293
|
host: this.opts.host,
|
|
36007
36294
|
port: this.opts.port,
|
|
36008
|
-
identity
|
|
36009
|
-
mcpId
|
|
36295
|
+
identity,
|
|
36296
|
+
mcpId,
|
|
36010
36297
|
serverName: this.opts.serverName,
|
|
36011
36298
|
version: this.opts.version,
|
|
36012
36299
|
domains: this.opts.domains,
|
|
@@ -36020,8 +36307,19 @@ var FetchproxyServer = class {
|
|
|
36020
36307
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36021
36308
|
});
|
|
36022
36309
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36310
|
+
this.peerHandle.onRenegotiate(() => this.rejectAllPending());
|
|
36311
|
+
this.peerHandle.onPendingPair((code) => {
|
|
36312
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36313
|
+
});
|
|
36314
|
+
if (this.opts.onPairCode) {
|
|
36315
|
+
const cb = this.opts.onPairCode;
|
|
36316
|
+
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36317
|
+
}
|
|
36023
36318
|
}
|
|
36024
36319
|
}
|
|
36320
|
+
pairingErrorMessage(code) {
|
|
36321
|
+
return `fetchproxy transport error: pairing required for ${this.opts.serverName}. Tell the user to open the Transporter browser extension popup and approve the pair request. The pair code is: ${code} \u2014 display this code to the user so they can verify it matches.`;
|
|
36322
|
+
}
|
|
36025
36323
|
/**
|
|
36026
36324
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
36027
36325
|
* the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
|
|
@@ -36037,9 +36335,75 @@ var FetchproxyServer = class {
|
|
|
36037
36335
|
* offline, etc.).
|
|
36038
36336
|
*/
|
|
36039
36337
|
async fetch(init) {
|
|
36040
|
-
|
|
36041
|
-
|
|
36338
|
+
await this.ensureConnected();
|
|
36339
|
+
const pendingCode = this.currentPendingPairCode();
|
|
36340
|
+
if (pendingCode !== null) {
|
|
36341
|
+
const error51 = this.pairingErrorMessage(pendingCode);
|
|
36342
|
+
return {
|
|
36343
|
+
ok: false,
|
|
36344
|
+
error: error51,
|
|
36345
|
+
kind: classifyFetchError(error51),
|
|
36346
|
+
retryAttempted: false
|
|
36347
|
+
};
|
|
36042
36348
|
}
|
|
36349
|
+
const first = await this._fetchOnceWithTimeout(init);
|
|
36350
|
+
const reviveMs = this.opts.bridgeReviveDelayMs;
|
|
36351
|
+
let final = first;
|
|
36352
|
+
if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
|
|
36353
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36354
|
+
const second = await this._fetchOnceWithTimeout(init);
|
|
36355
|
+
if (second.ok)
|
|
36356
|
+
this.recordSuccess();
|
|
36357
|
+
else
|
|
36358
|
+
this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
|
|
36359
|
+
return { ...second, retryAttempted: true };
|
|
36360
|
+
}
|
|
36361
|
+
if (first.ok)
|
|
36362
|
+
this.recordSuccess();
|
|
36363
|
+
else
|
|
36364
|
+
this.recordFailure(`${first.kind ?? "other"}: ${first.error}`);
|
|
36365
|
+
return { ...first, retryAttempted: false };
|
|
36366
|
+
}
|
|
36367
|
+
/**
|
|
36368
|
+
* 0.8.0+: snapshot of the bridge's process-wide freshness counters,
|
|
36369
|
+
* suitable for surfacing through a downstream MCP's healthcheck tool.
|
|
36370
|
+
* Counters reset on a success (consecutiveFailures), accumulate
|
|
36371
|
+
* across the process lifetime otherwise. Replaces the per-MCP
|
|
36372
|
+
* duplication the realty cohort had been rolling in their adapters.
|
|
36373
|
+
* `lastExtensionMessageAt` is updated whenever ANY inner frame
|
|
36374
|
+
* arrives from the extension — gives extension-side liveness
|
|
36375
|
+
* distinct from server-side success/failure of the user-visible
|
|
36376
|
+
* call (addresses #23 ask 4).
|
|
36377
|
+
*/
|
|
36378
|
+
bridgeHealth() {
|
|
36379
|
+
return {
|
|
36380
|
+
role: this.role,
|
|
36381
|
+
port: this.opts.port,
|
|
36382
|
+
serverVersion: this.opts.version,
|
|
36383
|
+
fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36384
|
+
bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
|
|
36385
|
+
lastSuccessAt: this.lastSuccessAt,
|
|
36386
|
+
lastFailureAt: this.lastFailureAt,
|
|
36387
|
+
lastFailureReason: this.lastFailureReason,
|
|
36388
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
36389
|
+
lastExtensionMessageAt: this.lastExtensionMessageAt
|
|
36390
|
+
};
|
|
36391
|
+
}
|
|
36392
|
+
recordSuccess() {
|
|
36393
|
+
this.lastSuccessAt = Date.now();
|
|
36394
|
+
this.consecutiveFailures = 0;
|
|
36395
|
+
}
|
|
36396
|
+
recordFailure(reason) {
|
|
36397
|
+
this.lastFailureAt = Date.now();
|
|
36398
|
+
this.lastFailureReason = reason;
|
|
36399
|
+
this.consecutiveFailures += 1;
|
|
36400
|
+
}
|
|
36401
|
+
/**
|
|
36402
|
+
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
36403
|
+
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
36404
|
+
* the throwing surface is the convenience methods.
|
|
36405
|
+
*/
|
|
36406
|
+
async _fetchOnceWithTimeout(init) {
|
|
36043
36407
|
const id = this.nextRequestId++;
|
|
36044
36408
|
const inner = { type: "request", id, op: "fetch", init };
|
|
36045
36409
|
const pending = new Promise((resolve2) => {
|
|
@@ -36050,7 +36414,61 @@ var FetchproxyServer = class {
|
|
|
36050
36414
|
} else if (this.peerHandle) {
|
|
36051
36415
|
await this.peerHandle.sendInner(inner);
|
|
36052
36416
|
}
|
|
36053
|
-
|
|
36417
|
+
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
36418
|
+
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
36419
|
+
return pending;
|
|
36420
|
+
let timer;
|
|
36421
|
+
const start = Date.now();
|
|
36422
|
+
try {
|
|
36423
|
+
return await Promise.race([
|
|
36424
|
+
pending,
|
|
36425
|
+
new Promise((resolve2) => {
|
|
36426
|
+
timer = setTimeout(() => {
|
|
36427
|
+
this.pending.delete(id);
|
|
36428
|
+
const elapsedMs = Date.now() - start;
|
|
36429
|
+
const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
36430
|
+
resolve2({
|
|
36431
|
+
ok: false,
|
|
36432
|
+
error: error51,
|
|
36433
|
+
kind: "timeout",
|
|
36434
|
+
retryAttempted: false,
|
|
36435
|
+
elapsedMs
|
|
36436
|
+
});
|
|
36437
|
+
}, timeoutMs);
|
|
36438
|
+
})
|
|
36439
|
+
]);
|
|
36440
|
+
} finally {
|
|
36441
|
+
if (timer)
|
|
36442
|
+
clearTimeout(timer);
|
|
36443
|
+
}
|
|
36444
|
+
}
|
|
36445
|
+
/**
|
|
36446
|
+
* Map an `ok:false` fetch result to its typed throwable. Centralizes
|
|
36447
|
+
* the kind-to-error-class switch so `request()` and (via the same
|
|
36448
|
+
* logic re-implemented inline) `captureRequestHeader()` agree on what
|
|
36449
|
+
* to throw.
|
|
36450
|
+
*/
|
|
36451
|
+
_typedErrorFor(result, url2, op, retryAttempted) {
|
|
36452
|
+
if (result.kind === "timeout") {
|
|
36453
|
+
return new FetchproxyTimeoutError({
|
|
36454
|
+
url: url2,
|
|
36455
|
+
timeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36456
|
+
role: this.role,
|
|
36457
|
+
port: this.opts.port,
|
|
36458
|
+
elapsedMs: result.elapsedMs
|
|
36459
|
+
});
|
|
36460
|
+
}
|
|
36461
|
+
if (result.kind === "content_script_unreachable") {
|
|
36462
|
+
return new FetchproxyBridgeDownError({
|
|
36463
|
+
originalError: result.error,
|
|
36464
|
+
retryAttempted,
|
|
36465
|
+
op,
|
|
36466
|
+
url: url2,
|
|
36467
|
+
role: this.role,
|
|
36468
|
+
port: this.opts.port
|
|
36469
|
+
});
|
|
36470
|
+
}
|
|
36471
|
+
return new FetchproxyProtocolError(result.error);
|
|
36054
36472
|
}
|
|
36055
36473
|
/**
|
|
36056
36474
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -36073,8 +36491,18 @@ var FetchproxyServer = class {
|
|
|
36073
36491
|
if (opts.subdomain !== void 0)
|
|
36074
36492
|
assertSubdomainLabel(opts.subdomain);
|
|
36075
36493
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
36076
|
-
const
|
|
36077
|
-
|
|
36494
|
+
const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
|
|
36495
|
+
let host;
|
|
36496
|
+
if (isAbsolute2) {
|
|
36497
|
+
try {
|
|
36498
|
+
host = new URL(path).host;
|
|
36499
|
+
} catch {
|
|
36500
|
+
throw new Error(`FetchproxyServer.request: absolute path is not a valid URL: ${JSON.stringify(path)}`);
|
|
36501
|
+
}
|
|
36502
|
+
} else {
|
|
36503
|
+
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
36504
|
+
}
|
|
36505
|
+
const url2 = isAbsolute2 ? path : `https://${host}${path}`;
|
|
36078
36506
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
36079
36507
|
const init = {
|
|
36080
36508
|
url: url2,
|
|
@@ -36085,7 +36513,7 @@ var FetchproxyServer = class {
|
|
|
36085
36513
|
};
|
|
36086
36514
|
const result = await this.fetch(init);
|
|
36087
36515
|
if (!result.ok) {
|
|
36088
|
-
throw
|
|
36516
|
+
throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
|
|
36089
36517
|
}
|
|
36090
36518
|
const response = {
|
|
36091
36519
|
status: result.status,
|
|
@@ -36179,9 +36607,8 @@ var FetchproxyServer = class {
|
|
|
36179
36607
|
if (!this.opts.capabilities.includes("read_cookies")) {
|
|
36180
36608
|
throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
36181
36609
|
}
|
|
36182
|
-
|
|
36183
|
-
|
|
36184
|
-
}
|
|
36610
|
+
await this.ensureConnected();
|
|
36611
|
+
this.throwIfPendingPair();
|
|
36185
36612
|
if (opts.subdomain !== void 0)
|
|
36186
36613
|
assertSubdomainLabel(opts.subdomain);
|
|
36187
36614
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
@@ -36238,9 +36665,8 @@ var FetchproxyServer = class {
|
|
|
36238
36665
|
if (!this.opts.capabilities.includes(op)) {
|
|
36239
36666
|
throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
|
|
36240
36667
|
}
|
|
36241
|
-
|
|
36242
|
-
|
|
36243
|
-
}
|
|
36668
|
+
await this.ensureConnected();
|
|
36669
|
+
this.throwIfPendingPair();
|
|
36244
36670
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36245
36671
|
throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
|
|
36246
36672
|
}
|
|
@@ -36295,13 +36721,75 @@ var FetchproxyServer = class {
|
|
|
36295
36721
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
36296
36722
|
throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
|
|
36297
36723
|
}
|
|
36298
|
-
|
|
36299
|
-
|
|
36724
|
+
await this.ensureConnected();
|
|
36725
|
+
this.throwIfPendingPair();
|
|
36726
|
+
const decls = this.opts.captureHeaders;
|
|
36727
|
+
let resolved;
|
|
36728
|
+
if (opts?.urlPattern !== void 0 && opts?.headerName !== void 0) {
|
|
36729
|
+
const found = decls.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
36730
|
+
if (!found) {
|
|
36731
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
36732
|
+
}
|
|
36733
|
+
resolved = found;
|
|
36734
|
+
} else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
|
|
36735
|
+
if (decls.length === 0) {
|
|
36736
|
+
throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {urlPattern, headerName} explicitly");
|
|
36737
|
+
}
|
|
36738
|
+
if (decls.length > 1) {
|
|
36739
|
+
const list = decls.map((d) => `${JSON.stringify(d.urlPattern)}/${JSON.stringify(d.headerName)}`).join(", ");
|
|
36740
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {urlPattern, headerName} to disambiguate`);
|
|
36741
|
+
}
|
|
36742
|
+
resolved = decls[0];
|
|
36743
|
+
} else {
|
|
36744
|
+
throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
|
|
36300
36745
|
}
|
|
36301
|
-
const
|
|
36302
|
-
|
|
36303
|
-
|
|
36746
|
+
const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
|
|
36747
|
+
try {
|
|
36748
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
36749
|
+
this.recordSuccess();
|
|
36750
|
+
return result;
|
|
36751
|
+
} catch (err) {
|
|
36752
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
36753
|
+
if (!swDown) {
|
|
36754
|
+
this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
|
|
36755
|
+
throw err;
|
|
36756
|
+
}
|
|
36757
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
36758
|
+
if (reviveMs > 0) {
|
|
36759
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36760
|
+
try {
|
|
36761
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
36762
|
+
this.recordSuccess();
|
|
36763
|
+
return result;
|
|
36764
|
+
} catch (retryErr) {
|
|
36765
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
36766
|
+
if (!stillDown) {
|
|
36767
|
+
this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
|
|
36768
|
+
throw retryErr;
|
|
36769
|
+
}
|
|
36770
|
+
this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
|
|
36771
|
+
throw new FetchproxyBridgeDownError({
|
|
36772
|
+
originalError: retryErr.message,
|
|
36773
|
+
retryAttempted: true,
|
|
36774
|
+
op: "capture_request_header",
|
|
36775
|
+
url: resolved.urlPattern,
|
|
36776
|
+
role: this.role,
|
|
36777
|
+
port: this.opts.port
|
|
36778
|
+
});
|
|
36779
|
+
}
|
|
36780
|
+
}
|
|
36781
|
+
this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
|
|
36782
|
+
throw new FetchproxyBridgeDownError({
|
|
36783
|
+
originalError: err.message,
|
|
36784
|
+
retryAttempted: false,
|
|
36785
|
+
op: "capture_request_header",
|
|
36786
|
+
url: resolved.urlPattern,
|
|
36787
|
+
role: this.role,
|
|
36788
|
+
port: this.opts.port
|
|
36789
|
+
});
|
|
36304
36790
|
}
|
|
36791
|
+
}
|
|
36792
|
+
async _captureRequestHeaderOnce(opts) {
|
|
36305
36793
|
const id = this.nextRequestId++;
|
|
36306
36794
|
const inner = {
|
|
36307
36795
|
type: "request",
|
|
@@ -36338,9 +36826,8 @@ var FetchproxyServer = class {
|
|
|
36338
36826
|
if (!this.opts.capabilities.includes("read_indexed_db")) {
|
|
36339
36827
|
throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
|
|
36340
36828
|
}
|
|
36341
|
-
|
|
36342
|
-
|
|
36343
|
-
}
|
|
36829
|
+
await this.ensureConnected();
|
|
36830
|
+
this.throwIfPendingPair();
|
|
36344
36831
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36345
36832
|
throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
|
|
36346
36833
|
}
|
|
@@ -36411,18 +36898,35 @@ var FetchproxyServer = class {
|
|
|
36411
36898
|
onInner(inner) {
|
|
36412
36899
|
if (inner.type !== "response")
|
|
36413
36900
|
return;
|
|
36901
|
+
this.lastExtensionMessageAt = Date.now();
|
|
36414
36902
|
const fetchCb = this.pending.get(inner.id);
|
|
36415
36903
|
if (fetchCb) {
|
|
36416
36904
|
this.pending.delete(inner.id);
|
|
36417
36905
|
if (inner.ok) {
|
|
36418
36906
|
if (inner.op === void 0 || inner.op === "fetch") {
|
|
36419
|
-
fetchCb({
|
|
36907
|
+
fetchCb({
|
|
36908
|
+
ok: true,
|
|
36909
|
+
status: inner.status,
|
|
36910
|
+
url: inner.url,
|
|
36911
|
+
body: inner.body,
|
|
36912
|
+
retryAttempted: false
|
|
36913
|
+
});
|
|
36420
36914
|
} else {
|
|
36421
36915
|
const error51 = `unexpected ${inner.op} response on fetch awaiter`;
|
|
36422
|
-
fetchCb({
|
|
36916
|
+
fetchCb({
|
|
36917
|
+
ok: false,
|
|
36918
|
+
error: error51,
|
|
36919
|
+
kind: classifyFetchError(error51),
|
|
36920
|
+
retryAttempted: false
|
|
36921
|
+
});
|
|
36423
36922
|
}
|
|
36424
36923
|
} else {
|
|
36425
|
-
fetchCb({
|
|
36924
|
+
fetchCb({
|
|
36925
|
+
ok: false,
|
|
36926
|
+
error: inner.error,
|
|
36927
|
+
kind: classifyFetchError(inner.error),
|
|
36928
|
+
retryAttempted: false
|
|
36929
|
+
});
|
|
36426
36930
|
}
|
|
36427
36931
|
return;
|
|
36428
36932
|
}
|
|
@@ -36489,10 +36993,15 @@ var FetchproxyServer = class {
|
|
|
36489
36993
|
}
|
|
36490
36994
|
}
|
|
36491
36995
|
}
|
|
36492
|
-
rejectAllPending() {
|
|
36493
|
-
const err = new FetchproxyProtocolError(
|
|
36996
|
+
rejectAllPending(reason = "extension disconnected") {
|
|
36997
|
+
const err = new FetchproxyProtocolError(reason);
|
|
36494
36998
|
for (const cb of this.pending.values()) {
|
|
36495
|
-
cb({
|
|
36999
|
+
cb({
|
|
37000
|
+
ok: false,
|
|
37001
|
+
error: err.message,
|
|
37002
|
+
kind: classifyFetchError(err.message),
|
|
37003
|
+
retryAttempted: false
|
|
37004
|
+
});
|
|
36496
37005
|
}
|
|
36497
37006
|
this.pending.clear();
|
|
36498
37007
|
for (const cb of this.pendingReadCookies.values()) {
|
|
@@ -36509,6 +37018,32 @@ var FetchproxyServer = class {
|
|
|
36509
37018
|
reject(err);
|
|
36510
37019
|
this.pendingIdb.clear();
|
|
36511
37020
|
}
|
|
37021
|
+
/**
|
|
37022
|
+
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
37023
|
+
* is active, returning null when none is pending. Public verbs call this
|
|
37024
|
+
* at the top so that a tool invoked while the bridge is waiting on user
|
|
37025
|
+
* approval fails fast with the actionable error rather than hanging on a
|
|
37026
|
+
* sealed frame the extension will never process.
|
|
37027
|
+
*/
|
|
37028
|
+
currentPendingPairCode() {
|
|
37029
|
+
if (this.hostHandle)
|
|
37030
|
+
return this.hostHandle.pendingPairCode();
|
|
37031
|
+
if (this.peerHandle)
|
|
37032
|
+
return this.peerHandle.pendingPairCode();
|
|
37033
|
+
return null;
|
|
37034
|
+
}
|
|
37035
|
+
/**
|
|
37036
|
+
* 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
|
|
37037
|
+
* message if the bridge is waiting on user approval. Used by the verb
|
|
37038
|
+
* methods (readCookies, readLocalStorage, etc.) that surface errors via
|
|
37039
|
+
* thrown exceptions rather than `ok:false` discriminated unions.
|
|
37040
|
+
*/
|
|
37041
|
+
throwIfPendingPair() {
|
|
37042
|
+
const code = this.currentPendingPairCode();
|
|
37043
|
+
if (code !== null) {
|
|
37044
|
+
throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
|
|
37045
|
+
}
|
|
37046
|
+
}
|
|
36512
37047
|
/**
|
|
36513
37048
|
* Shut down the bridge. Host: terminates the WebSocket server and any
|
|
36514
37049
|
* still-attached extension/peer clients. Peer: closes the upstream
|
|
@@ -36517,6 +37052,9 @@ var FetchproxyServer = class {
|
|
|
36517
37052
|
*/
|
|
36518
37053
|
async close() {
|
|
36519
37054
|
this.rejectAllPending();
|
|
37055
|
+
if (this.connectingPromise) {
|
|
37056
|
+
await this.connectingPromise.catch(() => void 0);
|
|
37057
|
+
}
|
|
36520
37058
|
if (this.hostHandle)
|
|
36521
37059
|
await this.hostHandle.close();
|
|
36522
37060
|
if (this.peerHandle)
|
|
@@ -36524,9 +37062,23 @@ var FetchproxyServer = class {
|
|
|
36524
37062
|
this.hostHandle = null;
|
|
36525
37063
|
this.peerHandle = null;
|
|
36526
37064
|
this.role = null;
|
|
37065
|
+
this.connectingPromise = null;
|
|
36527
37066
|
}
|
|
36528
37067
|
};
|
|
36529
37068
|
|
|
37069
|
+
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
37070
|
+
function classifyBridgeError(err) {
|
|
37071
|
+
if (err instanceof FetchproxyTimeoutError)
|
|
37072
|
+
return "timeout";
|
|
37073
|
+
if (err instanceof FetchproxyBridgeDownError)
|
|
37074
|
+
return "bridge_down";
|
|
37075
|
+
if (err instanceof FetchproxyHttpError)
|
|
37076
|
+
return "http";
|
|
37077
|
+
if (err instanceof FetchproxyProtocolError)
|
|
37078
|
+
return "protocol";
|
|
37079
|
+
return "other";
|
|
37080
|
+
}
|
|
37081
|
+
|
|
36530
37082
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
36531
37083
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
36532
37084
|
async function bootstrap(opts) {
|
|
@@ -36581,7 +37133,11 @@ async function bootstrap(opts) {
|
|
|
36581
37133
|
key: p.storageKey,
|
|
36582
37134
|
jsonPointer: p.jsonPointer
|
|
36583
37135
|
})),
|
|
36584
|
-
onPairCode: opts.onPairCode
|
|
37136
|
+
onPairCode: opts.onPairCode,
|
|
37137
|
+
// 0.8.0+ pass-through. Only forwarded when the caller set them;
|
|
37138
|
+
// unset → server defaults apply (30000 / 2000 in 0.8.0).
|
|
37139
|
+
...opts.fetchTimeoutMs !== void 0 ? { fetchTimeoutMs: opts.fetchTimeoutMs } : {},
|
|
37140
|
+
...opts.bridgeReviveDelayMs !== void 0 ? { bridgeReviveDelayMs: opts.bridgeReviveDelayMs } : {}
|
|
36585
37141
|
});
|
|
36586
37142
|
const storageDomainOpts = {};
|
|
36587
37143
|
if (opts.storageDomain !== void 0)
|
|
@@ -36766,7 +37322,7 @@ function getDefaultInlineAttachments() {
|
|
|
36766
37322
|
// package.json
|
|
36767
37323
|
var package_default = {
|
|
36768
37324
|
name: "ofw-mcp",
|
|
36769
|
-
version: "2.0
|
|
37325
|
+
version: "2.2.0",
|
|
36770
37326
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
36771
37327
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
36772
37328
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36793,17 +37349,18 @@ var package_default = {
|
|
|
36793
37349
|
"test:watch": "vitest"
|
|
36794
37350
|
},
|
|
36795
37351
|
dependencies: {
|
|
36796
|
-
"@fetchproxy/bootstrap": "^0.
|
|
37352
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
37353
|
+
"@fetchproxy/server": "^0.8.0",
|
|
36797
37354
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36798
|
-
dotenv: "^17.4.
|
|
36799
|
-
zod: "^4.4.
|
|
37355
|
+
dotenv: "^17.4.2",
|
|
37356
|
+
zod: "^4.4.3"
|
|
36800
37357
|
},
|
|
36801
37358
|
devDependencies: {
|
|
36802
|
-
"@types/node": "^25.
|
|
36803
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37359
|
+
"@types/node": "^25.9.1",
|
|
37360
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
36804
37361
|
esbuild: "^0.28.0",
|
|
36805
|
-
typescript: "^6.0.
|
|
36806
|
-
vitest: "^4.1.
|
|
37362
|
+
typescript: "^6.0.3",
|
|
37363
|
+
vitest: "^4.1.7"
|
|
36807
37364
|
}
|
|
36808
37365
|
};
|
|
36809
37366
|
|
|
@@ -36860,6 +37417,12 @@ async function resolveAuth() {
|
|
|
36860
37417
|
source: "fetchproxy"
|
|
36861
37418
|
};
|
|
36862
37419
|
} catch (e) {
|
|
37420
|
+
if (classifyBridgeError(e) === "bridge_down") {
|
|
37421
|
+
const downErr = e;
|
|
37422
|
+
throw new Error(
|
|
37423
|
+
`OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
37424
|
+
);
|
|
37425
|
+
}
|
|
36863
37426
|
const msg = e instanceof Error ? e.message : String(e);
|
|
36864
37427
|
throw new Error(
|
|
36865
37428
|
`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
|
|
@@ -36899,6 +37462,13 @@ function redactHeaders(h) {
|
|
|
36899
37462
|
if (out.Authorization) out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}\u2026`;
|
|
36900
37463
|
return out;
|
|
36901
37464
|
}
|
|
37465
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
37466
|
+
function getRequestTimeoutMs() {
|
|
37467
|
+
const raw = process.env.OFW_REQUEST_TIMEOUT_MS;
|
|
37468
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37469
|
+
const n = Number(raw.trim());
|
|
37470
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37471
|
+
}
|
|
36902
37472
|
var OFWClient = class {
|
|
36903
37473
|
token = null;
|
|
36904
37474
|
tokenExpiry = null;
|
|
@@ -36939,13 +37509,37 @@ var OFWClient = class {
|
|
|
36939
37509
|
console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
|
|
36940
37510
|
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
36941
37511
|
}
|
|
36942
|
-
const
|
|
36943
|
-
|
|
36944
|
-
|
|
36945
|
-
|
|
36946
|
-
|
|
37512
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
37513
|
+
const ac = new AbortController();
|
|
37514
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
37515
|
+
const startedAt = Date.now();
|
|
37516
|
+
let response;
|
|
37517
|
+
try {
|
|
37518
|
+
response = await fetch(url2, {
|
|
37519
|
+
method,
|
|
37520
|
+
headers,
|
|
37521
|
+
signal: ac.signal,
|
|
37522
|
+
...body !== void 0 ? { body: isFormData ? body : JSON.stringify(body) } : {}
|
|
37523
|
+
});
|
|
37524
|
+
} catch (err) {
|
|
37525
|
+
const elapsed = Date.now() - startedAt;
|
|
37526
|
+
if (ac.signal.aborted) {
|
|
37527
|
+
if (debugLogEnabled()) {
|
|
37528
|
+
console.error(`[ofw-debug] \u23F1 TIMEOUT after ${elapsed}ms: ${method} ${url2}`);
|
|
37529
|
+
}
|
|
37530
|
+
throw new Error(
|
|
37531
|
+
`OFW API request timed out after ${timeoutMs}ms: ${method} ${path}`
|
|
37532
|
+
);
|
|
37533
|
+
}
|
|
37534
|
+
if (debugLogEnabled()) {
|
|
37535
|
+
console.error(`[ofw-debug] \u2717 ${err.message} after ${elapsed}ms: ${method} ${url2}`);
|
|
37536
|
+
}
|
|
37537
|
+
throw err;
|
|
37538
|
+
} finally {
|
|
37539
|
+
clearTimeout(timer);
|
|
37540
|
+
}
|
|
36947
37541
|
if (debugLogEnabled()) {
|
|
36948
|
-
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText}`);
|
|
37542
|
+
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
36949
37543
|
}
|
|
36950
37544
|
if (response.status === 401 && !isRetry) {
|
|
36951
37545
|
this.token = null;
|
|
@@ -37680,18 +38274,55 @@ function registerMessageTools(server2, client2) {
|
|
|
37680
38274
|
return jsonResponse({ ...row, attachments });
|
|
37681
38275
|
});
|
|
37682
38276
|
server2.registerTool("ofw_send_message", {
|
|
37683
|
-
description: "Send a message via OurFamilyWizard.
|
|
38277
|
+
description: "Send a message via OurFamilyWizard. To send an existing draft, pass messageId \u2014 subject/body/recipientIds become optional overrides (missing fields default to the draft's cached values) and the draft is deleted after sending. To send a fresh message, supply subject/body/recipientIds directly. draftId is the legacy spelling of messageId and works the same way. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.",
|
|
37684
38278
|
annotations: { destructiveHint: true },
|
|
37685
38279
|
inputSchema: {
|
|
37686
|
-
subject: external_exports.string().describe("Message subject"),
|
|
37687
|
-
body: external_exports.string().describe("Message body text"),
|
|
37688
|
-
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile)"),
|
|
38280
|
+
subject: external_exports.string().describe("Message subject. Required unless messageId/draftId references a cached draft.").optional(),
|
|
38281
|
+
body: external_exports.string().describe("Message body text. Required unless messageId/draftId references a cached draft.").optional(),
|
|
38282
|
+
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile). Required unless messageId/draftId references a cached draft.").optional(),
|
|
37689
38283
|
replyToId: external_exports.number().describe("ID of the message being replied to").optional(),
|
|
37690
|
-
|
|
38284
|
+
messageId: external_exports.number().describe("ID of an existing draft to send. When set, missing subject/body/recipientIds default to the draft's cached values, and the draft is deleted after sending.").optional(),
|
|
38285
|
+
draftId: external_exports.number().describe("Legacy synonym for messageId. If both are passed they must be equal.").optional(),
|
|
37691
38286
|
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message").optional()
|
|
37692
38287
|
}
|
|
37693
38288
|
}, async (args) => {
|
|
37694
|
-
|
|
38289
|
+
if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
|
|
38290
|
+
throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
|
|
38291
|
+
}
|
|
38292
|
+
const draftRef = args.messageId ?? args.draftId;
|
|
38293
|
+
let subject = args.subject;
|
|
38294
|
+
let body = args.body;
|
|
38295
|
+
let recipientIds = args.recipientIds;
|
|
38296
|
+
let draftReplyToId = null;
|
|
38297
|
+
let draftLookupAttempted = false;
|
|
38298
|
+
let draftFound = false;
|
|
38299
|
+
if (draftRef !== void 0) {
|
|
38300
|
+
draftLookupAttempted = true;
|
|
38301
|
+
const draft = getDraft(draftRef);
|
|
38302
|
+
if (draft !== null) {
|
|
38303
|
+
draftFound = true;
|
|
38304
|
+
subject = subject ?? draft.subject;
|
|
38305
|
+
body = body ?? draft.body;
|
|
38306
|
+
recipientIds = recipientIds ?? draft.recipients.map((r) => r.userId);
|
|
38307
|
+
draftReplyToId = draft.replyToId;
|
|
38308
|
+
}
|
|
38309
|
+
}
|
|
38310
|
+
if (subject === void 0 || body === void 0 || recipientIds === void 0) {
|
|
38311
|
+
if (draftLookupAttempted && !draftFound) {
|
|
38312
|
+
throw new Error(
|
|
38313
|
+
`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`
|
|
38314
|
+
);
|
|
38315
|
+
}
|
|
38316
|
+
const missing = [
|
|
38317
|
+
subject === void 0 ? "subject" : null,
|
|
38318
|
+
body === void 0 ? "body" : null,
|
|
38319
|
+
recipientIds === void 0 ? "recipientIds" : null
|
|
38320
|
+
].filter((n) => n !== null).join(", ");
|
|
38321
|
+
throw new Error(
|
|
38322
|
+
`ofw_send_message requires ${missing}. Pass it directly, or pass messageId to default missing fields from a cached draft.`
|
|
38323
|
+
);
|
|
38324
|
+
}
|
|
38325
|
+
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
37695
38326
|
let resolvedReplyTo = requestedReplyTo;
|
|
37696
38327
|
let chainRootId = null;
|
|
37697
38328
|
let rewriteNote = null;
|
|
@@ -37705,9 +38336,9 @@ function registerMessageTools(server2, client2) {
|
|
|
37705
38336
|
}
|
|
37706
38337
|
const myFileIDs = args.myFileIDs ?? [];
|
|
37707
38338
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
|
|
37708
|
-
subject
|
|
37709
|
-
body
|
|
37710
|
-
recipientIds
|
|
38339
|
+
subject,
|
|
38340
|
+
body,
|
|
38341
|
+
recipientIds,
|
|
37711
38342
|
attachments: { myFileIDs },
|
|
37712
38343
|
draft: false,
|
|
37713
38344
|
includeOriginal: resolvedReplyTo !== null,
|
|
@@ -37718,11 +38349,11 @@ function registerMessageTools(server2, client2) {
|
|
|
37718
38349
|
persisted = {
|
|
37719
38350
|
id: newId,
|
|
37720
38351
|
folder: "sent",
|
|
37721
|
-
subject: detail.subject ??
|
|
38352
|
+
subject: detail.subject ?? subject,
|
|
37722
38353
|
fromUser: detail.from?.name ?? "",
|
|
37723
38354
|
sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
37724
38355
|
recipients: mapRecipients(detail.recipients),
|
|
37725
|
-
body: detail.body ??
|
|
38356
|
+
body: detail.body ?? body,
|
|
37726
38357
|
fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37727
38358
|
replyToId: resolvedReplyTo,
|
|
37728
38359
|
chainRootId,
|
|
@@ -37742,9 +38373,9 @@ function registerMessageTools(server2, client2) {
|
|
|
37742
38373
|
});
|
|
37743
38374
|
}
|
|
37744
38375
|
}
|
|
37745
|
-
if (
|
|
37746
|
-
await deleteOFWMessages(client2, [
|
|
37747
|
-
deleteDraft(
|
|
38376
|
+
if (draftRef !== void 0) {
|
|
38377
|
+
await deleteOFWMessages(client2, [draftRef]);
|
|
38378
|
+
deleteDraft(draftRef);
|
|
37748
38379
|
}
|
|
37749
38380
|
const responseObj = persisted ?? raw;
|
|
37750
38381
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
@@ -38153,7 +38784,7 @@ process.emit = function(event, ...args) {
|
|
|
38153
38784
|
}
|
|
38154
38785
|
return originalEmit(event, ...args);
|
|
38155
38786
|
};
|
|
38156
|
-
var server = new McpServer({ name: "ofw", version: "2.0
|
|
38787
|
+
var server = new McpServer({ name: "ofw", version: "2.2.0" });
|
|
38157
38788
|
registerUserTools(server, client);
|
|
38158
38789
|
registerMessageTools(server, client);
|
|
38159
38790
|
registerCalendarTools(server, client);
|