creditkarma-mcp 2.0.10 → 2.1.4

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 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/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,255}\*?$/;
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" || !SCOPE_KEY_GLOB_RE.test(entry.key)) {
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
- const ownSessionReady = new Promise((resolve, reject) => {
35966
- resolveOwnSession = resolve;
35967
- rejectOwnSession = reject;
35968
- });
35969
- ownSessionReady.catch(() => {
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("fetchproxy/0.1.0/session"), 32);
36040
- if (!ownSession) {
36041
- ownSession = new SessionState(key);
36042
- resolveOwnSession(ownSession);
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
- const onMessage = async (data) => {
36142
- try {
36143
- const raw = JSON.parse(data.toString());
36144
- const frame = validateFrame(raw);
36145
- if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
36146
- const extPub = fromB64(frame.extensionSessionPub);
36147
- const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
36148
- const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode("fetchproxy/0.1.0/session"), 32);
36149
- session = new SessionState(sessionKey);
36150
- resolve(session);
36151
- return;
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
- if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
36154
- if (!session)
36155
- return;
36156
- if (!session.acceptInboundSeq(frame.seq))
36157
- return;
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
- ws.on("message", onMessage);
36166
- ws.once("close", () => {
36167
- reject(new Error("peer WS closed before ready"));
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
- const s = await sessionPromise;
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) {
@@ -36278,7 +36433,11 @@ function assertUrlInDomains(field, url2, domains) {
36278
36433
  }
36279
36434
  var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
36280
36435
  var FetchproxyServer = class {
36281
- /** Set after `listen()` succeeds. Null while not listening. */
36436
+ /**
36437
+ * Bridge role. `null` until the first verb call (or an explicit
36438
+ * `connect()`) — `listen()` no longer triggers the role election
36439
+ * as of 0.5.3+. Reset to `null` on `close()`.
36440
+ */
36282
36441
  role = null;
36283
36442
  opts;
36284
36443
  hostHandle = null;
@@ -36300,6 +36459,12 @@ var FetchproxyServer = class {
36300
36459
  pendingIdb = /* @__PURE__ */ new Map();
36301
36460
  mcpId = null;
36302
36461
  identity = null;
36462
+ // 0.5.3+: in-flight role-election / handle-start promise. Set the
36463
+ // first time a verb call runs `ensureConnected`, awaited by concurrent
36464
+ // callers, cleared once the connection is up. Single source of truth
36465
+ // for "we're connecting right now" so two parallel first-calls don't
36466
+ // race the port bind.
36467
+ connectingPromise = null;
36303
36468
  constructor(opts) {
36304
36469
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36305
36470
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36351,23 +36516,87 @@ var FetchproxyServer = class {
36351
36516
  };
36352
36517
  }
36353
36518
  /**
36354
- * Start the WebSocket bridge. Loads the long-term identity keypair
36355
- * from disk (creating it on first call), elects the host-vs-peer
36356
- * role by attempting to bind the configured port, and stands up the
36357
- * matching handshake machinery. Idempotent only insofar as it leaves
36358
- * `role` non-null on success; calling `listen()` twice without an
36359
- * intervening `close()` is a programming error.
36519
+ * Prepare the bridge for use. Loads the long-term identity keypair
36520
+ * from disk (creating it on first call) and computes this instance's
36521
+ * `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
36522
+ * connection is established lazily on the first verb call (see
36523
+ * `ensureConnected` / `getOrConnect`).
36524
+ *
36525
+ * Pre-0.5.3 behavior: `listen()` also did role election and started
36526
+ * the host/peer immediately, which meant every configured-but-unused
36527
+ * MCP claimed bridge resources at MCP-client boot. Several MCPs
36528
+ * starting in parallel under Claude Desktop also produced noisy
36529
+ * `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
36530
+ * of the first MCP's port bind. Deferring keeps boot quiet and
36531
+ * leaves the port unowned until something actually needs it.
36532
+ *
36533
+ * Calling `listen()` twice without an intervening `close()` is a
36534
+ * no-op (the second call's identity load is idempotent).
36360
36535
  */
36361
36536
  async listen() {
36362
- this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
36363
- this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
36537
+ if (!this.identity) {
36538
+ this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
36539
+ }
36540
+ if (!this.mcpId) {
36541
+ this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
36542
+ }
36543
+ }
36544
+ /**
36545
+ * Force an eager bridge connection (role-election + host/peer handle
36546
+ * start + listener wiring) without waiting for the first verb call.
36547
+ * Useful for callers that want to surface the role / connection
36548
+ * outcome at boot, or for tests whose harness dials a mock extension
36549
+ * immediately after server construction. Production MCPs that just
36550
+ * answer tool calls should NOT call this — the lazy connect via
36551
+ * `ensureConnected` will do the right thing on first use, keeping
36552
+ * boot cheap and avoiding port-bind contention for MCPs that never
36553
+ * actually get invoked.
36554
+ *
36555
+ * Idempotent: a second call after the first has resolved is a no-op
36556
+ * (the existing handle is reused). Throws if `listen()` was never
36557
+ * called.
36558
+ */
36559
+ async connect() {
36560
+ await this.ensureConnected();
36561
+ }
36562
+ /**
36563
+ * Establish the bridge connection (role-election + host/peer handle
36564
+ * start + listener wiring) the first time a verb is invoked.
36565
+ * Idempotent after the connection is up; concurrent first-callers
36566
+ * share the same in-flight promise so only one election happens.
36567
+ *
36568
+ * Throws if `listen()` was never called — the contract is that the
36569
+ * MCP author still must wire `transport.start()` at boot to load
36570
+ * identity / set mcpId, even though the WS doesn't open until a
36571
+ * verb runs.
36572
+ */
36573
+ async ensureConnected() {
36574
+ if (this.hostHandle || this.peerHandle)
36575
+ return;
36576
+ if (this.connectingPromise) {
36577
+ await this.connectingPromise;
36578
+ return;
36579
+ }
36580
+ if (!this.identity || !this.mcpId) {
36581
+ throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
36582
+ }
36583
+ this.connectingPromise = this.doConnect();
36584
+ try {
36585
+ await this.connectingPromise;
36586
+ } finally {
36587
+ this.connectingPromise = null;
36588
+ }
36589
+ }
36590
+ async doConnect() {
36591
+ const identity = this.identity;
36592
+ const mcpId = this.mcpId;
36364
36593
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
36365
36594
  if (el.role === "host") {
36366
36595
  this.role = "host";
36367
36596
  this.hostHandle = await startHost({
36368
36597
  httpServer: el.server,
36369
- ownIdentity: this.identity,
36370
- ownMcpId: this.mcpId,
36598
+ ownIdentity: identity,
36599
+ ownMcpId: mcpId,
36371
36600
  ownServerName: this.opts.serverName,
36372
36601
  ownVersion: this.opts.version,
36373
36602
  ownDomains: this.opts.domains,
@@ -36382,13 +36611,17 @@ var FetchproxyServer = class {
36382
36611
  onPairCode: this.opts.onPairCode
36383
36612
  });
36384
36613
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36614
+ this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36615
+ this.hostHandle.onPendingPair((code) => {
36616
+ this.rejectAllPending(this.pairingErrorMessage(code));
36617
+ });
36385
36618
  } else {
36386
36619
  this.role = "peer";
36387
36620
  this.peerHandle = await startPeer({
36388
36621
  host: this.opts.host,
36389
36622
  port: this.opts.port,
36390
- identity: this.identity,
36391
- mcpId: this.mcpId,
36623
+ identity,
36624
+ mcpId,
36392
36625
  serverName: this.opts.serverName,
36393
36626
  version: this.opts.version,
36394
36627
  domains: this.opts.domains,
@@ -36402,8 +36635,19 @@ var FetchproxyServer = class {
36402
36635
  sessionStoragePointers: this.opts.sessionStoragePointers
36403
36636
  });
36404
36637
  this.peerHandle.onInner((inner) => this.onInner(inner));
36638
+ this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36639
+ this.peerHandle.onPendingPair((code) => {
36640
+ this.rejectAllPending(this.pairingErrorMessage(code));
36641
+ });
36642
+ if (this.opts.onPairCode) {
36643
+ const cb = this.opts.onPairCode;
36644
+ this.peerHandle.onPendingPair((code) => cb(code));
36645
+ }
36405
36646
  }
36406
36647
  }
36648
+ pairingErrorMessage(code) {
36649
+ return `fetchproxy: pairing required for ${this.opts.serverName} \u2014 open the fetchproxy extension popup in Chrome and approve the pair request. Verify the pair code matches: ${code}`;
36650
+ }
36407
36651
  /**
36408
36652
  * Raw single-shot fetch through the bridge. Most callers should prefer
36409
36653
  * the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
@@ -36419,8 +36663,11 @@ var FetchproxyServer = class {
36419
36663
  * offline, etc.).
36420
36664
  */
36421
36665
  async fetch(init) {
36422
- if (!this.hostHandle && !this.peerHandle) {
36423
- throw new Error("FetchproxyServer.fetch called before listen() \u2014 not listening");
36666
+ await this.ensureConnected();
36667
+ const pendingCode = this.currentPendingPairCode();
36668
+ if (pendingCode !== null) {
36669
+ const error51 = this.pairingErrorMessage(pendingCode);
36670
+ return { ok: false, error: error51, kind: classifyFetchError(error51) };
36424
36671
  }
36425
36672
  const id = this.nextRequestId++;
36426
36673
  const inner = { type: "request", id, op: "fetch", init };
@@ -36561,9 +36808,8 @@ var FetchproxyServer = class {
36561
36808
  if (!this.opts.capabilities.includes("read_cookies")) {
36562
36809
  throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
36563
36810
  }
36564
- if (!this.hostHandle && !this.peerHandle) {
36565
- throw new Error("FetchproxyServer.readCookies called before listen() \u2014 not listening");
36566
- }
36811
+ await this.ensureConnected();
36812
+ this.throwIfPendingPair();
36567
36813
  if (opts.subdomain !== void 0)
36568
36814
  assertSubdomainLabel(opts.subdomain);
36569
36815
  const baseDomain = this.resolveBaseDomain(opts.domain);
@@ -36620,9 +36866,8 @@ var FetchproxyServer = class {
36620
36866
  if (!this.opts.capabilities.includes(op)) {
36621
36867
  throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
36622
36868
  }
36623
- if (!this.hostHandle && !this.peerHandle) {
36624
- throw new Error(`FetchproxyServer.${op} called before listen() \u2014 not listening`);
36625
- }
36869
+ await this.ensureConnected();
36870
+ this.throwIfPendingPair();
36626
36871
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
36627
36872
  throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
36628
36873
  }
@@ -36677,9 +36922,8 @@ var FetchproxyServer = class {
36677
36922
  if (!this.opts.capabilities.includes("capture_request_header")) {
36678
36923
  throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
36679
36924
  }
36680
- if (!this.hostHandle && !this.peerHandle) {
36681
- throw new Error("FetchproxyServer.captureRequestHeader called before listen() \u2014 not listening");
36682
- }
36925
+ await this.ensureConnected();
36926
+ this.throwIfPendingPair();
36683
36927
  const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
36684
36928
  if (!declared) {
36685
36929
  throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
@@ -36720,9 +36964,8 @@ var FetchproxyServer = class {
36720
36964
  if (!this.opts.capabilities.includes("read_indexed_db")) {
36721
36965
  throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
36722
36966
  }
36723
- if (!this.hostHandle && !this.peerHandle) {
36724
- throw new Error("FetchproxyServer.readIndexedDb called before listen() \u2014 not listening");
36725
- }
36967
+ await this.ensureConnected();
36968
+ this.throwIfPendingPair();
36726
36969
  if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
36727
36970
  throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
36728
36971
  }
@@ -36800,10 +37043,11 @@ var FetchproxyServer = class {
36800
37043
  if (inner.op === void 0 || inner.op === "fetch") {
36801
37044
  fetchCb({ ok: true, status: inner.status, url: inner.url, body: inner.body });
36802
37045
  } else {
36803
- fetchCb({ ok: false, error: `unexpected ${inner.op} response on fetch awaiter` });
37046
+ const error51 = `unexpected ${inner.op} response on fetch awaiter`;
37047
+ fetchCb({ ok: false, error: error51, kind: classifyFetchError(error51) });
36804
37048
  }
36805
37049
  } else {
36806
- fetchCb({ ok: false, error: inner.error });
37050
+ fetchCb({ ok: false, error: inner.error, kind: classifyFetchError(inner.error) });
36807
37051
  }
36808
37052
  return;
36809
37053
  }
@@ -36870,6 +37114,52 @@ var FetchproxyServer = class {
36870
37114
  }
36871
37115
  }
36872
37116
  }
37117
+ rejectAllPending(reason = "extension disconnected") {
37118
+ const err = new FetchproxyProtocolError(reason);
37119
+ for (const cb of this.pending.values()) {
37120
+ cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
37121
+ }
37122
+ this.pending.clear();
37123
+ for (const cb of this.pendingReadCookies.values()) {
37124
+ cb({ ok: false, error: err.message });
37125
+ }
37126
+ this.pendingReadCookies.clear();
37127
+ for (const { reject } of this.pendingStorage.values())
37128
+ reject(err);
37129
+ this.pendingStorage.clear();
37130
+ for (const { reject } of this.pendingCapture.values())
37131
+ reject(err);
37132
+ this.pendingCapture.clear();
37133
+ for (const { reject } of this.pendingIdb.values())
37134
+ reject(err);
37135
+ this.pendingIdb.clear();
37136
+ }
37137
+ /**
37138
+ * 0.5.2+: read the current pair-pending pair code from whichever handle
37139
+ * is active, returning null when none is pending. Public verbs call this
37140
+ * at the top so that a tool invoked while the bridge is waiting on user
37141
+ * approval fails fast with the actionable error rather than hanging on a
37142
+ * sealed frame the extension will never process.
37143
+ */
37144
+ currentPendingPairCode() {
37145
+ if (this.hostHandle)
37146
+ return this.hostHandle.pendingPairCode();
37147
+ if (this.peerHandle)
37148
+ return this.peerHandle.pendingPairCode();
37149
+ return null;
37150
+ }
37151
+ /**
37152
+ * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
37153
+ * message if the bridge is waiting on user approval. Used by the verb
37154
+ * methods (readCookies, readLocalStorage, etc.) that surface errors via
37155
+ * thrown exceptions rather than `ok:false` discriminated unions.
37156
+ */
37157
+ throwIfPendingPair() {
37158
+ const code = this.currentPendingPairCode();
37159
+ if (code !== null) {
37160
+ throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
37161
+ }
37162
+ }
36873
37163
  /**
36874
37164
  * Shut down the bridge. Host: terminates the WebSocket server and any
36875
37165
  * still-attached extension/peer clients. Peer: closes the upstream
@@ -36877,6 +37167,10 @@ var FetchproxyServer = class {
36877
37167
  * twice in a row.
36878
37168
  */
36879
37169
  async close() {
37170
+ this.rejectAllPending();
37171
+ if (this.connectingPromise) {
37172
+ await this.connectingPromise.catch(() => void 0);
37173
+ }
36880
37174
  if (this.hostHandle)
36881
37175
  await this.hostHandle.close();
36882
37176
  if (this.peerHandle)
@@ -36884,6 +37178,7 @@ var FetchproxyServer = class {
36884
37178
  this.hostHandle = null;
36885
37179
  this.peerHandle = null;
36886
37180
  this.role = null;
37181
+ this.connectingPromise = null;
36887
37182
  }
36888
37183
  };
36889
37184
 
@@ -36972,8 +37267,7 @@ async function bootstrap(opts) {
36972
37267
  for (const p of localStoragePointers) {
36973
37268
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
36974
37269
  }
36975
- const stub = server;
36976
- localStorage = await stub.readLocalStorage({
37270
+ localStorage = await server.readLocalStorage({
36977
37271
  keys: allKeys,
36978
37272
  ...storageDomainOpts,
36979
37273
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -36986,8 +37280,7 @@ async function bootstrap(opts) {
36986
37280
  for (const p of sessionStoragePointers) {
36987
37281
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
36988
37282
  }
36989
- const stub = server;
36990
- sessionStorage = await stub.readSessionStorage({
37283
+ sessionStorage = await server.readSessionStorage({
36991
37284
  keys: allKeys,
36992
37285
  ...storageDomainOpts,
36993
37286
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -37010,9 +37303,6 @@ async function bootstrap(opts) {
37010
37303
  }
37011
37304
  const indexedDbBucket = {};
37012
37305
  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
37306
  const values = await server.readIndexedDb({
37017
37307
  database: d.database,
37018
37308
  store: d.store,
@@ -37049,7 +37339,7 @@ var BootstrapDisabledError = class extends Error {
37049
37339
  // package.json
37050
37340
  var package_default = {
37051
37341
  name: "creditkarma-mcp",
37052
- version: "2.0.10",
37342
+ version: "2.1.4",
37053
37343
  mcpName: "io.github.chrischall/creditkarma-mcp",
37054
37344
  description: "MCP server for Credit Karma \u2014 natural-language access to your transactions, spending, and accounts",
37055
37345
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37093,17 +37383,17 @@ var package_default = {
37093
37383
  "test:coverage": "vitest run --coverage"
37094
37384
  },
37095
37385
  dependencies: {
37096
- "@fetchproxy/bootstrap": "^0.4.2",
37386
+ "@fetchproxy/bootstrap": "^0.6.0",
37097
37387
  "@modelcontextprotocol/sdk": "^1.29.0",
37098
- dotenv: "^17.4.0",
37099
- zod: "^4.3.6"
37388
+ dotenv: "^17.4.2",
37389
+ zod: "^4.4.3"
37100
37390
  },
37101
37391
  devDependencies: {
37102
- "@types/node": "^25.5.2",
37103
- "@vitest/coverage-v8": "^4.1.2",
37392
+ "@types/node": "^25.9.1",
37393
+ "@vitest/coverage-v8": "^4.1.7",
37104
37394
  esbuild: "^0.28.0",
37105
- typescript: "^6.0.2",
37106
- vitest: "^4.1.2"
37395
+ typescript: "^6.0.3",
37396
+ vitest: "^4.1.7"
37107
37397
  }
37108
37398
  };
37109
37399
 
@@ -37623,7 +37913,8 @@ async function main() {
37623
37913
  mcpJsonPath
37624
37914
  };
37625
37915
  const server = new McpServer(
37626
- { name: "creditkarma-mcp", version: "2.0.10" }
37916
+ { name: "creditkarma-mcp", version: "2.1.4" }
37917
+ // x-release-please-version
37627
37918
  );
37628
37919
  registerAuthTools(server, ctx);
37629
37920
  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.10' });
66
+ const server = new McpServer({ name: 'creditkarma-mcp', version: '2.1.4' } // 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.10",
3
+ "version": "2.1.4",
4
4
  "mcpName": "io.github.chrischall/creditkarma-mcp",
5
- "description": "MCP server for Credit Karma \u2014 natural-language access to your transactions, spending, and accounts",
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,16 @@
44
44
  "test:coverage": "vitest run --coverage"
45
45
  },
46
46
  "dependencies": {
47
- "@fetchproxy/bootstrap": "^0.4.2",
47
+ "@fetchproxy/bootstrap": "^0.6.0",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "dotenv": "^17.4.0",
50
- "zod": "^4.3.6"
49
+ "dotenv": "^17.4.2",
50
+ "zod": "^4.4.3"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^25.5.2",
54
- "@vitest/coverage-v8": "^4.1.2",
53
+ "@types/node": "^25.9.1",
54
+ "@vitest/coverage-v8": "^4.1.7",
55
55
  "esbuild": "^0.28.0",
56
- "typescript": "^6.0.2",
57
- "vitest": "^4.1.2"
56
+ "typescript": "^6.0.3",
57
+ "vitest": "^4.1.7"
58
58
  }
59
59
  }
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.10",
9
+ "version": "2.1.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "creditkarma-mcp",
14
- "version": "2.0.10",
14
+ "version": "2.1.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },