creditkarma-mcp 2.2.1 → 2.2.2

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/dist/bundle.js CHANGED
@@ -7394,14 +7394,14 @@ var require_permessage_deflate = __commonJS({
7394
7394
  }
7395
7395
  };
7396
7396
  module.exports = PerMessageDeflate2;
7397
- function deflateOnData(chunk) {
7398
- this[kBuffers].push(chunk);
7399
- this[kTotalLength] += chunk.length;
7397
+ function deflateOnData(chunk2) {
7398
+ this[kBuffers].push(chunk2);
7399
+ this[kTotalLength] += chunk2.length;
7400
7400
  }
7401
- function inflateOnData(chunk) {
7402
- this[kTotalLength] += chunk.length;
7401
+ function inflateOnData(chunk2) {
7402
+ this[kTotalLength] += chunk2.length;
7403
7403
  if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) {
7404
- this[kBuffers].push(chunk);
7404
+ this[kBuffers].push(chunk2);
7405
7405
  return;
7406
7406
  }
7407
7407
  this[kError] = new RangeError("Max payload size exceeded");
@@ -7701,7 +7701,7 @@ var require_receiver = __commonJS({
7701
7701
  * @param {Function} cb Callback
7702
7702
  * @private
7703
7703
  */
7704
- _write(chunk, encoding, cb) {
7704
+ _write(chunk2, encoding, cb) {
7705
7705
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
7706
7706
  if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
7707
7707
  cb(
@@ -7715,8 +7715,8 @@ var require_receiver = __commonJS({
7715
7715
  );
7716
7716
  return;
7717
7717
  }
7718
- this._bufferedBytes += chunk.length;
7719
- this._buffers.push(chunk);
7718
+ this._bufferedBytes += chunk2.length;
7719
+ this._buffers.push(chunk2);
7720
7720
  this.startLoop(cb);
7721
7721
  }
7722
7722
  /**
@@ -9990,8 +9990,8 @@ var require_websocket = __commonJS({
9990
9990
  this.removeListener("end", socketOnEnd);
9991
9991
  websocket._readyState = WebSocket2.CLOSING;
9992
9992
  if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
9993
- const chunk = this.read(this._readableState.length);
9994
- websocket._receiver.write(chunk);
9993
+ const chunk2 = this.read(this._readableState.length);
9994
+ websocket._receiver.write(chunk2);
9995
9995
  }
9996
9996
  websocket._receiver.end();
9997
9997
  this[kWebSocket] = void 0;
@@ -10003,8 +10003,8 @@ var require_websocket = __commonJS({
10003
10003
  websocket._receiver.on("finish", receiverOnFinish);
10004
10004
  }
10005
10005
  }
10006
- function socketOnData(chunk) {
10007
- if (!this[kWebSocket]._receiver.write(chunk)) {
10006
+ function socketOnData(chunk2) {
10007
+ if (!this[kWebSocket]._receiver.write(chunk2)) {
10008
10008
  this.pause();
10009
10009
  }
10010
10010
  }
@@ -10107,14 +10107,14 @@ var require_stream = __commonJS({
10107
10107
  duplex._read = function() {
10108
10108
  if (ws.isPaused) ws.resume();
10109
10109
  };
10110
- duplex._write = function(chunk, encoding, callback) {
10110
+ duplex._write = function(chunk2, encoding, callback) {
10111
10111
  if (ws.readyState === ws.CONNECTING) {
10112
10112
  ws.once("open", function open() {
10113
- duplex._write(chunk, encoding, callback);
10113
+ duplex._write(chunk2, encoding, callback);
10114
10114
  });
10115
10115
  return;
10116
10116
  }
10117
- ws.send(chunk, callback);
10117
+ ws.send(chunk2, callback);
10118
10118
  };
10119
10119
  duplex.on("end", duplexOnEnd);
10120
10120
  duplex.on("error", duplexOnError);
@@ -34546,8 +34546,8 @@ import process3 from "node:process";
34546
34546
 
34547
34547
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
34548
34548
  var ReadBuffer = class {
34549
- append(chunk) {
34550
- this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
34549
+ append(chunk2) {
34550
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
34551
34551
  }
34552
34552
  readMessage() {
34553
34553
  if (!this._buffer) {
@@ -34579,8 +34579,8 @@ var StdioServerTransport = class {
34579
34579
  this._stdout = _stdout;
34580
34580
  this._readBuffer = new ReadBuffer();
34581
34581
  this._started = false;
34582
- this._ondata = (chunk) => {
34583
- this._readBuffer.append(chunk);
34582
+ this._ondata = (chunk2) => {
34583
+ this._readBuffer.append(chunk2);
34584
34584
  this.processReadBuffer();
34585
34585
  };
34586
34586
  this._onerror = (error51) => {
@@ -34633,6 +34633,139 @@ var StdioServerTransport = class {
34633
34633
  }
34634
34634
  };
34635
34635
 
34636
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
34637
+ async function createMcpServer(opts) {
34638
+ const server = new McpServer({ name: opts.name, version: opts.version });
34639
+ if (opts.banner !== void 0) {
34640
+ console.error(opts.banner);
34641
+ }
34642
+ const deps = opts.deps;
34643
+ for (const register of opts.tools) {
34644
+ await register(server, deps);
34645
+ }
34646
+ return server;
34647
+ }
34648
+ function withGracefulShutdown(server, opts = {}) {
34649
+ const shouldExit = opts.exit ?? true;
34650
+ let shuttingDown = false;
34651
+ const handler = (signal) => {
34652
+ if (shuttingDown)
34653
+ return;
34654
+ shuttingDown = true;
34655
+ void (async () => {
34656
+ try {
34657
+ if (opts.onSignal)
34658
+ await opts.onSignal(signal);
34659
+ await server.close();
34660
+ } catch (err) {
34661
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34662
+ } finally {
34663
+ if (shouldExit)
34664
+ process.exit(0);
34665
+ }
34666
+ })();
34667
+ };
34668
+ process.on("SIGINT", () => handler("SIGINT"));
34669
+ process.on("SIGTERM", () => handler("SIGTERM"));
34670
+ }
34671
+ async function runMcp(opts) {
34672
+ const server = await createMcpServer(opts);
34673
+ const shutdown = opts.shutdown ?? true;
34674
+ if (shutdown !== false) {
34675
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34676
+ }
34677
+ const spec = opts.transport ?? "stdio";
34678
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34679
+ await server.connect(transport);
34680
+ return server;
34681
+ }
34682
+
34683
+ // node_modules/@chrischall/mcp-utils/dist/response/index.js
34684
+ function textResult(data) {
34685
+ return {
34686
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34687
+ };
34688
+ }
34689
+ function rawTextResult(text) {
34690
+ return { content: [{ type: "text", text }] };
34691
+ }
34692
+
34693
+ // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34694
+ var DEFAULT_ERROR_MESSAGE_MAX = 500;
34695
+ var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
34696
+ var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
34697
+ function redactSecrets(text) {
34698
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34699
+ }
34700
+ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34701
+ const str = text === null || text === void 0 ? "" : String(text);
34702
+ const redacted = redactSecrets(str);
34703
+ if (redacted.length <= max)
34704
+ return redacted;
34705
+ return `${redacted.slice(0, max)}\u2026 [truncated]`;
34706
+ }
34707
+
34708
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
34709
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34710
+ function readEnvVar(key, opts = {}) {
34711
+ const env = opts.env ?? process.env;
34712
+ const raw = env[key];
34713
+ if (typeof raw === "string") {
34714
+ const trimmed = raw.trim();
34715
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
34716
+ return trimmed;
34717
+ }
34718
+ }
34719
+ return opts.default;
34720
+ }
34721
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34722
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34723
+ function parseBoolEnv(key, opts = {}) {
34724
+ const fallback = opts.default ?? false;
34725
+ const raw = readEnvVar(key, { env: opts.env });
34726
+ if (raw === void 0)
34727
+ return fallback;
34728
+ const token = raw.toLowerCase();
34729
+ if (TRUE_TOKENS.has(token))
34730
+ return true;
34731
+ if (FALSE_TOKENS.has(token))
34732
+ return false;
34733
+ return fallback;
34734
+ }
34735
+ async function loadDotenvSafely(opts = {}) {
34736
+ try {
34737
+ const mod = await import(
34738
+ /* @vite-ignore */
34739
+ "dotenv"
34740
+ );
34741
+ const result = mod.config({
34742
+ ...opts.path !== void 0 ? { path: opts.path } : {},
34743
+ override: opts.override ?? false,
34744
+ quiet: true
34745
+ });
34746
+ return result.error === void 0;
34747
+ } catch {
34748
+ return false;
34749
+ }
34750
+ }
34751
+
34752
+ // node_modules/@chrischall/mcp-utils/dist/zod/index.js
34753
+ var PositiveInt = external_exports.number().int().positive();
34754
+ var NonNegInt = external_exports.number().int().nonnegative();
34755
+ var NonEmptyString = external_exports.string().min(1);
34756
+ var IsoDate = external_exports.iso.date();
34757
+ var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34758
+ var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34759
+ var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34760
+ var paginationSchema = {
34761
+ offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
34762
+ limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
34763
+ };
34764
+ var pageSchema = {
34765
+ page_num: PositiveInt.default(1).describe("1-based page number."),
34766
+ page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34767
+ };
34768
+
34636
34769
  // src/index.ts
34637
34770
  import { homedir as homedir2 } from "os";
34638
34771
  import { join as join4, dirname as dirname4 } from "path";
@@ -34650,6 +34783,8 @@ var CreditKarmaClient = class {
34650
34783
  tokenSetAt = null;
34651
34784
  refreshToken = null;
34652
34785
  cookies = null;
34786
+ /** In-flight refresh, shared across concurrent callers (see refreshAccessToken). */
34787
+ refreshInFlight = null;
34653
34788
  constructor(token, refreshToken, cookies) {
34654
34789
  if (token) this.setToken(token);
34655
34790
  if (refreshToken) this.refreshToken = refreshToken;
@@ -34693,17 +34828,31 @@ var CreditKarmaClient = class {
34693
34828
  variables: buildVariables(afterCursor)
34694
34829
  });
34695
34830
  if (retry.status === 401) throw new Error("TOKEN_EXPIRED");
34696
- if (!retry.ok) throw new Error(`HTTP ${retry.status}`);
34831
+ if (!retry.ok) throw new Error(await httpErrorMessage(retry));
34697
34832
  return parseTransactionPage(await retry.json());
34698
34833
  }
34699
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
34834
+ if (!response.ok) throw new Error(await httpErrorMessage(response));
34700
34835
  return parseTransactionPage(await response.json());
34701
34836
  }
34702
34837
  /**
34703
34838
  * Refresh the access token using CK's native refresh endpoint.
34704
34839
  * Requires a refresh token and session cookies (captured after login).
34840
+ *
34841
+ * Concurrent callers share a single in-flight request: the first call starts
34842
+ * the refresh and stores its promise; overlapping callers (e.g. a multi-page
34843
+ * sync that 401s on several pages at once) await that same promise instead of
34844
+ * firing duplicate POSTs to /member/oauth2/refresh (wasted quota, rate-limit
34845
+ * risk). The slot is cleared in `finally`, so a later expiry refreshes anew.
34705
34846
  */
34706
- async refreshAccessToken() {
34847
+ refreshAccessToken() {
34848
+ if (this.refreshInFlight) return this.refreshInFlight;
34849
+ const p = this.doRefreshAccessToken().finally(() => {
34850
+ this.refreshInFlight = null;
34851
+ });
34852
+ this.refreshInFlight = p;
34853
+ return p;
34854
+ }
34855
+ async doRefreshAccessToken() {
34707
34856
  if (!this.refreshToken) throw new Error("NO_REFRESH_TOKEN: Call ck_set_session first.");
34708
34857
  const headers = {
34709
34858
  "content-type": "application/json",
@@ -34766,6 +34915,11 @@ function isJwtExpired(token) {
34766
34915
  if (!p || typeof p.exp !== "number") return false;
34767
34916
  return p.exp * 1e3 < Date.now();
34768
34917
  }
34918
+ function warnIfRefreshTokenExpired(refreshToken) {
34919
+ if (refreshToken && isJwtExpired(refreshToken)) {
34920
+ console.error("[creditkarma-mcp] Warning: refresh token in CK_COOKIES has expired. Sign back into creditkarma.com (with the fetchproxy extension installed) or call ck_set_session with a fresh Cookie header.");
34921
+ }
34922
+ }
34769
34923
  function extractGlidFromJwt(token) {
34770
34924
  const p = decodeJwtPayload(token);
34771
34925
  const glid = p?.glid;
@@ -34799,6 +34953,16 @@ function parseTransactionPage(json2) {
34799
34953
  function sleep(ms) {
34800
34954
  return new Promise((resolve) => setTimeout(resolve, ms));
34801
34955
  }
34956
+ async function httpErrorMessage(res) {
34957
+ let body = "";
34958
+ try {
34959
+ body = typeof res.text === "function" ? await res.text() : "";
34960
+ } catch {
34961
+ body = "";
34962
+ }
34963
+ const safe = truncateErrorMessage(body, 200).trim();
34964
+ return safe.length > 0 ? `HTTP ${res.status}: ${safe}` : `HTTP ${res.status}`;
34965
+ }
34802
34966
 
34803
34967
  // src/db.ts
34804
34968
  import { DatabaseSync } from "node:sqlite";
@@ -35025,7 +35189,7 @@ function registerAuthTools(server, ctx) {
35025
35189
  },
35026
35190
  async (args) => {
35027
35191
  const result = await handleSetSession(args, ctx);
35028
- return { content: [{ type: "text", text: result }] };
35192
+ return rawTextResult(result);
35029
35193
  }
35030
35194
  );
35031
35195
  }
@@ -35186,6 +35350,16 @@ function assertScopeKeyArray(value, label) {
35186
35350
  seen.add(k);
35187
35351
  }
35188
35352
  }
35353
+ var CAPTURE_PATH_RE = /^\/[A-Za-z0-9._~%\-/]*\*?$/;
35354
+ function hostMatchesAnyDomain(host, domains) {
35355
+ const h = host.toLowerCase();
35356
+ for (const d of domains) {
35357
+ const dom = d.toLowerCase();
35358
+ if (h === dom || h.endsWith("." + dom))
35359
+ return true;
35360
+ }
35361
+ return false;
35362
+ }
35189
35363
  function assertCaptureHeadersArray(value, label) {
35190
35364
  if (!Array.isArray(value)) {
35191
35365
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -35194,14 +35368,25 @@ function assertCaptureHeadersArray(value, label) {
35194
35368
  for (let i = 0; i < value.length; i++) {
35195
35369
  const entry = value[i];
35196
35370
  assertObject(entry, `${label}[${i}]`);
35197
- if (entry.urlPattern === void 0) {
35198
- throw new ProtocolError(`${label}[${i}].urlPattern: missing`);
35371
+ if (entry.host === void 0) {
35372
+ throw new ProtocolError(`${label}[${i}].host: missing`);
35199
35373
  }
35200
35374
  if (entry.headerName === void 0) {
35201
35375
  throw new ProtocolError(`${label}[${i}].headerName: missing`);
35202
35376
  }
35203
- if (typeof entry.urlPattern !== "string") {
35204
- throw new ProtocolError(`${label}[${i}].urlPattern: expected string, got ${typeof entry.urlPattern}`);
35377
+ if (typeof entry.host !== "string") {
35378
+ throw new ProtocolError(`${label}[${i}].host: expected string, got ${typeof entry.host}`);
35379
+ }
35380
+ if (!HOSTNAME_RE.test(entry.host)) {
35381
+ throw new ProtocolError(`${label}[${i}].host: invalid hostname ${JSON.stringify(entry.host)}`);
35382
+ }
35383
+ if (entry.path !== void 0) {
35384
+ if (typeof entry.path !== "string") {
35385
+ throw new ProtocolError(`${label}[${i}].path: expected string, got ${typeof entry.path}`);
35386
+ }
35387
+ if (!CAPTURE_PATH_RE.test(entry.path)) {
35388
+ throw new ProtocolError(`${label}[${i}].path: must start with '/' ${JSON.stringify(entry.path)}`);
35389
+ }
35205
35390
  }
35206
35391
  if (typeof entry.headerName !== "string") {
35207
35392
  throw new ProtocolError(`${label}[${i}].headerName: expected string, got ${typeof entry.headerName}`);
@@ -35209,19 +35394,29 @@ function assertCaptureHeadersArray(value, label) {
35209
35394
  if (!HEADER_NAME_RE.test(entry.headerName)) {
35210
35395
  throw new ProtocolError(`${label}[${i}].headerName: invalid name ${JSON.stringify(entry.headerName)}`);
35211
35396
  }
35212
- assertCaptureUrlPattern(entry.urlPattern, `${label}[${i}].urlPattern`);
35213
- const key = `${entry.urlPattern}\0${entry.headerName}`;
35397
+ const normalizedPath = entry.path ?? "/*";
35398
+ const key = `${entry.host}\0${normalizedPath}\0${entry.headerName}`;
35214
35399
  if (seen.has(key)) {
35215
- throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ urlPattern: entry.urlPattern, headerName: entry.headerName })}`);
35400
+ throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ host: entry.host, path: normalizedPath, headerName: entry.headerName })}`);
35216
35401
  }
35217
35402
  seen.add(key);
35218
35403
  for (const k of Object.keys(entry)) {
35219
- if (k !== "urlPattern" && k !== "headerName") {
35404
+ if (k !== "host" && k !== "path" && k !== "headerName") {
35220
35405
  throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
35221
35406
  }
35222
35407
  }
35223
35408
  }
35224
35409
  }
35410
+ function validateCaptureHeaderDecls(value, domains, label = "captureHeaders") {
35411
+ assertCaptureHeadersArray(value, label);
35412
+ const arr = value;
35413
+ for (let i = 0; i < arr.length; i++) {
35414
+ const host = arr[i].host;
35415
+ if (!hostMatchesAnyDomain(host, domains)) {
35416
+ throw new ProtocolError(`${label}[${i}].host: ${JSON.stringify(host)} is not a declared domain or subdomain of [${domains.join(", ")}]`);
35417
+ }
35418
+ }
35419
+ }
35225
35420
  function assertStoragePointersArray(value, label, declaredKeys) {
35226
35421
  if (!Array.isArray(value)) {
35227
35422
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -35309,23 +35504,6 @@ function assertIndexedDbScopesArray(value, label) {
35309
35504
  }
35310
35505
  }
35311
35506
  }
35312
- function assertCaptureUrlPattern(pattern, label) {
35313
- if (!pattern.startsWith("https://")) {
35314
- throw new ProtocolError(`${label}: must start with https:// (got ${JSON.stringify(pattern)})`);
35315
- }
35316
- const afterScheme = pattern.slice("https://".length);
35317
- const slash = afterScheme.indexOf("/");
35318
- const host = slash === -1 ? afterScheme : afterScheme.slice(0, slash);
35319
- if (host.length === 0) {
35320
- throw new ProtocolError(`${label}: missing host (got ${JSON.stringify(pattern)})`);
35321
- }
35322
- if (host.includes("*")) {
35323
- throw new ProtocolError(`${label}: wildcards not permitted in host (got ${JSON.stringify(pattern)})`);
35324
- }
35325
- if (!HOSTNAME_RE.test(host)) {
35326
- throw new ProtocolError(`${label}: invalid host ${JSON.stringify(host)} in ${JSON.stringify(pattern)}`);
35327
- }
35328
- }
35329
35507
  function validateFrame(raw) {
35330
35508
  assertObject(raw, "frame");
35331
35509
  const t = raw.type;
@@ -35390,7 +35568,7 @@ function validateHello(raw) {
35390
35568
  assertScopeKeyArray(raw.sessionStorageKeys, "hello.sessionStorageKeys");
35391
35569
  }
35392
35570
  if (raw.captureHeaders !== void 0) {
35393
- assertCaptureHeadersArray(raw.captureHeaders, "hello.captureHeaders");
35571
+ validateCaptureHeaderDecls(raw.captureHeaders, raw.domains, "hello.captureHeaders");
35394
35572
  }
35395
35573
  if (raw.indexedDbScopes !== void 0) {
35396
35574
  assertIndexedDbScopesArray(raw.indexedDbScopes, "hello.indexedDbScopes");
@@ -35558,19 +35736,28 @@ function validateInnerRequest(raw) {
35558
35736
  }
35559
35737
  if (raw.op === "capture_request_header") {
35560
35738
  assertObject(raw.init, "inner.init");
35561
- if (raw.init.urlPattern === void 0) {
35562
- throw new ProtocolError("inner.init.urlPattern: missing");
35739
+ if (raw.init.host === void 0) {
35740
+ throw new ProtocolError("inner.init.host: missing");
35563
35741
  }
35564
35742
  if (raw.init.headerName === void 0) {
35565
35743
  throw new ProtocolError("inner.init.headerName: missing");
35566
35744
  }
35567
- assertString(raw.init.urlPattern, "inner.init.urlPattern");
35745
+ assertString(raw.init.host, "inner.init.host");
35746
+ if (!HOSTNAME_RE.test(raw.init.host)) {
35747
+ throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
35748
+ }
35749
+ if (raw.init.path !== void 0) {
35750
+ assertString(raw.init.path, "inner.init.path");
35751
+ if (!CAPTURE_PATH_RE.test(raw.init.path)) {
35752
+ throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
35753
+ }
35754
+ }
35568
35755
  assertString(raw.init.headerName, "inner.init.headerName");
35569
35756
  if (raw.init.timeoutMs !== void 0) {
35570
35757
  assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35571
35758
  }
35572
35759
  for (const k of Object.keys(raw.init)) {
35573
- if (k !== "urlPattern" && k !== "headerName" && k !== "timeoutMs") {
35760
+ if (k !== "host" && k !== "path" && k !== "headerName" && k !== "timeoutMs") {
35574
35761
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_request_header`);
35575
35762
  }
35576
35763
  }
@@ -35950,7 +36137,8 @@ async function buildServerHello(opts) {
35950
36137
  }
35951
36138
  if (opts.captureHeaders && opts.captureHeaders.length > 0) {
35952
36139
  hello.captureHeaders = opts.captureHeaders.map((d) => ({
35953
- urlPattern: d.urlPattern,
36140
+ host: d.host,
36141
+ ...d.path !== void 0 ? { path: d.path } : {},
35954
36142
  headerName: d.headerName
35955
36143
  }));
35956
36144
  }
@@ -36188,7 +36376,9 @@ async function startHost(opts) {
36188
36376
  } catch {
36189
36377
  }
36190
36378
  }
36191
- wss.close(() => resolve());
36379
+ wss.close(() => {
36380
+ opts.httpServer.close(() => resolve());
36381
+ });
36192
36382
  }),
36193
36383
  sendOwnInner: async (inner) => {
36194
36384
  const session = await ownSessionReady;
@@ -36238,6 +36428,7 @@ async function startPeer(opts) {
36238
36428
  const innerListeners = [];
36239
36429
  const renegotiateListeners = [];
36240
36430
  const pendingPairListeners = [];
36431
+ const closeListeners = [];
36241
36432
  let session = null;
36242
36433
  let pendingPairCode = null;
36243
36434
  let resolveFirstReady;
@@ -36287,6 +36478,7 @@ async function startPeer(opts) {
36287
36478
  ws.on("message", onMessage);
36288
36479
  ws.once("close", () => {
36289
36480
  rejectFirstReady(new Error("peer WS closed before ready"));
36481
+ closeListeners.forEach((cb) => cb());
36290
36482
  });
36291
36483
  sessionPromise.catch(() => {
36292
36484
  });
@@ -36309,6 +36501,9 @@ async function startPeer(opts) {
36309
36501
  pendingPairListeners.push(cb);
36310
36502
  },
36311
36503
  pendingPairCode: () => pendingPairCode,
36504
+ onClose: (cb) => {
36505
+ closeListeners.push(cb);
36506
+ },
36312
36507
  close: () => ws.close()
36313
36508
  };
36314
36509
  return handle;
@@ -36391,6 +36586,19 @@ function classifyFetchError(error51) {
36391
36586
  return "other";
36392
36587
  }
36393
36588
 
36589
+ // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
36590
+ function classifyBridgeError(err) {
36591
+ if (err instanceof FetchproxyTimeoutError)
36592
+ return "timeout";
36593
+ if (err instanceof FetchproxyBridgeDownError)
36594
+ return "bridge_down";
36595
+ if (err instanceof FetchproxyHttpError)
36596
+ return "http";
36597
+ if (err instanceof FetchproxyProtocolError)
36598
+ return "protocol";
36599
+ return "other";
36600
+ }
36601
+
36394
36602
  // node_modules/@fetchproxy/server/dist/ws-server.js
36395
36603
  var FetchproxyProtocolError = class extends Error {
36396
36604
  constructor(message) {
@@ -36420,7 +36628,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
36420
36628
  const retryAttempted = args.retryAttempted ?? false;
36421
36629
  const op = args.op ?? "fetch";
36422
36630
  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.`;
36631
+ const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
36424
36632
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
36425
36633
  this.name = "FetchproxyBridgeDownError";
36426
36634
  this.originalError = args.originalError;
@@ -36442,6 +36650,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36442
36650
  port;
36443
36651
  /** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
36444
36652
  elapsedMs;
36653
+ /**
36654
+ * 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
36655
+ * fired for this timeout (a cold-start `timeout` symptom followed by
36656
+ * a warm-and-retry that also timed out). False when the retry was
36657
+ * disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
36658
+ * the first attempt.
36659
+ */
36660
+ retryAttempted;
36445
36661
  constructor(args) {
36446
36662
  super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
36447
36663
  this.name = "FetchproxyTimeoutError";
@@ -36450,6 +36666,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36450
36666
  this.role = args.role ?? null;
36451
36667
  this.port = args.port ?? 0;
36452
36668
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
36669
+ this.retryAttempted = args.retryAttempted ?? false;
36453
36670
  }
36454
36671
  };
36455
36672
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -36488,6 +36705,12 @@ var FetchproxyServer = class {
36488
36705
  opts;
36489
36706
  hostHandle = null;
36490
36707
  peerHandle = null;
36708
+ // 0.13.0+: true from the start of `close()` until the next `doConnect()`.
36709
+ // Distinguishes an intentional shutdown (whose WS close we must ignore)
36710
+ // from the host process dying (which strands a peer and must trigger
36711
+ // re-election). The WS `close` event fires asynchronously, so this stays
36712
+ // latched across `close()` rather than being reset in its tail.
36713
+ closing = false;
36491
36714
  nextRequestId = 1;
36492
36715
  // 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
36493
36716
  // Replaces the local copies every downstream MCP was rolling on top
@@ -36523,6 +36746,25 @@ var FetchproxyServer = class {
36523
36746
  // for "we're connecting right now" so two parallel first-calls don't
36524
36747
  // race the port bind.
36525
36748
  connectingPromise = null;
36749
+ // 0.8.1+ (#67): server-initiated keep-alive ping. Active when
36750
+ // `keepAliveIntervalMs` is set AND we've seen recent activity
36751
+ // (fetch/capture success or failure, or markActive()) within
36752
+ // `keepAliveMaxIdleMs`. The interval handle is created lazily on
36753
+ // first activity and torn down on close() / extension disconnect.
36754
+ keepAliveTimer = null;
36755
+ lastActiveAt = null;
36756
+ // 0.10.0+ (#73): observability counters surfaced via
36757
+ // bridgeHealth().keepAlive / .swEviction. Monotonic across the process
36758
+ // lifetime so a downstream healthcheck tool can verify the keep-alive
36759
+ // is actually preventing SW eviction. `lastPingAt` and `totalPings`
36760
+ // are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
36761
+ // `lastEvictionDetectedAt` are stamped from the lazy-revive code path
36762
+ // in fetch() / captureRequestHeader().
36763
+ lastPingAt = null;
36764
+ totalPings = 0;
36765
+ lazyReviveAttempts = 0;
36766
+ lazyReviveSuccesses = 0;
36767
+ lastEvictionDetectedAt = null;
36526
36768
  constructor(opts) {
36527
36769
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36528
36770
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36541,6 +36783,14 @@ var FetchproxyServer = class {
36541
36783
  }
36542
36784
  capabilities = [...opts.capabilities];
36543
36785
  }
36786
+ if (opts.captureHeaders !== void 0) {
36787
+ try {
36788
+ validateCaptureHeaderDecls(opts.captureHeaders, opts.domains);
36789
+ } catch (err) {
36790
+ const message = err instanceof Error ? err.message : String(err);
36791
+ throw new Error("FetchproxyServer: invalid captureHeaders \u2014 " + message);
36792
+ }
36793
+ }
36544
36794
  this.opts = {
36545
36795
  port: opts.port ?? 37149,
36546
36796
  host: opts.host ?? "127.0.0.1",
@@ -36552,7 +36802,8 @@ var FetchproxyServer = class {
36552
36802
  localStorageKeys: [...opts.localStorageKeys ?? []],
36553
36803
  sessionStorageKeys: [...opts.sessionStorageKeys ?? []],
36554
36804
  captureHeaders: (opts.captureHeaders ?? []).map((d) => ({
36555
- urlPattern: d.urlPattern,
36805
+ host: d.host,
36806
+ ...d.path !== void 0 ? { path: d.path } : {},
36556
36807
  headerName: d.headerName
36557
36808
  })),
36558
36809
  indexedDbScopes: (opts.indexedDbScopes ?? []).map((d) => ({
@@ -36575,6 +36826,22 @@ var FetchproxyServer = class {
36575
36826
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
36576
36827
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
36577
36828
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
36829
+ // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
36830
+ // wave showed every Pattern A consumer was opting into this same
36831
+ // value. Pass `0` to disable; the existing `<= 0` guards in
36832
+ // `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
36833
+ //
36834
+ // #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
36835
+ // Chrome's ~30s SW-eviction window — slack that timer drift, a
36836
+ // busy host event loop (CPU-bound response parsing between calls),
36837
+ // and the ping's own round-trip latency routinely ate, so the SW
36838
+ // evicted before the next ping landed and the next call cold-
36839
+ // started. 20s restores real margin. (The extension
36840
+ // `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
36841
+ // period, firing *at* the edge — it can't rescue a sub-30s race;
36842
+ // the server ping is the real defense.)
36843
+ keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
36844
+ keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
36578
36845
  identityDir: opts.identityDir,
36579
36846
  onPairCode: opts.onPairCode
36580
36847
  };
@@ -36654,6 +36921,7 @@ var FetchproxyServer = class {
36654
36921
  async doConnect() {
36655
36922
  const identity = this.identity;
36656
36923
  const mcpId = this.mcpId;
36924
+ this.closing = false;
36657
36925
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
36658
36926
  if (el.role === "host") {
36659
36927
  this.role = "host";
@@ -36675,7 +36943,10 @@ var FetchproxyServer = class {
36675
36943
  onPairCode: this.opts.onPairCode
36676
36944
  });
36677
36945
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36678
- this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36946
+ this.hostHandle.onExtensionDisconnect(() => {
36947
+ this.stopKeepalive();
36948
+ this.rejectAllPending();
36949
+ });
36679
36950
  this.hostHandle.onPendingPair((code) => {
36680
36951
  this.rejectAllPending(this.pairingErrorMessage(code));
36681
36952
  });
@@ -36699,7 +36970,10 @@ var FetchproxyServer = class {
36699
36970
  sessionStoragePointers: this.opts.sessionStoragePointers
36700
36971
  });
36701
36972
  this.peerHandle.onInner((inner) => this.onInner(inner));
36702
- this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36973
+ this.peerHandle.onRenegotiate(() => {
36974
+ this.stopKeepalive();
36975
+ this.rejectAllPending();
36976
+ });
36703
36977
  this.peerHandle.onPendingPair((code) => {
36704
36978
  this.rejectAllPending(this.pairingErrorMessage(code));
36705
36979
  });
@@ -36707,6 +36981,14 @@ var FetchproxyServer = class {
36707
36981
  const cb = this.opts.onPairCode;
36708
36982
  this.peerHandle.onPendingPair((code) => cb(code));
36709
36983
  }
36984
+ this.peerHandle.onClose(() => {
36985
+ if (this.closing || this.peerHandle === null)
36986
+ return;
36987
+ this.stopKeepalive();
36988
+ this.rejectAllPending();
36989
+ this.peerHandle = null;
36990
+ this.role = null;
36991
+ });
36710
36992
  }
36711
36993
  }
36712
36994
  pairingErrorMessage(code) {
@@ -36740,13 +37022,18 @@ var FetchproxyServer = class {
36740
37022
  }
36741
37023
  const first = await this._fetchOnceWithTimeout(init);
36742
37024
  const reviveMs = this.opts.bridgeReviveDelayMs;
36743
- let final = first;
36744
- if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
37025
+ const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
37026
+ if (isColdStartSymptom) {
37027
+ this.lastEvictionDetectedAt = Date.now();
37028
+ }
37029
+ if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
37030
+ this.lazyReviveAttempts += 1;
36745
37031
  await new Promise((r) => setTimeout(r, reviveMs));
36746
37032
  const second = await this._fetchOnceWithTimeout(init);
36747
- if (second.ok)
37033
+ if (second.ok) {
37034
+ this.lazyReviveSuccesses += 1;
36748
37035
  this.recordSuccess();
36749
- else
37036
+ } else
36750
37037
  this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
36751
37038
  return { ...second, retryAttempted: true };
36752
37039
  }
@@ -36768,6 +37055,9 @@ var FetchproxyServer = class {
36768
37055
  * call (addresses #23 ask 4).
36769
37056
  */
36770
37057
  bridgeHealth() {
37058
+ const intervalMs = this.opts.keepAliveIntervalMs;
37059
+ const maxIdleMs = this.opts.keepAliveMaxIdleMs;
37060
+ const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
36771
37061
  return {
36772
37062
  role: this.role,
36773
37063
  port: this.opts.port,
@@ -36778,17 +37068,85 @@ var FetchproxyServer = class {
36778
37068
  lastFailureAt: this.lastFailureAt,
36779
37069
  lastFailureReason: this.lastFailureReason,
36780
37070
  consecutiveFailures: this.consecutiveFailures,
36781
- lastExtensionMessageAt: this.lastExtensionMessageAt
37071
+ lastExtensionMessageAt: this.lastExtensionMessageAt,
37072
+ keepAlive: {
37073
+ enabled: intervalMs > 0,
37074
+ intervalMs,
37075
+ maxIdleMs,
37076
+ lastPingAt: this.lastPingAt,
37077
+ totalPings: this.totalPings,
37078
+ idleSinceMs
37079
+ },
37080
+ swEviction: {
37081
+ lazyReviveAttempts: this.lazyReviveAttempts,
37082
+ lazyReviveSuccesses: this.lazyReviveSuccesses,
37083
+ lastEvictionDetectedAt: this.lastEvictionDetectedAt
37084
+ }
36782
37085
  };
36783
37086
  }
36784
37087
  recordSuccess() {
36785
37088
  this.lastSuccessAt = Date.now();
36786
37089
  this.consecutiveFailures = 0;
37090
+ this.noteActivityForKeepalive();
36787
37091
  }
36788
37092
  recordFailure(reason) {
36789
37093
  this.lastFailureAt = Date.now();
36790
37094
  this.lastFailureReason = reason;
36791
37095
  this.consecutiveFailures += 1;
37096
+ this.noteActivityForKeepalive();
37097
+ }
37098
+ /**
37099
+ * 0.8.1+ (#67): caller-side hint that work is happening or about to
37100
+ * happen — bumps the keep-alive idle gate so the server keeps pinging
37101
+ * the extension. Useful for MCPs that do a chain of side-effectful
37102
+ * work between bridge calls and don't want the SW to evict in the
37103
+ * gap (e.g. server-side parsing of a previous response that takes
37104
+ * tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
37105
+ */
37106
+ markActive() {
37107
+ this.noteActivityForKeepalive();
37108
+ }
37109
+ noteActivityForKeepalive() {
37110
+ const intervalMs = this.opts.keepAliveIntervalMs;
37111
+ if (intervalMs <= 0)
37112
+ return;
37113
+ this.lastActiveAt = Date.now();
37114
+ this.startKeepaliveIfIdle();
37115
+ }
37116
+ startKeepaliveIfIdle() {
37117
+ if (this.keepAliveTimer !== null)
37118
+ return;
37119
+ const intervalMs = this.opts.keepAliveIntervalMs;
37120
+ if (intervalMs <= 0)
37121
+ return;
37122
+ this.keepAliveTimer = setInterval(() => {
37123
+ const now = Date.now();
37124
+ if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
37125
+ this.stopKeepalive();
37126
+ return;
37127
+ }
37128
+ this.totalPings += 1;
37129
+ this.lastPingAt = now;
37130
+ void this.sendKeepalivePing();
37131
+ }, intervalMs);
37132
+ }
37133
+ async sendKeepalivePing() {
37134
+ try {
37135
+ const inner = { type: "ping" };
37136
+ if (this.hostHandle) {
37137
+ await this.hostHandle.sendOwnInner(inner);
37138
+ } else if (this.peerHandle) {
37139
+ await this.peerHandle.sendInner(inner);
37140
+ }
37141
+ } catch (e) {
37142
+ console.error("[fetchproxy] keepalive ping send failed:", e);
37143
+ }
37144
+ }
37145
+ stopKeepalive() {
37146
+ if (this.keepAliveTimer !== null) {
37147
+ clearInterval(this.keepAliveTimer);
37148
+ this.keepAliveTimer = null;
37149
+ }
36792
37150
  }
36793
37151
  /**
36794
37152
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
@@ -36847,7 +37205,8 @@ var FetchproxyServer = class {
36847
37205
  timeoutMs: this.opts.fetchTimeoutMs ?? 0,
36848
37206
  role: this.role,
36849
37207
  port: this.opts.port,
36850
- elapsedMs: result.elapsedMs
37208
+ elapsedMs: result.elapsedMs,
37209
+ retryAttempted
36851
37210
  });
36852
37211
  }
36853
37212
  if (result.kind === "content_script_unreachable") {
@@ -36978,6 +37337,108 @@ var FetchproxyServer = class {
36978
37337
  const response = await this.get(path, this.applyJsonDefaults(opts));
36979
37338
  return response.body;
36980
37339
  }
37340
+ /**
37341
+ * 0.11.0+: method-generic JSON convenience helper. Generalizes the
37342
+ * `fetchJson<T>(path, { method, headers, body })` that
37343
+ * zillow/redfin/compass/homes hand-rolled char-for-char in their
37344
+ * `src/client.ts`:
37345
+ *
37346
+ * - sets `Accept: application/json`;
37347
+ * - adds `Content-Type: application/json` only for a non-GET request
37348
+ * that carries a `body` (and only if the caller didn't set one);
37349
+ * - `JSON.stringify`s the body (GET / no-body sends nothing);
37350
+ * - treats a `204` or an empty body as `data: null` (no parse);
37351
+ * - otherwise `JSON.parse`s the body.
37352
+ *
37353
+ * Scope is serialization + header defaults + 204-handling +
37354
+ * JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
37355
+ * or look for a sign-in interstitial — those guards differ per site
37356
+ * (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
37357
+ * returns BOTH the parsed `data` and the raw `FetchResult` and leaves
37358
+ * the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
37359
+ * over `result`.
37360
+ *
37361
+ * Bridge-level failures (no signed-in tab, SW down, timeout) still
37362
+ * throw the typed errors via `request()`, exactly like the verb
37363
+ * helpers — only successful round-trips (any HTTP status) return.
37364
+ */
37365
+ async requestJson(method, path, opts = {}) {
37366
+ const isGet = method.toUpperCase() === "GET";
37367
+ const sendBody = !isGet && opts.body !== void 0;
37368
+ const headers = {
37369
+ Accept: "application/json",
37370
+ ...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
37371
+ ...opts.headers ?? {}
37372
+ };
37373
+ const response = await this.request(method, path, {
37374
+ headers,
37375
+ body: sendBody ? JSON.stringify(opts.body) : void 0,
37376
+ ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
37377
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {}
37378
+ });
37379
+ const result = {
37380
+ ok: true,
37381
+ status: response.status,
37382
+ url: response.url,
37383
+ body: response.body
37384
+ };
37385
+ if (response.status === 204 || response.body === "") {
37386
+ return { data: null, result };
37387
+ }
37388
+ let data;
37389
+ try {
37390
+ data = JSON.parse(response.body);
37391
+ } catch (e) {
37392
+ throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
37393
+ }
37394
+ return { data, result };
37395
+ }
37396
+ /**
37397
+ * 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
37398
+ * the elapsed round-trip, classify any thrown error, and project the
37399
+ * post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
37400
+ *
37401
+ * This is the transport half of the probe loop zillow/redfin/homes
37402
+ * had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
37403
+ * supplies its own probe call (`(path) => client.fetchHtml(path)`)
37404
+ * and probe path (e.g. `'/robots.txt'`); the tool registration and
37405
+ * the site-specific plain-English hint text STAY in the consumer.
37406
+ *
37407
+ * `bridgeHealth()` is read AFTER the probe so its freshness counters
37408
+ * (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
37409
+ * round-trip rather than a stale pre-probe snapshot.
37410
+ */
37411
+ async runProbe(fetchFn, probePath) {
37412
+ const start = Date.now();
37413
+ let ok = false;
37414
+ let error51;
37415
+ try {
37416
+ await fetchFn(probePath);
37417
+ ok = true;
37418
+ } catch (e) {
37419
+ error51 = {
37420
+ kind: classifyBridgeError(e),
37421
+ message: e instanceof Error ? e.message : String(e)
37422
+ };
37423
+ }
37424
+ const elapsed_ms = Date.now() - start;
37425
+ const health = this.bridgeHealth();
37426
+ return {
37427
+ ok,
37428
+ elapsed_ms,
37429
+ bridge: {
37430
+ role: health.role,
37431
+ port: health.port,
37432
+ server_version: health.serverVersion,
37433
+ fetch_timeout_ms: health.fetchTimeoutMs,
37434
+ last_success_at: health.lastSuccessAt,
37435
+ last_failure_at: health.lastFailureAt,
37436
+ last_failure_reason: health.lastFailureReason,
37437
+ consecutive_failures: health.consecutiveFailures
37438
+ },
37439
+ ...error51 ? { error: error51 } : {}
37440
+ };
37441
+ }
36981
37442
  /**
36982
37443
  * Snapshot the user's non-HttpOnly cookies for the chosen domain.
36983
37444
  *
@@ -37102,12 +37563,12 @@ var FetchproxyServer = class {
37102
37563
  /**
37103
37564
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
37104
37565
  * shot: the extension registers a one-time `webRequest` listener
37105
- * filtered on `urlPattern`, captures the named header on the first
37106
- * match, removes itself, and resolves with the value. Times out
37107
- * after `timeoutMs` (default 30s on the extension).
37566
+ * filtered on `https://${host}${path ?? '/*'}`, captures the named
37567
+ * header on the first match, removes itself, and resolves with the
37568
+ * value. Times out after `timeoutMs` (default 30s on the extension).
37108
37569
  *
37109
- * `(urlPattern, headerName)` must exactly match a declared entry in
37110
- * `FetchproxyServerOpts.captureHeaders`.
37570
+ * `(host, path?, headerName)` must match a declared entry in
37571
+ * `FetchproxyServerOpts.captureHeaders` (omitted path ≡ `/*`).
37111
37572
  */
37112
37573
  async captureRequestHeader(opts) {
37113
37574
  if (!this.opts.capabilities.includes("capture_request_header")) {
@@ -37116,24 +37577,25 @@ var FetchproxyServer = class {
37116
37577
  await this.ensureConnected();
37117
37578
  this.throwIfPendingPair();
37118
37579
  const decls = this.opts.captureHeaders;
37580
+ const normPath = (p) => p ?? "/*";
37119
37581
  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);
37582
+ if (opts?.host !== void 0 && opts?.headerName !== void 0) {
37583
+ const found = decls.find((d) => d.host === opts.host && normPath(d.path) === normPath(opts.path) && d.headerName === opts.headerName);
37122
37584
  if (!found) {
37123
- throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
37585
+ throw new Error(`FetchproxyServer.captureRequestHeader: (host=${JSON.stringify(opts.host)}, path=${JSON.stringify(normPath(opts.path))}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
37124
37586
  }
37125
37587
  resolved = found;
37126
- } else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
37588
+ } else if (opts?.host === void 0 && opts?.headerName === void 0) {
37127
37589
  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");
37590
+ throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {host, headerName} explicitly");
37129
37591
  }
37130
37592
  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`);
37593
+ const list = decls.map((d) => `${JSON.stringify(d.host)}${JSON.stringify(normPath(d.path))}/${JSON.stringify(d.headerName)}`).join(", ");
37594
+ throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {host, headerName} to disambiguate`);
37133
37595
  }
37134
37596
  resolved = decls[0];
37135
37597
  } else {
37136
- throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
37598
+ throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
37137
37599
  }
37138
37600
  const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
37139
37601
  try {
@@ -37146,11 +37608,14 @@ var FetchproxyServer = class {
37146
37608
  this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
37147
37609
  throw err;
37148
37610
  }
37611
+ this.lastEvictionDetectedAt = Date.now();
37149
37612
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
37150
37613
  if (reviveMs > 0) {
37614
+ this.lazyReviveAttempts += 1;
37151
37615
  await new Promise((r) => setTimeout(r, reviveMs));
37152
37616
  try {
37153
37617
  const result = await this._captureRequestHeaderOnce(callOpts);
37618
+ this.lazyReviveSuccesses += 1;
37154
37619
  this.recordSuccess();
37155
37620
  return result;
37156
37621
  } catch (retryErr) {
@@ -37164,7 +37629,7 @@ var FetchproxyServer = class {
37164
37629
  originalError: retryErr.message,
37165
37630
  retryAttempted: true,
37166
37631
  op: "capture_request_header",
37167
- url: resolved.urlPattern,
37632
+ url: `https://${resolved.host}${resolved.path ?? "/*"}`,
37168
37633
  role: this.role,
37169
37634
  port: this.opts.port
37170
37635
  });
@@ -37175,7 +37640,7 @@ var FetchproxyServer = class {
37175
37640
  originalError: err.message,
37176
37641
  retryAttempted: false,
37177
37642
  op: "capture_request_header",
37178
- url: resolved.urlPattern,
37643
+ url: `https://${resolved.host}${resolved.path ?? "/*"}`,
37179
37644
  role: this.role,
37180
37645
  port: this.opts.port
37181
37646
  });
@@ -37188,7 +37653,8 @@ var FetchproxyServer = class {
37188
37653
  id,
37189
37654
  op: "capture_request_header",
37190
37655
  init: {
37191
- urlPattern: opts.urlPattern,
37656
+ host: opts.host,
37657
+ ...opts.path !== void 0 ? { path: opts.path } : {},
37192
37658
  headerName: opts.headerName,
37193
37659
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
37194
37660
  }
@@ -37443,6 +37909,8 @@ var FetchproxyServer = class {
37443
37909
  * twice in a row.
37444
37910
  */
37445
37911
  async close() {
37912
+ this.closing = true;
37913
+ this.stopKeepalive();
37446
37914
  this.rejectAllPending();
37447
37915
  if (this.connectingPromise) {
37448
37916
  await this.connectingPromise.catch(() => void 0);
@@ -37458,19 +37926,6 @@ var FetchproxyServer = class {
37458
37926
  }
37459
37927
  };
37460
37928
 
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
-
37474
37929
  // node_modules/@fetchproxy/bootstrap/dist/index.js
37475
37930
  var defaultFactory = (opts) => new FetchproxyServer(opts);
37476
37931
  async function bootstrap(opts) {
@@ -37582,15 +38037,11 @@ async function bootstrap(opts) {
37582
38037
  const capturedHeaders = {};
37583
38038
  for (const h of opts.declare.captureHeaders) {
37584
38039
  if (opts.onWaiting) {
37585
- try {
37586
- const url2 = new URL(h.urlPattern.replace(/\*+/g, "placeholder"));
37587
- opts.onWaiting(`waiting for next request to ${url2.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
37588
- } catch {
37589
- opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
37590
- }
38040
+ opts.onWaiting(`waiting for next request to ${h.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
37591
38041
  }
37592
38042
  capturedHeaders[h.headerName] = await server.captureRequestHeader({
37593
- urlPattern: h.urlPattern,
38043
+ host: h.host,
38044
+ ...h.path !== void 0 ? { path: h.path } : {},
37594
38045
  headerName: h.headerName
37595
38046
  });
37596
38047
  }
@@ -37632,7 +38083,7 @@ var BootstrapDisabledError = class extends Error {
37632
38083
  // package.json
37633
38084
  var package_default = {
37634
38085
  name: "creditkarma-mcp",
37635
- version: "2.2.1",
38086
+ version: "2.2.2",
37636
38087
  mcpName: "io.github.chrischall/creditkarma-mcp",
37637
38088
  description: "MCP server for Credit Karma \u2014 natural-language access to your transactions, spending, and accounts",
37638
38089
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37676,8 +38127,9 @@ var package_default = {
37676
38127
  "test:coverage": "vitest run --coverage"
37677
38128
  },
37678
38129
  dependencies: {
37679
- "@fetchproxy/bootstrap": "^0.8.0",
37680
- "@fetchproxy/server": "^0.8.0",
38130
+ "@chrischall/mcp-utils": "^0.5.0",
38131
+ "@fetchproxy/bootstrap": "^1.0.0",
38132
+ "@fetchproxy/server": "^1.0.0",
37681
38133
  "@modelcontextprotocol/sdk": "^1.29.0",
37682
38134
  dotenv: "^17.4.2",
37683
38135
  zod: "^4.4.3"
@@ -37692,22 +38144,11 @@ var package_default = {
37692
38144
  };
37693
38145
 
37694
38146
  // src/auth.ts
37695
- function readEnv(key) {
37696
- const raw = process.env[key];
37697
- if (typeof raw !== "string") return void 0;
37698
- const trimmed = raw.trim();
37699
- if (trimmed.length === 0) return void 0;
37700
- if (trimmed === "undefined" || trimmed === "null") return void 0;
37701
- if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
37702
- return trimmed;
37703
- }
37704
38147
  function fetchproxyDisabled() {
37705
- const raw = readEnv("CK_DISABLE_FETCHPROXY");
37706
- if (raw === void 0) return false;
37707
- return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
38148
+ return parseBoolEnv("CK_DISABLE_FETCHPROXY", { default: false });
37708
38149
  }
37709
38150
  async function resolveAuth() {
37710
- const envCookies = readEnv("CK_COOKIES");
38151
+ const envCookies = readEnvVar("CK_COOKIES");
37711
38152
  if (envCookies) {
37712
38153
  return { cookies: envCookies, source: "env" };
37713
38154
  }
@@ -37889,7 +38330,7 @@ function registerSyncTools(server, ctx) {
37889
38330
  },
37890
38331
  async (args) => {
37891
38332
  const result = await handleSyncTransactions(args, ctx);
37892
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38333
+ return textResult(result);
37893
38334
  }
37894
38335
  );
37895
38336
  }
@@ -38078,7 +38519,7 @@ function registerQueryTools(server, ctx) {
38078
38519
  },
38079
38520
  async (args) => {
38080
38521
  const result = await handleListTransactions(args, ctx);
38081
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38522
+ return textResult(result);
38082
38523
  }
38083
38524
  );
38084
38525
  server.registerTool(
@@ -38092,7 +38533,7 @@ function registerQueryTools(server, ctx) {
38092
38533
  },
38093
38534
  async (args) => {
38094
38535
  const result = await handleGetRecentTransactions(args, ctx);
38095
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38536
+ return textResult(result);
38096
38537
  }
38097
38538
  );
38098
38539
  server.registerTool(
@@ -38108,7 +38549,7 @@ function registerQueryTools(server, ctx) {
38108
38549
  },
38109
38550
  async (args) => {
38110
38551
  const result = await handleGetSpendingByCategory(args, ctx);
38111
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38552
+ return textResult(result);
38112
38553
  }
38113
38554
  );
38114
38555
  server.registerTool(
@@ -38125,7 +38566,7 @@ function registerQueryTools(server, ctx) {
38125
38566
  },
38126
38567
  async (args) => {
38127
38568
  const result = await handleGetSpendingByMerchant(args, ctx);
38128
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38569
+ return textResult(result);
38129
38570
  }
38130
38571
  );
38131
38572
  server.registerTool(
@@ -38140,7 +38581,7 @@ function registerQueryTools(server, ctx) {
38140
38581
  },
38141
38582
  async (args) => {
38142
38583
  const result = await handleGetAccountSummary(args, ctx);
38143
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38584
+ return textResult(result);
38144
38585
  }
38145
38586
  );
38146
38587
  }
@@ -38166,31 +38607,18 @@ function registerSqlTools(server, ctx) {
38166
38607
  },
38167
38608
  async (args) => {
38168
38609
  const result = await handleQuerySql(args, ctx);
38169
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
38610
+ return textResult(result);
38170
38611
  }
38171
38612
  );
38172
38613
  }
38173
38614
 
38174
38615
  // src/index.ts
38175
- function readVar(key) {
38176
- const raw = process.env[key];
38177
- if (typeof raw !== "string") return void 0;
38178
- const trimmed = raw.trim();
38179
- if (trimmed.length === 0) return void 0;
38180
- if (trimmed === "undefined" || trimmed === "null") return void 0;
38181
- if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
38182
- return trimmed;
38183
- }
38184
38616
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
38185
- try {
38186
- const { config: config2 } = await import("dotenv");
38187
- config2({ path: join4(__dirname, "..", ".env"), override: false, quiet: true });
38188
- } catch {
38189
- }
38617
+ await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
38190
38618
  async function main() {
38191
- const dbPath = readVar("CK_DB_PATH") || join4(homedir2(), ".creditkarma-mcp", "transactions.db");
38619
+ const dbPath = readEnvVar("CK_DB_PATH") || join4(homedir2(), ".creditkarma-mcp", "transactions.db");
38192
38620
  const mcpJsonPath = join4(__dirname, "..", ".mcp.json");
38193
- const cookies = readVar("CK_COOKIES") || void 0;
38621
+ const cookies = readEnvVar("CK_COOKIES") || void 0;
38194
38622
  let token;
38195
38623
  let refreshToken;
38196
38624
  if (cookies) {
@@ -38199,9 +38627,7 @@ async function main() {
38199
38627
  token = parts[0]?.trim() || void 0;
38200
38628
  refreshToken = parts[1]?.trim() || void 0;
38201
38629
  }
38202
- if (refreshToken && isJwtExpired(refreshToken)) {
38203
- console.error("[creditkarma-mcp] Warning: refresh token in CK_COOKIES has expired. Sign back into creditkarma.com (with the fetchproxy extension installed) or call ck_set_session with a fresh Cookie header.");
38204
- }
38630
+ warnIfRefreshTokenExpired(refreshToken);
38205
38631
  const db = initDb(dbPath);
38206
38632
  const repaired = backfillAccountIds(db);
38207
38633
  if (repaired.txsUpdated > 0) {
@@ -38212,15 +38638,17 @@ async function main() {
38212
38638
  db,
38213
38639
  mcpJsonPath
38214
38640
  };
38215
- const server = new McpServer(
38216
- { name: "creditkarma-mcp", version: "2.2.1" }
38641
+ await runMcp({
38642
+ name: "creditkarma-mcp",
38643
+ version: "2.2.2",
38217
38644
  // x-release-please-version
38218
- );
38219
- registerAuthTools(server, ctx);
38220
- registerSyncTools(server, ctx);
38221
- registerQueryTools(server, ctx);
38222
- registerSqlTools(server, ctx);
38223
- const transport = new StdioServerTransport();
38224
- await server.connect(transport);
38645
+ deps: ctx,
38646
+ tools: [
38647
+ registerAuthTools,
38648
+ registerSyncTools,
38649
+ registerQueryTools,
38650
+ registerSqlTools
38651
+ ]
38652
+ });
38225
38653
  }
38226
38654
  main().catch(console.error);