creditkarma-mcp 2.2.0 → 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/auth.js +7 -25
- package/dist/bundle.js +581 -153
- package/dist/client.js +49 -3
- package/dist/index.js +24 -42
- package/dist/tools/auth.js +2 -1
- package/dist/tools/query.js +6 -5
- package/dist/tools/sql.js +2 -1
- package/dist/tools/sync.js +2 -1
- package/package.json +4 -3
- package/server.json +2 -2
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(
|
|
7398
|
-
this[kBuffers].push(
|
|
7399
|
-
this[kTotalLength] +=
|
|
7397
|
+
function deflateOnData(chunk2) {
|
|
7398
|
+
this[kBuffers].push(chunk2);
|
|
7399
|
+
this[kTotalLength] += chunk2.length;
|
|
7400
7400
|
}
|
|
7401
|
-
function inflateOnData(
|
|
7402
|
-
this[kTotalLength] +=
|
|
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(
|
|
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(
|
|
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 +=
|
|
7719
|
-
this._buffers.push(
|
|
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
|
|
9994
|
-
websocket._receiver.write(
|
|
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(
|
|
10007
|
-
if (!this[kWebSocket]._receiver.write(
|
|
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(
|
|
10110
|
+
duplex._write = function(chunk2, encoding, callback) {
|
|
10111
10111
|
if (ws.readyState === ws.CONNECTING) {
|
|
10112
10112
|
ws.once("open", function open() {
|
|
10113
|
-
duplex._write(
|
|
10113
|
+
duplex._write(chunk2, encoding, callback);
|
|
10114
10114
|
});
|
|
10115
10115
|
return;
|
|
10116
10116
|
}
|
|
10117
|
-
ws.send(
|
|
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(
|
|
34550
|
-
this._buffer = this._buffer ? Buffer.concat([this._buffer,
|
|
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 = (
|
|
34583
|
-
this._readBuffer.append(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
35198
|
-
throw new ProtocolError(`${label}[${i}].
|
|
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.
|
|
35204
|
-
throw new ProtocolError(`${label}[${i}].
|
|
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
|
-
|
|
35213
|
-
const key = `${entry.
|
|
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({
|
|
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 !== "
|
|
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
|
-
|
|
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.
|
|
35562
|
-
throw new ProtocolError("inner.init.
|
|
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.
|
|
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 !== "
|
|
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
|
-
|
|
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(() =>
|
|
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}
|
|
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
|
-
|
|
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(() =>
|
|
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(() =>
|
|
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
|
-
|
|
36744
|
-
if (
|
|
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 `
|
|
37106
|
-
* match, removes itself, and resolves with the
|
|
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
|
-
* `(
|
|
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?.
|
|
37121
|
-
const found = decls.find((d) => d.
|
|
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: (
|
|
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?.
|
|
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 {
|
|
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.
|
|
37132
|
-
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"@
|
|
37680
|
-
"@fetchproxy/
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
38619
|
+
const dbPath = readEnvVar("CK_DB_PATH") || join4(homedir2(), ".creditkarma-mcp", "transactions.db");
|
|
38192
38620
|
const mcpJsonPath = join4(__dirname, "..", ".mcp.json");
|
|
38193
|
-
const cookies =
|
|
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
|
-
|
|
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
|
-
|
|
38216
|
-
|
|
38641
|
+
await runMcp({
|
|
38642
|
+
name: "creditkarma-mcp",
|
|
38643
|
+
version: "2.2.2",
|
|
38217
38644
|
// x-release-please-version
|
|
38218
|
-
|
|
38219
|
-
|
|
38220
|
-
|
|
38221
|
-
|
|
38222
|
-
|
|
38223
|
-
|
|
38224
|
-
|
|
38645
|
+
deps: ctx,
|
|
38646
|
+
tools: [
|
|
38647
|
+
registerAuthTools,
|
|
38648
|
+
registerSyncTools,
|
|
38649
|
+
registerQueryTools,
|
|
38650
|
+
registerSqlTools
|
|
38651
|
+
]
|
|
38652
|
+
});
|
|
38225
38653
|
}
|
|
38226
38654
|
main().catch(console.error);
|