ofw-mcp 2.1.0 → 2.3.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/bundle.js +495 -107
- package/dist/client.js +46 -6
- package/dist/index.js +1 -1
- package/dist/tools/messages.js +55 -14
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "OurFamilyWizard tools for Claude Code",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.3.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "OurFamilyWizard",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
|
|
17
|
-
"version": "2.
|
|
17
|
+
"version": "2.3.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
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,
|
|
@@ -7308,14 +7395,14 @@ var require_permessage_deflate = __commonJS({
|
|
|
7308
7395
|
}
|
|
7309
7396
|
};
|
|
7310
7397
|
module.exports = PerMessageDeflate2;
|
|
7311
|
-
function deflateOnData(
|
|
7312
|
-
this[kBuffers].push(
|
|
7313
|
-
this[kTotalLength] +=
|
|
7398
|
+
function deflateOnData(chunk2) {
|
|
7399
|
+
this[kBuffers].push(chunk2);
|
|
7400
|
+
this[kTotalLength] += chunk2.length;
|
|
7314
7401
|
}
|
|
7315
|
-
function inflateOnData(
|
|
7316
|
-
this[kTotalLength] +=
|
|
7402
|
+
function inflateOnData(chunk2) {
|
|
7403
|
+
this[kTotalLength] += chunk2.length;
|
|
7317
7404
|
if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) {
|
|
7318
|
-
this[kBuffers].push(
|
|
7405
|
+
this[kBuffers].push(chunk2);
|
|
7319
7406
|
return;
|
|
7320
7407
|
}
|
|
7321
7408
|
this[kError] = new RangeError("Max payload size exceeded");
|
|
@@ -7615,7 +7702,7 @@ var require_receiver = __commonJS({
|
|
|
7615
7702
|
* @param {Function} cb Callback
|
|
7616
7703
|
* @private
|
|
7617
7704
|
*/
|
|
7618
|
-
_write(
|
|
7705
|
+
_write(chunk2, encoding, cb) {
|
|
7619
7706
|
if (this._opcode === 8 && this._state == GET_INFO) return cb();
|
|
7620
7707
|
if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
|
|
7621
7708
|
cb(
|
|
@@ -7629,8 +7716,8 @@ var require_receiver = __commonJS({
|
|
|
7629
7716
|
);
|
|
7630
7717
|
return;
|
|
7631
7718
|
}
|
|
7632
|
-
this._bufferedBytes +=
|
|
7633
|
-
this._buffers.push(
|
|
7719
|
+
this._bufferedBytes += chunk2.length;
|
|
7720
|
+
this._buffers.push(chunk2);
|
|
7634
7721
|
this.startLoop(cb);
|
|
7635
7722
|
}
|
|
7636
7723
|
/**
|
|
@@ -9904,8 +9991,8 @@ var require_websocket = __commonJS({
|
|
|
9904
9991
|
this.removeListener("end", socketOnEnd);
|
|
9905
9992
|
websocket._readyState = WebSocket2.CLOSING;
|
|
9906
9993
|
if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
|
|
9907
|
-
const
|
|
9908
|
-
websocket._receiver.write(
|
|
9994
|
+
const chunk2 = this.read(this._readableState.length);
|
|
9995
|
+
websocket._receiver.write(chunk2);
|
|
9909
9996
|
}
|
|
9910
9997
|
websocket._receiver.end();
|
|
9911
9998
|
this[kWebSocket] = void 0;
|
|
@@ -9917,8 +10004,8 @@ var require_websocket = __commonJS({
|
|
|
9917
10004
|
websocket._receiver.on("finish", receiverOnFinish);
|
|
9918
10005
|
}
|
|
9919
10006
|
}
|
|
9920
|
-
function socketOnData(
|
|
9921
|
-
if (!this[kWebSocket]._receiver.write(
|
|
10007
|
+
function socketOnData(chunk2) {
|
|
10008
|
+
if (!this[kWebSocket]._receiver.write(chunk2)) {
|
|
9922
10009
|
this.pause();
|
|
9923
10010
|
}
|
|
9924
10011
|
}
|
|
@@ -10021,14 +10108,14 @@ var require_stream = __commonJS({
|
|
|
10021
10108
|
duplex._read = function() {
|
|
10022
10109
|
if (ws.isPaused) ws.resume();
|
|
10023
10110
|
};
|
|
10024
|
-
duplex._write = function(
|
|
10111
|
+
duplex._write = function(chunk2, encoding, callback) {
|
|
10025
10112
|
if (ws.readyState === ws.CONNECTING) {
|
|
10026
10113
|
ws.once("open", function open() {
|
|
10027
|
-
duplex._write(
|
|
10114
|
+
duplex._write(chunk2, encoding, callback);
|
|
10028
10115
|
});
|
|
10029
10116
|
return;
|
|
10030
10117
|
}
|
|
10031
|
-
ws.send(
|
|
10118
|
+
ws.send(chunk2, callback);
|
|
10032
10119
|
};
|
|
10033
10120
|
duplex.on("end", duplexOnEnd);
|
|
10034
10121
|
duplex.on("error", duplexOnError);
|
|
@@ -34460,8 +34547,8 @@ import process3 from "node:process";
|
|
|
34460
34547
|
|
|
34461
34548
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
34462
34549
|
var ReadBuffer = class {
|
|
34463
|
-
append(
|
|
34464
|
-
this._buffer = this._buffer ? Buffer.concat([this._buffer,
|
|
34550
|
+
append(chunk2) {
|
|
34551
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
34465
34552
|
}
|
|
34466
34553
|
readMessage() {
|
|
34467
34554
|
if (!this._buffer) {
|
|
@@ -34493,8 +34580,8 @@ var StdioServerTransport = class {
|
|
|
34493
34580
|
this._stdout = _stdout;
|
|
34494
34581
|
this._readBuffer = new ReadBuffer();
|
|
34495
34582
|
this._started = false;
|
|
34496
|
-
this._ondata = (
|
|
34497
|
-
this._readBuffer.append(
|
|
34583
|
+
this._ondata = (chunk2) => {
|
|
34584
|
+
this._readBuffer.append(chunk2);
|
|
34498
34585
|
this.processReadBuffer();
|
|
34499
34586
|
};
|
|
34500
34587
|
this._onerror = (error51) => {
|
|
@@ -35912,6 +35999,19 @@ function classifyFetchError(error51) {
|
|
|
35912
35999
|
return "other";
|
|
35913
36000
|
}
|
|
35914
36001
|
|
|
36002
|
+
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
36003
|
+
function classifyBridgeError(err) {
|
|
36004
|
+
if (err instanceof FetchproxyTimeoutError)
|
|
36005
|
+
return "timeout";
|
|
36006
|
+
if (err instanceof FetchproxyBridgeDownError)
|
|
36007
|
+
return "bridge_down";
|
|
36008
|
+
if (err instanceof FetchproxyHttpError)
|
|
36009
|
+
return "http";
|
|
36010
|
+
if (err instanceof FetchproxyProtocolError)
|
|
36011
|
+
return "protocol";
|
|
36012
|
+
return "other";
|
|
36013
|
+
}
|
|
36014
|
+
|
|
35915
36015
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
35916
36016
|
var FetchproxyProtocolError = class extends Error {
|
|
35917
36017
|
constructor(message) {
|
|
@@ -35941,7 +36041,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
35941
36041
|
const retryAttempted = args.retryAttempted ?? false;
|
|
35942
36042
|
const op = args.op ?? "fetch";
|
|
35943
36043
|
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). `;
|
|
35944
|
-
const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}
|
|
36044
|
+
const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
|
|
35945
36045
|
super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
|
|
35946
36046
|
this.name = "FetchproxyBridgeDownError";
|
|
35947
36047
|
this.originalError = args.originalError;
|
|
@@ -35963,6 +36063,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
35963
36063
|
port;
|
|
35964
36064
|
/** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
|
|
35965
36065
|
elapsedMs;
|
|
36066
|
+
/**
|
|
36067
|
+
* 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
|
|
36068
|
+
* fired for this timeout (a cold-start `timeout` symptom followed by
|
|
36069
|
+
* a warm-and-retry that also timed out). False when the retry was
|
|
36070
|
+
* disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
|
|
36071
|
+
* the first attempt.
|
|
36072
|
+
*/
|
|
36073
|
+
retryAttempted;
|
|
35966
36074
|
constructor(args) {
|
|
35967
36075
|
super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
|
|
35968
36076
|
this.name = "FetchproxyTimeoutError";
|
|
@@ -35971,6 +36079,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
35971
36079
|
this.role = args.role ?? null;
|
|
35972
36080
|
this.port = args.port ?? 0;
|
|
35973
36081
|
this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
|
|
36082
|
+
this.retryAttempted = args.retryAttempted ?? false;
|
|
35974
36083
|
}
|
|
35975
36084
|
};
|
|
35976
36085
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
@@ -36044,6 +36153,25 @@ var FetchproxyServer = class {
|
|
|
36044
36153
|
// for "we're connecting right now" so two parallel first-calls don't
|
|
36045
36154
|
// race the port bind.
|
|
36046
36155
|
connectingPromise = null;
|
|
36156
|
+
// 0.8.1+ (#67): server-initiated keep-alive ping. Active when
|
|
36157
|
+
// `keepAliveIntervalMs` is set AND we've seen recent activity
|
|
36158
|
+
// (fetch/capture success or failure, or markActive()) within
|
|
36159
|
+
// `keepAliveMaxIdleMs`. The interval handle is created lazily on
|
|
36160
|
+
// first activity and torn down on close() / extension disconnect.
|
|
36161
|
+
keepAliveTimer = null;
|
|
36162
|
+
lastActiveAt = null;
|
|
36163
|
+
// 0.10.0+ (#73): observability counters surfaced via
|
|
36164
|
+
// bridgeHealth().keepAlive / .swEviction. Monotonic across the process
|
|
36165
|
+
// lifetime so a downstream healthcheck tool can verify the keep-alive
|
|
36166
|
+
// is actually preventing SW eviction. `lastPingAt` and `totalPings`
|
|
36167
|
+
// are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
|
|
36168
|
+
// `lastEvictionDetectedAt` are stamped from the lazy-revive code path
|
|
36169
|
+
// in fetch() / captureRequestHeader().
|
|
36170
|
+
lastPingAt = null;
|
|
36171
|
+
totalPings = 0;
|
|
36172
|
+
lazyReviveAttempts = 0;
|
|
36173
|
+
lazyReviveSuccesses = 0;
|
|
36174
|
+
lastEvictionDetectedAt = null;
|
|
36047
36175
|
constructor(opts) {
|
|
36048
36176
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
36049
36177
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -36096,6 +36224,22 @@ var FetchproxyServer = class {
|
|
|
36096
36224
|
// the legacy hang-forever / fail-once-on-SW-eviction behavior.
|
|
36097
36225
|
fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
|
|
36098
36226
|
bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
|
|
36227
|
+
// 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
|
|
36228
|
+
// wave showed every Pattern A consumer was opting into this same
|
|
36229
|
+
// value. Pass `0` to disable; the existing `<= 0` guards in
|
|
36230
|
+
// `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
|
|
36231
|
+
//
|
|
36232
|
+
// #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
|
|
36233
|
+
// Chrome's ~30s SW-eviction window — slack that timer drift, a
|
|
36234
|
+
// busy host event loop (CPU-bound response parsing between calls),
|
|
36235
|
+
// and the ping's own round-trip latency routinely ate, so the SW
|
|
36236
|
+
// evicted before the next ping landed and the next call cold-
|
|
36237
|
+
// started. 20s restores real margin. (The extension
|
|
36238
|
+
// `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
|
|
36239
|
+
// period, firing *at* the edge — it can't rescue a sub-30s race;
|
|
36240
|
+
// the server ping is the real defense.)
|
|
36241
|
+
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
36242
|
+
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
36099
36243
|
identityDir: opts.identityDir,
|
|
36100
36244
|
onPairCode: opts.onPairCode
|
|
36101
36245
|
};
|
|
@@ -36196,7 +36340,10 @@ var FetchproxyServer = class {
|
|
|
36196
36340
|
onPairCode: this.opts.onPairCode
|
|
36197
36341
|
});
|
|
36198
36342
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36199
|
-
this.hostHandle.onExtensionDisconnect(() =>
|
|
36343
|
+
this.hostHandle.onExtensionDisconnect(() => {
|
|
36344
|
+
this.stopKeepalive();
|
|
36345
|
+
this.rejectAllPending();
|
|
36346
|
+
});
|
|
36200
36347
|
this.hostHandle.onPendingPair((code) => {
|
|
36201
36348
|
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36202
36349
|
});
|
|
@@ -36220,7 +36367,10 @@ var FetchproxyServer = class {
|
|
|
36220
36367
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36221
36368
|
});
|
|
36222
36369
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36223
|
-
this.peerHandle.onRenegotiate(() =>
|
|
36370
|
+
this.peerHandle.onRenegotiate(() => {
|
|
36371
|
+
this.stopKeepalive();
|
|
36372
|
+
this.rejectAllPending();
|
|
36373
|
+
});
|
|
36224
36374
|
this.peerHandle.onPendingPair((code) => {
|
|
36225
36375
|
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36226
36376
|
});
|
|
@@ -36261,13 +36411,18 @@ var FetchproxyServer = class {
|
|
|
36261
36411
|
}
|
|
36262
36412
|
const first = await this._fetchOnceWithTimeout(init);
|
|
36263
36413
|
const reviveMs = this.opts.bridgeReviveDelayMs;
|
|
36264
|
-
|
|
36265
|
-
if (
|
|
36414
|
+
const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
|
|
36415
|
+
if (isColdStartSymptom) {
|
|
36416
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
36417
|
+
}
|
|
36418
|
+
if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
|
|
36419
|
+
this.lazyReviveAttempts += 1;
|
|
36266
36420
|
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36267
36421
|
const second = await this._fetchOnceWithTimeout(init);
|
|
36268
|
-
if (second.ok)
|
|
36422
|
+
if (second.ok) {
|
|
36423
|
+
this.lazyReviveSuccesses += 1;
|
|
36269
36424
|
this.recordSuccess();
|
|
36270
|
-
else
|
|
36425
|
+
} else
|
|
36271
36426
|
this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
|
|
36272
36427
|
return { ...second, retryAttempted: true };
|
|
36273
36428
|
}
|
|
@@ -36289,6 +36444,9 @@ var FetchproxyServer = class {
|
|
|
36289
36444
|
* call (addresses #23 ask 4).
|
|
36290
36445
|
*/
|
|
36291
36446
|
bridgeHealth() {
|
|
36447
|
+
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
36448
|
+
const maxIdleMs = this.opts.keepAliveMaxIdleMs;
|
|
36449
|
+
const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
|
|
36292
36450
|
return {
|
|
36293
36451
|
role: this.role,
|
|
36294
36452
|
port: this.opts.port,
|
|
@@ -36299,17 +36457,85 @@ var FetchproxyServer = class {
|
|
|
36299
36457
|
lastFailureAt: this.lastFailureAt,
|
|
36300
36458
|
lastFailureReason: this.lastFailureReason,
|
|
36301
36459
|
consecutiveFailures: this.consecutiveFailures,
|
|
36302
|
-
lastExtensionMessageAt: this.lastExtensionMessageAt
|
|
36460
|
+
lastExtensionMessageAt: this.lastExtensionMessageAt,
|
|
36461
|
+
keepAlive: {
|
|
36462
|
+
enabled: intervalMs > 0,
|
|
36463
|
+
intervalMs,
|
|
36464
|
+
maxIdleMs,
|
|
36465
|
+
lastPingAt: this.lastPingAt,
|
|
36466
|
+
totalPings: this.totalPings,
|
|
36467
|
+
idleSinceMs
|
|
36468
|
+
},
|
|
36469
|
+
swEviction: {
|
|
36470
|
+
lazyReviveAttempts: this.lazyReviveAttempts,
|
|
36471
|
+
lazyReviveSuccesses: this.lazyReviveSuccesses,
|
|
36472
|
+
lastEvictionDetectedAt: this.lastEvictionDetectedAt
|
|
36473
|
+
}
|
|
36303
36474
|
};
|
|
36304
36475
|
}
|
|
36305
36476
|
recordSuccess() {
|
|
36306
36477
|
this.lastSuccessAt = Date.now();
|
|
36307
36478
|
this.consecutiveFailures = 0;
|
|
36479
|
+
this.noteActivityForKeepalive();
|
|
36308
36480
|
}
|
|
36309
36481
|
recordFailure(reason) {
|
|
36310
36482
|
this.lastFailureAt = Date.now();
|
|
36311
36483
|
this.lastFailureReason = reason;
|
|
36312
36484
|
this.consecutiveFailures += 1;
|
|
36485
|
+
this.noteActivityForKeepalive();
|
|
36486
|
+
}
|
|
36487
|
+
/**
|
|
36488
|
+
* 0.8.1+ (#67): caller-side hint that work is happening or about to
|
|
36489
|
+
* happen — bumps the keep-alive idle gate so the server keeps pinging
|
|
36490
|
+
* the extension. Useful for MCPs that do a chain of side-effectful
|
|
36491
|
+
* work between bridge calls and don't want the SW to evict in the
|
|
36492
|
+
* gap (e.g. server-side parsing of a previous response that takes
|
|
36493
|
+
* tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
|
|
36494
|
+
*/
|
|
36495
|
+
markActive() {
|
|
36496
|
+
this.noteActivityForKeepalive();
|
|
36497
|
+
}
|
|
36498
|
+
noteActivityForKeepalive() {
|
|
36499
|
+
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
36500
|
+
if (intervalMs <= 0)
|
|
36501
|
+
return;
|
|
36502
|
+
this.lastActiveAt = Date.now();
|
|
36503
|
+
this.startKeepaliveIfIdle();
|
|
36504
|
+
}
|
|
36505
|
+
startKeepaliveIfIdle() {
|
|
36506
|
+
if (this.keepAliveTimer !== null)
|
|
36507
|
+
return;
|
|
36508
|
+
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
36509
|
+
if (intervalMs <= 0)
|
|
36510
|
+
return;
|
|
36511
|
+
this.keepAliveTimer = setInterval(() => {
|
|
36512
|
+
const now = Date.now();
|
|
36513
|
+
if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
|
|
36514
|
+
this.stopKeepalive();
|
|
36515
|
+
return;
|
|
36516
|
+
}
|
|
36517
|
+
this.totalPings += 1;
|
|
36518
|
+
this.lastPingAt = now;
|
|
36519
|
+
void this.sendKeepalivePing();
|
|
36520
|
+
}, intervalMs);
|
|
36521
|
+
}
|
|
36522
|
+
async sendKeepalivePing() {
|
|
36523
|
+
try {
|
|
36524
|
+
const inner = { type: "ping" };
|
|
36525
|
+
if (this.hostHandle) {
|
|
36526
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
36527
|
+
} else if (this.peerHandle) {
|
|
36528
|
+
await this.peerHandle.sendInner(inner);
|
|
36529
|
+
}
|
|
36530
|
+
} catch (e) {
|
|
36531
|
+
console.error("[fetchproxy] keepalive ping send failed:", e);
|
|
36532
|
+
}
|
|
36533
|
+
}
|
|
36534
|
+
stopKeepalive() {
|
|
36535
|
+
if (this.keepAliveTimer !== null) {
|
|
36536
|
+
clearInterval(this.keepAliveTimer);
|
|
36537
|
+
this.keepAliveTimer = null;
|
|
36538
|
+
}
|
|
36313
36539
|
}
|
|
36314
36540
|
/**
|
|
36315
36541
|
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
@@ -36368,7 +36594,8 @@ var FetchproxyServer = class {
|
|
|
36368
36594
|
timeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36369
36595
|
role: this.role,
|
|
36370
36596
|
port: this.opts.port,
|
|
36371
|
-
elapsedMs: result.elapsedMs
|
|
36597
|
+
elapsedMs: result.elapsedMs,
|
|
36598
|
+
retryAttempted
|
|
36372
36599
|
});
|
|
36373
36600
|
}
|
|
36374
36601
|
if (result.kind === "content_script_unreachable") {
|
|
@@ -36499,6 +36726,108 @@ var FetchproxyServer = class {
|
|
|
36499
36726
|
const response = await this.get(path, this.applyJsonDefaults(opts));
|
|
36500
36727
|
return response.body;
|
|
36501
36728
|
}
|
|
36729
|
+
/**
|
|
36730
|
+
* 0.11.0+: method-generic JSON convenience helper. Generalizes the
|
|
36731
|
+
* `fetchJson<T>(path, { method, headers, body })` that
|
|
36732
|
+
* zillow/redfin/compass/homes hand-rolled char-for-char in their
|
|
36733
|
+
* `src/client.ts`:
|
|
36734
|
+
*
|
|
36735
|
+
* - sets `Accept: application/json`;
|
|
36736
|
+
* - adds `Content-Type: application/json` only for a non-GET request
|
|
36737
|
+
* that carries a `body` (and only if the caller didn't set one);
|
|
36738
|
+
* - `JSON.stringify`s the body (GET / no-body sends nothing);
|
|
36739
|
+
* - treats a `204` or an empty body as `data: null` (no parse);
|
|
36740
|
+
* - otherwise `JSON.parse`s the body.
|
|
36741
|
+
*
|
|
36742
|
+
* Scope is serialization + header defaults + 204-handling +
|
|
36743
|
+
* JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
|
|
36744
|
+
* or look for a sign-in interstitial — those guards differ per site
|
|
36745
|
+
* (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
|
|
36746
|
+
* returns BOTH the parsed `data` and the raw `FetchResult` and leaves
|
|
36747
|
+
* the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
|
|
36748
|
+
* over `result`.
|
|
36749
|
+
*
|
|
36750
|
+
* Bridge-level failures (no signed-in tab, SW down, timeout) still
|
|
36751
|
+
* throw the typed errors via `request()`, exactly like the verb
|
|
36752
|
+
* helpers — only successful round-trips (any HTTP status) return.
|
|
36753
|
+
*/
|
|
36754
|
+
async requestJson(method, path, opts = {}) {
|
|
36755
|
+
const isGet = method.toUpperCase() === "GET";
|
|
36756
|
+
const sendBody = !isGet && opts.body !== void 0;
|
|
36757
|
+
const headers = {
|
|
36758
|
+
Accept: "application/json",
|
|
36759
|
+
...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
|
|
36760
|
+
...opts.headers ?? {}
|
|
36761
|
+
};
|
|
36762
|
+
const response = await this.request(method, path, {
|
|
36763
|
+
headers,
|
|
36764
|
+
body: sendBody ? JSON.stringify(opts.body) : void 0,
|
|
36765
|
+
...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
|
|
36766
|
+
...opts.domain !== void 0 ? { domain: opts.domain } : {}
|
|
36767
|
+
});
|
|
36768
|
+
const result = {
|
|
36769
|
+
ok: true,
|
|
36770
|
+
status: response.status,
|
|
36771
|
+
url: response.url,
|
|
36772
|
+
body: response.body
|
|
36773
|
+
};
|
|
36774
|
+
if (response.status === 204 || response.body === "") {
|
|
36775
|
+
return { data: null, result };
|
|
36776
|
+
}
|
|
36777
|
+
let data;
|
|
36778
|
+
try {
|
|
36779
|
+
data = JSON.parse(response.body);
|
|
36780
|
+
} catch (e) {
|
|
36781
|
+
throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
36782
|
+
}
|
|
36783
|
+
return { data, result };
|
|
36784
|
+
}
|
|
36785
|
+
/**
|
|
36786
|
+
* 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
|
|
36787
|
+
* the elapsed round-trip, classify any thrown error, and project the
|
|
36788
|
+
* post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
|
|
36789
|
+
*
|
|
36790
|
+
* This is the transport half of the probe loop zillow/redfin/homes
|
|
36791
|
+
* had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
|
|
36792
|
+
* supplies its own probe call (`(path) => client.fetchHtml(path)`)
|
|
36793
|
+
* and probe path (e.g. `'/robots.txt'`); the tool registration and
|
|
36794
|
+
* the site-specific plain-English hint text STAY in the consumer.
|
|
36795
|
+
*
|
|
36796
|
+
* `bridgeHealth()` is read AFTER the probe so its freshness counters
|
|
36797
|
+
* (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
|
|
36798
|
+
* round-trip rather than a stale pre-probe snapshot.
|
|
36799
|
+
*/
|
|
36800
|
+
async runProbe(fetchFn, probePath) {
|
|
36801
|
+
const start = Date.now();
|
|
36802
|
+
let ok = false;
|
|
36803
|
+
let error51;
|
|
36804
|
+
try {
|
|
36805
|
+
await fetchFn(probePath);
|
|
36806
|
+
ok = true;
|
|
36807
|
+
} catch (e) {
|
|
36808
|
+
error51 = {
|
|
36809
|
+
kind: classifyBridgeError(e),
|
|
36810
|
+
message: e instanceof Error ? e.message : String(e)
|
|
36811
|
+
};
|
|
36812
|
+
}
|
|
36813
|
+
const elapsed_ms = Date.now() - start;
|
|
36814
|
+
const health = this.bridgeHealth();
|
|
36815
|
+
return {
|
|
36816
|
+
ok,
|
|
36817
|
+
elapsed_ms,
|
|
36818
|
+
bridge: {
|
|
36819
|
+
role: health.role,
|
|
36820
|
+
port: health.port,
|
|
36821
|
+
server_version: health.serverVersion,
|
|
36822
|
+
fetch_timeout_ms: health.fetchTimeoutMs,
|
|
36823
|
+
last_success_at: health.lastSuccessAt,
|
|
36824
|
+
last_failure_at: health.lastFailureAt,
|
|
36825
|
+
last_failure_reason: health.lastFailureReason,
|
|
36826
|
+
consecutive_failures: health.consecutiveFailures
|
|
36827
|
+
},
|
|
36828
|
+
...error51 ? { error: error51 } : {}
|
|
36829
|
+
};
|
|
36830
|
+
}
|
|
36502
36831
|
/**
|
|
36503
36832
|
* Snapshot the user's non-HttpOnly cookies for the chosen domain.
|
|
36504
36833
|
*
|
|
@@ -36667,11 +36996,14 @@ var FetchproxyServer = class {
|
|
|
36667
36996
|
this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
|
|
36668
36997
|
throw err;
|
|
36669
36998
|
}
|
|
36999
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
36670
37000
|
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
36671
37001
|
if (reviveMs > 0) {
|
|
37002
|
+
this.lazyReviveAttempts += 1;
|
|
36672
37003
|
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36673
37004
|
try {
|
|
36674
37005
|
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
37006
|
+
this.lazyReviveSuccesses += 1;
|
|
36675
37007
|
this.recordSuccess();
|
|
36676
37008
|
return result;
|
|
36677
37009
|
} catch (retryErr) {
|
|
@@ -36964,6 +37296,7 @@ var FetchproxyServer = class {
|
|
|
36964
37296
|
* twice in a row.
|
|
36965
37297
|
*/
|
|
36966
37298
|
async close() {
|
|
37299
|
+
this.stopKeepalive();
|
|
36967
37300
|
this.rejectAllPending();
|
|
36968
37301
|
if (this.connectingPromise) {
|
|
36969
37302
|
await this.connectingPromise.catch(() => void 0);
|
|
@@ -36979,19 +37312,6 @@ var FetchproxyServer = class {
|
|
|
36979
37312
|
}
|
|
36980
37313
|
};
|
|
36981
37314
|
|
|
36982
|
-
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
36983
|
-
function classifyBridgeError(err) {
|
|
36984
|
-
if (err instanceof FetchproxyTimeoutError)
|
|
36985
|
-
return "timeout";
|
|
36986
|
-
if (err instanceof FetchproxyBridgeDownError)
|
|
36987
|
-
return "bridge_down";
|
|
36988
|
-
if (err instanceof FetchproxyHttpError)
|
|
36989
|
-
return "http";
|
|
36990
|
-
if (err instanceof FetchproxyProtocolError)
|
|
36991
|
-
return "protocol";
|
|
36992
|
-
return "other";
|
|
36993
|
-
}
|
|
36994
|
-
|
|
36995
37315
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
36996
37316
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
36997
37317
|
async function bootstrap(opts) {
|
|
@@ -37235,7 +37555,7 @@ function getDefaultInlineAttachments() {
|
|
|
37235
37555
|
// package.json
|
|
37236
37556
|
var package_default = {
|
|
37237
37557
|
name: "ofw-mcp",
|
|
37238
|
-
version: "2.
|
|
37558
|
+
version: "2.3.0",
|
|
37239
37559
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
37240
37560
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
37241
37561
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37262,8 +37582,8 @@ var package_default = {
|
|
|
37262
37582
|
"test:watch": "vitest"
|
|
37263
37583
|
},
|
|
37264
37584
|
dependencies: {
|
|
37265
|
-
"@fetchproxy/bootstrap": "^0.
|
|
37266
|
-
"@fetchproxy/server": "^0.
|
|
37585
|
+
"@fetchproxy/bootstrap": "^0.11.0",
|
|
37586
|
+
"@fetchproxy/server": "^0.11.0",
|
|
37267
37587
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37268
37588
|
dotenv: "^17.4.2",
|
|
37269
37589
|
zod: "^4.4.3"
|
|
@@ -37375,6 +37695,13 @@ function redactHeaders(h) {
|
|
|
37375
37695
|
if (out.Authorization) out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}\u2026`;
|
|
37376
37696
|
return out;
|
|
37377
37697
|
}
|
|
37698
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
37699
|
+
function getRequestTimeoutMs() {
|
|
37700
|
+
const raw = process.env.OFW_REQUEST_TIMEOUT_MS;
|
|
37701
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37702
|
+
const n = Number(raw.trim());
|
|
37703
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37704
|
+
}
|
|
37378
37705
|
var OFWClient = class {
|
|
37379
37706
|
token = null;
|
|
37380
37707
|
tokenExpiry = null;
|
|
@@ -37415,13 +37742,37 @@ var OFWClient = class {
|
|
|
37415
37742
|
console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
|
|
37416
37743
|
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
37417
37744
|
}
|
|
37418
|
-
const
|
|
37419
|
-
|
|
37420
|
-
|
|
37421
|
-
|
|
37422
|
-
|
|
37745
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
37746
|
+
const ac = new AbortController();
|
|
37747
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
37748
|
+
const startedAt = Date.now();
|
|
37749
|
+
let response;
|
|
37750
|
+
try {
|
|
37751
|
+
response = await fetch(url2, {
|
|
37752
|
+
method,
|
|
37753
|
+
headers,
|
|
37754
|
+
signal: ac.signal,
|
|
37755
|
+
...body !== void 0 ? { body: isFormData ? body : JSON.stringify(body) } : {}
|
|
37756
|
+
});
|
|
37757
|
+
} catch (err) {
|
|
37758
|
+
const elapsed = Date.now() - startedAt;
|
|
37759
|
+
if (ac.signal.aborted) {
|
|
37760
|
+
if (debugLogEnabled()) {
|
|
37761
|
+
console.error(`[ofw-debug] \u23F1 TIMEOUT after ${elapsed}ms: ${method} ${url2}`);
|
|
37762
|
+
}
|
|
37763
|
+
throw new Error(
|
|
37764
|
+
`OFW API request timed out after ${timeoutMs}ms: ${method} ${path}`
|
|
37765
|
+
);
|
|
37766
|
+
}
|
|
37767
|
+
if (debugLogEnabled()) {
|
|
37768
|
+
console.error(`[ofw-debug] \u2717 ${err.message} after ${elapsed}ms: ${method} ${url2}`);
|
|
37769
|
+
}
|
|
37770
|
+
throw err;
|
|
37771
|
+
} finally {
|
|
37772
|
+
clearTimeout(timer);
|
|
37773
|
+
}
|
|
37423
37774
|
if (debugLogEnabled()) {
|
|
37424
|
-
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText}`);
|
|
37775
|
+
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
37425
37776
|
}
|
|
37426
37777
|
if (response.status === 401 && !isRetry) {
|
|
37427
37778
|
this.token = null;
|
|
@@ -38156,18 +38507,55 @@ function registerMessageTools(server2, client2) {
|
|
|
38156
38507
|
return jsonResponse({ ...row, attachments });
|
|
38157
38508
|
});
|
|
38158
38509
|
server2.registerTool("ofw_send_message", {
|
|
38159
|
-
description: "Send a message via OurFamilyWizard.
|
|
38510
|
+
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.",
|
|
38160
38511
|
annotations: { destructiveHint: true },
|
|
38161
38512
|
inputSchema: {
|
|
38162
|
-
subject: external_exports.string().describe("Message subject"),
|
|
38163
|
-
body: external_exports.string().describe("Message body text"),
|
|
38164
|
-
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile)"),
|
|
38513
|
+
subject: external_exports.string().describe("Message subject. Required unless messageId/draftId references a cached draft.").optional(),
|
|
38514
|
+
body: external_exports.string().describe("Message body text. Required unless messageId/draftId references a cached draft.").optional(),
|
|
38515
|
+
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(),
|
|
38165
38516
|
replyToId: external_exports.number().describe("ID of the message being replied to").optional(),
|
|
38166
|
-
|
|
38517
|
+
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(),
|
|
38518
|
+
draftId: external_exports.number().describe("Legacy synonym for messageId. If both are passed they must be equal.").optional(),
|
|
38167
38519
|
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message").optional()
|
|
38168
38520
|
}
|
|
38169
38521
|
}, async (args) => {
|
|
38170
|
-
|
|
38522
|
+
if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
|
|
38523
|
+
throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
|
|
38524
|
+
}
|
|
38525
|
+
const draftRef = args.messageId ?? args.draftId;
|
|
38526
|
+
let subject = args.subject;
|
|
38527
|
+
let body = args.body;
|
|
38528
|
+
let recipientIds = args.recipientIds;
|
|
38529
|
+
let draftReplyToId = null;
|
|
38530
|
+
let draftLookupAttempted = false;
|
|
38531
|
+
let draftFound = false;
|
|
38532
|
+
if (draftRef !== void 0) {
|
|
38533
|
+
draftLookupAttempted = true;
|
|
38534
|
+
const draft = getDraft(draftRef);
|
|
38535
|
+
if (draft !== null) {
|
|
38536
|
+
draftFound = true;
|
|
38537
|
+
subject = subject ?? draft.subject;
|
|
38538
|
+
body = body ?? draft.body;
|
|
38539
|
+
recipientIds = recipientIds ?? draft.recipients.map((r) => r.userId);
|
|
38540
|
+
draftReplyToId = draft.replyToId;
|
|
38541
|
+
}
|
|
38542
|
+
}
|
|
38543
|
+
if (subject === void 0 || body === void 0 || recipientIds === void 0) {
|
|
38544
|
+
if (draftLookupAttempted && !draftFound) {
|
|
38545
|
+
throw new Error(
|
|
38546
|
+
`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`
|
|
38547
|
+
);
|
|
38548
|
+
}
|
|
38549
|
+
const missing = [
|
|
38550
|
+
subject === void 0 ? "subject" : null,
|
|
38551
|
+
body === void 0 ? "body" : null,
|
|
38552
|
+
recipientIds === void 0 ? "recipientIds" : null
|
|
38553
|
+
].filter((n) => n !== null).join(", ");
|
|
38554
|
+
throw new Error(
|
|
38555
|
+
`ofw_send_message requires ${missing}. Pass it directly, or pass messageId to default missing fields from a cached draft.`
|
|
38556
|
+
);
|
|
38557
|
+
}
|
|
38558
|
+
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
38171
38559
|
let resolvedReplyTo = requestedReplyTo;
|
|
38172
38560
|
let chainRootId = null;
|
|
38173
38561
|
let rewriteNote = null;
|
|
@@ -38181,9 +38569,9 @@ function registerMessageTools(server2, client2) {
|
|
|
38181
38569
|
}
|
|
38182
38570
|
const myFileIDs = args.myFileIDs ?? [];
|
|
38183
38571
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
|
|
38184
|
-
subject
|
|
38185
|
-
body
|
|
38186
|
-
recipientIds
|
|
38572
|
+
subject,
|
|
38573
|
+
body,
|
|
38574
|
+
recipientIds,
|
|
38187
38575
|
attachments: { myFileIDs },
|
|
38188
38576
|
draft: false,
|
|
38189
38577
|
includeOriginal: resolvedReplyTo !== null,
|
|
@@ -38194,11 +38582,11 @@ function registerMessageTools(server2, client2) {
|
|
|
38194
38582
|
persisted = {
|
|
38195
38583
|
id: newId,
|
|
38196
38584
|
folder: "sent",
|
|
38197
|
-
subject: detail.subject ??
|
|
38585
|
+
subject: detail.subject ?? subject,
|
|
38198
38586
|
fromUser: detail.from?.name ?? "",
|
|
38199
38587
|
sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
38200
38588
|
recipients: mapRecipients(detail.recipients),
|
|
38201
|
-
body: detail.body ??
|
|
38589
|
+
body: detail.body ?? body,
|
|
38202
38590
|
fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38203
38591
|
replyToId: resolvedReplyTo,
|
|
38204
38592
|
chainRootId,
|
|
@@ -38218,9 +38606,9 @@ function registerMessageTools(server2, client2) {
|
|
|
38218
38606
|
});
|
|
38219
38607
|
}
|
|
38220
38608
|
}
|
|
38221
|
-
if (
|
|
38222
|
-
await deleteOFWMessages(client2, [
|
|
38223
|
-
deleteDraft(
|
|
38609
|
+
if (draftRef !== void 0) {
|
|
38610
|
+
await deleteOFWMessages(client2, [draftRef]);
|
|
38611
|
+
deleteDraft(draftRef);
|
|
38224
38612
|
}
|
|
38225
38613
|
const responseObj = persisted ?? raw;
|
|
38226
38614
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
@@ -38629,7 +39017,7 @@ process.emit = function(event, ...args) {
|
|
|
38629
39017
|
}
|
|
38630
39018
|
return originalEmit(event, ...args);
|
|
38631
39019
|
};
|
|
38632
|
-
var server = new McpServer({ name: "ofw", version: "2.
|
|
39020
|
+
var server = new McpServer({ name: "ofw", version: "2.3.0" });
|
|
38633
39021
|
registerUserTools(server, client);
|
|
38634
39022
|
registerMessageTools(server, client);
|
|
38635
39023
|
registerCalendarTools(server, client);
|
package/dist/client.js
CHANGED
|
@@ -40,6 +40,19 @@ function redactHeaders(h) {
|
|
|
40
40
|
out.Authorization = `Bearer ${out.Authorization.slice(7, 17)}…`;
|
|
41
41
|
return out;
|
|
42
42
|
}
|
|
43
|
+
// Per-request timeout. Overridable via OFW_REQUEST_TIMEOUT_MS. The default
|
|
44
|
+
// (30s) is comfortably above OFW's typical p99 but low enough that a stuck
|
|
45
|
+
// upstream fails fast instead of burning the MCP client-side budget — which
|
|
46
|
+
// is what produced the multi-minute hangs we've seen on ofw_list_messages
|
|
47
|
+
// and ofw_save_draft. Each retry (401/429 replay) gets its own fresh window.
|
|
48
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
49
|
+
function getRequestTimeoutMs() {
|
|
50
|
+
const raw = process.env.OFW_REQUEST_TIMEOUT_MS;
|
|
51
|
+
if (typeof raw !== 'string' || raw.trim().length === 0)
|
|
52
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
53
|
+
const n = Number(raw.trim());
|
|
54
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
55
|
+
}
|
|
43
56
|
export class OFWClient {
|
|
44
57
|
token = null;
|
|
45
58
|
tokenExpiry = null;
|
|
@@ -85,13 +98,40 @@ export class OFWClient {
|
|
|
85
98
|
console.error(`[ofw-debug] headers: ${JSON.stringify(redactHeaders(headers))}`);
|
|
86
99
|
console.error(`[ofw-debug] body: ${bodyPreview}`);
|
|
87
100
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
// AbortController + setTimeout (not AbortSignal.timeout) so vitest fake
|
|
102
|
+
// timers can drive the timeout in tests, and so we can attach a clear
|
|
103
|
+
// error message instead of a bare DOMException on the abort path.
|
|
104
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
105
|
+
const ac = new AbortController();
|
|
106
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
107
|
+
const startedAt = Date.now();
|
|
108
|
+
let response;
|
|
109
|
+
try {
|
|
110
|
+
response = await fetch(url, {
|
|
111
|
+
method,
|
|
112
|
+
headers,
|
|
113
|
+
signal: ac.signal,
|
|
114
|
+
...(body !== undefined ? { body: isFormData ? body : JSON.stringify(body) } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const elapsed = Date.now() - startedAt;
|
|
119
|
+
if (ac.signal.aborted) {
|
|
120
|
+
if (debugLogEnabled()) {
|
|
121
|
+
console.error(`[ofw-debug] ⏱ TIMEOUT after ${elapsed}ms: ${method} ${url}`);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`OFW API request timed out after ${timeoutMs}ms: ${method} ${path}`);
|
|
124
|
+
}
|
|
125
|
+
if (debugLogEnabled()) {
|
|
126
|
+
console.error(`[ofw-debug] ✗ ${err.message} after ${elapsed}ms: ${method} ${url}`);
|
|
127
|
+
}
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
93
133
|
if (debugLogEnabled()) {
|
|
94
|
-
console.error(`[ofw-debug] ← ${response.status} ${response.statusText}`);
|
|
134
|
+
console.error(`[ofw-debug] ← ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
95
135
|
}
|
|
96
136
|
if (response.status === 401 && !isRetry) {
|
|
97
137
|
this.token = null;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { registerMessageTools } from './tools/messages.js';
|
|
|
17
17
|
import { registerCalendarTools } from './tools/calendar.js';
|
|
18
18
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
19
19
|
import { registerJournalTools } from './tools/journal.js';
|
|
20
|
-
const server = new McpServer({ name: 'ofw', version: '2.
|
|
20
|
+
const server = new McpServer({ name: 'ofw', version: '2.3.0' }); // x-release-please-version
|
|
21
21
|
registerUserTools(server, client);
|
|
22
22
|
registerMessageTools(server, client);
|
|
23
23
|
registerCalendarTools(server, client);
|
package/dist/tools/messages.js
CHANGED
|
@@ -175,18 +175,59 @@ export function registerMessageTools(server, client) {
|
|
|
175
175
|
return jsonResponse({ ...row, attachments });
|
|
176
176
|
});
|
|
177
177
|
server.registerTool('ofw_send_message', {
|
|
178
|
-
description: 'Send a message via OurFamilyWizard.
|
|
178
|
+
description: 'Send a message via OurFamilyWizard. To send an existing draft, pass messageId — 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.',
|
|
179
179
|
annotations: { destructiveHint: true },
|
|
180
180
|
inputSchema: {
|
|
181
|
-
subject: z.string().describe('Message subject'),
|
|
182
|
-
body: z.string().describe('Message body text'),
|
|
183
|
-
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (get from ofw_get_profile)'),
|
|
181
|
+
subject: z.string().describe('Message subject. Required unless messageId/draftId references a cached draft.').optional(),
|
|
182
|
+
body: z.string().describe('Message body text. Required unless messageId/draftId references a cached draft.').optional(),
|
|
183
|
+
recipientIds: z.array(z.number()).describe('Array of recipient user IDs (get from ofw_get_profile). Required unless messageId/draftId references a cached draft.').optional(),
|
|
184
184
|
replyToId: z.number().describe('ID of the message being replied to').optional(),
|
|
185
|
-
|
|
185
|
+
messageId: z.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(),
|
|
186
|
+
draftId: z.number().describe('Legacy synonym for messageId. If both are passed they must be equal.').optional(),
|
|
186
187
|
myFileIDs: z.array(z.number()).describe('Attachment file ids (from ofw_upload_attachment) to attach to the message').optional(),
|
|
187
188
|
},
|
|
188
189
|
}, async (args) => {
|
|
189
|
-
|
|
190
|
+
if (args.messageId !== undefined && args.draftId !== undefined && args.messageId !== args.draftId) {
|
|
191
|
+
throw new Error(`messageId (${args.messageId}) and draftId (${args.draftId}) refer to different drafts; pass only one.`);
|
|
192
|
+
}
|
|
193
|
+
const draftRef = args.messageId ?? args.draftId;
|
|
194
|
+
// Best-effort draft lookup: when draftRef points at a cached draft, use
|
|
195
|
+
// its stored fields (including replyToId) as defaults for anything the
|
|
196
|
+
// caller didn't supply. The "missing draft" case only matters when we
|
|
197
|
+
// actually NEED the defaults — a caller passing all fields explicitly
|
|
198
|
+
// can use draftId as a pure delete-target even on an empty cache.
|
|
199
|
+
let subject = args.subject;
|
|
200
|
+
let body = args.body;
|
|
201
|
+
let recipientIds = args.recipientIds;
|
|
202
|
+
let draftReplyToId = null;
|
|
203
|
+
let draftLookupAttempted = false;
|
|
204
|
+
let draftFound = false;
|
|
205
|
+
if (draftRef !== undefined) {
|
|
206
|
+
draftLookupAttempted = true;
|
|
207
|
+
const draft = getDraft(draftRef);
|
|
208
|
+
if (draft !== null) {
|
|
209
|
+
draftFound = true;
|
|
210
|
+
subject = subject ?? draft.subject;
|
|
211
|
+
body = body ?? draft.body;
|
|
212
|
+
recipientIds = recipientIds ?? draft.recipients.map((r) => r.userId);
|
|
213
|
+
draftReplyToId = draft.replyToId;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (subject === undefined || body === undefined || recipientIds === undefined) {
|
|
217
|
+
if (draftLookupAttempted && !draftFound) {
|
|
218
|
+
throw new Error(`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`);
|
|
219
|
+
}
|
|
220
|
+
const missing = [
|
|
221
|
+
subject === undefined ? 'subject' : null,
|
|
222
|
+
body === undefined ? 'body' : null,
|
|
223
|
+
recipientIds === undefined ? 'recipientIds' : null,
|
|
224
|
+
].filter((n) => n !== null).join(', ');
|
|
225
|
+
throw new Error(`ofw_send_message requires ${missing}. Pass it directly, or pass messageId to default missing fields from a cached draft.`);
|
|
226
|
+
}
|
|
227
|
+
// Inherit the draft's replyToId when the caller didn't supply one. A
|
|
228
|
+
// reply-draft saved with replyToId would otherwise be sent as a
|
|
229
|
+
// top-level message — silently losing the thread.
|
|
230
|
+
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
190
231
|
let resolvedReplyTo = requestedReplyTo;
|
|
191
232
|
let chainRootId = null;
|
|
192
233
|
let rewriteNote = null;
|
|
@@ -200,9 +241,9 @@ export function registerMessageTools(server, client) {
|
|
|
200
241
|
}
|
|
201
242
|
const myFileIDs = args.myFileIDs ?? [];
|
|
202
243
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client, {
|
|
203
|
-
subject
|
|
204
|
-
body
|
|
205
|
-
recipientIds
|
|
244
|
+
subject,
|
|
245
|
+
body,
|
|
246
|
+
recipientIds,
|
|
206
247
|
attachments: { myFileIDs },
|
|
207
248
|
draft: false,
|
|
208
249
|
includeOriginal: resolvedReplyTo !== null,
|
|
@@ -213,11 +254,11 @@ export function registerMessageTools(server, client) {
|
|
|
213
254
|
persisted = {
|
|
214
255
|
id: newId,
|
|
215
256
|
folder: 'sent',
|
|
216
|
-
subject: detail.subject ??
|
|
257
|
+
subject: detail.subject ?? subject,
|
|
217
258
|
fromUser: detail.from?.name ?? '',
|
|
218
259
|
sentAt: detail.date?.dateTime ?? new Date().toISOString(),
|
|
219
260
|
recipients: mapRecipients(detail.recipients),
|
|
220
|
-
body: detail.body ??
|
|
261
|
+
body: detail.body ?? body,
|
|
221
262
|
fetchedBodyAt: new Date().toISOString(),
|
|
222
263
|
replyToId: resolvedReplyTo,
|
|
223
264
|
chainRootId,
|
|
@@ -240,9 +281,9 @@ export function registerMessageTools(server, client) {
|
|
|
240
281
|
});
|
|
241
282
|
}
|
|
242
283
|
}
|
|
243
|
-
if (
|
|
244
|
-
await deleteOFWMessages(client, [
|
|
245
|
-
deleteDraft(
|
|
284
|
+
if (draftRef !== undefined) {
|
|
285
|
+
await deleteOFWMessages(client, [draftRef]);
|
|
286
|
+
deleteDraft(draftRef);
|
|
246
287
|
}
|
|
247
288
|
const responseObj = persisted ?? raw;
|
|
248
289
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : 'Message sent successfully.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/ofw-mcp",
|
|
5
5
|
"description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@fetchproxy/bootstrap": "^0.
|
|
31
|
-
"@fetchproxy/server": "^0.
|
|
30
|
+
"@fetchproxy/bootstrap": "^0.11.0",
|
|
31
|
+
"@fetchproxy/server": "^0.11.0",
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
33
|
"dotenv": "^17.4.2",
|
|
34
34
|
"zod": "^4.4.3"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/ofw-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.3.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.3.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|