ofw-mcp 2.0.19 → 2.1.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 +550 -74
- package/dist/index.js +1 -1
- package/package.json +9 -8
- 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.0
|
|
9
|
+
"version": "2.1.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.0
|
|
17
|
+
"version": "2.1.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
package/dist/auth.js
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
// specifically so it can be mocked here too. This keeps the
|
|
47
47
|
// selection logic independent of either implementation.
|
|
48
48
|
import { bootstrap } from '@fetchproxy/bootstrap';
|
|
49
|
+
import { classifyBridgeError } from '@fetchproxy/server';
|
|
49
50
|
import { loginWithPassword } from './auth-password.js';
|
|
50
51
|
import { parseBoolEnv } from './config.js';
|
|
51
52
|
import pkg from '../package.json' with { type: 'json' };
|
|
@@ -121,6 +122,11 @@ export async function resolveAuth() {
|
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
catch (e) {
|
|
125
|
+
// FetchproxyBridgeDownError only escapes bootstrap() after the lazy-revive retry fails — surface .hint verbatim (actionable "click toolbar icon" copy).
|
|
126
|
+
if (classifyBridgeError(e) === 'bridge_down') {
|
|
127
|
+
const downErr = e;
|
|
128
|
+
throw new Error(`OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
129
|
+
}
|
|
124
130
|
const msg = e instanceof Error ? e.message : String(e);
|
|
125
131
|
throw new Error(`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`);
|
|
126
132
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -34856,6 +34856,8 @@ function validateFrame(raw) {
|
|
|
34856
34856
|
return validateReady(raw);
|
|
34857
34857
|
if (t === "frame")
|
|
34858
34858
|
return validateEncrypted(raw);
|
|
34859
|
+
if (t === "pair-pending")
|
|
34860
|
+
return validatePairPending(raw);
|
|
34859
34861
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
34860
34862
|
}
|
|
34861
34863
|
function validateHello(raw) {
|
|
@@ -34957,6 +34959,17 @@ function validateEncrypted(raw) {
|
|
|
34957
34959
|
assertBase64(raw.ciphertext, "frame.ciphertext");
|
|
34958
34960
|
return raw;
|
|
34959
34961
|
}
|
|
34962
|
+
var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
|
|
34963
|
+
function validatePairPending(raw) {
|
|
34964
|
+
assertString(raw.mcpId, "pair-pending.mcpId");
|
|
34965
|
+
if (!isValidMcpId(raw.mcpId))
|
|
34966
|
+
throw new ProtocolError("pair-pending.mcpId: invalid format");
|
|
34967
|
+
assertString(raw.pairCode, "pair-pending.pairCode");
|
|
34968
|
+
if (!PAIR_CODE_RE.test(raw.pairCode)) {
|
|
34969
|
+
throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
|
|
34970
|
+
}
|
|
34971
|
+
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
34972
|
+
}
|
|
34960
34973
|
function validateInnerFrame(raw) {
|
|
34961
34974
|
assertObject(raw, "inner");
|
|
34962
34975
|
const t = raw.type;
|
|
@@ -35540,7 +35553,9 @@ async function startHost(opts) {
|
|
|
35540
35553
|
const peers = /* @__PURE__ */ new Map();
|
|
35541
35554
|
const ownInnerListeners = [];
|
|
35542
35555
|
const disconnectListeners = [];
|
|
35556
|
+
const pendingPairListeners = [];
|
|
35543
35557
|
let ownSession = null;
|
|
35558
|
+
let ownPendingPairCode = null;
|
|
35544
35559
|
let resolveOwnSession;
|
|
35545
35560
|
let rejectOwnSession;
|
|
35546
35561
|
let ownSessionReady;
|
|
@@ -35625,6 +35640,7 @@ async function startHost(opts) {
|
|
|
35625
35640
|
if (extensionWs !== ws)
|
|
35626
35641
|
return;
|
|
35627
35642
|
ownSession = new SessionState(key);
|
|
35643
|
+
ownPendingPairCode = null;
|
|
35628
35644
|
resolveOwnSession(ownSession);
|
|
35629
35645
|
} else {
|
|
35630
35646
|
const slot = peers.get(frame.mcpId);
|
|
@@ -35652,6 +35668,16 @@ async function startHost(opts) {
|
|
|
35652
35668
|
extensionWs.send(JSON.stringify(frame));
|
|
35653
35669
|
}
|
|
35654
35670
|
}
|
|
35671
|
+
if (frame.type === "pair-pending" && identified === "extension") {
|
|
35672
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
35673
|
+
ownPendingPairCode = frame.pairCode;
|
|
35674
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35675
|
+
} else {
|
|
35676
|
+
const slot = peers.get(frame.mcpId);
|
|
35677
|
+
if (slot)
|
|
35678
|
+
slot.ws.send(JSON.stringify(frame));
|
|
35679
|
+
}
|
|
35680
|
+
}
|
|
35655
35681
|
} catch (e) {
|
|
35656
35682
|
console.error("[fetchproxy] host: message handler error:", e);
|
|
35657
35683
|
try {
|
|
@@ -35697,7 +35723,11 @@ async function startHost(opts) {
|
|
|
35697
35723
|
},
|
|
35698
35724
|
onExtensionDisconnect: (cb) => {
|
|
35699
35725
|
disconnectListeners.push(cb);
|
|
35700
|
-
}
|
|
35726
|
+
},
|
|
35727
|
+
onPendingPair: (cb) => {
|
|
35728
|
+
pendingPairListeners.push(cb);
|
|
35729
|
+
},
|
|
35730
|
+
pendingPairCode: () => ownPendingPairCode
|
|
35701
35731
|
};
|
|
35702
35732
|
}
|
|
35703
35733
|
|
|
@@ -35727,36 +35757,57 @@ async function startPeer(opts) {
|
|
|
35727
35757
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
35728
35758
|
ws.send(JSON.stringify(hello));
|
|
35729
35759
|
const innerListeners = [];
|
|
35760
|
+
const renegotiateListeners = [];
|
|
35761
|
+
const pendingPairListeners = [];
|
|
35730
35762
|
let session = null;
|
|
35763
|
+
let pendingPairCode = null;
|
|
35764
|
+
let resolveFirstReady;
|
|
35765
|
+
let rejectFirstReady;
|
|
35731
35766
|
const sessionPromise = new Promise((resolve2, reject) => {
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
35742
|
-
|
|
35767
|
+
resolveFirstReady = resolve2;
|
|
35768
|
+
rejectFirstReady = reject;
|
|
35769
|
+
});
|
|
35770
|
+
const onMessage = async (data) => {
|
|
35771
|
+
try {
|
|
35772
|
+
const raw = JSON.parse(data.toString());
|
|
35773
|
+
const frame = validateFrame(raw);
|
|
35774
|
+
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
35775
|
+
const extPub = fromB64(frame.extensionSessionPub);
|
|
35776
|
+
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
35777
|
+
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
35778
|
+
const isRenegotiation = session !== null;
|
|
35779
|
+
session = new SessionState(sessionKey);
|
|
35780
|
+
pendingPairCode = null;
|
|
35781
|
+
if (isRenegotiation) {
|
|
35782
|
+
renegotiateListeners.forEach((cb) => cb());
|
|
35783
|
+
} else {
|
|
35784
|
+
resolveFirstReady(session);
|
|
35743
35785
|
}
|
|
35744
|
-
|
|
35745
|
-
|
|
35746
|
-
|
|
35747
|
-
|
|
35748
|
-
|
|
35786
|
+
return;
|
|
35787
|
+
}
|
|
35788
|
+
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
35789
|
+
pendingPairCode = frame.pairCode;
|
|
35790
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35791
|
+
return;
|
|
35792
|
+
}
|
|
35793
|
+
if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
|
|
35794
|
+
if (!session)
|
|
35795
|
+
return;
|
|
35796
|
+
if (!session.acceptInboundSeq(frame.seq))
|
|
35797
|
+
return;
|
|
35798
|
+
try {
|
|
35749
35799
|
const inner = await openEncryptedFrame(session.sessionKey, frame);
|
|
35750
35800
|
innerListeners.forEach((cb) => cb(inner));
|
|
35801
|
+
} catch {
|
|
35751
35802
|
}
|
|
35752
|
-
} catch (e) {
|
|
35753
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
35754
35803
|
}
|
|
35755
|
-
}
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35804
|
+
} catch (e) {
|
|
35805
|
+
rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
|
|
35806
|
+
}
|
|
35807
|
+
};
|
|
35808
|
+
ws.on("message", onMessage);
|
|
35809
|
+
ws.once("close", () => {
|
|
35810
|
+
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
35760
35811
|
});
|
|
35761
35812
|
sessionPromise.catch(() => {
|
|
35762
35813
|
});
|
|
@@ -35764,13 +35815,21 @@ async function startPeer(opts) {
|
|
|
35764
35815
|
ws,
|
|
35765
35816
|
session: sessionPromise,
|
|
35766
35817
|
sendInner: async (inner) => {
|
|
35767
|
-
|
|
35818
|
+
await sessionPromise;
|
|
35819
|
+
const s = session;
|
|
35768
35820
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
35769
35821
|
ws.send(JSON.stringify(sealed));
|
|
35770
35822
|
},
|
|
35771
35823
|
onInner: (cb) => {
|
|
35772
35824
|
innerListeners.push(cb);
|
|
35773
35825
|
},
|
|
35826
|
+
onRenegotiate: (cb) => {
|
|
35827
|
+
renegotiateListeners.push(cb);
|
|
35828
|
+
},
|
|
35829
|
+
onPendingPair: (cb) => {
|
|
35830
|
+
pendingPairListeners.push(cb);
|
|
35831
|
+
},
|
|
35832
|
+
pendingPairCode: () => pendingPairCode,
|
|
35774
35833
|
close: () => ws.close()
|
|
35775
35834
|
};
|
|
35776
35835
|
return handle;
|
|
@@ -35868,6 +35927,52 @@ var FetchproxyHttpError = class extends Error {
|
|
|
35868
35927
|
this.name = "FetchproxyHttpError";
|
|
35869
35928
|
}
|
|
35870
35929
|
};
|
|
35930
|
+
var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
35931
|
+
originalError;
|
|
35932
|
+
retryAttempted;
|
|
35933
|
+
op;
|
|
35934
|
+
url;
|
|
35935
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
35936
|
+
role;
|
|
35937
|
+
/** 0.8.0+: bridge port at throw time (the same port `listen()` bound to). */
|
|
35938
|
+
port;
|
|
35939
|
+
hint;
|
|
35940
|
+
constructor(args) {
|
|
35941
|
+
const retryAttempted = args.retryAttempted ?? false;
|
|
35942
|
+
const op = args.op ?? "fetch";
|
|
35943
|
+
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}Wake it by clicking the fetchproxy extension toolbar icon, then retry. If it keeps happening, reload the extension from chrome://extensions.`;
|
|
35945
|
+
super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
|
|
35946
|
+
this.name = "FetchproxyBridgeDownError";
|
|
35947
|
+
this.originalError = args.originalError;
|
|
35948
|
+
this.retryAttempted = retryAttempted;
|
|
35949
|
+
this.op = op;
|
|
35950
|
+
if (args.url !== void 0)
|
|
35951
|
+
this.url = args.url;
|
|
35952
|
+
this.role = args.role ?? null;
|
|
35953
|
+
this.port = args.port ?? 0;
|
|
35954
|
+
this.hint = hint;
|
|
35955
|
+
}
|
|
35956
|
+
};
|
|
35957
|
+
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
35958
|
+
url;
|
|
35959
|
+
timeoutMs;
|
|
35960
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
35961
|
+
role;
|
|
35962
|
+
/** 0.8.0+: bridge port at throw time. */
|
|
35963
|
+
port;
|
|
35964
|
+
/** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
|
|
35965
|
+
elapsedMs;
|
|
35966
|
+
constructor(args) {
|
|
35967
|
+
super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
|
|
35968
|
+
this.name = "FetchproxyTimeoutError";
|
|
35969
|
+
this.url = args.url;
|
|
35970
|
+
this.timeoutMs = args.timeoutMs;
|
|
35971
|
+
this.role = args.role ?? null;
|
|
35972
|
+
this.port = args.port ?? 0;
|
|
35973
|
+
this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
|
|
35974
|
+
}
|
|
35975
|
+
};
|
|
35871
35976
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
35872
35977
|
function assertSubdomainLabel(label) {
|
|
35873
35978
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
@@ -35895,12 +36000,28 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
35895
36000
|
}
|
|
35896
36001
|
var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
|
|
35897
36002
|
var FetchproxyServer = class {
|
|
35898
|
-
/**
|
|
36003
|
+
/**
|
|
36004
|
+
* Bridge role. `null` until the first verb call (or an explicit
|
|
36005
|
+
* `connect()`) — `listen()` no longer triggers the role election
|
|
36006
|
+
* as of 0.5.3+. Reset to `null` on `close()`.
|
|
36007
|
+
*/
|
|
35899
36008
|
role = null;
|
|
35900
36009
|
opts;
|
|
35901
36010
|
hostHandle = null;
|
|
35902
36011
|
peerHandle = null;
|
|
35903
36012
|
nextRequestId = 1;
|
|
36013
|
+
// 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
|
|
36014
|
+
// Replaces the local copies every downstream MCP was rolling on top
|
|
36015
|
+
// of its own transport adapter — see realty-mcp cohort drift notes.
|
|
36016
|
+
// Updated by recordSuccess / recordFailure from fetch + capture paths.
|
|
36017
|
+
// `lastExtensionMessageAt` (#23 ask 4) is updated whenever any inner
|
|
36018
|
+
// frame from the extension arrives — gives extension-side liveness
|
|
36019
|
+
// distinct from per-call success/failure.
|
|
36020
|
+
lastSuccessAt = null;
|
|
36021
|
+
lastFailureAt = null;
|
|
36022
|
+
lastFailureReason = null;
|
|
36023
|
+
consecutiveFailures = 0;
|
|
36024
|
+
lastExtensionMessageAt = null;
|
|
35904
36025
|
pending = /* @__PURE__ */ new Map();
|
|
35905
36026
|
// Separate pending map for read_cookies so the response shape (cookies
|
|
35906
36027
|
// string vs status/body) doesn't have to share a union type with fetch.
|
|
@@ -35917,6 +36038,12 @@ var FetchproxyServer = class {
|
|
|
35917
36038
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
35918
36039
|
mcpId = null;
|
|
35919
36040
|
identity = null;
|
|
36041
|
+
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
36042
|
+
// first time a verb call runs `ensureConnected`, awaited by concurrent
|
|
36043
|
+
// callers, cleared once the connection is up. Single source of truth
|
|
36044
|
+
// for "we're connecting right now" so two parallel first-calls don't
|
|
36045
|
+
// race the port bind.
|
|
36046
|
+
connectingPromise = null;
|
|
35920
36047
|
constructor(opts) {
|
|
35921
36048
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
35922
36049
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -35963,28 +36090,98 @@ var FetchproxyServer = class {
|
|
|
35963
36090
|
key: d.key,
|
|
35964
36091
|
jsonPointer: d.jsonPointer
|
|
35965
36092
|
})),
|
|
36093
|
+
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36094
|
+
// adapter was about to set these to the same numbers anyway; the
|
|
36095
|
+
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
36096
|
+
// the legacy hang-forever / fail-once-on-SW-eviction behavior.
|
|
36097
|
+
fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
|
|
36098
|
+
bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
|
|
35966
36099
|
identityDir: opts.identityDir,
|
|
35967
36100
|
onPairCode: opts.onPairCode
|
|
35968
36101
|
};
|
|
35969
36102
|
}
|
|
35970
36103
|
/**
|
|
35971
|
-
*
|
|
35972
|
-
* from disk (creating it on first call)
|
|
35973
|
-
*
|
|
35974
|
-
*
|
|
35975
|
-
* `
|
|
35976
|
-
*
|
|
36104
|
+
* Prepare the bridge for use. Loads the long-term identity keypair
|
|
36105
|
+
* from disk (creating it on first call) and computes this instance's
|
|
36106
|
+
* `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
|
|
36107
|
+
* connection is established lazily on the first verb call (see
|
|
36108
|
+
* `ensureConnected` / `getOrConnect`).
|
|
36109
|
+
*
|
|
36110
|
+
* Pre-0.5.3 behavior: `listen()` also did role election and started
|
|
36111
|
+
* the host/peer immediately, which meant every configured-but-unused
|
|
36112
|
+
* MCP claimed bridge resources at MCP-client boot. Several MCPs
|
|
36113
|
+
* starting in parallel under Claude Desktop also produced noisy
|
|
36114
|
+
* `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
|
|
36115
|
+
* of the first MCP's port bind. Deferring keeps boot quiet and
|
|
36116
|
+
* leaves the port unowned until something actually needs it.
|
|
36117
|
+
*
|
|
36118
|
+
* Calling `listen()` twice without an intervening `close()` is a
|
|
36119
|
+
* no-op (the second call's identity load is idempotent).
|
|
35977
36120
|
*/
|
|
35978
36121
|
async listen() {
|
|
35979
|
-
|
|
35980
|
-
|
|
36122
|
+
if (!this.identity) {
|
|
36123
|
+
this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
|
|
36124
|
+
}
|
|
36125
|
+
if (!this.mcpId) {
|
|
36126
|
+
this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
|
|
36127
|
+
}
|
|
36128
|
+
}
|
|
36129
|
+
/**
|
|
36130
|
+
* Force an eager bridge connection (role-election + host/peer handle
|
|
36131
|
+
* start + listener wiring) without waiting for the first verb call.
|
|
36132
|
+
* Useful for callers that want to surface the role / connection
|
|
36133
|
+
* outcome at boot, or for tests whose harness dials a mock extension
|
|
36134
|
+
* immediately after server construction. Production MCPs that just
|
|
36135
|
+
* answer tool calls should NOT call this — the lazy connect via
|
|
36136
|
+
* `ensureConnected` will do the right thing on first use, keeping
|
|
36137
|
+
* boot cheap and avoiding port-bind contention for MCPs that never
|
|
36138
|
+
* actually get invoked.
|
|
36139
|
+
*
|
|
36140
|
+
* Idempotent: a second call after the first has resolved is a no-op
|
|
36141
|
+
* (the existing handle is reused). Throws if `listen()` was never
|
|
36142
|
+
* called.
|
|
36143
|
+
*/
|
|
36144
|
+
async connect() {
|
|
36145
|
+
await this.ensureConnected();
|
|
36146
|
+
}
|
|
36147
|
+
/**
|
|
36148
|
+
* Establish the bridge connection (role-election + host/peer handle
|
|
36149
|
+
* start + listener wiring) the first time a verb is invoked.
|
|
36150
|
+
* Idempotent after the connection is up; concurrent first-callers
|
|
36151
|
+
* share the same in-flight promise so only one election happens.
|
|
36152
|
+
*
|
|
36153
|
+
* Throws if `listen()` was never called — the contract is that the
|
|
36154
|
+
* MCP author still must wire `transport.start()` at boot to load
|
|
36155
|
+
* identity / set mcpId, even though the WS doesn't open until a
|
|
36156
|
+
* verb runs.
|
|
36157
|
+
*/
|
|
36158
|
+
async ensureConnected() {
|
|
36159
|
+
if (this.hostHandle || this.peerHandle)
|
|
36160
|
+
return;
|
|
36161
|
+
if (this.connectingPromise) {
|
|
36162
|
+
await this.connectingPromise;
|
|
36163
|
+
return;
|
|
36164
|
+
}
|
|
36165
|
+
if (!this.identity || !this.mcpId) {
|
|
36166
|
+
throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
|
|
36167
|
+
}
|
|
36168
|
+
this.connectingPromise = this.doConnect();
|
|
36169
|
+
try {
|
|
36170
|
+
await this.connectingPromise;
|
|
36171
|
+
} finally {
|
|
36172
|
+
this.connectingPromise = null;
|
|
36173
|
+
}
|
|
36174
|
+
}
|
|
36175
|
+
async doConnect() {
|
|
36176
|
+
const identity = this.identity;
|
|
36177
|
+
const mcpId = this.mcpId;
|
|
35981
36178
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
35982
36179
|
if (el.role === "host") {
|
|
35983
36180
|
this.role = "host";
|
|
35984
36181
|
this.hostHandle = await startHost({
|
|
35985
36182
|
httpServer: el.server,
|
|
35986
|
-
ownIdentity:
|
|
35987
|
-
ownMcpId:
|
|
36183
|
+
ownIdentity: identity,
|
|
36184
|
+
ownMcpId: mcpId,
|
|
35988
36185
|
ownServerName: this.opts.serverName,
|
|
35989
36186
|
ownVersion: this.opts.version,
|
|
35990
36187
|
ownDomains: this.opts.domains,
|
|
@@ -36000,13 +36197,16 @@ var FetchproxyServer = class {
|
|
|
36000
36197
|
});
|
|
36001
36198
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36002
36199
|
this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
|
|
36200
|
+
this.hostHandle.onPendingPair((code) => {
|
|
36201
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36202
|
+
});
|
|
36003
36203
|
} else {
|
|
36004
36204
|
this.role = "peer";
|
|
36005
36205
|
this.peerHandle = await startPeer({
|
|
36006
36206
|
host: this.opts.host,
|
|
36007
36207
|
port: this.opts.port,
|
|
36008
|
-
identity
|
|
36009
|
-
mcpId
|
|
36208
|
+
identity,
|
|
36209
|
+
mcpId,
|
|
36010
36210
|
serverName: this.opts.serverName,
|
|
36011
36211
|
version: this.opts.version,
|
|
36012
36212
|
domains: this.opts.domains,
|
|
@@ -36020,8 +36220,19 @@ var FetchproxyServer = class {
|
|
|
36020
36220
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36021
36221
|
});
|
|
36022
36222
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36223
|
+
this.peerHandle.onRenegotiate(() => this.rejectAllPending());
|
|
36224
|
+
this.peerHandle.onPendingPair((code) => {
|
|
36225
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36226
|
+
});
|
|
36227
|
+
if (this.opts.onPairCode) {
|
|
36228
|
+
const cb = this.opts.onPairCode;
|
|
36229
|
+
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36230
|
+
}
|
|
36023
36231
|
}
|
|
36024
36232
|
}
|
|
36233
|
+
pairingErrorMessage(code) {
|
|
36234
|
+
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.`;
|
|
36235
|
+
}
|
|
36025
36236
|
/**
|
|
36026
36237
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
36027
36238
|
* the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
|
|
@@ -36037,9 +36248,75 @@ var FetchproxyServer = class {
|
|
|
36037
36248
|
* offline, etc.).
|
|
36038
36249
|
*/
|
|
36039
36250
|
async fetch(init) {
|
|
36040
|
-
|
|
36041
|
-
|
|
36251
|
+
await this.ensureConnected();
|
|
36252
|
+
const pendingCode = this.currentPendingPairCode();
|
|
36253
|
+
if (pendingCode !== null) {
|
|
36254
|
+
const error51 = this.pairingErrorMessage(pendingCode);
|
|
36255
|
+
return {
|
|
36256
|
+
ok: false,
|
|
36257
|
+
error: error51,
|
|
36258
|
+
kind: classifyFetchError(error51),
|
|
36259
|
+
retryAttempted: false
|
|
36260
|
+
};
|
|
36042
36261
|
}
|
|
36262
|
+
const first = await this._fetchOnceWithTimeout(init);
|
|
36263
|
+
const reviveMs = this.opts.bridgeReviveDelayMs;
|
|
36264
|
+
let final = first;
|
|
36265
|
+
if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
|
|
36266
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36267
|
+
const second = await this._fetchOnceWithTimeout(init);
|
|
36268
|
+
if (second.ok)
|
|
36269
|
+
this.recordSuccess();
|
|
36270
|
+
else
|
|
36271
|
+
this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
|
|
36272
|
+
return { ...second, retryAttempted: true };
|
|
36273
|
+
}
|
|
36274
|
+
if (first.ok)
|
|
36275
|
+
this.recordSuccess();
|
|
36276
|
+
else
|
|
36277
|
+
this.recordFailure(`${first.kind ?? "other"}: ${first.error}`);
|
|
36278
|
+
return { ...first, retryAttempted: false };
|
|
36279
|
+
}
|
|
36280
|
+
/**
|
|
36281
|
+
* 0.8.0+: snapshot of the bridge's process-wide freshness counters,
|
|
36282
|
+
* suitable for surfacing through a downstream MCP's healthcheck tool.
|
|
36283
|
+
* Counters reset on a success (consecutiveFailures), accumulate
|
|
36284
|
+
* across the process lifetime otherwise. Replaces the per-MCP
|
|
36285
|
+
* duplication the realty cohort had been rolling in their adapters.
|
|
36286
|
+
* `lastExtensionMessageAt` is updated whenever ANY inner frame
|
|
36287
|
+
* arrives from the extension — gives extension-side liveness
|
|
36288
|
+
* distinct from server-side success/failure of the user-visible
|
|
36289
|
+
* call (addresses #23 ask 4).
|
|
36290
|
+
*/
|
|
36291
|
+
bridgeHealth() {
|
|
36292
|
+
return {
|
|
36293
|
+
role: this.role,
|
|
36294
|
+
port: this.opts.port,
|
|
36295
|
+
serverVersion: this.opts.version,
|
|
36296
|
+
fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36297
|
+
bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
|
|
36298
|
+
lastSuccessAt: this.lastSuccessAt,
|
|
36299
|
+
lastFailureAt: this.lastFailureAt,
|
|
36300
|
+
lastFailureReason: this.lastFailureReason,
|
|
36301
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
36302
|
+
lastExtensionMessageAt: this.lastExtensionMessageAt
|
|
36303
|
+
};
|
|
36304
|
+
}
|
|
36305
|
+
recordSuccess() {
|
|
36306
|
+
this.lastSuccessAt = Date.now();
|
|
36307
|
+
this.consecutiveFailures = 0;
|
|
36308
|
+
}
|
|
36309
|
+
recordFailure(reason) {
|
|
36310
|
+
this.lastFailureAt = Date.now();
|
|
36311
|
+
this.lastFailureReason = reason;
|
|
36312
|
+
this.consecutiveFailures += 1;
|
|
36313
|
+
}
|
|
36314
|
+
/**
|
|
36315
|
+
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
36316
|
+
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
36317
|
+
* the throwing surface is the convenience methods.
|
|
36318
|
+
*/
|
|
36319
|
+
async _fetchOnceWithTimeout(init) {
|
|
36043
36320
|
const id = this.nextRequestId++;
|
|
36044
36321
|
const inner = { type: "request", id, op: "fetch", init };
|
|
36045
36322
|
const pending = new Promise((resolve2) => {
|
|
@@ -36050,7 +36327,61 @@ var FetchproxyServer = class {
|
|
|
36050
36327
|
} else if (this.peerHandle) {
|
|
36051
36328
|
await this.peerHandle.sendInner(inner);
|
|
36052
36329
|
}
|
|
36053
|
-
|
|
36330
|
+
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
36331
|
+
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
36332
|
+
return pending;
|
|
36333
|
+
let timer;
|
|
36334
|
+
const start = Date.now();
|
|
36335
|
+
try {
|
|
36336
|
+
return await Promise.race([
|
|
36337
|
+
pending,
|
|
36338
|
+
new Promise((resolve2) => {
|
|
36339
|
+
timer = setTimeout(() => {
|
|
36340
|
+
this.pending.delete(id);
|
|
36341
|
+
const elapsedMs = Date.now() - start;
|
|
36342
|
+
const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
36343
|
+
resolve2({
|
|
36344
|
+
ok: false,
|
|
36345
|
+
error: error51,
|
|
36346
|
+
kind: "timeout",
|
|
36347
|
+
retryAttempted: false,
|
|
36348
|
+
elapsedMs
|
|
36349
|
+
});
|
|
36350
|
+
}, timeoutMs);
|
|
36351
|
+
})
|
|
36352
|
+
]);
|
|
36353
|
+
} finally {
|
|
36354
|
+
if (timer)
|
|
36355
|
+
clearTimeout(timer);
|
|
36356
|
+
}
|
|
36357
|
+
}
|
|
36358
|
+
/**
|
|
36359
|
+
* Map an `ok:false` fetch result to its typed throwable. Centralizes
|
|
36360
|
+
* the kind-to-error-class switch so `request()` and (via the same
|
|
36361
|
+
* logic re-implemented inline) `captureRequestHeader()` agree on what
|
|
36362
|
+
* to throw.
|
|
36363
|
+
*/
|
|
36364
|
+
_typedErrorFor(result, url2, op, retryAttempted) {
|
|
36365
|
+
if (result.kind === "timeout") {
|
|
36366
|
+
return new FetchproxyTimeoutError({
|
|
36367
|
+
url: url2,
|
|
36368
|
+
timeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36369
|
+
role: this.role,
|
|
36370
|
+
port: this.opts.port,
|
|
36371
|
+
elapsedMs: result.elapsedMs
|
|
36372
|
+
});
|
|
36373
|
+
}
|
|
36374
|
+
if (result.kind === "content_script_unreachable") {
|
|
36375
|
+
return new FetchproxyBridgeDownError({
|
|
36376
|
+
originalError: result.error,
|
|
36377
|
+
retryAttempted,
|
|
36378
|
+
op,
|
|
36379
|
+
url: url2,
|
|
36380
|
+
role: this.role,
|
|
36381
|
+
port: this.opts.port
|
|
36382
|
+
});
|
|
36383
|
+
}
|
|
36384
|
+
return new FetchproxyProtocolError(result.error);
|
|
36054
36385
|
}
|
|
36055
36386
|
/**
|
|
36056
36387
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -36073,8 +36404,18 @@ var FetchproxyServer = class {
|
|
|
36073
36404
|
if (opts.subdomain !== void 0)
|
|
36074
36405
|
assertSubdomainLabel(opts.subdomain);
|
|
36075
36406
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
36076
|
-
const
|
|
36077
|
-
|
|
36407
|
+
const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
|
|
36408
|
+
let host;
|
|
36409
|
+
if (isAbsolute2) {
|
|
36410
|
+
try {
|
|
36411
|
+
host = new URL(path).host;
|
|
36412
|
+
} catch {
|
|
36413
|
+
throw new Error(`FetchproxyServer.request: absolute path is not a valid URL: ${JSON.stringify(path)}`);
|
|
36414
|
+
}
|
|
36415
|
+
} else {
|
|
36416
|
+
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
36417
|
+
}
|
|
36418
|
+
const url2 = isAbsolute2 ? path : `https://${host}${path}`;
|
|
36078
36419
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
36079
36420
|
const init = {
|
|
36080
36421
|
url: url2,
|
|
@@ -36085,7 +36426,7 @@ var FetchproxyServer = class {
|
|
|
36085
36426
|
};
|
|
36086
36427
|
const result = await this.fetch(init);
|
|
36087
36428
|
if (!result.ok) {
|
|
36088
|
-
throw
|
|
36429
|
+
throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
|
|
36089
36430
|
}
|
|
36090
36431
|
const response = {
|
|
36091
36432
|
status: result.status,
|
|
@@ -36179,9 +36520,8 @@ var FetchproxyServer = class {
|
|
|
36179
36520
|
if (!this.opts.capabilities.includes("read_cookies")) {
|
|
36180
36521
|
throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
36181
36522
|
}
|
|
36182
|
-
|
|
36183
|
-
|
|
36184
|
-
}
|
|
36523
|
+
await this.ensureConnected();
|
|
36524
|
+
this.throwIfPendingPair();
|
|
36185
36525
|
if (opts.subdomain !== void 0)
|
|
36186
36526
|
assertSubdomainLabel(opts.subdomain);
|
|
36187
36527
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
@@ -36238,9 +36578,8 @@ var FetchproxyServer = class {
|
|
|
36238
36578
|
if (!this.opts.capabilities.includes(op)) {
|
|
36239
36579
|
throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
|
|
36240
36580
|
}
|
|
36241
|
-
|
|
36242
|
-
|
|
36243
|
-
}
|
|
36581
|
+
await this.ensureConnected();
|
|
36582
|
+
this.throwIfPendingPair();
|
|
36244
36583
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36245
36584
|
throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
|
|
36246
36585
|
}
|
|
@@ -36295,13 +36634,75 @@ var FetchproxyServer = class {
|
|
|
36295
36634
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
36296
36635
|
throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
|
|
36297
36636
|
}
|
|
36298
|
-
|
|
36299
|
-
|
|
36637
|
+
await this.ensureConnected();
|
|
36638
|
+
this.throwIfPendingPair();
|
|
36639
|
+
const decls = this.opts.captureHeaders;
|
|
36640
|
+
let resolved;
|
|
36641
|
+
if (opts?.urlPattern !== void 0 && opts?.headerName !== void 0) {
|
|
36642
|
+
const found = decls.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
36643
|
+
if (!found) {
|
|
36644
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
36645
|
+
}
|
|
36646
|
+
resolved = found;
|
|
36647
|
+
} else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
|
|
36648
|
+
if (decls.length === 0) {
|
|
36649
|
+
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");
|
|
36650
|
+
}
|
|
36651
|
+
if (decls.length > 1) {
|
|
36652
|
+
const list = decls.map((d) => `${JSON.stringify(d.urlPattern)}/${JSON.stringify(d.headerName)}`).join(", ");
|
|
36653
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {urlPattern, headerName} to disambiguate`);
|
|
36654
|
+
}
|
|
36655
|
+
resolved = decls[0];
|
|
36656
|
+
} else {
|
|
36657
|
+
throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
|
|
36300
36658
|
}
|
|
36301
|
-
const
|
|
36302
|
-
|
|
36303
|
-
|
|
36659
|
+
const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
|
|
36660
|
+
try {
|
|
36661
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
36662
|
+
this.recordSuccess();
|
|
36663
|
+
return result;
|
|
36664
|
+
} catch (err) {
|
|
36665
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
36666
|
+
if (!swDown) {
|
|
36667
|
+
this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
|
|
36668
|
+
throw err;
|
|
36669
|
+
}
|
|
36670
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
36671
|
+
if (reviveMs > 0) {
|
|
36672
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36673
|
+
try {
|
|
36674
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
36675
|
+
this.recordSuccess();
|
|
36676
|
+
return result;
|
|
36677
|
+
} catch (retryErr) {
|
|
36678
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
36679
|
+
if (!stillDown) {
|
|
36680
|
+
this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
|
|
36681
|
+
throw retryErr;
|
|
36682
|
+
}
|
|
36683
|
+
this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
|
|
36684
|
+
throw new FetchproxyBridgeDownError({
|
|
36685
|
+
originalError: retryErr.message,
|
|
36686
|
+
retryAttempted: true,
|
|
36687
|
+
op: "capture_request_header",
|
|
36688
|
+
url: resolved.urlPattern,
|
|
36689
|
+
role: this.role,
|
|
36690
|
+
port: this.opts.port
|
|
36691
|
+
});
|
|
36692
|
+
}
|
|
36693
|
+
}
|
|
36694
|
+
this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
|
|
36695
|
+
throw new FetchproxyBridgeDownError({
|
|
36696
|
+
originalError: err.message,
|
|
36697
|
+
retryAttempted: false,
|
|
36698
|
+
op: "capture_request_header",
|
|
36699
|
+
url: resolved.urlPattern,
|
|
36700
|
+
role: this.role,
|
|
36701
|
+
port: this.opts.port
|
|
36702
|
+
});
|
|
36304
36703
|
}
|
|
36704
|
+
}
|
|
36705
|
+
async _captureRequestHeaderOnce(opts) {
|
|
36305
36706
|
const id = this.nextRequestId++;
|
|
36306
36707
|
const inner = {
|
|
36307
36708
|
type: "request",
|
|
@@ -36338,9 +36739,8 @@ var FetchproxyServer = class {
|
|
|
36338
36739
|
if (!this.opts.capabilities.includes("read_indexed_db")) {
|
|
36339
36740
|
throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
|
|
36340
36741
|
}
|
|
36341
|
-
|
|
36342
|
-
|
|
36343
|
-
}
|
|
36742
|
+
await this.ensureConnected();
|
|
36743
|
+
this.throwIfPendingPair();
|
|
36344
36744
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36345
36745
|
throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
|
|
36346
36746
|
}
|
|
@@ -36411,18 +36811,35 @@ var FetchproxyServer = class {
|
|
|
36411
36811
|
onInner(inner) {
|
|
36412
36812
|
if (inner.type !== "response")
|
|
36413
36813
|
return;
|
|
36814
|
+
this.lastExtensionMessageAt = Date.now();
|
|
36414
36815
|
const fetchCb = this.pending.get(inner.id);
|
|
36415
36816
|
if (fetchCb) {
|
|
36416
36817
|
this.pending.delete(inner.id);
|
|
36417
36818
|
if (inner.ok) {
|
|
36418
36819
|
if (inner.op === void 0 || inner.op === "fetch") {
|
|
36419
|
-
fetchCb({
|
|
36820
|
+
fetchCb({
|
|
36821
|
+
ok: true,
|
|
36822
|
+
status: inner.status,
|
|
36823
|
+
url: inner.url,
|
|
36824
|
+
body: inner.body,
|
|
36825
|
+
retryAttempted: false
|
|
36826
|
+
});
|
|
36420
36827
|
} else {
|
|
36421
36828
|
const error51 = `unexpected ${inner.op} response on fetch awaiter`;
|
|
36422
|
-
fetchCb({
|
|
36829
|
+
fetchCb({
|
|
36830
|
+
ok: false,
|
|
36831
|
+
error: error51,
|
|
36832
|
+
kind: classifyFetchError(error51),
|
|
36833
|
+
retryAttempted: false
|
|
36834
|
+
});
|
|
36423
36835
|
}
|
|
36424
36836
|
} else {
|
|
36425
|
-
fetchCb({
|
|
36837
|
+
fetchCb({
|
|
36838
|
+
ok: false,
|
|
36839
|
+
error: inner.error,
|
|
36840
|
+
kind: classifyFetchError(inner.error),
|
|
36841
|
+
retryAttempted: false
|
|
36842
|
+
});
|
|
36426
36843
|
}
|
|
36427
36844
|
return;
|
|
36428
36845
|
}
|
|
@@ -36489,10 +36906,15 @@ var FetchproxyServer = class {
|
|
|
36489
36906
|
}
|
|
36490
36907
|
}
|
|
36491
36908
|
}
|
|
36492
|
-
rejectAllPending() {
|
|
36493
|
-
const err = new FetchproxyProtocolError(
|
|
36909
|
+
rejectAllPending(reason = "extension disconnected") {
|
|
36910
|
+
const err = new FetchproxyProtocolError(reason);
|
|
36494
36911
|
for (const cb of this.pending.values()) {
|
|
36495
|
-
cb({
|
|
36912
|
+
cb({
|
|
36913
|
+
ok: false,
|
|
36914
|
+
error: err.message,
|
|
36915
|
+
kind: classifyFetchError(err.message),
|
|
36916
|
+
retryAttempted: false
|
|
36917
|
+
});
|
|
36496
36918
|
}
|
|
36497
36919
|
this.pending.clear();
|
|
36498
36920
|
for (const cb of this.pendingReadCookies.values()) {
|
|
@@ -36509,6 +36931,32 @@ var FetchproxyServer = class {
|
|
|
36509
36931
|
reject(err);
|
|
36510
36932
|
this.pendingIdb.clear();
|
|
36511
36933
|
}
|
|
36934
|
+
/**
|
|
36935
|
+
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
36936
|
+
* is active, returning null when none is pending. Public verbs call this
|
|
36937
|
+
* at the top so that a tool invoked while the bridge is waiting on user
|
|
36938
|
+
* approval fails fast with the actionable error rather than hanging on a
|
|
36939
|
+
* sealed frame the extension will never process.
|
|
36940
|
+
*/
|
|
36941
|
+
currentPendingPairCode() {
|
|
36942
|
+
if (this.hostHandle)
|
|
36943
|
+
return this.hostHandle.pendingPairCode();
|
|
36944
|
+
if (this.peerHandle)
|
|
36945
|
+
return this.peerHandle.pendingPairCode();
|
|
36946
|
+
return null;
|
|
36947
|
+
}
|
|
36948
|
+
/**
|
|
36949
|
+
* 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
|
|
36950
|
+
* message if the bridge is waiting on user approval. Used by the verb
|
|
36951
|
+
* methods (readCookies, readLocalStorage, etc.) that surface errors via
|
|
36952
|
+
* thrown exceptions rather than `ok:false` discriminated unions.
|
|
36953
|
+
*/
|
|
36954
|
+
throwIfPendingPair() {
|
|
36955
|
+
const code = this.currentPendingPairCode();
|
|
36956
|
+
if (code !== null) {
|
|
36957
|
+
throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
|
|
36958
|
+
}
|
|
36959
|
+
}
|
|
36512
36960
|
/**
|
|
36513
36961
|
* Shut down the bridge. Host: terminates the WebSocket server and any
|
|
36514
36962
|
* still-attached extension/peer clients. Peer: closes the upstream
|
|
@@ -36517,6 +36965,9 @@ var FetchproxyServer = class {
|
|
|
36517
36965
|
*/
|
|
36518
36966
|
async close() {
|
|
36519
36967
|
this.rejectAllPending();
|
|
36968
|
+
if (this.connectingPromise) {
|
|
36969
|
+
await this.connectingPromise.catch(() => void 0);
|
|
36970
|
+
}
|
|
36520
36971
|
if (this.hostHandle)
|
|
36521
36972
|
await this.hostHandle.close();
|
|
36522
36973
|
if (this.peerHandle)
|
|
@@ -36524,9 +36975,23 @@ var FetchproxyServer = class {
|
|
|
36524
36975
|
this.hostHandle = null;
|
|
36525
36976
|
this.peerHandle = null;
|
|
36526
36977
|
this.role = null;
|
|
36978
|
+
this.connectingPromise = null;
|
|
36527
36979
|
}
|
|
36528
36980
|
};
|
|
36529
36981
|
|
|
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
|
+
|
|
36530
36995
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
36531
36996
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
36532
36997
|
async function bootstrap(opts) {
|
|
@@ -36581,7 +37046,11 @@ async function bootstrap(opts) {
|
|
|
36581
37046
|
key: p.storageKey,
|
|
36582
37047
|
jsonPointer: p.jsonPointer
|
|
36583
37048
|
})),
|
|
36584
|
-
onPairCode: opts.onPairCode
|
|
37049
|
+
onPairCode: opts.onPairCode,
|
|
37050
|
+
// 0.8.0+ pass-through. Only forwarded when the caller set them;
|
|
37051
|
+
// unset → server defaults apply (30000 / 2000 in 0.8.0).
|
|
37052
|
+
...opts.fetchTimeoutMs !== void 0 ? { fetchTimeoutMs: opts.fetchTimeoutMs } : {},
|
|
37053
|
+
...opts.bridgeReviveDelayMs !== void 0 ? { bridgeReviveDelayMs: opts.bridgeReviveDelayMs } : {}
|
|
36585
37054
|
});
|
|
36586
37055
|
const storageDomainOpts = {};
|
|
36587
37056
|
if (opts.storageDomain !== void 0)
|
|
@@ -36766,7 +37235,7 @@ function getDefaultInlineAttachments() {
|
|
|
36766
37235
|
// package.json
|
|
36767
37236
|
var package_default = {
|
|
36768
37237
|
name: "ofw-mcp",
|
|
36769
|
-
version: "2.0
|
|
37238
|
+
version: "2.1.0",
|
|
36770
37239
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
36771
37240
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
36772
37241
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36793,17 +37262,18 @@ var package_default = {
|
|
|
36793
37262
|
"test:watch": "vitest"
|
|
36794
37263
|
},
|
|
36795
37264
|
dependencies: {
|
|
36796
|
-
"@fetchproxy/bootstrap": "^0.
|
|
37265
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
37266
|
+
"@fetchproxy/server": "^0.8.0",
|
|
36797
37267
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36798
|
-
dotenv: "^17.4.
|
|
36799
|
-
zod: "^4.4.
|
|
37268
|
+
dotenv: "^17.4.2",
|
|
37269
|
+
zod: "^4.4.3"
|
|
36800
37270
|
},
|
|
36801
37271
|
devDependencies: {
|
|
36802
|
-
"@types/node": "^25.
|
|
36803
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37272
|
+
"@types/node": "^25.9.1",
|
|
37273
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
36804
37274
|
esbuild: "^0.28.0",
|
|
36805
|
-
typescript: "^6.0.
|
|
36806
|
-
vitest: "^4.1.
|
|
37275
|
+
typescript: "^6.0.3",
|
|
37276
|
+
vitest: "^4.1.7"
|
|
36807
37277
|
}
|
|
36808
37278
|
};
|
|
36809
37279
|
|
|
@@ -36860,6 +37330,12 @@ async function resolveAuth() {
|
|
|
36860
37330
|
source: "fetchproxy"
|
|
36861
37331
|
};
|
|
36862
37332
|
} catch (e) {
|
|
37333
|
+
if (classifyBridgeError(e) === "bridge_down") {
|
|
37334
|
+
const downErr = e;
|
|
37335
|
+
throw new Error(
|
|
37336
|
+
`OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
37337
|
+
);
|
|
37338
|
+
}
|
|
36863
37339
|
const msg = e instanceof Error ? e.message : String(e);
|
|
36864
37340
|
throw new Error(
|
|
36865
37341
|
`OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
|
|
@@ -38153,7 +38629,7 @@ process.emit = function(event, ...args) {
|
|
|
38153
38629
|
}
|
|
38154
38630
|
return originalEmit(event, ...args);
|
|
38155
38631
|
};
|
|
38156
|
-
var server = new McpServer({ name: "ofw", version: "2.0
|
|
38632
|
+
var server = new McpServer({ name: "ofw", version: "2.1.0" });
|
|
38157
38633
|
registerUserTools(server, client);
|
|
38158
38634
|
registerMessageTools(server, client);
|
|
38159
38635
|
registerCalendarTools(server, client);
|
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.0
|
|
20
|
+
const server = new McpServer({ name: 'ofw', version: '2.1.0' }); // x-release-please-version
|
|
21
21
|
registerUserTools(server, client);
|
|
22
22
|
registerMessageTools(server, client);
|
|
23
23
|
registerCalendarTools(server, client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofw-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.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,16 +27,17 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@fetchproxy/bootstrap": "^0.
|
|
30
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
31
|
+
"@fetchproxy/server": "^0.8.0",
|
|
31
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
-
"dotenv": "^17.4.
|
|
33
|
-
"zod": "^4.4.
|
|
33
|
+
"dotenv": "^17.4.2",
|
|
34
|
+
"zod": "^4.4.3"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@types/node": "^25.
|
|
37
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37
|
+
"@types/node": "^25.9.1",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
38
39
|
"esbuild": "^0.28.0",
|
|
39
|
-
"typescript": "^6.0.
|
|
40
|
-
"vitest": "^4.1.
|
|
40
|
+
"typescript": "^6.0.3",
|
|
41
|
+
"vitest": "^4.1.7"
|
|
41
42
|
}
|
|
42
43
|
}
|
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.0
|
|
9
|
+
"version": "2.1.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "ofw-mcp",
|
|
14
|
-
"version": "2.0
|
|
14
|
+
"version": "2.1.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|