creditkarma-mcp 2.0.10 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/auth.js +13 -0
- package/dist/bundle.js +682 -91
- package/dist/index.js +2 -1
- package/package.json +10 -9
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,28 @@ Ask Claude things like:
|
|
|
23
23
|
- A Credit Karma account
|
|
24
24
|
- For the no-env-var path: the [fetchproxy 0.3.0 Chrome / Safari extension](https://github.com/chrischall/fetchproxy)
|
|
25
25
|
|
|
26
|
+
## Acknowledgement of Terms
|
|
27
|
+
|
|
28
|
+
By using this MCP server, you acknowledge and agree to the following:
|
|
29
|
+
|
|
30
|
+
**1. This server accesses your own Credit Karma account.** Every request is dispatched through your own signed-in browser tab via the fetchproxy extension. **You** are the one logged in. It does not — and cannot — access anyone else's account.
|
|
31
|
+
|
|
32
|
+
**2. [Credit Karma's Terms](https://www.creditkarma.com/about/terms) govern your use of this server**, just as they govern your direct use of creditkarma.com. The clauses most relevant here:
|
|
33
|
+
|
|
34
|
+
> You must not sell, transfer, or assign your account to anyone else… you may not allow anyone else to log into our Services as you.
|
|
35
|
+
|
|
36
|
+
CK does contemplate third-party data retrieval at the user's direction (Section 3.7). There is no explicit anti-scraping clause in the membership agreement; Section 4.1 restricts copying or distributing CK content without express prior written consent.
|
|
37
|
+
|
|
38
|
+
You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. Critically: this server runs **as you**, not as a third party logging in on your behalf. You direct the tool.
|
|
39
|
+
|
|
40
|
+
**3. Personal, non-commercial use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with Intuit, Credit Karma, or any financial institution. It is a personal automation tool that reads your transaction history, spending categories, and account snapshots — the same data Credit Karma already shows you in their app. Do not use it on someone else's account, do not redistribute their content, and do not use it to make trading or lending decisions on behalf of others.
|
|
41
|
+
|
|
42
|
+
**4. This server may break.** Credit Karma rotates its internal endpoints; what works today may 404 tomorrow. This is the nature of unofficial integrations.
|
|
43
|
+
|
|
44
|
+
**5. You accept full responsibility** for any consequences of using this server in connection with your Credit Karma account — rate limiting, account warnings, suspension, or any enforcement action Intuit takes. If Credit Karma objects to your use, stop using this server. **Do not commit your `.env` to git** — your CK session/auth artifacts are credentials, and the Membership Agreement holds you responsible for their confidentiality.
|
|
45
|
+
|
|
46
|
+
This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede Credit Karma's actual Membership Agreement.
|
|
47
|
+
|
|
26
48
|
## Installation
|
|
27
49
|
|
|
28
50
|
### 1. Clone and build
|
package/dist/auth.js
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
// `src/tools/auth.ts` extracts the CKAT JWTs the same way it does
|
|
59
59
|
// today.
|
|
60
60
|
import { bootstrap } from '@fetchproxy/bootstrap';
|
|
61
|
+
import { classifyBridgeError } from '@fetchproxy/server';
|
|
61
62
|
import pkg from '../package.json' with { type: 'json' };
|
|
62
63
|
import { extractCookieValue } from './client.js';
|
|
63
64
|
/**
|
|
@@ -143,6 +144,18 @@ export async function resolveAuth() {
|
|
|
143
144
|
return { cookies, source: 'fetchproxy' };
|
|
144
145
|
}
|
|
145
146
|
catch (e) {
|
|
147
|
+
// 0.8.0+ typed-error discrimination. The fetchproxy server already
|
|
148
|
+
// retries once on SW eviction (bridgeReviveDelayMs=2000 default), so
|
|
149
|
+
// a thrown FetchproxyBridgeDownError means the retry also failed —
|
|
150
|
+
// the extension's service worker is genuinely down and the user
|
|
151
|
+
// needs to wake it. The `.hint` is the actionable copy
|
|
152
|
+
// ("click the extension toolbar icon...") that we'd otherwise have
|
|
153
|
+
// to hand-write here. Surface it verbatim so users in path 3 get
|
|
154
|
+
// the same self-service guidance as path 4.
|
|
155
|
+
if (classifyBridgeError(e) === 'bridge_down') {
|
|
156
|
+
const downErr = e;
|
|
157
|
+
throw new Error(`CK auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
158
|
+
}
|
|
146
159
|
const msg = e instanceof Error ? e.message : String(e);
|
|
147
160
|
throw new Error(`CK auth: no CK_COOKIES set, and fetchproxy fallback failed: ${msg}`);
|
|
148
161
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -7658,6 +7658,10 @@ var require_receiver = __commonJS({
|
|
|
7658
7658
|
* extensions
|
|
7659
7659
|
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
|
7660
7660
|
* client or server mode
|
|
7661
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
7662
|
+
* buffered data chunks
|
|
7663
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
7664
|
+
* fragments
|
|
7661
7665
|
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
|
7662
7666
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
7663
7667
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -7668,6 +7672,8 @@ var require_receiver = __commonJS({
|
|
|
7668
7672
|
this._binaryType = options.binaryType || BINARY_TYPES[0];
|
|
7669
7673
|
this._extensions = options.extensions || {};
|
|
7670
7674
|
this._isServer = !!options.isServer;
|
|
7675
|
+
this._maxBufferedChunks = options.maxBufferedChunks | 0;
|
|
7676
|
+
this._maxFragments = options.maxFragments | 0;
|
|
7671
7677
|
this._maxPayload = options.maxPayload | 0;
|
|
7672
7678
|
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
|
7673
7679
|
this[kWebSocket] = void 0;
|
|
@@ -7697,6 +7703,18 @@ var require_receiver = __commonJS({
|
|
|
7697
7703
|
*/
|
|
7698
7704
|
_write(chunk, encoding, cb) {
|
|
7699
7705
|
if (this._opcode === 8 && this._state == GET_INFO) return cb();
|
|
7706
|
+
if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
|
|
7707
|
+
cb(
|
|
7708
|
+
this.createError(
|
|
7709
|
+
RangeError,
|
|
7710
|
+
"Too many buffered chunks",
|
|
7711
|
+
false,
|
|
7712
|
+
1008,
|
|
7713
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
7714
|
+
)
|
|
7715
|
+
);
|
|
7716
|
+
return;
|
|
7717
|
+
}
|
|
7700
7718
|
this._bufferedBytes += chunk.length;
|
|
7701
7719
|
this._buffers.push(chunk);
|
|
7702
7720
|
this.startLoop(cb);
|
|
@@ -8026,6 +8044,17 @@ var require_receiver = __commonJS({
|
|
|
8026
8044
|
return;
|
|
8027
8045
|
}
|
|
8028
8046
|
if (data.length) {
|
|
8047
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8048
|
+
const error51 = this.createError(
|
|
8049
|
+
RangeError,
|
|
8050
|
+
"Too many message fragments",
|
|
8051
|
+
false,
|
|
8052
|
+
1008,
|
|
8053
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8054
|
+
);
|
|
8055
|
+
cb(error51);
|
|
8056
|
+
return;
|
|
8057
|
+
}
|
|
8029
8058
|
this._messageLength = this._totalPayloadLength;
|
|
8030
8059
|
this._fragments.push(data);
|
|
8031
8060
|
}
|
|
@@ -8055,6 +8084,17 @@ var require_receiver = __commonJS({
|
|
|
8055
8084
|
cb(error51);
|
|
8056
8085
|
return;
|
|
8057
8086
|
}
|
|
8087
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8088
|
+
const error51 = this.createError(
|
|
8089
|
+
RangeError,
|
|
8090
|
+
"Too many message fragments",
|
|
8091
|
+
false,
|
|
8092
|
+
1008,
|
|
8093
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8094
|
+
);
|
|
8095
|
+
cb(error51);
|
|
8096
|
+
return;
|
|
8097
|
+
}
|
|
8058
8098
|
this._fragments.push(buf);
|
|
8059
8099
|
}
|
|
8060
8100
|
this.dataMessage(cb);
|
|
@@ -9261,6 +9301,10 @@ var require_websocket = __commonJS({
|
|
|
9261
9301
|
* multiple times in the same tick
|
|
9262
9302
|
* @param {Function} [options.generateMask] The function used to generate the
|
|
9263
9303
|
* masking key
|
|
9304
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
9305
|
+
* buffered data chunks
|
|
9306
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
9307
|
+
* fragments
|
|
9264
9308
|
* @param {Number} [options.maxPayload=0] The maximum allowed message size
|
|
9265
9309
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
9266
9310
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -9272,6 +9316,8 @@ var require_websocket = __commonJS({
|
|
|
9272
9316
|
binaryType: this.binaryType,
|
|
9273
9317
|
extensions: this._extensions,
|
|
9274
9318
|
isServer: this._isServer,
|
|
9319
|
+
maxBufferedChunks: options.maxBufferedChunks,
|
|
9320
|
+
maxFragments: options.maxFragments,
|
|
9275
9321
|
maxPayload: options.maxPayload,
|
|
9276
9322
|
skipUTF8Validation: options.skipUTF8Validation
|
|
9277
9323
|
});
|
|
@@ -9571,6 +9617,8 @@ var require_websocket = __commonJS({
|
|
|
9571
9617
|
autoPong: true,
|
|
9572
9618
|
closeTimeout: CLOSE_TIMEOUT,
|
|
9573
9619
|
protocolVersion: protocolVersions[1],
|
|
9620
|
+
maxBufferedChunks: 1024 * 1024,
|
|
9621
|
+
maxFragments: 128 * 1024,
|
|
9574
9622
|
maxPayload: 100 * 1024 * 1024,
|
|
9575
9623
|
skipUTF8Validation: false,
|
|
9576
9624
|
perMessageDeflate: true,
|
|
@@ -9813,6 +9861,8 @@ var require_websocket = __commonJS({
|
|
|
9813
9861
|
websocket.setSocket(socket, head, {
|
|
9814
9862
|
allowSynchronousEvents: opts.allowSynchronousEvents,
|
|
9815
9863
|
generateMask: opts.generateMask,
|
|
9864
|
+
maxBufferedChunks: opts.maxBufferedChunks,
|
|
9865
|
+
maxFragments: opts.maxFragments,
|
|
9816
9866
|
maxPayload: opts.maxPayload,
|
|
9817
9867
|
skipUTF8Validation: opts.skipUTF8Validation
|
|
9818
9868
|
});
|
|
@@ -10155,6 +10205,10 @@ var require_websocket_server = __commonJS({
|
|
|
10155
10205
|
* called
|
|
10156
10206
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
10157
10207
|
* @param {String} [options.host] The hostname where to bind the server
|
|
10208
|
+
* @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
|
|
10209
|
+
* buffered data chunks
|
|
10210
|
+
* @param {Number} [options.maxFragments=131072] The maximum number of message
|
|
10211
|
+
* fragments
|
|
10158
10212
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
10159
10213
|
* size
|
|
10160
10214
|
* @param {Boolean} [options.noServer=false] Enable no server mode
|
|
@@ -10176,6 +10230,8 @@ var require_websocket_server = __commonJS({
|
|
|
10176
10230
|
options = {
|
|
10177
10231
|
allowSynchronousEvents: true,
|
|
10178
10232
|
autoPong: true,
|
|
10233
|
+
maxBufferedChunks: 1024 * 1024,
|
|
10234
|
+
maxFragments: 128 * 1024,
|
|
10179
10235
|
maxPayload: 100 * 1024 * 1024,
|
|
10180
10236
|
skipUTF8Validation: false,
|
|
10181
10237
|
perMessageDeflate: false,
|
|
@@ -10455,6 +10511,8 @@ var require_websocket_server = __commonJS({
|
|
|
10455
10511
|
socket.removeListener("error", socketOnError);
|
|
10456
10512
|
ws.setSocket(socket, head, {
|
|
10457
10513
|
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
|
10514
|
+
maxBufferedChunks: this.options.maxBufferedChunks,
|
|
10515
|
+
maxFragments: this.options.maxFragments,
|
|
10458
10516
|
maxPayload: this.options.maxPayload,
|
|
10459
10517
|
skipUTF8Validation: this.options.skipUTF8Validation
|
|
10460
10518
|
});
|
|
@@ -34974,6 +35032,7 @@ function registerAuthTools(server, ctx) {
|
|
|
34974
35032
|
|
|
34975
35033
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
34976
35034
|
var PROTOCOL_VERSION = 2;
|
|
35035
|
+
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
34977
35036
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
34978
35037
|
"fetch",
|
|
34979
35038
|
"read_cookies",
|
|
@@ -35048,7 +35107,7 @@ var ProtocolError = class extends Error {
|
|
|
35048
35107
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
35049
35108
|
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
35050
35109
|
var SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
|
|
35051
|
-
var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,
|
|
35110
|
+
var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
|
|
35052
35111
|
var HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
|
|
35053
35112
|
var HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
35054
35113
|
function assertObject(x, label) {
|
|
@@ -35177,7 +35236,7 @@ function assertStoragePointersArray(value, label, declaredKeys) {
|
|
|
35177
35236
|
if (entry.jsonPointer === void 0) {
|
|
35178
35237
|
throw new ProtocolError(`${label}[${i}].jsonPointer: missing`);
|
|
35179
35238
|
}
|
|
35180
|
-
if (typeof entry.key !== "string" || !
|
|
35239
|
+
if (typeof entry.key !== "string" || !SCOPE_KEY_RE.test(entry.key)) {
|
|
35181
35240
|
throw new ProtocolError(`${label}[${i}].key: invalid key ${JSON.stringify(entry.key)}`);
|
|
35182
35241
|
}
|
|
35183
35242
|
if (typeof entry.jsonPointer !== "string" || !isValidJsonPointer(entry.jsonPointer)) {
|
|
@@ -35276,6 +35335,8 @@ function validateFrame(raw) {
|
|
|
35276
35335
|
return validateReady(raw);
|
|
35277
35336
|
if (t === "frame")
|
|
35278
35337
|
return validateEncrypted(raw);
|
|
35338
|
+
if (t === "pair-pending")
|
|
35339
|
+
return validatePairPending(raw);
|
|
35279
35340
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
35280
35341
|
}
|
|
35281
35342
|
function validateHello(raw) {
|
|
@@ -35377,6 +35438,17 @@ function validateEncrypted(raw) {
|
|
|
35377
35438
|
assertBase64(raw.ciphertext, "frame.ciphertext");
|
|
35378
35439
|
return raw;
|
|
35379
35440
|
}
|
|
35441
|
+
var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
|
|
35442
|
+
function validatePairPending(raw) {
|
|
35443
|
+
assertString(raw.mcpId, "pair-pending.mcpId");
|
|
35444
|
+
if (!isValidMcpId(raw.mcpId))
|
|
35445
|
+
throw new ProtocolError("pair-pending.mcpId: invalid format");
|
|
35446
|
+
assertString(raw.pairCode, "pair-pending.pairCode");
|
|
35447
|
+
if (!PAIR_CODE_RE.test(raw.pairCode)) {
|
|
35448
|
+
throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
|
|
35449
|
+
}
|
|
35450
|
+
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
35451
|
+
}
|
|
35380
35452
|
function validateInnerFrame(raw) {
|
|
35381
35453
|
assertObject(raw, "inner");
|
|
35382
35454
|
const t = raw.type;
|
|
@@ -35959,15 +36031,22 @@ async function startHost(opts) {
|
|
|
35959
36031
|
let extensionWs = null;
|
|
35960
36032
|
const peers = /* @__PURE__ */ new Map();
|
|
35961
36033
|
const ownInnerListeners = [];
|
|
36034
|
+
const disconnectListeners = [];
|
|
36035
|
+
const pendingPairListeners = [];
|
|
35962
36036
|
let ownSession = null;
|
|
36037
|
+
let ownPendingPairCode = null;
|
|
35963
36038
|
let resolveOwnSession;
|
|
35964
36039
|
let rejectOwnSession;
|
|
35965
|
-
|
|
35966
|
-
|
|
35967
|
-
|
|
35968
|
-
|
|
35969
|
-
|
|
35970
|
-
|
|
36040
|
+
let ownSessionReady;
|
|
36041
|
+
function resetSessionPromise() {
|
|
36042
|
+
ownSessionReady = new Promise((resolve, reject) => {
|
|
36043
|
+
resolveOwnSession = resolve;
|
|
36044
|
+
rejectOwnSession = reject;
|
|
36045
|
+
});
|
|
36046
|
+
ownSessionReady.catch(() => {
|
|
36047
|
+
});
|
|
36048
|
+
}
|
|
36049
|
+
resetSessionPromise();
|
|
35971
36050
|
let extensionHello = null;
|
|
35972
36051
|
wss.on("connection", (ws) => {
|
|
35973
36052
|
let identified = null;
|
|
@@ -36036,11 +36115,12 @@ async function startHost(opts) {
|
|
|
36036
36115
|
}
|
|
36037
36116
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36038
36117
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36039
|
-
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(
|
|
36040
|
-
if (
|
|
36041
|
-
|
|
36042
|
-
|
|
36043
|
-
|
|
36118
|
+
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
36119
|
+
if (extensionWs !== ws)
|
|
36120
|
+
return;
|
|
36121
|
+
ownSession = new SessionState(key);
|
|
36122
|
+
ownPendingPairCode = null;
|
|
36123
|
+
resolveOwnSession(ownSession);
|
|
36044
36124
|
} else {
|
|
36045
36125
|
const slot = peers.get(frame.mcpId);
|
|
36046
36126
|
if (slot)
|
|
@@ -36067,6 +36147,16 @@ async function startHost(opts) {
|
|
|
36067
36147
|
extensionWs.send(JSON.stringify(frame));
|
|
36068
36148
|
}
|
|
36069
36149
|
}
|
|
36150
|
+
if (frame.type === "pair-pending" && identified === "extension") {
|
|
36151
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
36152
|
+
ownPendingPairCode = frame.pairCode;
|
|
36153
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
36154
|
+
} else {
|
|
36155
|
+
const slot = peers.get(frame.mcpId);
|
|
36156
|
+
if (slot)
|
|
36157
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36158
|
+
}
|
|
36159
|
+
}
|
|
36070
36160
|
} catch (e) {
|
|
36071
36161
|
console.error("[fetchproxy] host: message handler error:", e);
|
|
36072
36162
|
try {
|
|
@@ -36082,6 +36172,9 @@ async function startHost(opts) {
|
|
|
36082
36172
|
if (!ownSession) {
|
|
36083
36173
|
rejectOwnSession(new Error("extension disconnected before ready"));
|
|
36084
36174
|
}
|
|
36175
|
+
ownSession = null;
|
|
36176
|
+
resetSessionPromise();
|
|
36177
|
+
disconnectListeners.forEach((cb) => cb());
|
|
36085
36178
|
}
|
|
36086
36179
|
if (identified === "peer" && peerMcpId)
|
|
36087
36180
|
peers.delete(peerMcpId);
|
|
@@ -36106,7 +36199,14 @@ async function startHost(opts) {
|
|
|
36106
36199
|
},
|
|
36107
36200
|
onOwnInner: (cb) => {
|
|
36108
36201
|
ownInnerListeners.push(cb);
|
|
36109
|
-
}
|
|
36202
|
+
},
|
|
36203
|
+
onExtensionDisconnect: (cb) => {
|
|
36204
|
+
disconnectListeners.push(cb);
|
|
36205
|
+
},
|
|
36206
|
+
onPendingPair: (cb) => {
|
|
36207
|
+
pendingPairListeners.push(cb);
|
|
36208
|
+
},
|
|
36209
|
+
pendingPairCode: () => ownPendingPairCode
|
|
36110
36210
|
};
|
|
36111
36211
|
}
|
|
36112
36212
|
|
|
@@ -36136,36 +36236,57 @@ async function startPeer(opts) {
|
|
|
36136
36236
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
36137
36237
|
ws.send(JSON.stringify(hello));
|
|
36138
36238
|
const innerListeners = [];
|
|
36239
|
+
const renegotiateListeners = [];
|
|
36240
|
+
const pendingPairListeners = [];
|
|
36139
36241
|
let session = null;
|
|
36242
|
+
let pendingPairCode = null;
|
|
36243
|
+
let resolveFirstReady;
|
|
36244
|
+
let rejectFirstReady;
|
|
36140
36245
|
const sessionPromise = new Promise((resolve, reject) => {
|
|
36141
|
-
|
|
36142
|
-
|
|
36143
|
-
|
|
36144
|
-
|
|
36145
|
-
|
|
36146
|
-
|
|
36147
|
-
|
|
36148
|
-
|
|
36149
|
-
|
|
36150
|
-
|
|
36151
|
-
|
|
36246
|
+
resolveFirstReady = resolve;
|
|
36247
|
+
rejectFirstReady = reject;
|
|
36248
|
+
});
|
|
36249
|
+
const onMessage = async (data) => {
|
|
36250
|
+
try {
|
|
36251
|
+
const raw = JSON.parse(data.toString());
|
|
36252
|
+
const frame = validateFrame(raw);
|
|
36253
|
+
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
36254
|
+
const extPub = fromB64(frame.extensionSessionPub);
|
|
36255
|
+
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36256
|
+
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
36257
|
+
const isRenegotiation = session !== null;
|
|
36258
|
+
session = new SessionState(sessionKey);
|
|
36259
|
+
pendingPairCode = null;
|
|
36260
|
+
if (isRenegotiation) {
|
|
36261
|
+
renegotiateListeners.forEach((cb) => cb());
|
|
36262
|
+
} else {
|
|
36263
|
+
resolveFirstReady(session);
|
|
36152
36264
|
}
|
|
36153
|
-
|
|
36154
|
-
|
|
36155
|
-
|
|
36156
|
-
|
|
36157
|
-
|
|
36265
|
+
return;
|
|
36266
|
+
}
|
|
36267
|
+
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
36268
|
+
pendingPairCode = frame.pairCode;
|
|
36269
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
36270
|
+
return;
|
|
36271
|
+
}
|
|
36272
|
+
if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
|
|
36273
|
+
if (!session)
|
|
36274
|
+
return;
|
|
36275
|
+
if (!session.acceptInboundSeq(frame.seq))
|
|
36276
|
+
return;
|
|
36277
|
+
try {
|
|
36158
36278
|
const inner = await openEncryptedFrame(session.sessionKey, frame);
|
|
36159
36279
|
innerListeners.forEach((cb) => cb(inner));
|
|
36280
|
+
} catch {
|
|
36160
36281
|
}
|
|
36161
|
-
} catch (e) {
|
|
36162
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
36163
36282
|
}
|
|
36164
|
-
}
|
|
36165
|
-
|
|
36166
|
-
|
|
36167
|
-
|
|
36168
|
-
|
|
36283
|
+
} catch (e) {
|
|
36284
|
+
rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
|
|
36285
|
+
}
|
|
36286
|
+
};
|
|
36287
|
+
ws.on("message", onMessage);
|
|
36288
|
+
ws.once("close", () => {
|
|
36289
|
+
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
36169
36290
|
});
|
|
36170
36291
|
sessionPromise.catch(() => {
|
|
36171
36292
|
});
|
|
@@ -36173,13 +36294,21 @@ async function startPeer(opts) {
|
|
|
36173
36294
|
ws,
|
|
36174
36295
|
session: sessionPromise,
|
|
36175
36296
|
sendInner: async (inner) => {
|
|
36176
|
-
|
|
36297
|
+
await sessionPromise;
|
|
36298
|
+
const s = session;
|
|
36177
36299
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
36178
36300
|
ws.send(JSON.stringify(sealed));
|
|
36179
36301
|
},
|
|
36180
36302
|
onInner: (cb) => {
|
|
36181
36303
|
innerListeners.push(cb);
|
|
36182
36304
|
},
|
|
36305
|
+
onRenegotiate: (cb) => {
|
|
36306
|
+
renegotiateListeners.push(cb);
|
|
36307
|
+
},
|
|
36308
|
+
onPendingPair: (cb) => {
|
|
36309
|
+
pendingPairListeners.push(cb);
|
|
36310
|
+
},
|
|
36311
|
+
pendingPairCode: () => pendingPairCode,
|
|
36183
36312
|
close: () => ws.close()
|
|
36184
36313
|
};
|
|
36185
36314
|
return handle;
|
|
@@ -36236,6 +36365,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
|
36236
36365
|
return id;
|
|
36237
36366
|
}
|
|
36238
36367
|
|
|
36368
|
+
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36369
|
+
function classifyFetchError(error51) {
|
|
36370
|
+
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
36371
|
+
return "content_script_unreachable";
|
|
36372
|
+
}
|
|
36373
|
+
if (/^tab fetch failed:/.test(error51)) {
|
|
36374
|
+
return "tab_fetch_failed";
|
|
36375
|
+
}
|
|
36376
|
+
if (/^fetch threw:/.test(error51)) {
|
|
36377
|
+
return "tab_fetch_failed";
|
|
36378
|
+
}
|
|
36379
|
+
if (/^no tab matching /.test(error51)) {
|
|
36380
|
+
return "no_tab";
|
|
36381
|
+
}
|
|
36382
|
+
if (/not in domains \[/.test(error51)) {
|
|
36383
|
+
return "domain_denied";
|
|
36384
|
+
}
|
|
36385
|
+
if (/^capability .+ not granted/.test(error51)) {
|
|
36386
|
+
return "capability_denied";
|
|
36387
|
+
}
|
|
36388
|
+
if (/^(request|response) body too large:/.test(error51)) {
|
|
36389
|
+
return "body_too_large";
|
|
36390
|
+
}
|
|
36391
|
+
return "other";
|
|
36392
|
+
}
|
|
36393
|
+
|
|
36239
36394
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
36240
36395
|
var FetchproxyProtocolError = class extends Error {
|
|
36241
36396
|
constructor(message) {
|
|
@@ -36251,6 +36406,52 @@ var FetchproxyHttpError = class extends Error {
|
|
|
36251
36406
|
this.name = "FetchproxyHttpError";
|
|
36252
36407
|
}
|
|
36253
36408
|
};
|
|
36409
|
+
var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
36410
|
+
originalError;
|
|
36411
|
+
retryAttempted;
|
|
36412
|
+
op;
|
|
36413
|
+
url;
|
|
36414
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
36415
|
+
role;
|
|
36416
|
+
/** 0.8.0+: bridge port at throw time (the same port `listen()` bound to). */
|
|
36417
|
+
port;
|
|
36418
|
+
hint;
|
|
36419
|
+
constructor(args) {
|
|
36420
|
+
const retryAttempted = args.retryAttempted ?? false;
|
|
36421
|
+
const op = args.op ?? "fetch";
|
|
36422
|
+
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). `;
|
|
36423
|
+
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.`;
|
|
36424
|
+
super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
|
|
36425
|
+
this.name = "FetchproxyBridgeDownError";
|
|
36426
|
+
this.originalError = args.originalError;
|
|
36427
|
+
this.retryAttempted = retryAttempted;
|
|
36428
|
+
this.op = op;
|
|
36429
|
+
if (args.url !== void 0)
|
|
36430
|
+
this.url = args.url;
|
|
36431
|
+
this.role = args.role ?? null;
|
|
36432
|
+
this.port = args.port ?? 0;
|
|
36433
|
+
this.hint = hint;
|
|
36434
|
+
}
|
|
36435
|
+
};
|
|
36436
|
+
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36437
|
+
url;
|
|
36438
|
+
timeoutMs;
|
|
36439
|
+
/** 0.8.0+: bridge role at throw time; `null` if listen() hadn't bound yet. */
|
|
36440
|
+
role;
|
|
36441
|
+
/** 0.8.0+: bridge port at throw time. */
|
|
36442
|
+
port;
|
|
36443
|
+
/** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
|
|
36444
|
+
elapsedMs;
|
|
36445
|
+
constructor(args) {
|
|
36446
|
+
super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
|
|
36447
|
+
this.name = "FetchproxyTimeoutError";
|
|
36448
|
+
this.url = args.url;
|
|
36449
|
+
this.timeoutMs = args.timeoutMs;
|
|
36450
|
+
this.role = args.role ?? null;
|
|
36451
|
+
this.port = args.port ?? 0;
|
|
36452
|
+
this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
|
|
36453
|
+
}
|
|
36454
|
+
};
|
|
36254
36455
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
36255
36456
|
function assertSubdomainLabel(label) {
|
|
36256
36457
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
@@ -36278,12 +36479,28 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
36278
36479
|
}
|
|
36279
36480
|
var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
|
|
36280
36481
|
var FetchproxyServer = class {
|
|
36281
|
-
/**
|
|
36482
|
+
/**
|
|
36483
|
+
* Bridge role. `null` until the first verb call (or an explicit
|
|
36484
|
+
* `connect()`) — `listen()` no longer triggers the role election
|
|
36485
|
+
* as of 0.5.3+. Reset to `null` on `close()`.
|
|
36486
|
+
*/
|
|
36282
36487
|
role = null;
|
|
36283
36488
|
opts;
|
|
36284
36489
|
hostHandle = null;
|
|
36285
36490
|
peerHandle = null;
|
|
36286
36491
|
nextRequestId = 1;
|
|
36492
|
+
// 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
|
|
36493
|
+
// Replaces the local copies every downstream MCP was rolling on top
|
|
36494
|
+
// of its own transport adapter — see realty-mcp cohort drift notes.
|
|
36495
|
+
// Updated by recordSuccess / recordFailure from fetch + capture paths.
|
|
36496
|
+
// `lastExtensionMessageAt` (#23 ask 4) is updated whenever any inner
|
|
36497
|
+
// frame from the extension arrives — gives extension-side liveness
|
|
36498
|
+
// distinct from per-call success/failure.
|
|
36499
|
+
lastSuccessAt = null;
|
|
36500
|
+
lastFailureAt = null;
|
|
36501
|
+
lastFailureReason = null;
|
|
36502
|
+
consecutiveFailures = 0;
|
|
36503
|
+
lastExtensionMessageAt = null;
|
|
36287
36504
|
pending = /* @__PURE__ */ new Map();
|
|
36288
36505
|
// Separate pending map for read_cookies so the response shape (cookies
|
|
36289
36506
|
// string vs status/body) doesn't have to share a union type with fetch.
|
|
@@ -36300,6 +36517,12 @@ var FetchproxyServer = class {
|
|
|
36300
36517
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36301
36518
|
mcpId = null;
|
|
36302
36519
|
identity = null;
|
|
36520
|
+
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
36521
|
+
// first time a verb call runs `ensureConnected`, awaited by concurrent
|
|
36522
|
+
// callers, cleared once the connection is up. Single source of truth
|
|
36523
|
+
// for "we're connecting right now" so two parallel first-calls don't
|
|
36524
|
+
// race the port bind.
|
|
36525
|
+
connectingPromise = null;
|
|
36303
36526
|
constructor(opts) {
|
|
36304
36527
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
36305
36528
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -36346,28 +36569,98 @@ var FetchproxyServer = class {
|
|
|
36346
36569
|
key: d.key,
|
|
36347
36570
|
jsonPointer: d.jsonPointer
|
|
36348
36571
|
})),
|
|
36572
|
+
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36573
|
+
// adapter was about to set these to the same numbers anyway; the
|
|
36574
|
+
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
36575
|
+
// the legacy hang-forever / fail-once-on-SW-eviction behavior.
|
|
36576
|
+
fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
|
|
36577
|
+
bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
|
|
36349
36578
|
identityDir: opts.identityDir,
|
|
36350
36579
|
onPairCode: opts.onPairCode
|
|
36351
36580
|
};
|
|
36352
36581
|
}
|
|
36353
36582
|
/**
|
|
36354
|
-
*
|
|
36355
|
-
* from disk (creating it on first call)
|
|
36356
|
-
*
|
|
36357
|
-
*
|
|
36358
|
-
* `
|
|
36359
|
-
*
|
|
36583
|
+
* Prepare the bridge for use. Loads the long-term identity keypair
|
|
36584
|
+
* from disk (creating it on first call) and computes this instance's
|
|
36585
|
+
* `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
|
|
36586
|
+
* connection is established lazily on the first verb call (see
|
|
36587
|
+
* `ensureConnected` / `getOrConnect`).
|
|
36588
|
+
*
|
|
36589
|
+
* Pre-0.5.3 behavior: `listen()` also did role election and started
|
|
36590
|
+
* the host/peer immediately, which meant every configured-but-unused
|
|
36591
|
+
* MCP claimed bridge resources at MCP-client boot. Several MCPs
|
|
36592
|
+
* starting in parallel under Claude Desktop also produced noisy
|
|
36593
|
+
* `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
|
|
36594
|
+
* of the first MCP's port bind. Deferring keeps boot quiet and
|
|
36595
|
+
* leaves the port unowned until something actually needs it.
|
|
36596
|
+
*
|
|
36597
|
+
* Calling `listen()` twice without an intervening `close()` is a
|
|
36598
|
+
* no-op (the second call's identity load is idempotent).
|
|
36360
36599
|
*/
|
|
36361
36600
|
async listen() {
|
|
36362
|
-
|
|
36363
|
-
|
|
36601
|
+
if (!this.identity) {
|
|
36602
|
+
this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
|
|
36603
|
+
}
|
|
36604
|
+
if (!this.mcpId) {
|
|
36605
|
+
this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
|
|
36606
|
+
}
|
|
36607
|
+
}
|
|
36608
|
+
/**
|
|
36609
|
+
* Force an eager bridge connection (role-election + host/peer handle
|
|
36610
|
+
* start + listener wiring) without waiting for the first verb call.
|
|
36611
|
+
* Useful for callers that want to surface the role / connection
|
|
36612
|
+
* outcome at boot, or for tests whose harness dials a mock extension
|
|
36613
|
+
* immediately after server construction. Production MCPs that just
|
|
36614
|
+
* answer tool calls should NOT call this — the lazy connect via
|
|
36615
|
+
* `ensureConnected` will do the right thing on first use, keeping
|
|
36616
|
+
* boot cheap and avoiding port-bind contention for MCPs that never
|
|
36617
|
+
* actually get invoked.
|
|
36618
|
+
*
|
|
36619
|
+
* Idempotent: a second call after the first has resolved is a no-op
|
|
36620
|
+
* (the existing handle is reused). Throws if `listen()` was never
|
|
36621
|
+
* called.
|
|
36622
|
+
*/
|
|
36623
|
+
async connect() {
|
|
36624
|
+
await this.ensureConnected();
|
|
36625
|
+
}
|
|
36626
|
+
/**
|
|
36627
|
+
* Establish the bridge connection (role-election + host/peer handle
|
|
36628
|
+
* start + listener wiring) the first time a verb is invoked.
|
|
36629
|
+
* Idempotent after the connection is up; concurrent first-callers
|
|
36630
|
+
* share the same in-flight promise so only one election happens.
|
|
36631
|
+
*
|
|
36632
|
+
* Throws if `listen()` was never called — the contract is that the
|
|
36633
|
+
* MCP author still must wire `transport.start()` at boot to load
|
|
36634
|
+
* identity / set mcpId, even though the WS doesn't open until a
|
|
36635
|
+
* verb runs.
|
|
36636
|
+
*/
|
|
36637
|
+
async ensureConnected() {
|
|
36638
|
+
if (this.hostHandle || this.peerHandle)
|
|
36639
|
+
return;
|
|
36640
|
+
if (this.connectingPromise) {
|
|
36641
|
+
await this.connectingPromise;
|
|
36642
|
+
return;
|
|
36643
|
+
}
|
|
36644
|
+
if (!this.identity || !this.mcpId) {
|
|
36645
|
+
throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
|
|
36646
|
+
}
|
|
36647
|
+
this.connectingPromise = this.doConnect();
|
|
36648
|
+
try {
|
|
36649
|
+
await this.connectingPromise;
|
|
36650
|
+
} finally {
|
|
36651
|
+
this.connectingPromise = null;
|
|
36652
|
+
}
|
|
36653
|
+
}
|
|
36654
|
+
async doConnect() {
|
|
36655
|
+
const identity = this.identity;
|
|
36656
|
+
const mcpId = this.mcpId;
|
|
36364
36657
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
36365
36658
|
if (el.role === "host") {
|
|
36366
36659
|
this.role = "host";
|
|
36367
36660
|
this.hostHandle = await startHost({
|
|
36368
36661
|
httpServer: el.server,
|
|
36369
|
-
ownIdentity:
|
|
36370
|
-
ownMcpId:
|
|
36662
|
+
ownIdentity: identity,
|
|
36663
|
+
ownMcpId: mcpId,
|
|
36371
36664
|
ownServerName: this.opts.serverName,
|
|
36372
36665
|
ownVersion: this.opts.version,
|
|
36373
36666
|
ownDomains: this.opts.domains,
|
|
@@ -36382,13 +36675,17 @@ var FetchproxyServer = class {
|
|
|
36382
36675
|
onPairCode: this.opts.onPairCode
|
|
36383
36676
|
});
|
|
36384
36677
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36678
|
+
this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
|
|
36679
|
+
this.hostHandle.onPendingPair((code) => {
|
|
36680
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36681
|
+
});
|
|
36385
36682
|
} else {
|
|
36386
36683
|
this.role = "peer";
|
|
36387
36684
|
this.peerHandle = await startPeer({
|
|
36388
36685
|
host: this.opts.host,
|
|
36389
36686
|
port: this.opts.port,
|
|
36390
|
-
identity
|
|
36391
|
-
mcpId
|
|
36687
|
+
identity,
|
|
36688
|
+
mcpId,
|
|
36392
36689
|
serverName: this.opts.serverName,
|
|
36393
36690
|
version: this.opts.version,
|
|
36394
36691
|
domains: this.opts.domains,
|
|
@@ -36402,8 +36699,19 @@ var FetchproxyServer = class {
|
|
|
36402
36699
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36403
36700
|
});
|
|
36404
36701
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36702
|
+
this.peerHandle.onRenegotiate(() => this.rejectAllPending());
|
|
36703
|
+
this.peerHandle.onPendingPair((code) => {
|
|
36704
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36705
|
+
});
|
|
36706
|
+
if (this.opts.onPairCode) {
|
|
36707
|
+
const cb = this.opts.onPairCode;
|
|
36708
|
+
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36709
|
+
}
|
|
36405
36710
|
}
|
|
36406
36711
|
}
|
|
36712
|
+
pairingErrorMessage(code) {
|
|
36713
|
+
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.`;
|
|
36714
|
+
}
|
|
36407
36715
|
/**
|
|
36408
36716
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
36409
36717
|
* the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
|
|
@@ -36419,9 +36727,75 @@ var FetchproxyServer = class {
|
|
|
36419
36727
|
* offline, etc.).
|
|
36420
36728
|
*/
|
|
36421
36729
|
async fetch(init) {
|
|
36422
|
-
|
|
36423
|
-
|
|
36730
|
+
await this.ensureConnected();
|
|
36731
|
+
const pendingCode = this.currentPendingPairCode();
|
|
36732
|
+
if (pendingCode !== null) {
|
|
36733
|
+
const error51 = this.pairingErrorMessage(pendingCode);
|
|
36734
|
+
return {
|
|
36735
|
+
ok: false,
|
|
36736
|
+
error: error51,
|
|
36737
|
+
kind: classifyFetchError(error51),
|
|
36738
|
+
retryAttempted: false
|
|
36739
|
+
};
|
|
36424
36740
|
}
|
|
36741
|
+
const first = await this._fetchOnceWithTimeout(init);
|
|
36742
|
+
const reviveMs = this.opts.bridgeReviveDelayMs;
|
|
36743
|
+
let final = first;
|
|
36744
|
+
if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
|
|
36745
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36746
|
+
const second = await this._fetchOnceWithTimeout(init);
|
|
36747
|
+
if (second.ok)
|
|
36748
|
+
this.recordSuccess();
|
|
36749
|
+
else
|
|
36750
|
+
this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
|
|
36751
|
+
return { ...second, retryAttempted: true };
|
|
36752
|
+
}
|
|
36753
|
+
if (first.ok)
|
|
36754
|
+
this.recordSuccess();
|
|
36755
|
+
else
|
|
36756
|
+
this.recordFailure(`${first.kind ?? "other"}: ${first.error}`);
|
|
36757
|
+
return { ...first, retryAttempted: false };
|
|
36758
|
+
}
|
|
36759
|
+
/**
|
|
36760
|
+
* 0.8.0+: snapshot of the bridge's process-wide freshness counters,
|
|
36761
|
+
* suitable for surfacing through a downstream MCP's healthcheck tool.
|
|
36762
|
+
* Counters reset on a success (consecutiveFailures), accumulate
|
|
36763
|
+
* across the process lifetime otherwise. Replaces the per-MCP
|
|
36764
|
+
* duplication the realty cohort had been rolling in their adapters.
|
|
36765
|
+
* `lastExtensionMessageAt` is updated whenever ANY inner frame
|
|
36766
|
+
* arrives from the extension — gives extension-side liveness
|
|
36767
|
+
* distinct from server-side success/failure of the user-visible
|
|
36768
|
+
* call (addresses #23 ask 4).
|
|
36769
|
+
*/
|
|
36770
|
+
bridgeHealth() {
|
|
36771
|
+
return {
|
|
36772
|
+
role: this.role,
|
|
36773
|
+
port: this.opts.port,
|
|
36774
|
+
serverVersion: this.opts.version,
|
|
36775
|
+
fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36776
|
+
bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
|
|
36777
|
+
lastSuccessAt: this.lastSuccessAt,
|
|
36778
|
+
lastFailureAt: this.lastFailureAt,
|
|
36779
|
+
lastFailureReason: this.lastFailureReason,
|
|
36780
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
36781
|
+
lastExtensionMessageAt: this.lastExtensionMessageAt
|
|
36782
|
+
};
|
|
36783
|
+
}
|
|
36784
|
+
recordSuccess() {
|
|
36785
|
+
this.lastSuccessAt = Date.now();
|
|
36786
|
+
this.consecutiveFailures = 0;
|
|
36787
|
+
}
|
|
36788
|
+
recordFailure(reason) {
|
|
36789
|
+
this.lastFailureAt = Date.now();
|
|
36790
|
+
this.lastFailureReason = reason;
|
|
36791
|
+
this.consecutiveFailures += 1;
|
|
36792
|
+
}
|
|
36793
|
+
/**
|
|
36794
|
+
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
36795
|
+
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
36796
|
+
* the throwing surface is the convenience methods.
|
|
36797
|
+
*/
|
|
36798
|
+
async _fetchOnceWithTimeout(init) {
|
|
36425
36799
|
const id = this.nextRequestId++;
|
|
36426
36800
|
const inner = { type: "request", id, op: "fetch", init };
|
|
36427
36801
|
const pending = new Promise((resolve) => {
|
|
@@ -36432,7 +36806,61 @@ var FetchproxyServer = class {
|
|
|
36432
36806
|
} else if (this.peerHandle) {
|
|
36433
36807
|
await this.peerHandle.sendInner(inner);
|
|
36434
36808
|
}
|
|
36435
|
-
|
|
36809
|
+
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
36810
|
+
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
36811
|
+
return pending;
|
|
36812
|
+
let timer;
|
|
36813
|
+
const start = Date.now();
|
|
36814
|
+
try {
|
|
36815
|
+
return await Promise.race([
|
|
36816
|
+
pending,
|
|
36817
|
+
new Promise((resolve) => {
|
|
36818
|
+
timer = setTimeout(() => {
|
|
36819
|
+
this.pending.delete(id);
|
|
36820
|
+
const elapsedMs = Date.now() - start;
|
|
36821
|
+
const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
36822
|
+
resolve({
|
|
36823
|
+
ok: false,
|
|
36824
|
+
error: error51,
|
|
36825
|
+
kind: "timeout",
|
|
36826
|
+
retryAttempted: false,
|
|
36827
|
+
elapsedMs
|
|
36828
|
+
});
|
|
36829
|
+
}, timeoutMs);
|
|
36830
|
+
})
|
|
36831
|
+
]);
|
|
36832
|
+
} finally {
|
|
36833
|
+
if (timer)
|
|
36834
|
+
clearTimeout(timer);
|
|
36835
|
+
}
|
|
36836
|
+
}
|
|
36837
|
+
/**
|
|
36838
|
+
* Map an `ok:false` fetch result to its typed throwable. Centralizes
|
|
36839
|
+
* the kind-to-error-class switch so `request()` and (via the same
|
|
36840
|
+
* logic re-implemented inline) `captureRequestHeader()` agree on what
|
|
36841
|
+
* to throw.
|
|
36842
|
+
*/
|
|
36843
|
+
_typedErrorFor(result, url2, op, retryAttempted) {
|
|
36844
|
+
if (result.kind === "timeout") {
|
|
36845
|
+
return new FetchproxyTimeoutError({
|
|
36846
|
+
url: url2,
|
|
36847
|
+
timeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36848
|
+
role: this.role,
|
|
36849
|
+
port: this.opts.port,
|
|
36850
|
+
elapsedMs: result.elapsedMs
|
|
36851
|
+
});
|
|
36852
|
+
}
|
|
36853
|
+
if (result.kind === "content_script_unreachable") {
|
|
36854
|
+
return new FetchproxyBridgeDownError({
|
|
36855
|
+
originalError: result.error,
|
|
36856
|
+
retryAttempted,
|
|
36857
|
+
op,
|
|
36858
|
+
url: url2,
|
|
36859
|
+
role: this.role,
|
|
36860
|
+
port: this.opts.port
|
|
36861
|
+
});
|
|
36862
|
+
}
|
|
36863
|
+
return new FetchproxyProtocolError(result.error);
|
|
36436
36864
|
}
|
|
36437
36865
|
/**
|
|
36438
36866
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -36455,8 +36883,18 @@ var FetchproxyServer = class {
|
|
|
36455
36883
|
if (opts.subdomain !== void 0)
|
|
36456
36884
|
assertSubdomainLabel(opts.subdomain);
|
|
36457
36885
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
36458
|
-
const
|
|
36459
|
-
|
|
36886
|
+
const isAbsolute = path.startsWith("http://") || path.startsWith("https://");
|
|
36887
|
+
let host;
|
|
36888
|
+
if (isAbsolute) {
|
|
36889
|
+
try {
|
|
36890
|
+
host = new URL(path).host;
|
|
36891
|
+
} catch {
|
|
36892
|
+
throw new Error(`FetchproxyServer.request: absolute path is not a valid URL: ${JSON.stringify(path)}`);
|
|
36893
|
+
}
|
|
36894
|
+
} else {
|
|
36895
|
+
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
36896
|
+
}
|
|
36897
|
+
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
36460
36898
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
36461
36899
|
const init = {
|
|
36462
36900
|
url: url2,
|
|
@@ -36467,7 +36905,7 @@ var FetchproxyServer = class {
|
|
|
36467
36905
|
};
|
|
36468
36906
|
const result = await this.fetch(init);
|
|
36469
36907
|
if (!result.ok) {
|
|
36470
|
-
throw
|
|
36908
|
+
throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false);
|
|
36471
36909
|
}
|
|
36472
36910
|
const response = {
|
|
36473
36911
|
status: result.status,
|
|
@@ -36561,9 +36999,8 @@ var FetchproxyServer = class {
|
|
|
36561
36999
|
if (!this.opts.capabilities.includes("read_cookies")) {
|
|
36562
37000
|
throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
36563
37001
|
}
|
|
36564
|
-
|
|
36565
|
-
|
|
36566
|
-
}
|
|
37002
|
+
await this.ensureConnected();
|
|
37003
|
+
this.throwIfPendingPair();
|
|
36567
37004
|
if (opts.subdomain !== void 0)
|
|
36568
37005
|
assertSubdomainLabel(opts.subdomain);
|
|
36569
37006
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
@@ -36620,9 +37057,8 @@ var FetchproxyServer = class {
|
|
|
36620
37057
|
if (!this.opts.capabilities.includes(op)) {
|
|
36621
37058
|
throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
|
|
36622
37059
|
}
|
|
36623
|
-
|
|
36624
|
-
|
|
36625
|
-
}
|
|
37060
|
+
await this.ensureConnected();
|
|
37061
|
+
this.throwIfPendingPair();
|
|
36626
37062
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36627
37063
|
throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
|
|
36628
37064
|
}
|
|
@@ -36677,13 +37113,75 @@ var FetchproxyServer = class {
|
|
|
36677
37113
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
36678
37114
|
throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
|
|
36679
37115
|
}
|
|
36680
|
-
|
|
36681
|
-
|
|
37116
|
+
await this.ensureConnected();
|
|
37117
|
+
this.throwIfPendingPair();
|
|
37118
|
+
const decls = this.opts.captureHeaders;
|
|
37119
|
+
let resolved;
|
|
37120
|
+
if (opts?.urlPattern !== void 0 && opts?.headerName !== void 0) {
|
|
37121
|
+
const found = decls.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
37122
|
+
if (!found) {
|
|
37123
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
37124
|
+
}
|
|
37125
|
+
resolved = found;
|
|
37126
|
+
} else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
|
|
37127
|
+
if (decls.length === 0) {
|
|
37128
|
+
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");
|
|
37129
|
+
}
|
|
37130
|
+
if (decls.length > 1) {
|
|
37131
|
+
const list = decls.map((d) => `${JSON.stringify(d.urlPattern)}/${JSON.stringify(d.headerName)}`).join(", ");
|
|
37132
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {urlPattern, headerName} to disambiguate`);
|
|
37133
|
+
}
|
|
37134
|
+
resolved = decls[0];
|
|
37135
|
+
} else {
|
|
37136
|
+
throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
|
|
36682
37137
|
}
|
|
36683
|
-
const
|
|
36684
|
-
|
|
36685
|
-
|
|
37138
|
+
const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
|
|
37139
|
+
try {
|
|
37140
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
37141
|
+
this.recordSuccess();
|
|
37142
|
+
return result;
|
|
37143
|
+
} catch (err) {
|
|
37144
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
37145
|
+
if (!swDown) {
|
|
37146
|
+
this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
|
|
37147
|
+
throw err;
|
|
37148
|
+
}
|
|
37149
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
37150
|
+
if (reviveMs > 0) {
|
|
37151
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
37152
|
+
try {
|
|
37153
|
+
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
37154
|
+
this.recordSuccess();
|
|
37155
|
+
return result;
|
|
37156
|
+
} catch (retryErr) {
|
|
37157
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
37158
|
+
if (!stillDown) {
|
|
37159
|
+
this.recordFailure(`capture_request_header: ${retryErr.message ?? String(retryErr)}`);
|
|
37160
|
+
throw retryErr;
|
|
37161
|
+
}
|
|
37162
|
+
this.recordFailure(`capture_request_header bridge-down: ${retryErr.message}`);
|
|
37163
|
+
throw new FetchproxyBridgeDownError({
|
|
37164
|
+
originalError: retryErr.message,
|
|
37165
|
+
retryAttempted: true,
|
|
37166
|
+
op: "capture_request_header",
|
|
37167
|
+
url: resolved.urlPattern,
|
|
37168
|
+
role: this.role,
|
|
37169
|
+
port: this.opts.port
|
|
37170
|
+
});
|
|
37171
|
+
}
|
|
37172
|
+
}
|
|
37173
|
+
this.recordFailure(`capture_request_header bridge-down: ${err.message}`);
|
|
37174
|
+
throw new FetchproxyBridgeDownError({
|
|
37175
|
+
originalError: err.message,
|
|
37176
|
+
retryAttempted: false,
|
|
37177
|
+
op: "capture_request_header",
|
|
37178
|
+
url: resolved.urlPattern,
|
|
37179
|
+
role: this.role,
|
|
37180
|
+
port: this.opts.port
|
|
37181
|
+
});
|
|
36686
37182
|
}
|
|
37183
|
+
}
|
|
37184
|
+
async _captureRequestHeaderOnce(opts) {
|
|
36687
37185
|
const id = this.nextRequestId++;
|
|
36688
37186
|
const inner = {
|
|
36689
37187
|
type: "request",
|
|
@@ -36720,9 +37218,8 @@ var FetchproxyServer = class {
|
|
|
36720
37218
|
if (!this.opts.capabilities.includes("read_indexed_db")) {
|
|
36721
37219
|
throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
|
|
36722
37220
|
}
|
|
36723
|
-
|
|
36724
|
-
|
|
36725
|
-
}
|
|
37221
|
+
await this.ensureConnected();
|
|
37222
|
+
this.throwIfPendingPair();
|
|
36726
37223
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36727
37224
|
throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
|
|
36728
37225
|
}
|
|
@@ -36793,17 +37290,35 @@ var FetchproxyServer = class {
|
|
|
36793
37290
|
onInner(inner) {
|
|
36794
37291
|
if (inner.type !== "response")
|
|
36795
37292
|
return;
|
|
37293
|
+
this.lastExtensionMessageAt = Date.now();
|
|
36796
37294
|
const fetchCb = this.pending.get(inner.id);
|
|
36797
37295
|
if (fetchCb) {
|
|
36798
37296
|
this.pending.delete(inner.id);
|
|
36799
37297
|
if (inner.ok) {
|
|
36800
37298
|
if (inner.op === void 0 || inner.op === "fetch") {
|
|
36801
|
-
fetchCb({
|
|
37299
|
+
fetchCb({
|
|
37300
|
+
ok: true,
|
|
37301
|
+
status: inner.status,
|
|
37302
|
+
url: inner.url,
|
|
37303
|
+
body: inner.body,
|
|
37304
|
+
retryAttempted: false
|
|
37305
|
+
});
|
|
36802
37306
|
} else {
|
|
36803
|
-
|
|
37307
|
+
const error51 = `unexpected ${inner.op} response on fetch awaiter`;
|
|
37308
|
+
fetchCb({
|
|
37309
|
+
ok: false,
|
|
37310
|
+
error: error51,
|
|
37311
|
+
kind: classifyFetchError(error51),
|
|
37312
|
+
retryAttempted: false
|
|
37313
|
+
});
|
|
36804
37314
|
}
|
|
36805
37315
|
} else {
|
|
36806
|
-
fetchCb({
|
|
37316
|
+
fetchCb({
|
|
37317
|
+
ok: false,
|
|
37318
|
+
error: inner.error,
|
|
37319
|
+
kind: classifyFetchError(inner.error),
|
|
37320
|
+
retryAttempted: false
|
|
37321
|
+
});
|
|
36807
37322
|
}
|
|
36808
37323
|
return;
|
|
36809
37324
|
}
|
|
@@ -36870,6 +37385,57 @@ var FetchproxyServer = class {
|
|
|
36870
37385
|
}
|
|
36871
37386
|
}
|
|
36872
37387
|
}
|
|
37388
|
+
rejectAllPending(reason = "extension disconnected") {
|
|
37389
|
+
const err = new FetchproxyProtocolError(reason);
|
|
37390
|
+
for (const cb of this.pending.values()) {
|
|
37391
|
+
cb({
|
|
37392
|
+
ok: false,
|
|
37393
|
+
error: err.message,
|
|
37394
|
+
kind: classifyFetchError(err.message),
|
|
37395
|
+
retryAttempted: false
|
|
37396
|
+
});
|
|
37397
|
+
}
|
|
37398
|
+
this.pending.clear();
|
|
37399
|
+
for (const cb of this.pendingReadCookies.values()) {
|
|
37400
|
+
cb({ ok: false, error: err.message });
|
|
37401
|
+
}
|
|
37402
|
+
this.pendingReadCookies.clear();
|
|
37403
|
+
for (const { reject } of this.pendingStorage.values())
|
|
37404
|
+
reject(err);
|
|
37405
|
+
this.pendingStorage.clear();
|
|
37406
|
+
for (const { reject } of this.pendingCapture.values())
|
|
37407
|
+
reject(err);
|
|
37408
|
+
this.pendingCapture.clear();
|
|
37409
|
+
for (const { reject } of this.pendingIdb.values())
|
|
37410
|
+
reject(err);
|
|
37411
|
+
this.pendingIdb.clear();
|
|
37412
|
+
}
|
|
37413
|
+
/**
|
|
37414
|
+
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
37415
|
+
* is active, returning null when none is pending. Public verbs call this
|
|
37416
|
+
* at the top so that a tool invoked while the bridge is waiting on user
|
|
37417
|
+
* approval fails fast with the actionable error rather than hanging on a
|
|
37418
|
+
* sealed frame the extension will never process.
|
|
37419
|
+
*/
|
|
37420
|
+
currentPendingPairCode() {
|
|
37421
|
+
if (this.hostHandle)
|
|
37422
|
+
return this.hostHandle.pendingPairCode();
|
|
37423
|
+
if (this.peerHandle)
|
|
37424
|
+
return this.peerHandle.pendingPairCode();
|
|
37425
|
+
return null;
|
|
37426
|
+
}
|
|
37427
|
+
/**
|
|
37428
|
+
* 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
|
|
37429
|
+
* message if the bridge is waiting on user approval. Used by the verb
|
|
37430
|
+
* methods (readCookies, readLocalStorage, etc.) that surface errors via
|
|
37431
|
+
* thrown exceptions rather than `ok:false` discriminated unions.
|
|
37432
|
+
*/
|
|
37433
|
+
throwIfPendingPair() {
|
|
37434
|
+
const code = this.currentPendingPairCode();
|
|
37435
|
+
if (code !== null) {
|
|
37436
|
+
throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
|
|
37437
|
+
}
|
|
37438
|
+
}
|
|
36873
37439
|
/**
|
|
36874
37440
|
* Shut down the bridge. Host: terminates the WebSocket server and any
|
|
36875
37441
|
* still-attached extension/peer clients. Peer: closes the upstream
|
|
@@ -36877,6 +37443,10 @@ var FetchproxyServer = class {
|
|
|
36877
37443
|
* twice in a row.
|
|
36878
37444
|
*/
|
|
36879
37445
|
async close() {
|
|
37446
|
+
this.rejectAllPending();
|
|
37447
|
+
if (this.connectingPromise) {
|
|
37448
|
+
await this.connectingPromise.catch(() => void 0);
|
|
37449
|
+
}
|
|
36880
37450
|
if (this.hostHandle)
|
|
36881
37451
|
await this.hostHandle.close();
|
|
36882
37452
|
if (this.peerHandle)
|
|
@@ -36884,9 +37454,23 @@ var FetchproxyServer = class {
|
|
|
36884
37454
|
this.hostHandle = null;
|
|
36885
37455
|
this.peerHandle = null;
|
|
36886
37456
|
this.role = null;
|
|
37457
|
+
this.connectingPromise = null;
|
|
36887
37458
|
}
|
|
36888
37459
|
};
|
|
36889
37460
|
|
|
37461
|
+
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
37462
|
+
function classifyBridgeError(err) {
|
|
37463
|
+
if (err instanceof FetchproxyTimeoutError)
|
|
37464
|
+
return "timeout";
|
|
37465
|
+
if (err instanceof FetchproxyBridgeDownError)
|
|
37466
|
+
return "bridge_down";
|
|
37467
|
+
if (err instanceof FetchproxyHttpError)
|
|
37468
|
+
return "http";
|
|
37469
|
+
if (err instanceof FetchproxyProtocolError)
|
|
37470
|
+
return "protocol";
|
|
37471
|
+
return "other";
|
|
37472
|
+
}
|
|
37473
|
+
|
|
36890
37474
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
36891
37475
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
36892
37476
|
async function bootstrap(opts) {
|
|
@@ -36941,7 +37525,11 @@ async function bootstrap(opts) {
|
|
|
36941
37525
|
key: p.storageKey,
|
|
36942
37526
|
jsonPointer: p.jsonPointer
|
|
36943
37527
|
})),
|
|
36944
|
-
onPairCode: opts.onPairCode
|
|
37528
|
+
onPairCode: opts.onPairCode,
|
|
37529
|
+
// 0.8.0+ pass-through. Only forwarded when the caller set them;
|
|
37530
|
+
// unset → server defaults apply (30000 / 2000 in 0.8.0).
|
|
37531
|
+
...opts.fetchTimeoutMs !== void 0 ? { fetchTimeoutMs: opts.fetchTimeoutMs } : {},
|
|
37532
|
+
...opts.bridgeReviveDelayMs !== void 0 ? { bridgeReviveDelayMs: opts.bridgeReviveDelayMs } : {}
|
|
36945
37533
|
});
|
|
36946
37534
|
const storageDomainOpts = {};
|
|
36947
37535
|
if (opts.storageDomain !== void 0)
|
|
@@ -36972,8 +37560,7 @@ async function bootstrap(opts) {
|
|
|
36972
37560
|
for (const p of localStoragePointers) {
|
|
36973
37561
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
36974
37562
|
}
|
|
36975
|
-
|
|
36976
|
-
localStorage = await stub.readLocalStorage({
|
|
37563
|
+
localStorage = await server.readLocalStorage({
|
|
36977
37564
|
keys: allKeys,
|
|
36978
37565
|
...storageDomainOpts,
|
|
36979
37566
|
...localStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -36986,8 +37573,7 @@ async function bootstrap(opts) {
|
|
|
36986
37573
|
for (const p of sessionStoragePointers) {
|
|
36987
37574
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
36988
37575
|
}
|
|
36989
|
-
|
|
36990
|
-
sessionStorage = await stub.readSessionStorage({
|
|
37576
|
+
sessionStorage = await server.readSessionStorage({
|
|
36991
37577
|
keys: allKeys,
|
|
36992
37578
|
...storageDomainOpts,
|
|
36993
37579
|
...sessionStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37010,9 +37596,6 @@ async function bootstrap(opts) {
|
|
|
37010
37596
|
}
|
|
37011
37597
|
const indexedDbBucket = {};
|
|
37012
37598
|
for (const d of indexedDb) {
|
|
37013
|
-
if (!server.readIndexedDb) {
|
|
37014
|
-
throw new Error("bootstrap: server factory does not implement readIndexedDb (declared indexedDb but server stub omits it)");
|
|
37015
|
-
}
|
|
37016
37599
|
const values = await server.readIndexedDb({
|
|
37017
37600
|
database: d.database,
|
|
37018
37601
|
store: d.store,
|
|
@@ -37049,7 +37632,7 @@ var BootstrapDisabledError = class extends Error {
|
|
|
37049
37632
|
// package.json
|
|
37050
37633
|
var package_default = {
|
|
37051
37634
|
name: "creditkarma-mcp",
|
|
37052
|
-
version: "2.0
|
|
37635
|
+
version: "2.2.0",
|
|
37053
37636
|
mcpName: "io.github.chrischall/creditkarma-mcp",
|
|
37054
37637
|
description: "MCP server for Credit Karma \u2014 natural-language access to your transactions, spending, and accounts",
|
|
37055
37638
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37093,17 +37676,18 @@ var package_default = {
|
|
|
37093
37676
|
"test:coverage": "vitest run --coverage"
|
|
37094
37677
|
},
|
|
37095
37678
|
dependencies: {
|
|
37096
|
-
"@fetchproxy/bootstrap": "^0.
|
|
37679
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
37680
|
+
"@fetchproxy/server": "^0.8.0",
|
|
37097
37681
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37098
|
-
dotenv: "^17.4.
|
|
37099
|
-
zod: "^4.3
|
|
37682
|
+
dotenv: "^17.4.2",
|
|
37683
|
+
zod: "^4.4.3"
|
|
37100
37684
|
},
|
|
37101
37685
|
devDependencies: {
|
|
37102
|
-
"@types/node": "^25.
|
|
37103
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37686
|
+
"@types/node": "^25.9.1",
|
|
37687
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
37104
37688
|
esbuild: "^0.28.0",
|
|
37105
|
-
typescript: "^6.0.
|
|
37106
|
-
vitest: "^4.1.
|
|
37689
|
+
typescript: "^6.0.3",
|
|
37690
|
+
vitest: "^4.1.7"
|
|
37107
37691
|
}
|
|
37108
37692
|
};
|
|
37109
37693
|
|
|
@@ -37163,6 +37747,12 @@ async function resolveAuth() {
|
|
|
37163
37747
|
const cookies = `CKTRKID=${cktrkid}; CKAT=${ckat}`;
|
|
37164
37748
|
return { cookies, source: "fetchproxy" };
|
|
37165
37749
|
} catch (e) {
|
|
37750
|
+
if (classifyBridgeError(e) === "bridge_down") {
|
|
37751
|
+
const downErr = e;
|
|
37752
|
+
throw new Error(
|
|
37753
|
+
`CK auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
37754
|
+
);
|
|
37755
|
+
}
|
|
37166
37756
|
const msg = e instanceof Error ? e.message : String(e);
|
|
37167
37757
|
throw new Error(
|
|
37168
37758
|
`CK auth: no CK_COOKIES set, and fetchproxy fallback failed: ${msg}`
|
|
@@ -37623,7 +38213,8 @@ async function main() {
|
|
|
37623
38213
|
mcpJsonPath
|
|
37624
38214
|
};
|
|
37625
38215
|
const server = new McpServer(
|
|
37626
|
-
{ name: "creditkarma-mcp", version: "2.0
|
|
38216
|
+
{ name: "creditkarma-mcp", version: "2.2.0" }
|
|
38217
|
+
// x-release-please-version
|
|
37627
38218
|
);
|
|
37628
38219
|
registerAuthTools(server, ctx);
|
|
37629
38220
|
registerSyncTools(server, ctx);
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,8 @@ async function main() {
|
|
|
63
63
|
db,
|
|
64
64
|
mcpJsonPath
|
|
65
65
|
};
|
|
66
|
-
const server = new McpServer({ name: 'creditkarma-mcp', version: '2.0
|
|
66
|
+
const server = new McpServer({ name: 'creditkarma-mcp', version: '2.2.0' } // x-release-please-version
|
|
67
|
+
);
|
|
67
68
|
registerAuthTools(server, ctx);
|
|
68
69
|
registerSyncTools(server, ctx);
|
|
69
70
|
registerQueryTools(server, ctx);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "creditkarma-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/creditkarma-mcp",
|
|
5
|
-
"description": "MCP server for Credit Karma
|
|
5
|
+
"description": "MCP server for Credit Karma — natural-language access to your transactions, spending, and accounts",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -44,16 +44,17 @@
|
|
|
44
44
|
"test:coverage": "vitest run --coverage"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@fetchproxy/bootstrap": "^0.
|
|
47
|
+
"@fetchproxy/bootstrap": "^0.8.0",
|
|
48
|
+
"@fetchproxy/server": "^0.8.0",
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"dotenv": "^17.4.
|
|
50
|
-
"zod": "^4.3
|
|
50
|
+
"dotenv": "^17.4.2",
|
|
51
|
+
"zod": "^4.4.3"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@types/node": "^25.
|
|
54
|
-
"@vitest/coverage-v8": "^4.1.
|
|
54
|
+
"@types/node": "^25.9.1",
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
55
56
|
"esbuild": "^0.28.0",
|
|
56
|
-
"typescript": "^6.0.
|
|
57
|
-
"vitest": "^4.1.
|
|
57
|
+
"typescript": "^6.0.3",
|
|
58
|
+
"vitest": "^4.1.7"
|
|
58
59
|
}
|
|
59
60
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/creditkarma-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.0
|
|
9
|
+
"version": "2.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "creditkarma-mcp",
|
|
14
|
-
"version": "2.0
|
|
14
|
+
"version": "2.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|