ofw-mcp 2.3.0 → 2.3.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/auth.js +9 -16
- package/dist/bundle.js +814 -224
- package/dist/cache.js +20 -1
- package/dist/client.js +73 -59
- package/dist/config.js +6 -4
- package/dist/index.js +20 -11
- package/dist/tools/_shared.js +9 -12
- package/dist/tools/messages.js +6 -4
- package/package.json +4 -3
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -10342,10 +10342,10 @@ var require_websocket_server = __commonJS({
|
|
|
10342
10342
|
process.nextTick(emitClose, this);
|
|
10343
10343
|
}
|
|
10344
10344
|
} else {
|
|
10345
|
-
const
|
|
10345
|
+
const server = this._server;
|
|
10346
10346
|
this._removeListeners();
|
|
10347
10347
|
this._removeListeners = this._server = null;
|
|
10348
|
-
|
|
10348
|
+
server.close(() => {
|
|
10349
10349
|
emitClose(this);
|
|
10350
10350
|
});
|
|
10351
10351
|
}
|
|
@@ -10530,17 +10530,17 @@ var require_websocket_server = __commonJS({
|
|
|
10530
10530
|
}
|
|
10531
10531
|
};
|
|
10532
10532
|
module.exports = WebSocketServer2;
|
|
10533
|
-
function addListeners(
|
|
10534
|
-
for (const event of Object.keys(map2))
|
|
10533
|
+
function addListeners(server, map2) {
|
|
10534
|
+
for (const event of Object.keys(map2)) server.on(event, map2[event]);
|
|
10535
10535
|
return function removeListeners() {
|
|
10536
10536
|
for (const event of Object.keys(map2)) {
|
|
10537
|
-
|
|
10537
|
+
server.removeListener(event, map2[event]);
|
|
10538
10538
|
}
|
|
10539
10539
|
};
|
|
10540
10540
|
}
|
|
10541
|
-
function emitClose(
|
|
10542
|
-
|
|
10543
|
-
|
|
10541
|
+
function emitClose(server) {
|
|
10542
|
+
server._state = CLOSED;
|
|
10543
|
+
server.emit("close");
|
|
10544
10544
|
}
|
|
10545
10545
|
function socketOnError() {
|
|
10546
10546
|
this.destroy();
|
|
@@ -10559,11 +10559,11 @@ var require_websocket_server = __commonJS({
|
|
|
10559
10559
|
` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
|
|
10560
10560
|
);
|
|
10561
10561
|
}
|
|
10562
|
-
function abortHandshakeOrEmitwsClientError(
|
|
10563
|
-
if (
|
|
10562
|
+
function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) {
|
|
10563
|
+
if (server.listenerCount("wsClientError")) {
|
|
10564
10564
|
const err = new Error(message);
|
|
10565
10565
|
Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
|
|
10566
|
-
|
|
10566
|
+
server.emit("wsClientError", err, socket, req);
|
|
10567
10567
|
} else {
|
|
10568
10568
|
abortHandshake(socket, code, message, headers);
|
|
10569
10569
|
}
|
|
@@ -32189,11 +32189,11 @@ var Protocol = class {
|
|
|
32189
32189
|
*
|
|
32190
32190
|
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
32191
32191
|
*/
|
|
32192
|
-
async connect(
|
|
32192
|
+
async connect(transport) {
|
|
32193
32193
|
if (this._transport) {
|
|
32194
32194
|
throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
|
|
32195
32195
|
}
|
|
32196
|
-
this._transport =
|
|
32196
|
+
this._transport = transport;
|
|
32197
32197
|
const _onclose = this.transport?.onclose;
|
|
32198
32198
|
this._transport.onclose = () => {
|
|
32199
32199
|
_onclose?.();
|
|
@@ -33783,8 +33783,8 @@ var McpServer = class {
|
|
|
33783
33783
|
*
|
|
33784
33784
|
* The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
33785
33785
|
*/
|
|
33786
|
-
async connect(
|
|
33787
|
-
return await this.server.connect(
|
|
33786
|
+
async connect(transport) {
|
|
33787
|
+
return await this.server.connect(transport);
|
|
33788
33788
|
}
|
|
33789
33789
|
/**
|
|
33790
33790
|
* Closes the connection.
|
|
@@ -34634,8 +34634,247 @@ var StdioServerTransport = class {
|
|
|
34634
34634
|
}
|
|
34635
34635
|
};
|
|
34636
34636
|
|
|
34637
|
+
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
34638
|
+
async function createMcpServer(opts) {
|
|
34639
|
+
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
34640
|
+
if (opts.banner !== void 0) {
|
|
34641
|
+
console.error(opts.banner);
|
|
34642
|
+
}
|
|
34643
|
+
const deps = opts.deps;
|
|
34644
|
+
for (const register of opts.tools) {
|
|
34645
|
+
await register(server, deps);
|
|
34646
|
+
}
|
|
34647
|
+
return server;
|
|
34648
|
+
}
|
|
34649
|
+
function withGracefulShutdown(server, opts = {}) {
|
|
34650
|
+
const shouldExit = opts.exit ?? true;
|
|
34651
|
+
let shuttingDown = false;
|
|
34652
|
+
const handler = (signal) => {
|
|
34653
|
+
if (shuttingDown)
|
|
34654
|
+
return;
|
|
34655
|
+
shuttingDown = true;
|
|
34656
|
+
void (async () => {
|
|
34657
|
+
try {
|
|
34658
|
+
if (opts.onSignal)
|
|
34659
|
+
await opts.onSignal(signal);
|
|
34660
|
+
await server.close();
|
|
34661
|
+
} catch (err) {
|
|
34662
|
+
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
34663
|
+
} finally {
|
|
34664
|
+
if (shouldExit)
|
|
34665
|
+
process.exit(0);
|
|
34666
|
+
}
|
|
34667
|
+
})();
|
|
34668
|
+
};
|
|
34669
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
34670
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
34671
|
+
}
|
|
34672
|
+
async function runMcp(opts) {
|
|
34673
|
+
const server = await createMcpServer(opts);
|
|
34674
|
+
const shutdown = opts.shutdown ?? true;
|
|
34675
|
+
if (shutdown !== false) {
|
|
34676
|
+
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
34677
|
+
}
|
|
34678
|
+
const spec = opts.transport ?? "stdio";
|
|
34679
|
+
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
34680
|
+
await server.connect(transport);
|
|
34681
|
+
return server;
|
|
34682
|
+
}
|
|
34683
|
+
|
|
34684
|
+
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34685
|
+
var API_KEY_RE = new RegExp([
|
|
34686
|
+
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
34687
|
+
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
34688
|
+
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
34689
|
+
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
34690
|
+
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
34691
|
+
// Slack
|
|
34692
|
+
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
34693
|
+
// Google API key (39 chars total)
|
|
34694
|
+
"AKIA[0-9A-Z]{16}\\b",
|
|
34695
|
+
// AWS access key id (20 chars total)
|
|
34696
|
+
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
34697
|
+
// webhook signing secret (Stripe-style)
|
|
34698
|
+
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34699
|
+
|
|
34700
|
+
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34701
|
+
function textResult(data) {
|
|
34702
|
+
return {
|
|
34703
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
34704
|
+
};
|
|
34705
|
+
}
|
|
34706
|
+
function rawTextResult(text) {
|
|
34707
|
+
return { content: [{ type: "text", text }] };
|
|
34708
|
+
}
|
|
34709
|
+
|
|
34710
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34711
|
+
import { homedir } from "node:os";
|
|
34712
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
34713
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
34714
|
+
function readEnvVar(key, opts = {}) {
|
|
34715
|
+
const env = opts.env ?? process.env;
|
|
34716
|
+
const raw = env[key];
|
|
34717
|
+
if (typeof raw === "string") {
|
|
34718
|
+
const trimmed = raw.trim();
|
|
34719
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
34720
|
+
return trimmed;
|
|
34721
|
+
}
|
|
34722
|
+
}
|
|
34723
|
+
return opts.default;
|
|
34724
|
+
}
|
|
34725
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
34726
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
34727
|
+
function parseBoolEnv(key, opts = {}) {
|
|
34728
|
+
const fallback = opts.default ?? false;
|
|
34729
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
34730
|
+
if (raw === void 0)
|
|
34731
|
+
return fallback;
|
|
34732
|
+
const token = raw.toLowerCase();
|
|
34733
|
+
if (TRUE_TOKENS.has(token))
|
|
34734
|
+
return true;
|
|
34735
|
+
if (FALSE_TOKENS.has(token))
|
|
34736
|
+
return false;
|
|
34737
|
+
return fallback;
|
|
34738
|
+
}
|
|
34739
|
+
function expandPath(p) {
|
|
34740
|
+
let expanded = p;
|
|
34741
|
+
if (p === "~") {
|
|
34742
|
+
expanded = homedir();
|
|
34743
|
+
} else if (p.startsWith("~/")) {
|
|
34744
|
+
expanded = join(homedir(), p.slice(2));
|
|
34745
|
+
}
|
|
34746
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
34747
|
+
}
|
|
34748
|
+
async function loadDotenvSafely(opts = {}) {
|
|
34749
|
+
try {
|
|
34750
|
+
const mod = await import(
|
|
34751
|
+
/* @vite-ignore */
|
|
34752
|
+
"dotenv"
|
|
34753
|
+
);
|
|
34754
|
+
const result = mod.config({
|
|
34755
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
34756
|
+
override: opts.override ?? false,
|
|
34757
|
+
quiet: true
|
|
34758
|
+
});
|
|
34759
|
+
return result.error === void 0;
|
|
34760
|
+
} catch {
|
|
34761
|
+
return false;
|
|
34762
|
+
}
|
|
34763
|
+
}
|
|
34764
|
+
|
|
34765
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
34766
|
+
import { openAsBlob } from "node:fs";
|
|
34767
|
+
async function fileBlob(path, opts = {}) {
|
|
34768
|
+
let blob;
|
|
34769
|
+
try {
|
|
34770
|
+
blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
|
|
34771
|
+
} catch {
|
|
34772
|
+
throw new Error(`Cannot read file for upload: ${path}`);
|
|
34773
|
+
}
|
|
34774
|
+
if (opts.maxBytes !== void 0 && blob.size > opts.maxBytes) {
|
|
34775
|
+
throw new Error(`${opts.label ?? "File"} is ${blob.size} bytes, over the ${opts.maxBytes}-byte limit: ${path}`);
|
|
34776
|
+
}
|
|
34777
|
+
return blob;
|
|
34778
|
+
}
|
|
34779
|
+
|
|
34780
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
34781
|
+
var PositiveInt = external_exports.number().int().positive();
|
|
34782
|
+
var NonNegInt = external_exports.number().int().nonnegative();
|
|
34783
|
+
var NonEmptyString = external_exports.string().min(1);
|
|
34784
|
+
var IsoDate = external_exports.iso.date();
|
|
34785
|
+
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
34786
|
+
var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
|
|
34787
|
+
var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
|
|
34788
|
+
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.");
|
|
34789
|
+
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
34790
|
+
var paginationSchema = {
|
|
34791
|
+
offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
|
|
34792
|
+
limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
|
|
34793
|
+
};
|
|
34794
|
+
var pageSchema = {
|
|
34795
|
+
page_num: PositiveInt.default(1).describe("1-based page number."),
|
|
34796
|
+
page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
|
|
34797
|
+
};
|
|
34798
|
+
|
|
34799
|
+
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
34800
|
+
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
34801
|
+
var TokenManager = class {
|
|
34802
|
+
accessToken;
|
|
34803
|
+
refreshToken;
|
|
34804
|
+
expiresAt;
|
|
34805
|
+
refreshFn;
|
|
34806
|
+
skewMs;
|
|
34807
|
+
inFlight;
|
|
34808
|
+
constructor(opts) {
|
|
34809
|
+
this.accessToken = opts.initial.accessToken;
|
|
34810
|
+
this.refreshToken = opts.initial.refreshToken;
|
|
34811
|
+
this.expiresAt = opts.initial.expiresAt;
|
|
34812
|
+
this.refreshFn = opts.refresh;
|
|
34813
|
+
this.skewMs = opts.skewMs ?? TOKEN_REFRESH_SKEW_MS;
|
|
34814
|
+
}
|
|
34815
|
+
/** Whether the token is within the skew window of (or past) expiry. */
|
|
34816
|
+
needsRefresh() {
|
|
34817
|
+
return Date.now() >= this.expiresAt - this.skewMs;
|
|
34818
|
+
}
|
|
34819
|
+
/**
|
|
34820
|
+
* Single-flight refresh. Concurrent callers share one in-flight promise; it is
|
|
34821
|
+
* cleared on settle (success or failure) so a subsequent refresh can proceed.
|
|
34822
|
+
*/
|
|
34823
|
+
refreshNow() {
|
|
34824
|
+
if (!this.inFlight) {
|
|
34825
|
+
const rt = this.refreshToken;
|
|
34826
|
+
if (rt === void 0) {
|
|
34827
|
+
return Promise.reject(new Error("TokenManager: cannot refresh \u2014 no refresh token is available."));
|
|
34828
|
+
}
|
|
34829
|
+
this.inFlight = (async () => {
|
|
34830
|
+
const tok = await this.refreshFn(rt);
|
|
34831
|
+
this.accessToken = tok.accessToken;
|
|
34832
|
+
if (tok.refreshToken !== void 0 && tok.refreshToken !== "") {
|
|
34833
|
+
this.refreshToken = tok.refreshToken;
|
|
34834
|
+
}
|
|
34835
|
+
this.expiresAt = tok.expiresAt;
|
|
34836
|
+
})().finally(() => {
|
|
34837
|
+
this.inFlight = void 0;
|
|
34838
|
+
});
|
|
34839
|
+
}
|
|
34840
|
+
return this.inFlight;
|
|
34841
|
+
}
|
|
34842
|
+
/** Get a valid access token, refreshing proactively inside the skew window. */
|
|
34843
|
+
async getAccessToken() {
|
|
34844
|
+
if (this.needsRefresh())
|
|
34845
|
+
await this.refreshNow();
|
|
34846
|
+
return this.accessToken;
|
|
34847
|
+
}
|
|
34848
|
+
/** Current absolute expiry (epoch ms). */
|
|
34849
|
+
getExpiresAt() {
|
|
34850
|
+
return this.expiresAt;
|
|
34851
|
+
}
|
|
34852
|
+
/**
|
|
34853
|
+
* Run an authenticated request with reactive 401-replay. `call` receives a
|
|
34854
|
+
* valid access token and returns a `Response`. On `401`, the token is
|
|
34855
|
+
* refreshed once and `call` is invoked again exactly once.
|
|
34856
|
+
*
|
|
34857
|
+
* Guarded against double-refresh: if a concurrent caller already rotated the
|
|
34858
|
+
* token while this request was in flight (single-flight settled and cleared),
|
|
34859
|
+
* a late `401` from a request sent under the OLD token does NOT trigger a
|
|
34860
|
+
* second refresh — under refresh-token rotation that would consume and
|
|
34861
|
+
* invalidate the freshly-issued refresh token. It just replays with the
|
|
34862
|
+
* current token.
|
|
34863
|
+
*/
|
|
34864
|
+
async withAuth(call) {
|
|
34865
|
+
const usedToken = await this.getAccessToken();
|
|
34866
|
+
let res = await call(usedToken);
|
|
34867
|
+
if (res.status === 401) {
|
|
34868
|
+
if (this.accessToken === usedToken)
|
|
34869
|
+
await this.refreshNow();
|
|
34870
|
+
res = await call(this.accessToken);
|
|
34871
|
+
}
|
|
34872
|
+
return res;
|
|
34873
|
+
}
|
|
34874
|
+
};
|
|
34875
|
+
|
|
34637
34876
|
// src/client.ts
|
|
34638
|
-
import { dirname, join as
|
|
34877
|
+
import { dirname, join as join4 } from "path";
|
|
34639
34878
|
import { fileURLToPath } from "url";
|
|
34640
34879
|
|
|
34641
34880
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
@@ -34647,7 +34886,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34647
34886
|
"read_local_storage",
|
|
34648
34887
|
"read_session_storage",
|
|
34649
34888
|
"capture_request_header",
|
|
34650
|
-
"
|
|
34889
|
+
"capture_redirect",
|
|
34890
|
+
"read_indexed_db",
|
|
34891
|
+
"download"
|
|
34651
34892
|
]);
|
|
34652
34893
|
|
|
34653
34894
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -34794,6 +35035,16 @@ function assertScopeKeyArray(value, label) {
|
|
|
34794
35035
|
seen.add(k);
|
|
34795
35036
|
}
|
|
34796
35037
|
}
|
|
35038
|
+
var CAPTURE_PATH_RE = /^\/[A-Za-z0-9._~%\-/]*\*?$/;
|
|
35039
|
+
function hostMatchesAnyDomain(host, domains) {
|
|
35040
|
+
const h = host.toLowerCase();
|
|
35041
|
+
for (const d of domains) {
|
|
35042
|
+
const dom = d.toLowerCase();
|
|
35043
|
+
if (h === dom || h.endsWith("." + dom))
|
|
35044
|
+
return true;
|
|
35045
|
+
}
|
|
35046
|
+
return false;
|
|
35047
|
+
}
|
|
34797
35048
|
function assertCaptureHeadersArray(value, label) {
|
|
34798
35049
|
if (!Array.isArray(value)) {
|
|
34799
35050
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -34802,14 +35053,25 @@ function assertCaptureHeadersArray(value, label) {
|
|
|
34802
35053
|
for (let i = 0; i < value.length; i++) {
|
|
34803
35054
|
const entry = value[i];
|
|
34804
35055
|
assertObject(entry, `${label}[${i}]`);
|
|
34805
|
-
if (entry.
|
|
34806
|
-
throw new ProtocolError(`${label}[${i}].
|
|
35056
|
+
if (entry.host === void 0) {
|
|
35057
|
+
throw new ProtocolError(`${label}[${i}].host: missing`);
|
|
34807
35058
|
}
|
|
34808
35059
|
if (entry.headerName === void 0) {
|
|
34809
35060
|
throw new ProtocolError(`${label}[${i}].headerName: missing`);
|
|
34810
35061
|
}
|
|
34811
|
-
if (typeof entry.
|
|
34812
|
-
throw new ProtocolError(`${label}[${i}].
|
|
35062
|
+
if (typeof entry.host !== "string") {
|
|
35063
|
+
throw new ProtocolError(`${label}[${i}].host: expected string, got ${typeof entry.host}`);
|
|
35064
|
+
}
|
|
35065
|
+
if (!HOSTNAME_RE.test(entry.host)) {
|
|
35066
|
+
throw new ProtocolError(`${label}[${i}].host: invalid hostname ${JSON.stringify(entry.host)}`);
|
|
35067
|
+
}
|
|
35068
|
+
if (entry.path !== void 0) {
|
|
35069
|
+
if (typeof entry.path !== "string") {
|
|
35070
|
+
throw new ProtocolError(`${label}[${i}].path: expected string, got ${typeof entry.path}`);
|
|
35071
|
+
}
|
|
35072
|
+
if (!CAPTURE_PATH_RE.test(entry.path)) {
|
|
35073
|
+
throw new ProtocolError(`${label}[${i}].path: must start with '/' ${JSON.stringify(entry.path)}`);
|
|
35074
|
+
}
|
|
34813
35075
|
}
|
|
34814
35076
|
if (typeof entry.headerName !== "string") {
|
|
34815
35077
|
throw new ProtocolError(`${label}[${i}].headerName: expected string, got ${typeof entry.headerName}`);
|
|
@@ -34817,19 +35079,29 @@ function assertCaptureHeadersArray(value, label) {
|
|
|
34817
35079
|
if (!HEADER_NAME_RE.test(entry.headerName)) {
|
|
34818
35080
|
throw new ProtocolError(`${label}[${i}].headerName: invalid name ${JSON.stringify(entry.headerName)}`);
|
|
34819
35081
|
}
|
|
34820
|
-
|
|
34821
|
-
const key = `${entry.
|
|
35082
|
+
const normalizedPath = entry.path ?? "/*";
|
|
35083
|
+
const key = `${entry.host}\0${normalizedPath}\0${entry.headerName}`;
|
|
34822
35084
|
if (seen.has(key)) {
|
|
34823
|
-
throw new ProtocolError(`${label}: duplicate ${JSON.stringify({
|
|
35085
|
+
throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ host: entry.host, path: normalizedPath, headerName: entry.headerName })}`);
|
|
34824
35086
|
}
|
|
34825
35087
|
seen.add(key);
|
|
34826
35088
|
for (const k of Object.keys(entry)) {
|
|
34827
|
-
if (k !== "
|
|
35089
|
+
if (k !== "host" && k !== "path" && k !== "headerName") {
|
|
34828
35090
|
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
34829
35091
|
}
|
|
34830
35092
|
}
|
|
34831
35093
|
}
|
|
34832
35094
|
}
|
|
35095
|
+
function validateCaptureHeaderDecls(value, domains, label = "captureHeaders") {
|
|
35096
|
+
assertCaptureHeadersArray(value, label);
|
|
35097
|
+
const arr = value;
|
|
35098
|
+
for (let i = 0; i < arr.length; i++) {
|
|
35099
|
+
const host = arr[i].host;
|
|
35100
|
+
if (!hostMatchesAnyDomain(host, domains)) {
|
|
35101
|
+
throw new ProtocolError(`${label}[${i}].host: ${JSON.stringify(host)} is not a declared domain or subdomain of [${domains.join(", ")}]`);
|
|
35102
|
+
}
|
|
35103
|
+
}
|
|
35104
|
+
}
|
|
34833
35105
|
function assertStoragePointersArray(value, label, declaredKeys) {
|
|
34834
35106
|
if (!Array.isArray(value)) {
|
|
34835
35107
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -34917,23 +35189,6 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
34917
35189
|
}
|
|
34918
35190
|
}
|
|
34919
35191
|
}
|
|
34920
|
-
function assertCaptureUrlPattern(pattern, label) {
|
|
34921
|
-
if (!pattern.startsWith("https://")) {
|
|
34922
|
-
throw new ProtocolError(`${label}: must start with https:// (got ${JSON.stringify(pattern)})`);
|
|
34923
|
-
}
|
|
34924
|
-
const afterScheme = pattern.slice("https://".length);
|
|
34925
|
-
const slash = afterScheme.indexOf("/");
|
|
34926
|
-
const host = slash === -1 ? afterScheme : afterScheme.slice(0, slash);
|
|
34927
|
-
if (host.length === 0) {
|
|
34928
|
-
throw new ProtocolError(`${label}: missing host (got ${JSON.stringify(pattern)})`);
|
|
34929
|
-
}
|
|
34930
|
-
if (host.includes("*")) {
|
|
34931
|
-
throw new ProtocolError(`${label}: wildcards not permitted in host (got ${JSON.stringify(pattern)})`);
|
|
34932
|
-
}
|
|
34933
|
-
if (!HOSTNAME_RE.test(host)) {
|
|
34934
|
-
throw new ProtocolError(`${label}: invalid host ${JSON.stringify(host)} in ${JSON.stringify(pattern)}`);
|
|
34935
|
-
}
|
|
34936
|
-
}
|
|
34937
35192
|
function validateFrame(raw) {
|
|
34938
35193
|
assertObject(raw, "frame");
|
|
34939
35194
|
const t = raw.type;
|
|
@@ -34998,7 +35253,7 @@ function validateHello(raw) {
|
|
|
34998
35253
|
assertScopeKeyArray(raw.sessionStorageKeys, "hello.sessionStorageKeys");
|
|
34999
35254
|
}
|
|
35000
35255
|
if (raw.captureHeaders !== void 0) {
|
|
35001
|
-
|
|
35256
|
+
validateCaptureHeaderDecls(raw.captureHeaders, raw.domains, "hello.captureHeaders");
|
|
35002
35257
|
}
|
|
35003
35258
|
if (raw.indexedDbScopes !== void 0) {
|
|
35004
35259
|
assertIndexedDbScopesArray(raw.indexedDbScopes, "hello.indexedDbScopes");
|
|
@@ -35166,24 +35421,58 @@ function validateInnerRequest(raw) {
|
|
|
35166
35421
|
}
|
|
35167
35422
|
if (raw.op === "capture_request_header") {
|
|
35168
35423
|
assertObject(raw.init, "inner.init");
|
|
35169
|
-
if (raw.init.
|
|
35170
|
-
throw new ProtocolError("inner.init.
|
|
35424
|
+
if (raw.init.host === void 0) {
|
|
35425
|
+
throw new ProtocolError("inner.init.host: missing");
|
|
35171
35426
|
}
|
|
35172
35427
|
if (raw.init.headerName === void 0) {
|
|
35173
35428
|
throw new ProtocolError("inner.init.headerName: missing");
|
|
35174
35429
|
}
|
|
35175
|
-
assertString(raw.init.
|
|
35430
|
+
assertString(raw.init.host, "inner.init.host");
|
|
35431
|
+
if (!HOSTNAME_RE.test(raw.init.host)) {
|
|
35432
|
+
throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
|
|
35433
|
+
}
|
|
35434
|
+
if (raw.init.path !== void 0) {
|
|
35435
|
+
assertString(raw.init.path, "inner.init.path");
|
|
35436
|
+
if (!CAPTURE_PATH_RE.test(raw.init.path)) {
|
|
35437
|
+
throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
|
|
35438
|
+
}
|
|
35439
|
+
}
|
|
35176
35440
|
assertString(raw.init.headerName, "inner.init.headerName");
|
|
35177
35441
|
if (raw.init.timeoutMs !== void 0) {
|
|
35178
35442
|
assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
|
|
35179
35443
|
}
|
|
35180
35444
|
for (const k of Object.keys(raw.init)) {
|
|
35181
|
-
if (k !== "
|
|
35445
|
+
if (k !== "host" && k !== "path" && k !== "headerName" && k !== "timeoutMs") {
|
|
35182
35446
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_request_header`);
|
|
35183
35447
|
}
|
|
35184
35448
|
}
|
|
35185
35449
|
return raw;
|
|
35186
35450
|
}
|
|
35451
|
+
if (raw.op === "capture_redirect") {
|
|
35452
|
+
assertObject(raw.init, "inner.init");
|
|
35453
|
+
if (raw.init.host === void 0) {
|
|
35454
|
+
throw new ProtocolError("inner.init.host: missing");
|
|
35455
|
+
}
|
|
35456
|
+
assertString(raw.init.host, "inner.init.host");
|
|
35457
|
+
if (!HOSTNAME_RE.test(raw.init.host)) {
|
|
35458
|
+
throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
|
|
35459
|
+
}
|
|
35460
|
+
if (raw.init.path !== void 0) {
|
|
35461
|
+
assertString(raw.init.path, "inner.init.path");
|
|
35462
|
+
if (!CAPTURE_PATH_RE.test(raw.init.path)) {
|
|
35463
|
+
throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
|
|
35464
|
+
}
|
|
35465
|
+
}
|
|
35466
|
+
if (raw.init.timeoutMs !== void 0) {
|
|
35467
|
+
assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
|
|
35468
|
+
}
|
|
35469
|
+
for (const k of Object.keys(raw.init)) {
|
|
35470
|
+
if (k !== "host" && k !== "path" && k !== "timeoutMs") {
|
|
35471
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_redirect`);
|
|
35472
|
+
}
|
|
35473
|
+
}
|
|
35474
|
+
return raw;
|
|
35475
|
+
}
|
|
35187
35476
|
if (raw.op === "read_indexed_db") {
|
|
35188
35477
|
assertObject(raw.init, "inner.init");
|
|
35189
35478
|
if (raw.init.origin === void 0)
|
|
@@ -35209,7 +35498,32 @@ function validateInnerRequest(raw) {
|
|
|
35209
35498
|
}
|
|
35210
35499
|
return raw;
|
|
35211
35500
|
}
|
|
35212
|
-
|
|
35501
|
+
if (raw.op === "download") {
|
|
35502
|
+
assertObject(raw.init, "inner.init");
|
|
35503
|
+
if (raw.init.url === void 0) {
|
|
35504
|
+
throw new ProtocolError("inner.init.url: missing");
|
|
35505
|
+
}
|
|
35506
|
+
assertHttpUrl(raw.init.url, "inner.init.url");
|
|
35507
|
+
if (raw.init.filename !== void 0) {
|
|
35508
|
+
assertString(raw.init.filename, "inner.init.filename");
|
|
35509
|
+
if (/^([/\\]|[A-Za-z]:)/.test(raw.init.filename)) {
|
|
35510
|
+
throw new ProtocolError(`inner.init.filename: must be relative ${JSON.stringify(raw.init.filename)}`);
|
|
35511
|
+
}
|
|
35512
|
+
if (raw.init.filename.split(/[/\\]/).includes("..")) {
|
|
35513
|
+
throw new ProtocolError(`inner.init.filename: must not contain '..' ${JSON.stringify(raw.init.filename)}`);
|
|
35514
|
+
}
|
|
35515
|
+
}
|
|
35516
|
+
if (raw.init.timeoutMs !== void 0) {
|
|
35517
|
+
assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
|
|
35518
|
+
}
|
|
35519
|
+
for (const k of Object.keys(raw.init)) {
|
|
35520
|
+
if (k !== "url" && k !== "filename" && k !== "timeoutMs") {
|
|
35521
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on download`);
|
|
35522
|
+
}
|
|
35523
|
+
}
|
|
35524
|
+
return raw;
|
|
35525
|
+
}
|
|
35526
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35213
35527
|
}
|
|
35214
35528
|
function assertNonEmptyKeyArray(value, label) {
|
|
35215
35529
|
if (!Array.isArray(value)) {
|
|
@@ -35280,6 +35594,13 @@ function validateInnerResponse(raw) {
|
|
|
35280
35594
|
assertString(raw.value, "inner.value");
|
|
35281
35595
|
return raw;
|
|
35282
35596
|
}
|
|
35597
|
+
if (op === "capture_redirect") {
|
|
35598
|
+
if (raw.value === void 0) {
|
|
35599
|
+
throw new ProtocolError("inner.value: missing on capture_redirect response");
|
|
35600
|
+
}
|
|
35601
|
+
assertString(raw.value, "inner.value");
|
|
35602
|
+
return raw;
|
|
35603
|
+
}
|
|
35283
35604
|
if (op === "read_indexed_db") {
|
|
35284
35605
|
if (raw.values === void 0) {
|
|
35285
35606
|
throw new ProtocolError("inner.values: missing on read_indexed_db response");
|
|
@@ -35287,6 +35608,25 @@ function validateInnerResponse(raw) {
|
|
|
35287
35608
|
assertObject(raw.values, "inner.values");
|
|
35288
35609
|
return raw;
|
|
35289
35610
|
}
|
|
35611
|
+
if (op === "download") {
|
|
35612
|
+
assertObject(raw.value, "inner.value");
|
|
35613
|
+
assertString(raw.value.path, "inner.value.path");
|
|
35614
|
+
if (typeof raw.value.bytes !== "number" || !Number.isInteger(raw.value.bytes) || raw.value.bytes < 0) {
|
|
35615
|
+
throw new ProtocolError("inner.value.bytes: expected non-negative integer");
|
|
35616
|
+
}
|
|
35617
|
+
if (raw.value.mime !== void 0) {
|
|
35618
|
+
assertString(raw.value.mime, "inner.value.mime");
|
|
35619
|
+
}
|
|
35620
|
+
if (raw.value.finalUrl !== void 0) {
|
|
35621
|
+
assertString(raw.value.finalUrl, "inner.value.finalUrl");
|
|
35622
|
+
}
|
|
35623
|
+
for (const k of Object.keys(raw.value)) {
|
|
35624
|
+
if (k !== "path" && k !== "bytes" && k !== "mime" && k !== "finalUrl") {
|
|
35625
|
+
throw new ProtocolError(`inner.value: unexpected field ${JSON.stringify(k)} on download response`);
|
|
35626
|
+
}
|
|
35627
|
+
}
|
|
35628
|
+
return raw;
|
|
35629
|
+
}
|
|
35290
35630
|
throw new ProtocolError(`inner.op: unknown success-response op ${JSON.stringify(raw.op)}`);
|
|
35291
35631
|
}
|
|
35292
35632
|
if (raw.ok === false) {
|
|
@@ -35494,13 +35834,13 @@ async function openEncryptedFrame(sessionKey, frame) {
|
|
|
35494
35834
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
35495
35835
|
import { createServer, Server as HttpServer } from "node:http";
|
|
35496
35836
|
async function electRole(opts) {
|
|
35497
|
-
const
|
|
35837
|
+
const server = createServer();
|
|
35498
35838
|
return new Promise((resolve2, reject) => {
|
|
35499
35839
|
const onError = (e) => {
|
|
35500
|
-
|
|
35840
|
+
server.removeListener("listening", onListening);
|
|
35501
35841
|
if (e.code === "EADDRINUSE") {
|
|
35502
35842
|
try {
|
|
35503
|
-
|
|
35843
|
+
server.close();
|
|
35504
35844
|
} catch {
|
|
35505
35845
|
}
|
|
35506
35846
|
resolve2({ role: "peer" });
|
|
@@ -35509,12 +35849,12 @@ async function electRole(opts) {
|
|
|
35509
35849
|
}
|
|
35510
35850
|
};
|
|
35511
35851
|
const onListening = () => {
|
|
35512
|
-
|
|
35513
|
-
resolve2({ role: "host", server
|
|
35852
|
+
server.removeListener("error", onError);
|
|
35853
|
+
resolve2({ role: "host", server });
|
|
35514
35854
|
};
|
|
35515
|
-
|
|
35516
|
-
|
|
35517
|
-
|
|
35855
|
+
server.once("error", onError);
|
|
35856
|
+
server.once("listening", onListening);
|
|
35857
|
+
server.listen(opts.port, opts.host);
|
|
35518
35858
|
});
|
|
35519
35859
|
}
|
|
35520
35860
|
|
|
@@ -35558,7 +35898,8 @@ async function buildServerHello(opts) {
|
|
|
35558
35898
|
}
|
|
35559
35899
|
if (opts.captureHeaders && opts.captureHeaders.length > 0) {
|
|
35560
35900
|
hello.captureHeaders = opts.captureHeaders.map((d) => ({
|
|
35561
|
-
|
|
35901
|
+
host: d.host,
|
|
35902
|
+
...d.path !== void 0 ? { path: d.path } : {},
|
|
35562
35903
|
headerName: d.headerName
|
|
35563
35904
|
}));
|
|
35564
35905
|
}
|
|
@@ -35796,7 +36137,9 @@ async function startHost(opts) {
|
|
|
35796
36137
|
} catch {
|
|
35797
36138
|
}
|
|
35798
36139
|
}
|
|
35799
|
-
wss.close(() =>
|
|
36140
|
+
wss.close(() => {
|
|
36141
|
+
opts.httpServer.close(() => resolve2());
|
|
36142
|
+
});
|
|
35800
36143
|
}),
|
|
35801
36144
|
sendOwnInner: async (inner) => {
|
|
35802
36145
|
const session = await ownSessionReady;
|
|
@@ -35846,6 +36189,7 @@ async function startPeer(opts) {
|
|
|
35846
36189
|
const innerListeners = [];
|
|
35847
36190
|
const renegotiateListeners = [];
|
|
35848
36191
|
const pendingPairListeners = [];
|
|
36192
|
+
const closeListeners = [];
|
|
35849
36193
|
let session = null;
|
|
35850
36194
|
let pendingPairCode = null;
|
|
35851
36195
|
let resolveFirstReady;
|
|
@@ -35895,6 +36239,7 @@ async function startPeer(opts) {
|
|
|
35895
36239
|
ws.on("message", onMessage);
|
|
35896
36240
|
ws.once("close", () => {
|
|
35897
36241
|
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
36242
|
+
closeListeners.forEach((cb) => cb());
|
|
35898
36243
|
});
|
|
35899
36244
|
sessionPromise.catch(() => {
|
|
35900
36245
|
});
|
|
@@ -35917,6 +36262,9 @@ async function startPeer(opts) {
|
|
|
35917
36262
|
pendingPairListeners.push(cb);
|
|
35918
36263
|
},
|
|
35919
36264
|
pendingPairCode: () => pendingPairCode,
|
|
36265
|
+
onClose: (cb) => {
|
|
36266
|
+
closeListeners.push(cb);
|
|
36267
|
+
},
|
|
35920
36268
|
close: () => ws.close()
|
|
35921
36269
|
};
|
|
35922
36270
|
return handle;
|
|
@@ -35924,19 +36272,19 @@ async function startPeer(opts) {
|
|
|
35924
36272
|
|
|
35925
36273
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
35926
36274
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
35927
|
-
import { join } from "node:path";
|
|
35928
|
-
import { homedir } from "node:os";
|
|
36275
|
+
import { join as join2 } from "node:path";
|
|
36276
|
+
import { homedir as homedir2 } from "node:os";
|
|
35929
36277
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
35930
36278
|
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
35931
36279
|
function defaultIdentityDir() {
|
|
35932
|
-
return
|
|
36280
|
+
return join2(homedir2(), ".fetchproxy", "identity");
|
|
35933
36281
|
}
|
|
35934
36282
|
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
35935
36283
|
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
35936
36284
|
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
35937
36285
|
}
|
|
35938
36286
|
const safeFile = serverName.replace(/\//g, "_");
|
|
35939
|
-
const path =
|
|
36287
|
+
const path = join2(dir, `${safeFile}.json`);
|
|
35940
36288
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
35941
36289
|
try {
|
|
35942
36290
|
const raw = await readFile(path, "utf8");
|
|
@@ -36118,6 +36466,12 @@ var FetchproxyServer = class {
|
|
|
36118
36466
|
opts;
|
|
36119
36467
|
hostHandle = null;
|
|
36120
36468
|
peerHandle = null;
|
|
36469
|
+
// 0.13.0+: true from the start of `close()` until the next `doConnect()`.
|
|
36470
|
+
// Distinguishes an intentional shutdown (whose WS close we must ignore)
|
|
36471
|
+
// from the host process dying (which strands a peer and must trigger
|
|
36472
|
+
// re-election). The WS `close` event fires asynchronously, so this stays
|
|
36473
|
+
// latched across `close()` rather than being reset in its tail.
|
|
36474
|
+
closing = false;
|
|
36121
36475
|
nextRequestId = 1;
|
|
36122
36476
|
// 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
|
|
36123
36477
|
// Replaces the local copies every downstream MCP was rolling on top
|
|
@@ -36143,8 +36497,12 @@ var FetchproxyServer = class {
|
|
|
36143
36497
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
36144
36498
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36145
36499
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36500
|
+
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
36501
|
+
pendingRedirect = /* @__PURE__ */ new Map();
|
|
36146
36502
|
// 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
|
|
36147
36503
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36504
|
+
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
36505
|
+
pendingDownload = /* @__PURE__ */ new Map();
|
|
36148
36506
|
mcpId = null;
|
|
36149
36507
|
identity = null;
|
|
36150
36508
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -36190,6 +36548,14 @@ var FetchproxyServer = class {
|
|
|
36190
36548
|
}
|
|
36191
36549
|
capabilities = [...opts.capabilities];
|
|
36192
36550
|
}
|
|
36551
|
+
if (opts.captureHeaders !== void 0) {
|
|
36552
|
+
try {
|
|
36553
|
+
validateCaptureHeaderDecls(opts.captureHeaders, opts.domains);
|
|
36554
|
+
} catch (err) {
|
|
36555
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36556
|
+
throw new Error("FetchproxyServer: invalid captureHeaders \u2014 " + message);
|
|
36557
|
+
}
|
|
36558
|
+
}
|
|
36193
36559
|
this.opts = {
|
|
36194
36560
|
port: opts.port ?? 37149,
|
|
36195
36561
|
host: opts.host ?? "127.0.0.1",
|
|
@@ -36201,7 +36567,8 @@ var FetchproxyServer = class {
|
|
|
36201
36567
|
localStorageKeys: [...opts.localStorageKeys ?? []],
|
|
36202
36568
|
sessionStorageKeys: [...opts.sessionStorageKeys ?? []],
|
|
36203
36569
|
captureHeaders: (opts.captureHeaders ?? []).map((d) => ({
|
|
36204
|
-
|
|
36570
|
+
host: d.host,
|
|
36571
|
+
...d.path !== void 0 ? { path: d.path } : {},
|
|
36205
36572
|
headerName: d.headerName
|
|
36206
36573
|
})),
|
|
36207
36574
|
indexedDbScopes: (opts.indexedDbScopes ?? []).map((d) => ({
|
|
@@ -36319,6 +36686,7 @@ var FetchproxyServer = class {
|
|
|
36319
36686
|
async doConnect() {
|
|
36320
36687
|
const identity = this.identity;
|
|
36321
36688
|
const mcpId = this.mcpId;
|
|
36689
|
+
this.closing = false;
|
|
36322
36690
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
36323
36691
|
if (el.role === "host") {
|
|
36324
36692
|
this.role = "host";
|
|
@@ -36378,6 +36746,14 @@ var FetchproxyServer = class {
|
|
|
36378
36746
|
const cb = this.opts.onPairCode;
|
|
36379
36747
|
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36380
36748
|
}
|
|
36749
|
+
this.peerHandle.onClose(() => {
|
|
36750
|
+
if (this.closing || this.peerHandle === null)
|
|
36751
|
+
return;
|
|
36752
|
+
this.stopKeepalive();
|
|
36753
|
+
this.rejectAllPending();
|
|
36754
|
+
this.peerHandle = null;
|
|
36755
|
+
this.role = null;
|
|
36756
|
+
});
|
|
36381
36757
|
}
|
|
36382
36758
|
}
|
|
36383
36759
|
pairingErrorMessage(code) {
|
|
@@ -36952,12 +37328,12 @@ var FetchproxyServer = class {
|
|
|
36952
37328
|
/**
|
|
36953
37329
|
* 0.3.0+: snapshot the next outgoing request's named header. Single-
|
|
36954
37330
|
* shot: the extension registers a one-time `webRequest` listener
|
|
36955
|
-
* filtered on `
|
|
36956
|
-
* match, removes itself, and resolves with the
|
|
36957
|
-
* after `timeoutMs` (default 30s on the extension).
|
|
37331
|
+
* filtered on `https://${host}${path ?? '/*'}`, captures the named
|
|
37332
|
+
* header on the first match, removes itself, and resolves with the
|
|
37333
|
+
* value. Times out after `timeoutMs` (default 30s on the extension).
|
|
36958
37334
|
*
|
|
36959
|
-
* `(
|
|
36960
|
-
* `FetchproxyServerOpts.captureHeaders
|
|
37335
|
+
* `(host, path?, headerName)` must match a declared entry in
|
|
37336
|
+
* `FetchproxyServerOpts.captureHeaders` (omitted path ≡ `/*`).
|
|
36961
37337
|
*/
|
|
36962
37338
|
async captureRequestHeader(opts) {
|
|
36963
37339
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
@@ -36966,24 +37342,25 @@ var FetchproxyServer = class {
|
|
|
36966
37342
|
await this.ensureConnected();
|
|
36967
37343
|
this.throwIfPendingPair();
|
|
36968
37344
|
const decls = this.opts.captureHeaders;
|
|
37345
|
+
const normPath = (p) => p ?? "/*";
|
|
36969
37346
|
let resolved;
|
|
36970
|
-
if (opts?.
|
|
36971
|
-
const found = decls.find((d) => d.
|
|
37347
|
+
if (opts?.host !== void 0 && opts?.headerName !== void 0) {
|
|
37348
|
+
const found = decls.find((d) => d.host === opts.host && normPath(d.path) === normPath(opts.path) && d.headerName === opts.headerName);
|
|
36972
37349
|
if (!found) {
|
|
36973
|
-
throw new Error(`FetchproxyServer.captureRequestHeader: (
|
|
37350
|
+
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`);
|
|
36974
37351
|
}
|
|
36975
37352
|
resolved = found;
|
|
36976
|
-
} else if (opts?.
|
|
37353
|
+
} else if (opts?.host === void 0 && opts?.headerName === void 0) {
|
|
36977
37354
|
if (decls.length === 0) {
|
|
36978
|
-
throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {
|
|
37355
|
+
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");
|
|
36979
37356
|
}
|
|
36980
37357
|
if (decls.length > 1) {
|
|
36981
|
-
const list = decls.map((d) => `${JSON.stringify(d.
|
|
36982
|
-
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {
|
|
37358
|
+
const list = decls.map((d) => `${JSON.stringify(d.host)}${JSON.stringify(normPath(d.path))}/${JSON.stringify(d.headerName)}`).join(", ");
|
|
37359
|
+
throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {host, headerName} to disambiguate`);
|
|
36983
37360
|
}
|
|
36984
37361
|
resolved = decls[0];
|
|
36985
37362
|
} else {
|
|
36986
|
-
throw new Error("FetchproxyServer.captureRequestHeader: pass both
|
|
37363
|
+
throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
|
|
36987
37364
|
}
|
|
36988
37365
|
const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
|
|
36989
37366
|
try {
|
|
@@ -37017,7 +37394,7 @@ var FetchproxyServer = class {
|
|
|
37017
37394
|
originalError: retryErr.message,
|
|
37018
37395
|
retryAttempted: true,
|
|
37019
37396
|
op: "capture_request_header",
|
|
37020
|
-
url: resolved.
|
|
37397
|
+
url: `https://${resolved.host}${resolved.path ?? "/*"}`,
|
|
37021
37398
|
role: this.role,
|
|
37022
37399
|
port: this.opts.port
|
|
37023
37400
|
});
|
|
@@ -37028,7 +37405,7 @@ var FetchproxyServer = class {
|
|
|
37028
37405
|
originalError: err.message,
|
|
37029
37406
|
retryAttempted: false,
|
|
37030
37407
|
op: "capture_request_header",
|
|
37031
|
-
url: resolved.
|
|
37408
|
+
url: `https://${resolved.host}${resolved.path ?? "/*"}`,
|
|
37032
37409
|
role: this.role,
|
|
37033
37410
|
port: this.opts.port
|
|
37034
37411
|
});
|
|
@@ -37041,7 +37418,8 @@ var FetchproxyServer = class {
|
|
|
37041
37418
|
id,
|
|
37042
37419
|
op: "capture_request_header",
|
|
37043
37420
|
init: {
|
|
37044
|
-
|
|
37421
|
+
host: opts.host,
|
|
37422
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
37045
37423
|
headerName: opts.headerName,
|
|
37046
37424
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37047
37425
|
}
|
|
@@ -37056,6 +37434,181 @@ var FetchproxyServer = class {
|
|
|
37056
37434
|
}
|
|
37057
37435
|
return pending;
|
|
37058
37436
|
}
|
|
37437
|
+
/**
|
|
37438
|
+
* Snapshot the redirect target URL of the next request the browser
|
|
37439
|
+
* makes to `(host, path?)`. Single-shot: the extension registers a
|
|
37440
|
+
* one-time `chrome.webRequest.onBeforeRedirect` listener filtered on
|
|
37441
|
+
* `https://${host}${path ?? '/*'}`, captures `details.redirectUrl` on
|
|
37442
|
+
* the first match, removes itself, and resolves with the URL. Times out
|
|
37443
|
+
* after `timeoutMs` (default 30s on the extension).
|
|
37444
|
+
*
|
|
37445
|
+
* Use case: a Cloudflare-walled endpoint that 302-redirects cross-origin
|
|
37446
|
+
* to a presigned URL — a page-level fetch sees only an opaque redirect,
|
|
37447
|
+
* but `onBeforeRedirect` exposes the target. Capture is limited to the
|
|
37448
|
+
* MCP's own declared `domains`; no per-entry declared scope is required.
|
|
37449
|
+
*/
|
|
37450
|
+
async captureRedirect(opts) {
|
|
37451
|
+
if (!this.opts.capabilities.includes("capture_redirect")) {
|
|
37452
|
+
throw new Error('FetchproxyServer.captureRedirect(): MCP did not declare "capture_redirect" in capabilities');
|
|
37453
|
+
}
|
|
37454
|
+
await this.ensureConnected();
|
|
37455
|
+
this.throwIfPendingPair();
|
|
37456
|
+
try {
|
|
37457
|
+
const result = await this._captureRedirectOnce(opts);
|
|
37458
|
+
this.recordSuccess();
|
|
37459
|
+
return result;
|
|
37460
|
+
} catch (err) {
|
|
37461
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
37462
|
+
if (!swDown) {
|
|
37463
|
+
this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
|
|
37464
|
+
throw err;
|
|
37465
|
+
}
|
|
37466
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
37467
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
37468
|
+
if (reviveMs > 0) {
|
|
37469
|
+
this.lazyReviveAttempts += 1;
|
|
37470
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
37471
|
+
try {
|
|
37472
|
+
const result = await this._captureRedirectOnce(opts);
|
|
37473
|
+
this.lazyReviveSuccesses += 1;
|
|
37474
|
+
this.recordSuccess();
|
|
37475
|
+
return result;
|
|
37476
|
+
} catch (retryErr) {
|
|
37477
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
37478
|
+
if (!stillDown) {
|
|
37479
|
+
this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
|
|
37480
|
+
throw retryErr;
|
|
37481
|
+
}
|
|
37482
|
+
this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
|
|
37483
|
+
throw new FetchproxyBridgeDownError({
|
|
37484
|
+
originalError: retryErr.message,
|
|
37485
|
+
retryAttempted: true,
|
|
37486
|
+
op: "capture_redirect",
|
|
37487
|
+
url: `https://${opts.host}${opts.path ?? "/*"}`,
|
|
37488
|
+
role: this.role,
|
|
37489
|
+
port: this.opts.port
|
|
37490
|
+
});
|
|
37491
|
+
}
|
|
37492
|
+
}
|
|
37493
|
+
this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
|
|
37494
|
+
throw new FetchproxyBridgeDownError({
|
|
37495
|
+
originalError: err.message,
|
|
37496
|
+
retryAttempted: false,
|
|
37497
|
+
op: "capture_redirect",
|
|
37498
|
+
url: `https://${opts.host}${opts.path ?? "/*"}`,
|
|
37499
|
+
role: this.role,
|
|
37500
|
+
port: this.opts.port
|
|
37501
|
+
});
|
|
37502
|
+
}
|
|
37503
|
+
}
|
|
37504
|
+
async _captureRedirectOnce(opts) {
|
|
37505
|
+
const id = this.nextRequestId++;
|
|
37506
|
+
const inner = {
|
|
37507
|
+
type: "request",
|
|
37508
|
+
id,
|
|
37509
|
+
op: "capture_redirect",
|
|
37510
|
+
init: {
|
|
37511
|
+
host: opts.host,
|
|
37512
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
37513
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37514
|
+
}
|
|
37515
|
+
};
|
|
37516
|
+
const pending = new Promise((resolve2, reject) => {
|
|
37517
|
+
this.pendingRedirect.set(id, { resolve: resolve2, reject });
|
|
37518
|
+
});
|
|
37519
|
+
if (this.hostHandle) {
|
|
37520
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
37521
|
+
} else if (this.peerHandle) {
|
|
37522
|
+
await this.peerHandle.sendInner(inner);
|
|
37523
|
+
}
|
|
37524
|
+
return pending;
|
|
37525
|
+
}
|
|
37526
|
+
/**
|
|
37527
|
+
* Download `url` through the BROWSER's own network stack via
|
|
37528
|
+
* `chrome.downloads` (real cookies + TLS/JA3 fingerprint). Unlike a
|
|
37529
|
+
* page-level `fetch()` (cors mode), this clears a Cloudflare bot-challenge
|
|
37530
|
+
* on the endpoint and follows the cross-origin redirect to the final file.
|
|
37531
|
+
* Resolves the saved local file path + size; the bridge is loopback-only /
|
|
37532
|
+
* single-host, so the MCP reads the bytes from the same disk. Requires
|
|
37533
|
+
* `'download'` in capabilities and the `url` host to be a declared `domain`.
|
|
37534
|
+
*/
|
|
37535
|
+
async download(opts) {
|
|
37536
|
+
if (!this.opts.capabilities.includes("download")) {
|
|
37537
|
+
throw new Error('FetchproxyServer.download(): MCP did not declare "download" in capabilities');
|
|
37538
|
+
}
|
|
37539
|
+
assertUrlInDomains("download url", opts.url, this.opts.domains);
|
|
37540
|
+
await this.ensureConnected();
|
|
37541
|
+
this.throwIfPendingPair();
|
|
37542
|
+
try {
|
|
37543
|
+
const result = await this._downloadOnce(opts);
|
|
37544
|
+
this.recordSuccess();
|
|
37545
|
+
return result;
|
|
37546
|
+
} catch (err) {
|
|
37547
|
+
const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
|
|
37548
|
+
if (!swDown) {
|
|
37549
|
+
this.recordFailure(`download: ${err.message ?? String(err)}`);
|
|
37550
|
+
throw err;
|
|
37551
|
+
}
|
|
37552
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
37553
|
+
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
37554
|
+
if (reviveMs > 0) {
|
|
37555
|
+
this.lazyReviveAttempts += 1;
|
|
37556
|
+
await new Promise((r) => setTimeout(r, reviveMs));
|
|
37557
|
+
try {
|
|
37558
|
+
const result = await this._downloadOnce(opts);
|
|
37559
|
+
this.lazyReviveSuccesses += 1;
|
|
37560
|
+
this.recordSuccess();
|
|
37561
|
+
return result;
|
|
37562
|
+
} catch (retryErr) {
|
|
37563
|
+
const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
|
|
37564
|
+
if (!stillDown) {
|
|
37565
|
+
this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
|
|
37566
|
+
throw retryErr;
|
|
37567
|
+
}
|
|
37568
|
+
this.recordFailure(`download bridge-down: ${retryErr.message}`);
|
|
37569
|
+
throw new FetchproxyBridgeDownError({
|
|
37570
|
+
originalError: retryErr.message,
|
|
37571
|
+
retryAttempted: true,
|
|
37572
|
+
op: "download",
|
|
37573
|
+
url: opts.url,
|
|
37574
|
+
role: this.role,
|
|
37575
|
+
port: this.opts.port
|
|
37576
|
+
});
|
|
37577
|
+
}
|
|
37578
|
+
}
|
|
37579
|
+
this.recordFailure(`download bridge-down: ${err.message}`);
|
|
37580
|
+
throw new FetchproxyBridgeDownError({
|
|
37581
|
+
originalError: err.message,
|
|
37582
|
+
retryAttempted: false,
|
|
37583
|
+
op: "download",
|
|
37584
|
+
url: opts.url,
|
|
37585
|
+
role: this.role,
|
|
37586
|
+
port: this.opts.port
|
|
37587
|
+
});
|
|
37588
|
+
}
|
|
37589
|
+
}
|
|
37590
|
+
async _downloadOnce(opts) {
|
|
37591
|
+
const id = this.nextRequestId++;
|
|
37592
|
+
const inner = {
|
|
37593
|
+
type: "request",
|
|
37594
|
+
id,
|
|
37595
|
+
op: "download",
|
|
37596
|
+
init: {
|
|
37597
|
+
url: opts.url,
|
|
37598
|
+
...opts.filename !== void 0 ? { filename: opts.filename } : {},
|
|
37599
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37600
|
+
}
|
|
37601
|
+
};
|
|
37602
|
+
const pending = new Promise((resolve2, reject) => {
|
|
37603
|
+
this.pendingDownload.set(id, { resolve: resolve2, reject });
|
|
37604
|
+
});
|
|
37605
|
+
if (this.hostHandle) {
|
|
37606
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
37607
|
+
} else if (this.peerHandle) {
|
|
37608
|
+
await this.peerHandle.sendInner(inner);
|
|
37609
|
+
}
|
|
37610
|
+
return pending;
|
|
37611
|
+
}
|
|
37059
37612
|
/**
|
|
37060
37613
|
* 0.4.0+: read declared IndexedDB keys from the user's signed-in
|
|
37061
37614
|
* tab. Requires `'read_indexed_db'` in capabilities AND the
|
|
@@ -37203,6 +37756,20 @@ var FetchproxyServer = class {
|
|
|
37203
37756
|
}
|
|
37204
37757
|
return;
|
|
37205
37758
|
}
|
|
37759
|
+
const redirectCb = this.pendingRedirect.get(inner.id);
|
|
37760
|
+
if (redirectCb) {
|
|
37761
|
+
this.pendingRedirect.delete(inner.id);
|
|
37762
|
+
if (inner.ok) {
|
|
37763
|
+
if (inner.op === "capture_redirect" && typeof inner.value === "string") {
|
|
37764
|
+
redirectCb.resolve(inner.value);
|
|
37765
|
+
} else {
|
|
37766
|
+
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
37767
|
+
}
|
|
37768
|
+
} else {
|
|
37769
|
+
redirectCb.reject(new FetchproxyProtocolError(inner.error));
|
|
37770
|
+
}
|
|
37771
|
+
return;
|
|
37772
|
+
}
|
|
37206
37773
|
const idbCb = this.pendingIdb.get(inner.id);
|
|
37207
37774
|
if (idbCb) {
|
|
37208
37775
|
this.pendingIdb.delete(inner.id);
|
|
@@ -37217,6 +37784,20 @@ var FetchproxyServer = class {
|
|
|
37217
37784
|
}
|
|
37218
37785
|
return;
|
|
37219
37786
|
}
|
|
37787
|
+
const downloadCb = this.pendingDownload.get(inner.id);
|
|
37788
|
+
if (downloadCb) {
|
|
37789
|
+
this.pendingDownload.delete(inner.id);
|
|
37790
|
+
if (inner.ok) {
|
|
37791
|
+
if (inner.op === "download" && inner.value && typeof inner.value === "object") {
|
|
37792
|
+
downloadCb.resolve({ ...inner.value });
|
|
37793
|
+
} else {
|
|
37794
|
+
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
37795
|
+
}
|
|
37796
|
+
} else {
|
|
37797
|
+
downloadCb.reject(new FetchproxyProtocolError(inner.error));
|
|
37798
|
+
}
|
|
37799
|
+
return;
|
|
37800
|
+
}
|
|
37220
37801
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
37221
37802
|
if (cookiesCb) {
|
|
37222
37803
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -37259,9 +37840,15 @@ var FetchproxyServer = class {
|
|
|
37259
37840
|
for (const { reject } of this.pendingCapture.values())
|
|
37260
37841
|
reject(err);
|
|
37261
37842
|
this.pendingCapture.clear();
|
|
37843
|
+
for (const { reject } of this.pendingRedirect.values())
|
|
37844
|
+
reject(err);
|
|
37845
|
+
this.pendingRedirect.clear();
|
|
37262
37846
|
for (const { reject } of this.pendingIdb.values())
|
|
37263
37847
|
reject(err);
|
|
37264
37848
|
this.pendingIdb.clear();
|
|
37849
|
+
for (const { reject } of this.pendingDownload.values())
|
|
37850
|
+
reject(err);
|
|
37851
|
+
this.pendingDownload.clear();
|
|
37265
37852
|
}
|
|
37266
37853
|
/**
|
|
37267
37854
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
@@ -37296,6 +37883,7 @@ var FetchproxyServer = class {
|
|
|
37296
37883
|
* twice in a row.
|
|
37297
37884
|
*/
|
|
37298
37885
|
async close() {
|
|
37886
|
+
this.closing = true;
|
|
37299
37887
|
this.stopKeepalive();
|
|
37300
37888
|
this.rejectAllPending();
|
|
37301
37889
|
if (this.connectingPromise) {
|
|
@@ -37343,7 +37931,7 @@ async function bootstrap(opts) {
|
|
|
37343
37931
|
const sessionStorageKeys = new Set(opts.declare.sessionStorage);
|
|
37344
37932
|
for (const p of sessionStoragePointers)
|
|
37345
37933
|
sessionStorageKeys.add(p.storageKey);
|
|
37346
|
-
const
|
|
37934
|
+
const server = factory({
|
|
37347
37935
|
serverName: opts.serverName,
|
|
37348
37936
|
version: opts.version,
|
|
37349
37937
|
domains: [...opts.domains],
|
|
@@ -37378,10 +37966,10 @@ async function bootstrap(opts) {
|
|
|
37378
37966
|
if (opts.storageSubdomain !== void 0)
|
|
37379
37967
|
storageDomainOpts.subdomain = opts.storageSubdomain;
|
|
37380
37968
|
try {
|
|
37381
|
-
await
|
|
37969
|
+
await server.listen();
|
|
37382
37970
|
const cookies = {};
|
|
37383
37971
|
if (opts.declare.cookies.length > 0) {
|
|
37384
|
-
const joined = await
|
|
37972
|
+
const joined = await server.readCookies({
|
|
37385
37973
|
keys: opts.declare.cookies,
|
|
37386
37974
|
...storageDomainOpts
|
|
37387
37975
|
});
|
|
@@ -37401,7 +37989,7 @@ async function bootstrap(opts) {
|
|
|
37401
37989
|
for (const p of localStoragePointers) {
|
|
37402
37990
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
37403
37991
|
}
|
|
37404
|
-
localStorage = await
|
|
37992
|
+
localStorage = await server.readLocalStorage({
|
|
37405
37993
|
keys: allKeys,
|
|
37406
37994
|
...storageDomainOpts,
|
|
37407
37995
|
...localStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37414,7 +38002,7 @@ async function bootstrap(opts) {
|
|
|
37414
38002
|
for (const p of sessionStoragePointers) {
|
|
37415
38003
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
37416
38004
|
}
|
|
37417
|
-
sessionStorage = await
|
|
38005
|
+
sessionStorage = await server.readSessionStorage({
|
|
37418
38006
|
keys: allKeys,
|
|
37419
38007
|
...storageDomainOpts,
|
|
37420
38008
|
...sessionStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37423,21 +38011,17 @@ async function bootstrap(opts) {
|
|
|
37423
38011
|
const capturedHeaders = {};
|
|
37424
38012
|
for (const h of opts.declare.captureHeaders) {
|
|
37425
38013
|
if (opts.onWaiting) {
|
|
37426
|
-
|
|
37427
|
-
const url2 = new URL(h.urlPattern.replace(/\*+/g, "placeholder"));
|
|
37428
|
-
opts.onWaiting(`waiting for next request to ${url2.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37429
|
-
} catch {
|
|
37430
|
-
opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37431
|
-
}
|
|
38014
|
+
opts.onWaiting(`waiting for next request to ${h.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37432
38015
|
}
|
|
37433
|
-
capturedHeaders[h.headerName] = await
|
|
37434
|
-
|
|
38016
|
+
capturedHeaders[h.headerName] = await server.captureRequestHeader({
|
|
38017
|
+
host: h.host,
|
|
38018
|
+
...h.path !== void 0 ? { path: h.path } : {},
|
|
37435
38019
|
headerName: h.headerName
|
|
37436
38020
|
});
|
|
37437
38021
|
}
|
|
37438
38022
|
const indexedDbBucket = {};
|
|
37439
38023
|
for (const d of indexedDb) {
|
|
37440
|
-
const values = await
|
|
38024
|
+
const values = await server.readIndexedDb({
|
|
37441
38025
|
database: d.database,
|
|
37442
38026
|
store: d.store,
|
|
37443
38027
|
keys: [...d.keys],
|
|
@@ -37454,7 +38038,7 @@ async function bootstrap(opts) {
|
|
|
37454
38038
|
};
|
|
37455
38039
|
} finally {
|
|
37456
38040
|
try {
|
|
37457
|
-
await
|
|
38041
|
+
await server.close();
|
|
37458
38042
|
} catch {
|
|
37459
38043
|
}
|
|
37460
38044
|
}
|
|
@@ -37519,8 +38103,8 @@ async function loginWithPassword(username, password) {
|
|
|
37519
38103
|
|
|
37520
38104
|
// src/config.ts
|
|
37521
38105
|
import { createHash } from "node:crypto";
|
|
37522
|
-
import { homedir as
|
|
37523
|
-
import { join as
|
|
38106
|
+
import { homedir as homedir3 } from "node:os";
|
|
38107
|
+
import { join as join3 } from "node:path";
|
|
37524
38108
|
function readCacheIdentity() {
|
|
37525
38109
|
const explicit = process.env.OFW_CACHE_IDENTITY;
|
|
37526
38110
|
if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
|
|
@@ -37531,31 +38115,29 @@ function readCacheIdentity() {
|
|
|
37531
38115
|
function getCacheDir() {
|
|
37532
38116
|
const override = process.env.OFW_CACHE_DIR;
|
|
37533
38117
|
if (override && override.trim().length > 0) return override.trim();
|
|
37534
|
-
return
|
|
38118
|
+
return join3(homedir3(), ".cache", "ofw-mcp");
|
|
37535
38119
|
}
|
|
37536
38120
|
function getCacheDbPath() {
|
|
37537
38121
|
const identity = readCacheIdentity();
|
|
37538
38122
|
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
37539
|
-
return
|
|
38123
|
+
return join3(getCacheDir(), `${hash2}.db`);
|
|
37540
38124
|
}
|
|
37541
38125
|
function getAttachmentsDir() {
|
|
37542
38126
|
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
37543
38127
|
if (override && override.trim().length > 0) return override.trim();
|
|
37544
|
-
return
|
|
38128
|
+
return join3(homedir3(), "Downloads", "ofw-mcp");
|
|
37545
38129
|
}
|
|
37546
|
-
function
|
|
37547
|
-
|
|
37548
|
-
if (typeof raw !== "string") return false;
|
|
37549
|
-
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
|
|
38130
|
+
function parseBoolEnv2(name) {
|
|
38131
|
+
return parseBoolEnv(name);
|
|
37550
38132
|
}
|
|
37551
38133
|
function getDefaultInlineAttachments() {
|
|
37552
|
-
return
|
|
38134
|
+
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
37553
38135
|
}
|
|
37554
38136
|
|
|
37555
38137
|
// package.json
|
|
37556
38138
|
var package_default = {
|
|
37557
38139
|
name: "ofw-mcp",
|
|
37558
|
-
version: "2.3.
|
|
38140
|
+
version: "2.3.2",
|
|
37559
38141
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
37560
38142
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
37561
38143
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37579,11 +38161,12 @@ var package_default = {
|
|
|
37579
38161
|
bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
|
|
37580
38162
|
dev: "node --env-file=.env dist/index.js",
|
|
37581
38163
|
test: "vitest run",
|
|
38164
|
+
"test:coverage": "vitest run --coverage",
|
|
37582
38165
|
"test:watch": "vitest"
|
|
37583
38166
|
},
|
|
37584
38167
|
dependencies: {
|
|
37585
|
-
"@
|
|
37586
|
-
"@fetchproxy/
|
|
38168
|
+
"@chrischall/mcp-utils": "^0.9.0",
|
|
38169
|
+
"@fetchproxy/bootstrap": "^1.3.0",
|
|
37587
38170
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37588
38171
|
dotenv: "^17.4.2",
|
|
37589
38172
|
zod: "^4.4.3"
|
|
@@ -37599,16 +38182,10 @@ var package_default = {
|
|
|
37599
38182
|
|
|
37600
38183
|
// src/auth.ts
|
|
37601
38184
|
function readEnv(key) {
|
|
37602
|
-
|
|
37603
|
-
if (typeof raw !== "string") return void 0;
|
|
37604
|
-
const trimmed = raw.trim();
|
|
37605
|
-
if (trimmed.length === 0) return void 0;
|
|
37606
|
-
if (trimmed === "undefined" || trimmed === "null") return void 0;
|
|
37607
|
-
if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
|
|
37608
|
-
return trimmed;
|
|
38185
|
+
return readEnvVar(key);
|
|
37609
38186
|
}
|
|
37610
38187
|
function fetchproxyDisabled() {
|
|
37611
|
-
return
|
|
38188
|
+
return parseBoolEnv2("OFW_DISABLE_FETCHPROXY");
|
|
37612
38189
|
}
|
|
37613
38190
|
async function resolveAuth() {
|
|
37614
38191
|
const username = readEnv("OFW_USERNAME");
|
|
@@ -37668,12 +38245,8 @@ async function resolveAuth() {
|
|
|
37668
38245
|
}
|
|
37669
38246
|
|
|
37670
38247
|
// src/client.ts
|
|
37671
|
-
|
|
37672
|
-
|
|
37673
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37674
|
-
config2({ path: join3(__dirname, "..", ".env"), override: false, quiet: true });
|
|
37675
|
-
} catch {
|
|
37676
|
-
}
|
|
38248
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38249
|
+
await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
|
|
37677
38250
|
function parseContentDispositionFilename(cd) {
|
|
37678
38251
|
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
37679
38252
|
if (extMatch) {
|
|
@@ -37688,7 +38261,7 @@ function parseContentDispositionFilename(cd) {
|
|
|
37688
38261
|
return m ? m[1] : null;
|
|
37689
38262
|
}
|
|
37690
38263
|
function debugLogEnabled() {
|
|
37691
|
-
return
|
|
38264
|
+
return parseBoolEnv2("OFW_DEBUG_LOG");
|
|
37692
38265
|
}
|
|
37693
38266
|
function redactHeaders(h) {
|
|
37694
38267
|
const out = { ...h };
|
|
@@ -37702,12 +38275,39 @@ function getRequestTimeoutMs() {
|
|
|
37702
38275
|
const n = Number(raw.trim());
|
|
37703
38276
|
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37704
38277
|
}
|
|
38278
|
+
var OFW_REFRESH_SENTINEL = "ofw";
|
|
37705
38279
|
var OFWClient = class {
|
|
37706
|
-
token
|
|
37707
|
-
|
|
38280
|
+
// Bearer-token lifecycle is delegated to the shared, race-safe TokenManager
|
|
38281
|
+
// (proactive refresh inside the skew window, single-flight refresh so a burst
|
|
38282
|
+
// of concurrent callers coalesces onto ONE `resolveAuth()`, and a 401-replay
|
|
38283
|
+
// guarded against double-refresh). It is created lazily, seeded with an
|
|
38284
|
+
// already-expired placeholder token so the first request drives the refresh
|
|
38285
|
+
// callback — i.e. the original "log in on first request" behavior.
|
|
38286
|
+
tokenManager;
|
|
38287
|
+
getTokenManager() {
|
|
38288
|
+
if (!this.tokenManager) {
|
|
38289
|
+
this.tokenManager = new TokenManager({
|
|
38290
|
+
initial: { accessToken: "", refreshToken: OFW_REFRESH_SENTINEL, expiresAt: 0 },
|
|
38291
|
+
skewMs: OFW_TOKEN_EXPIRY_SKEW_MS,
|
|
38292
|
+
// Map OFW's mint/refresh onto the refresh callback. `resolveAuth()`
|
|
38293
|
+
// returns a token and a best-effort expiry; when the fetchproxy path
|
|
38294
|
+
// can't supply one we fall back to the same 6h estimate the password
|
|
38295
|
+
// path uses (the 401-replay covers a wrong guess). We re-arm the
|
|
38296
|
+
// sentinel so the manager can refresh again later.
|
|
38297
|
+
refresh: async () => {
|
|
38298
|
+
const { token, expiresAt } = await resolveAuth();
|
|
38299
|
+
return {
|
|
38300
|
+
accessToken: token,
|
|
38301
|
+
refreshToken: OFW_REFRESH_SENTINEL,
|
|
38302
|
+
expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
|
|
38303
|
+
};
|
|
38304
|
+
}
|
|
38305
|
+
});
|
|
38306
|
+
}
|
|
38307
|
+
return this.tokenManager;
|
|
38308
|
+
}
|
|
37708
38309
|
async request(method, path, body) {
|
|
37709
|
-
await this.
|
|
37710
|
-
const response = await this.fetchWithRetry(method, path, body, "application/json", false);
|
|
38310
|
+
const response = await this.fetchAuthed(method, path, body, "application/json");
|
|
37711
38311
|
const text = await response.text();
|
|
37712
38312
|
if (debugLogEnabled()) {
|
|
37713
38313
|
console.error(`[ofw-debug] response body: ${text || "<empty>"}`);
|
|
@@ -37716,23 +38316,45 @@ var OFWClient = class {
|
|
|
37716
38316
|
}
|
|
37717
38317
|
/** Like `request`, but returns the raw bytes plus Content-Type/-Disposition metadata. */
|
|
37718
38318
|
async requestBinary(method, path) {
|
|
37719
|
-
await this.
|
|
37720
|
-
const response = await this.fetchWithRetry(method, path, void 0, "application/octet-stream", false);
|
|
38319
|
+
const response = await this.fetchAuthed(method, path, void 0, "application/octet-stream");
|
|
37721
38320
|
return {
|
|
37722
38321
|
body: Buffer.from(await response.arrayBuffer()),
|
|
37723
38322
|
contentType: response.headers.get("content-type"),
|
|
37724
38323
|
suggestedFileName: parseContentDispositionFilename(response.headers.get("content-disposition") ?? "")
|
|
37725
38324
|
};
|
|
37726
38325
|
}
|
|
37727
|
-
//
|
|
37728
|
-
//
|
|
37729
|
-
//
|
|
37730
|
-
|
|
38326
|
+
// Authenticated fetch for both JSON and binary callers. Auth (proactive
|
|
38327
|
+
// refresh inside the skew window + one 401-replay, guarded against a
|
|
38328
|
+
// double-refresh under concurrency) is delegated to the shared TokenManager's
|
|
38329
|
+
// `withAuth`. The 429 wait-and-replay and the non-2xx → throw remain here.
|
|
38330
|
+
async fetchAuthed(method, path, body, accept) {
|
|
38331
|
+
let attempt = 0;
|
|
38332
|
+
let response = await this.getTokenManager().withAuth(
|
|
38333
|
+
(token) => this.fetchOnce(method, path, body, accept, token, attempt++ > 0)
|
|
38334
|
+
);
|
|
38335
|
+
if (response.status === 429) {
|
|
38336
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
38337
|
+
response = await this.getTokenManager().withAuth(
|
|
38338
|
+
(token) => this.fetchOnce(method, path, body, accept, token, true)
|
|
38339
|
+
);
|
|
38340
|
+
if (response.status === 429) throw new Error("Rate limited by OFW API");
|
|
38341
|
+
}
|
|
38342
|
+
if (!response.ok) {
|
|
38343
|
+
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
38344
|
+
}
|
|
38345
|
+
return response;
|
|
38346
|
+
}
|
|
38347
|
+
// A single OFW API fetch with the bearer token supplied by `withAuth`.
|
|
38348
|
+
// Carries the per-request timeout (AbortController + setTimeout so vitest
|
|
38349
|
+
// fake timers can drive it and we attach a clear error message) and the
|
|
38350
|
+
// OFW_DEBUG_LOG instrumentation. Returns the raw Response — 401/429/non-2xx
|
|
38351
|
+
// handling lives in the callers (`withAuth` and `fetchAuthed`).
|
|
38352
|
+
async fetchOnce(method, path, body, accept, token, isRetry = false) {
|
|
37731
38353
|
const isFormData = body instanceof FormData;
|
|
37732
38354
|
const headers = {
|
|
37733
38355
|
...OFW_PROTOCOL_HEADERS,
|
|
37734
38356
|
Accept: accept,
|
|
37735
|
-
Authorization: `Bearer ${
|
|
38357
|
+
Authorization: `Bearer ${token}`
|
|
37736
38358
|
};
|
|
37737
38359
|
if (body !== void 0 && !isFormData) headers["Content-Type"] = "application/json";
|
|
37738
38360
|
const url2 = `${BASE_URL}${path}`;
|
|
@@ -37774,56 +38396,14 @@ var OFWClient = class {
|
|
|
37774
38396
|
if (debugLogEnabled()) {
|
|
37775
38397
|
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
37776
38398
|
}
|
|
37777
|
-
if (response.status === 401 && !isRetry) {
|
|
37778
|
-
this.token = null;
|
|
37779
|
-
this.tokenExpiry = null;
|
|
37780
|
-
await this.ensureAuthenticated();
|
|
37781
|
-
return this.fetchWithRetry(method, path, body, accept, true);
|
|
37782
|
-
}
|
|
37783
|
-
if (response.status === 429) {
|
|
37784
|
-
if (!isRetry) {
|
|
37785
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
37786
|
-
return this.fetchWithRetry(method, path, body, accept, true);
|
|
37787
|
-
}
|
|
37788
|
-
throw new Error("Rate limited by OFW API");
|
|
37789
|
-
}
|
|
37790
|
-
if (!response.ok) {
|
|
37791
|
-
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
37792
|
-
}
|
|
37793
38399
|
return response;
|
|
37794
38400
|
}
|
|
37795
|
-
async ensureAuthenticated() {
|
|
37796
|
-
if (!this.isTokenExpiredSoon()) return;
|
|
37797
|
-
await this.login();
|
|
37798
|
-
}
|
|
37799
|
-
// Auth resolution is delegated to `./auth.ts`. This client doesn't care
|
|
37800
|
-
// whether the token came from a password POST or from a one-shot
|
|
37801
|
-
// fetchproxy session-snapshot — it just consumes the result.
|
|
37802
|
-
//
|
|
37803
|
-
// If `expiresAt` is missing (the fetchproxy path on a tab whose
|
|
37804
|
-
// browser didn't persist tokenExpiry), we fall back to the same 6h
|
|
37805
|
-
// estimate the password path uses. The 401-replay path covers us if
|
|
37806
|
-
// the estimate is wrong.
|
|
37807
|
-
async login() {
|
|
37808
|
-
const { token, expiresAt } = await resolveAuth();
|
|
37809
|
-
this.token = token;
|
|
37810
|
-
this.tokenExpiry = expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS);
|
|
37811
|
-
}
|
|
37812
|
-
isTokenExpiredSoon() {
|
|
37813
|
-
if (!this.token || !this.tokenExpiry) return true;
|
|
37814
|
-
return this.tokenExpiry.getTime() - Date.now() < OFW_TOKEN_EXPIRY_SKEW_MS;
|
|
37815
|
-
}
|
|
37816
38401
|
};
|
|
37817
38402
|
var client = new OFWClient();
|
|
37818
38403
|
|
|
37819
38404
|
// src/tools/_shared.ts
|
|
37820
|
-
|
|
37821
|
-
|
|
37822
|
-
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
37823
|
-
}
|
|
37824
|
-
function textResponse(text) {
|
|
37825
|
-
return { content: [{ type: "text", text }] };
|
|
37826
|
-
}
|
|
38405
|
+
var jsonResponse = textResult;
|
|
38406
|
+
var textResponse = rawTextResult;
|
|
37827
38407
|
function mapRecipients(items) {
|
|
37828
38408
|
return (items ?? []).map((r) => ({
|
|
37829
38409
|
userId: r.user?.id ?? 0,
|
|
@@ -37831,10 +38411,7 @@ function mapRecipients(items) {
|
|
|
37831
38411
|
viewedAt: r.viewed?.dateTime ?? null
|
|
37832
38412
|
}));
|
|
37833
38413
|
}
|
|
37834
|
-
|
|
37835
|
-
const expanded = p.startsWith("~/") ? join4(process.env.HOME ?? "", p.slice(2)) : p;
|
|
37836
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
37837
|
-
}
|
|
38414
|
+
var expandPath2 = expandPath;
|
|
37838
38415
|
async function postMessageAndRefetch(client2, payload) {
|
|
37839
38416
|
const raw = await client2.request(
|
|
37840
38417
|
"POST",
|
|
@@ -37848,15 +38425,15 @@ async function postMessageAndRefetch(client2, payload) {
|
|
|
37848
38425
|
}
|
|
37849
38426
|
|
|
37850
38427
|
// src/tools/user.ts
|
|
37851
|
-
function registerUserTools(
|
|
37852
|
-
|
|
38428
|
+
function registerUserTools(server, client2) {
|
|
38429
|
+
server.registerTool("ofw_get_profile", {
|
|
37853
38430
|
description: "Get current user and co-parent profile information from OurFamilyWizard",
|
|
37854
38431
|
annotations: { readOnlyHint: true }
|
|
37855
38432
|
}, async () => {
|
|
37856
38433
|
const data = await client2.request("GET", "/pub/v2/profiles");
|
|
37857
38434
|
return jsonResponse(data);
|
|
37858
38435
|
});
|
|
37859
|
-
|
|
38436
|
+
server.registerTool("ofw_get_notifications", {
|
|
37860
38437
|
description: "Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.",
|
|
37861
38438
|
annotations: { readOnlyHint: false }
|
|
37862
38439
|
}, async () => {
|
|
@@ -37867,7 +38444,7 @@ function registerUserTools(server2, client2) {
|
|
|
37867
38444
|
|
|
37868
38445
|
// src/cache.ts
|
|
37869
38446
|
import { DatabaseSync } from "node:sqlite";
|
|
37870
|
-
import { mkdirSync } from "node:fs";
|
|
38447
|
+
import { mkdirSync, chmodSync, existsSync } from "node:fs";
|
|
37871
38448
|
import { dirname as dirname2 } from "node:path";
|
|
37872
38449
|
var instance = null;
|
|
37873
38450
|
var SCHEMA_V1 = `
|
|
@@ -37930,14 +38507,23 @@ function migrate(db) {
|
|
|
37930
38507
|
"INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
|
|
37931
38508
|
).run("schema_version", "2");
|
|
37932
38509
|
}
|
|
38510
|
+
function enforceCachePermissions(dbPath) {
|
|
38511
|
+
chmodSync(dirname2(dbPath), 448);
|
|
38512
|
+
chmodSync(dbPath, 384);
|
|
38513
|
+
for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
38514
|
+
if (existsSync(sibling)) chmodSync(sibling, 384);
|
|
38515
|
+
}
|
|
38516
|
+
}
|
|
37933
38517
|
function openCache() {
|
|
37934
38518
|
if (instance) return instance;
|
|
37935
38519
|
const path = getCacheDbPath();
|
|
37936
38520
|
mkdirSync(dirname2(path), { recursive: true });
|
|
37937
38521
|
const db = new DatabaseSync(path);
|
|
38522
|
+
enforceCachePermissions(path);
|
|
37938
38523
|
db.exec("PRAGMA journal_mode = WAL");
|
|
37939
38524
|
db.exec("PRAGMA foreign_keys = ON");
|
|
37940
38525
|
migrate(db);
|
|
38526
|
+
enforceCachePermissions(path);
|
|
37941
38527
|
instance = { db };
|
|
37942
38528
|
return instance;
|
|
37943
38529
|
}
|
|
@@ -38397,15 +38983,15 @@ function listDataHintsAtFiles(listData) {
|
|
|
38397
38983
|
if (Array.isArray(ld.files)) return ld.files.length > 0;
|
|
38398
38984
|
return false;
|
|
38399
38985
|
}
|
|
38400
|
-
function registerMessageTools(
|
|
38401
|
-
|
|
38986
|
+
function registerMessageTools(server, client2) {
|
|
38987
|
+
server.registerTool("ofw_list_message_folders", {
|
|
38402
38988
|
description: "List OurFamilyWizard message folders (inbox, sent, etc.) and their unread counts. Returns folder IDs needed to call ofw_list_messages. Does NOT return message content.",
|
|
38403
38989
|
annotations: { readOnlyHint: true }
|
|
38404
38990
|
}, async () => {
|
|
38405
38991
|
const data = await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true");
|
|
38406
38992
|
return jsonResponse(data);
|
|
38407
38993
|
});
|
|
38408
|
-
|
|
38994
|
+
server.registerTool("ofw_list_messages", {
|
|
38409
38995
|
description: "List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based but if you know what you want (a date range, a topic), prefer the filters over walking pages \u2014 the cache may have 1000+ messages. Call ofw_sync_messages first if the cache is empty or stale.",
|
|
38410
38996
|
annotations: { readOnlyHint: true },
|
|
38411
38997
|
inputSchema: {
|
|
@@ -38441,7 +39027,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38441
39027
|
}
|
|
38442
39028
|
return jsonResponse(payload);
|
|
38443
39029
|
});
|
|
38444
|
-
|
|
39030
|
+
server.registerTool("ofw_get_message", {
|
|
38445
39031
|
description: 'Get a single OurFamilyWizard message OR draft by ID. Reads from local cache when available; otherwise fetches from OFW (which will mark unread inbox messages as read on OFW). For ids that match a draft (in the drafts cache), the response carries folder="drafts" and the body/subject/recipients reflect the drafts cache (which ofw_sync_messages keeps fresh) \u2014 drafts have no `fromUser`, and `sentAt`/`fetchedBodyAt` mirror the draft\'s `modifiedAt`. For inbox/sent messages, folder is "inbox" or "sent" as before.',
|
|
38446
39032
|
annotations: { readOnlyHint: false },
|
|
38447
39033
|
inputSchema: {
|
|
@@ -38506,7 +39092,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38506
39092
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
38507
39093
|
return jsonResponse({ ...row, attachments });
|
|
38508
39094
|
});
|
|
38509
|
-
|
|
39095
|
+
server.registerTool("ofw_send_message", {
|
|
38510
39096
|
description: "Send a message via OurFamilyWizard. To send an existing draft, pass messageId \u2014 subject/body/recipientIds become optional overrides (missing fields default to the draft's cached values) and the draft is deleted after sending. To send a fresh message, supply subject/body/recipientIds directly. draftId is the legacy spelling of messageId and works the same way. If replyToId is provided, the cache may rewrite it to the latest reply in the same thread (a note is included in the response when this happens). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After sending, the tool re-fetches the message from OFW to populate the local cache and link attachments to the new message id.",
|
|
38511
39097
|
annotations: { destructiveHint: true },
|
|
38512
39098
|
inputSchema: {
|
|
@@ -38616,7 +39202,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38616
39202
|
|
|
38617
39203
|
${text}` : text);
|
|
38618
39204
|
});
|
|
38619
|
-
|
|
39205
|
+
server.registerTool("ofw_list_drafts", {
|
|
38620
39206
|
description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
|
|
38621
39207
|
annotations: { readOnlyHint: true },
|
|
38622
39208
|
inputSchema: {
|
|
@@ -38630,7 +39216,7 @@ ${text}` : text);
|
|
|
38630
39216
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
38631
39217
|
return jsonResponse(payload);
|
|
38632
39218
|
});
|
|
38633
|
-
|
|
39219
|
+
server.registerTool("ofw_save_draft", {
|
|
38634
39220
|
description: "Save a message as a draft in OurFamilyWizard. Recipients are optional. Pass messageId to replace an existing draft \u2014 note that under the hood this creates a NEW draft and deletes the old one (OFW's update-in-place endpoint silently no-ops while echoing the posted body, so we don't use it); the response.id will be the NEW id, not the messageId you passed, and the change is documented in a transparency NOTE in the response. If replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included in response). Attach files by passing their fileIds (from ofw_upload_attachment) in myFileIDs. After saving, the tool re-fetches the draft from OFW to populate the local cache from authoritative server state.",
|
|
38635
39221
|
annotations: { readOnlyHint: false },
|
|
38636
39222
|
inputSchema: {
|
|
@@ -38692,7 +39278,7 @@ ${text}` : text);
|
|
|
38692
39278
|
|
|
38693
39279
|
${text}` : text);
|
|
38694
39280
|
});
|
|
38695
|
-
|
|
39281
|
+
server.registerTool("ofw_delete_draft", {
|
|
38696
39282
|
description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
|
|
38697
39283
|
annotations: { destructiveHint: true },
|
|
38698
39284
|
inputSchema: {
|
|
@@ -38703,7 +39289,7 @@ ${text}` : text);
|
|
|
38703
39289
|
deleteDraft(args.messageId);
|
|
38704
39290
|
return data ? jsonResponse(data) : textResponse("Draft deleted.");
|
|
38705
39291
|
});
|
|
38706
|
-
|
|
39292
|
+
server.registerTool("ofw_get_unread_sent", {
|
|
38707
39293
|
description: "List sent messages that have not been read by one or more recipients. Reads from local cache; call ofw_sync_messages first if cache is stale.",
|
|
38708
39294
|
annotations: { readOnlyHint: true },
|
|
38709
39295
|
inputSchema: {
|
|
@@ -38729,7 +39315,7 @@ ${text}` : text);
|
|
|
38729
39315
|
}
|
|
38730
39316
|
return jsonResponse(unread);
|
|
38731
39317
|
});
|
|
38732
|
-
|
|
39318
|
+
server.registerTool("ofw_upload_attachment", {
|
|
38733
39319
|
description: `Upload a local file to OurFamilyWizard's "My Files" so it can be attached to a message. Returns the fileId \u2014 pass that to ofw_send_message or ofw_save_draft in myFileIDs to attach it. The file is uploaded as PRIVATE (visible only to you) by default; pass shareClass:"SHARED" to share with co-parents directly via the My Files area.`,
|
|
38734
39320
|
annotations: { destructiveHint: false },
|
|
38735
39321
|
inputSchema: {
|
|
@@ -38739,14 +39325,13 @@ ${text}` : text);
|
|
|
38739
39325
|
description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
|
|
38740
39326
|
}
|
|
38741
39327
|
}, async (args) => {
|
|
38742
|
-
const abs =
|
|
39328
|
+
const abs = expandPath2(args.path);
|
|
38743
39329
|
const stat = statSync(abs);
|
|
38744
39330
|
if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
|
|
38745
|
-
const buf = readFileSync(abs);
|
|
38746
39331
|
const fileName = basename(abs);
|
|
38747
39332
|
const mime = mimeFromName(fileName);
|
|
38748
39333
|
const form = new FormData();
|
|
38749
|
-
form.append("file",
|
|
39334
|
+
form.append("file", await fileBlob(abs, { type: mime }), fileName);
|
|
38750
39335
|
form.append("source", "message");
|
|
38751
39336
|
form.append("description", args.description ?? fileName);
|
|
38752
39337
|
form.append("label", args.label ?? fileName);
|
|
@@ -38758,7 +39343,7 @@ ${text}` : text);
|
|
|
38758
39343
|
fileName: meta3.fileName ?? fileName,
|
|
38759
39344
|
label: meta3.label ?? args.label ?? fileName,
|
|
38760
39345
|
mimeType: meta3.fileType ?? mime,
|
|
38761
|
-
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes :
|
|
39346
|
+
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : stat.size,
|
|
38762
39347
|
metadata: meta3,
|
|
38763
39348
|
messageId: 0
|
|
38764
39349
|
});
|
|
@@ -38766,12 +39351,12 @@ ${text}` : text);
|
|
|
38766
39351
|
fileId: meta3.fileId,
|
|
38767
39352
|
fileName: meta3.fileName ?? fileName,
|
|
38768
39353
|
mimeType: meta3.fileType ?? mime,
|
|
38769
|
-
sizeBytes: meta3.sizeInBytes ??
|
|
39354
|
+
sizeBytes: meta3.sizeInBytes ?? stat.size,
|
|
38770
39355
|
shareClass: meta3.shareClass ?? args.shareClass ?? "PRIVATE",
|
|
38771
39356
|
note: "Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it."
|
|
38772
39357
|
});
|
|
38773
39358
|
});
|
|
38774
|
-
|
|
39359
|
+
server.registerTool("ofw_download_attachment", {
|
|
38775
39360
|
description: 'Download an OFW message attachment by fileId. By default, bytes are saved to disk (~/Downloads/ofw-mcp/) and the response carries the absolute path, mime type, and size for the caller to read back. Pass inline:true to skip disk entirely and return the bytes as MCP content blocks \u2014 images come back as ImageContent (the model sees them directly); other files come back as an EmbeddedResource blob. Use inline for small files where you want the model to read content immediately and the host is sandboxed; use disk for large files or when you want a persistent local copy. The default for `inline` can be flipped server-side via the OFW_INLINE_ATTACHMENTS env var (set to "true" to make inline the default). fileId comes from attachments[].fileId on ofw_get_message. Override disk destination with OFW_ATTACHMENTS_DIR or saveTo. Re-downloading to the same path is a no-op (disk mode only).',
|
|
38776
39361
|
annotations: { readOnlyHint: false },
|
|
38777
39362
|
inputSchema: {
|
|
@@ -38825,7 +39410,7 @@ ${text}` : text);
|
|
|
38825
39410
|
let dest;
|
|
38826
39411
|
if (args.saveTo) {
|
|
38827
39412
|
const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
|
|
38828
|
-
const abs =
|
|
39413
|
+
const abs = expandPath2(args.saveTo);
|
|
38829
39414
|
dest = isDirArg ? join5(abs, `${fileId}-${cached2.fileName}`) : abs;
|
|
38830
39415
|
} else {
|
|
38831
39416
|
dest = join5(getAttachmentsDir(), `${fileId}-${cached2.fileName}`);
|
|
@@ -38852,7 +39437,7 @@ ${text}` : text);
|
|
|
38852
39437
|
fileName: response.suggestedFileName ?? cached2.fileName
|
|
38853
39438
|
});
|
|
38854
39439
|
});
|
|
38855
|
-
|
|
39440
|
+
server.registerTool("ofw_sync_messages", {
|
|
38856
39441
|
description: "Sync messages from OurFamilyWizard into the local cache. Returns counts per folder and a list of unread inbox messages whose bodies were NOT fetched (to avoid mark-as-read on OFW). Call ofw_get_message(id) on those to read them. Pass deep:true to walk all OFW pages instead of stopping at the first all-cached page (use to backfill suspected gaps).",
|
|
38857
39442
|
annotations: { readOnlyHint: false },
|
|
38858
39443
|
inputSchema: {
|
|
@@ -38876,8 +39461,8 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
38876
39461
|
}
|
|
38877
39462
|
|
|
38878
39463
|
// src/tools/calendar.ts
|
|
38879
|
-
function registerCalendarTools(
|
|
38880
|
-
|
|
39464
|
+
function registerCalendarTools(server, client2) {
|
|
39465
|
+
server.registerTool("ofw_list_events", {
|
|
38881
39466
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
38882
39467
|
annotations: { readOnlyHint: true },
|
|
38883
39468
|
inputSchema: {
|
|
@@ -38893,7 +39478,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38893
39478
|
);
|
|
38894
39479
|
return jsonResponse(data);
|
|
38895
39480
|
});
|
|
38896
|
-
|
|
39481
|
+
server.registerTool("ofw_create_event", {
|
|
38897
39482
|
description: "Create a calendar event in OurFamilyWizard",
|
|
38898
39483
|
annotations: { destructiveHint: false },
|
|
38899
39484
|
inputSchema: {
|
|
@@ -38913,7 +39498,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38913
39498
|
const data = await client2.request("POST", "/pub/v1/calendar/events", args);
|
|
38914
39499
|
return jsonResponse(data);
|
|
38915
39500
|
});
|
|
38916
|
-
|
|
39501
|
+
server.registerTool("ofw_update_event", {
|
|
38917
39502
|
description: "Update an existing OurFamilyWizard calendar event",
|
|
38918
39503
|
annotations: { destructiveHint: true },
|
|
38919
39504
|
inputSchema: {
|
|
@@ -38931,7 +39516,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38931
39516
|
const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
38932
39517
|
return jsonResponse(data);
|
|
38933
39518
|
});
|
|
38934
|
-
|
|
39519
|
+
server.registerTool("ofw_delete_event", {
|
|
38935
39520
|
description: "Delete an OurFamilyWizard calendar event",
|
|
38936
39521
|
annotations: { destructiveHint: true },
|
|
38937
39522
|
inputSchema: {
|
|
@@ -38944,15 +39529,15 @@ function registerCalendarTools(server2, client2) {
|
|
|
38944
39529
|
}
|
|
38945
39530
|
|
|
38946
39531
|
// src/tools/expenses.ts
|
|
38947
|
-
function registerExpenseTools(
|
|
38948
|
-
|
|
39532
|
+
function registerExpenseTools(server, client2) {
|
|
39533
|
+
server.registerTool("ofw_get_expense_totals", {
|
|
38949
39534
|
description: "Get OurFamilyWizard expense summary totals (owed/paid)",
|
|
38950
39535
|
annotations: { readOnlyHint: true }
|
|
38951
39536
|
}, async () => {
|
|
38952
39537
|
const data = await client2.request("GET", "/pub/v2/expense/expenses/totals");
|
|
38953
39538
|
return jsonResponse(data);
|
|
38954
39539
|
});
|
|
38955
|
-
|
|
39540
|
+
server.registerTool("ofw_list_expenses", {
|
|
38956
39541
|
description: "List OurFamilyWizard expenses with pagination",
|
|
38957
39542
|
annotations: { readOnlyHint: true },
|
|
38958
39543
|
inputSchema: {
|
|
@@ -38965,7 +39550,7 @@ function registerExpenseTools(server2, client2) {
|
|
|
38965
39550
|
const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
38966
39551
|
return jsonResponse(data);
|
|
38967
39552
|
});
|
|
38968
|
-
|
|
39553
|
+
server.registerTool("ofw_create_expense", {
|
|
38969
39554
|
description: "Log a new expense in OurFamilyWizard",
|
|
38970
39555
|
annotations: { destructiveHint: false },
|
|
38971
39556
|
inputSchema: {
|
|
@@ -38979,8 +39564,8 @@ function registerExpenseTools(server2, client2) {
|
|
|
38979
39564
|
}
|
|
38980
39565
|
|
|
38981
39566
|
// src/tools/journal.ts
|
|
38982
|
-
function registerJournalTools(
|
|
38983
|
-
|
|
39567
|
+
function registerJournalTools(server, client2) {
|
|
39568
|
+
server.registerTool("ofw_list_journal_entries", {
|
|
38984
39569
|
description: "List OurFamilyWizard journal entries",
|
|
38985
39570
|
annotations: { readOnlyHint: true },
|
|
38986
39571
|
inputSchema: {
|
|
@@ -38993,7 +39578,7 @@ function registerJournalTools(server2, client2) {
|
|
|
38993
39578
|
const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
|
|
38994
39579
|
return jsonResponse(data);
|
|
38995
39580
|
});
|
|
38996
|
-
|
|
39581
|
+
server.registerTool("ofw_create_journal_entry", {
|
|
38997
39582
|
description: "Create a new journal entry in OurFamilyWizard",
|
|
38998
39583
|
annotations: { destructiveHint: false },
|
|
38999
39584
|
inputSchema: {
|
|
@@ -39017,12 +39602,17 @@ process.emit = function(event, ...args) {
|
|
|
39017
39602
|
}
|
|
39018
39603
|
return originalEmit(event, ...args);
|
|
39019
39604
|
};
|
|
39020
|
-
|
|
39021
|
-
|
|
39022
|
-
|
|
39023
|
-
|
|
39024
|
-
|
|
39025
|
-
|
|
39026
|
-
|
|
39027
|
-
|
|
39028
|
-
|
|
39605
|
+
await runMcp({
|
|
39606
|
+
name: "ofw",
|
|
39607
|
+
version: "2.3.2",
|
|
39608
|
+
// x-release-please-version
|
|
39609
|
+
deps: client,
|
|
39610
|
+
tools: [
|
|
39611
|
+
registerUserTools,
|
|
39612
|
+
registerMessageTools,
|
|
39613
|
+
registerCalendarTools,
|
|
39614
|
+
registerExpenseTools,
|
|
39615
|
+
registerJournalTools
|
|
39616
|
+
],
|
|
39617
|
+
banner: "[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion."
|
|
39618
|
+
});
|