ofw-mcp 2.2.0 → 2.3.1
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 +6 -13
- package/dist/bundle.js +521 -159
- package/dist/cache.js +1 -0
- package/dist/client.js +7 -9
- 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
|
@@ -7395,14 +7395,14 @@ var require_permessage_deflate = __commonJS({
|
|
|
7395
7395
|
}
|
|
7396
7396
|
};
|
|
7397
7397
|
module.exports = PerMessageDeflate2;
|
|
7398
|
-
function deflateOnData(
|
|
7399
|
-
this[kBuffers].push(
|
|
7400
|
-
this[kTotalLength] +=
|
|
7398
|
+
function deflateOnData(chunk2) {
|
|
7399
|
+
this[kBuffers].push(chunk2);
|
|
7400
|
+
this[kTotalLength] += chunk2.length;
|
|
7401
7401
|
}
|
|
7402
|
-
function inflateOnData(
|
|
7403
|
-
this[kTotalLength] +=
|
|
7402
|
+
function inflateOnData(chunk2) {
|
|
7403
|
+
this[kTotalLength] += chunk2.length;
|
|
7404
7404
|
if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) {
|
|
7405
|
-
this[kBuffers].push(
|
|
7405
|
+
this[kBuffers].push(chunk2);
|
|
7406
7406
|
return;
|
|
7407
7407
|
}
|
|
7408
7408
|
this[kError] = new RangeError("Max payload size exceeded");
|
|
@@ -7702,7 +7702,7 @@ var require_receiver = __commonJS({
|
|
|
7702
7702
|
* @param {Function} cb Callback
|
|
7703
7703
|
* @private
|
|
7704
7704
|
*/
|
|
7705
|
-
_write(
|
|
7705
|
+
_write(chunk2, encoding, cb) {
|
|
7706
7706
|
if (this._opcode === 8 && this._state == GET_INFO) return cb();
|
|
7707
7707
|
if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
|
|
7708
7708
|
cb(
|
|
@@ -7716,8 +7716,8 @@ var require_receiver = __commonJS({
|
|
|
7716
7716
|
);
|
|
7717
7717
|
return;
|
|
7718
7718
|
}
|
|
7719
|
-
this._bufferedBytes +=
|
|
7720
|
-
this._buffers.push(
|
|
7719
|
+
this._bufferedBytes += chunk2.length;
|
|
7720
|
+
this._buffers.push(chunk2);
|
|
7721
7721
|
this.startLoop(cb);
|
|
7722
7722
|
}
|
|
7723
7723
|
/**
|
|
@@ -9991,8 +9991,8 @@ var require_websocket = __commonJS({
|
|
|
9991
9991
|
this.removeListener("end", socketOnEnd);
|
|
9992
9992
|
websocket._readyState = WebSocket2.CLOSING;
|
|
9993
9993
|
if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
|
|
9994
|
-
const
|
|
9995
|
-
websocket._receiver.write(
|
|
9994
|
+
const chunk2 = this.read(this._readableState.length);
|
|
9995
|
+
websocket._receiver.write(chunk2);
|
|
9996
9996
|
}
|
|
9997
9997
|
websocket._receiver.end();
|
|
9998
9998
|
this[kWebSocket] = void 0;
|
|
@@ -10004,8 +10004,8 @@ var require_websocket = __commonJS({
|
|
|
10004
10004
|
websocket._receiver.on("finish", receiverOnFinish);
|
|
10005
10005
|
}
|
|
10006
10006
|
}
|
|
10007
|
-
function socketOnData(
|
|
10008
|
-
if (!this[kWebSocket]._receiver.write(
|
|
10007
|
+
function socketOnData(chunk2) {
|
|
10008
|
+
if (!this[kWebSocket]._receiver.write(chunk2)) {
|
|
10009
10009
|
this.pause();
|
|
10010
10010
|
}
|
|
10011
10011
|
}
|
|
@@ -10108,14 +10108,14 @@ var require_stream = __commonJS({
|
|
|
10108
10108
|
duplex._read = function() {
|
|
10109
10109
|
if (ws.isPaused) ws.resume();
|
|
10110
10110
|
};
|
|
10111
|
-
duplex._write = function(
|
|
10111
|
+
duplex._write = function(chunk2, encoding, callback) {
|
|
10112
10112
|
if (ws.readyState === ws.CONNECTING) {
|
|
10113
10113
|
ws.once("open", function open() {
|
|
10114
|
-
duplex._write(
|
|
10114
|
+
duplex._write(chunk2, encoding, callback);
|
|
10115
10115
|
});
|
|
10116
10116
|
return;
|
|
10117
10117
|
}
|
|
10118
|
-
ws.send(
|
|
10118
|
+
ws.send(chunk2, callback);
|
|
10119
10119
|
};
|
|
10120
10120
|
duplex.on("end", duplexOnEnd);
|
|
10121
10121
|
duplex.on("error", duplexOnError);
|
|
@@ -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.
|
|
@@ -34547,8 +34547,8 @@ import process3 from "node:process";
|
|
|
34547
34547
|
|
|
34548
34548
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
34549
34549
|
var ReadBuffer = class {
|
|
34550
|
-
append(
|
|
34551
|
-
this._buffer = this._buffer ? Buffer.concat([this._buffer,
|
|
34550
|
+
append(chunk2) {
|
|
34551
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
34552
34552
|
}
|
|
34553
34553
|
readMessage() {
|
|
34554
34554
|
if (!this._buffer) {
|
|
@@ -34580,8 +34580,8 @@ var StdioServerTransport = class {
|
|
|
34580
34580
|
this._stdout = _stdout;
|
|
34581
34581
|
this._readBuffer = new ReadBuffer();
|
|
34582
34582
|
this._started = false;
|
|
34583
|
-
this._ondata = (
|
|
34584
|
-
this._readBuffer.append(
|
|
34583
|
+
this._ondata = (chunk2) => {
|
|
34584
|
+
this._readBuffer.append(chunk2);
|
|
34585
34585
|
this.processReadBuffer();
|
|
34586
34586
|
};
|
|
34587
34587
|
this._onerror = (error51) => {
|
|
@@ -34634,8 +34634,152 @@ 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/response/index.js
|
|
34685
|
+
function textResult(data) {
|
|
34686
|
+
return {
|
|
34687
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
34688
|
+
};
|
|
34689
|
+
}
|
|
34690
|
+
function rawTextResult(text) {
|
|
34691
|
+
return { content: [{ type: "text", text }] };
|
|
34692
|
+
}
|
|
34693
|
+
|
|
34694
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
34695
|
+
import { homedir } from "node:os";
|
|
34696
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
34697
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
34698
|
+
function readEnvVar(key, opts = {}) {
|
|
34699
|
+
const env = opts.env ?? process.env;
|
|
34700
|
+
const raw = env[key];
|
|
34701
|
+
if (typeof raw === "string") {
|
|
34702
|
+
const trimmed = raw.trim();
|
|
34703
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
34704
|
+
return trimmed;
|
|
34705
|
+
}
|
|
34706
|
+
}
|
|
34707
|
+
return opts.default;
|
|
34708
|
+
}
|
|
34709
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
34710
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
34711
|
+
function parseBoolEnv(key, opts = {}) {
|
|
34712
|
+
const fallback = opts.default ?? false;
|
|
34713
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
34714
|
+
if (raw === void 0)
|
|
34715
|
+
return fallback;
|
|
34716
|
+
const token = raw.toLowerCase();
|
|
34717
|
+
if (TRUE_TOKENS.has(token))
|
|
34718
|
+
return true;
|
|
34719
|
+
if (FALSE_TOKENS.has(token))
|
|
34720
|
+
return false;
|
|
34721
|
+
return fallback;
|
|
34722
|
+
}
|
|
34723
|
+
function expandPath(p) {
|
|
34724
|
+
let expanded = p;
|
|
34725
|
+
if (p === "~") {
|
|
34726
|
+
expanded = homedir();
|
|
34727
|
+
} else if (p.startsWith("~/")) {
|
|
34728
|
+
expanded = join(homedir(), p.slice(2));
|
|
34729
|
+
}
|
|
34730
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
34731
|
+
}
|
|
34732
|
+
async function loadDotenvSafely(opts = {}) {
|
|
34733
|
+
try {
|
|
34734
|
+
const mod = await import(
|
|
34735
|
+
/* @vite-ignore */
|
|
34736
|
+
"dotenv"
|
|
34737
|
+
);
|
|
34738
|
+
const result = mod.config({
|
|
34739
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
34740
|
+
override: opts.override ?? false,
|
|
34741
|
+
quiet: true
|
|
34742
|
+
});
|
|
34743
|
+
return result.error === void 0;
|
|
34744
|
+
} catch {
|
|
34745
|
+
return false;
|
|
34746
|
+
}
|
|
34747
|
+
}
|
|
34748
|
+
|
|
34749
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
34750
|
+
import { openAsBlob } from "node:fs";
|
|
34751
|
+
async function fileBlob(path, opts = {}) {
|
|
34752
|
+
let blob;
|
|
34753
|
+
try {
|
|
34754
|
+
blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
|
|
34755
|
+
} catch {
|
|
34756
|
+
throw new Error(`Cannot read file for upload: ${path}`);
|
|
34757
|
+
}
|
|
34758
|
+
if (opts.maxBytes !== void 0 && blob.size > opts.maxBytes) {
|
|
34759
|
+
throw new Error(`${opts.label ?? "File"} is ${blob.size} bytes, over the ${opts.maxBytes}-byte limit: ${path}`);
|
|
34760
|
+
}
|
|
34761
|
+
return blob;
|
|
34762
|
+
}
|
|
34763
|
+
|
|
34764
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
34765
|
+
var PositiveInt = external_exports.number().int().positive();
|
|
34766
|
+
var NonNegInt = external_exports.number().int().nonnegative();
|
|
34767
|
+
var NonEmptyString = external_exports.string().min(1);
|
|
34768
|
+
var IsoDate = external_exports.iso.date();
|
|
34769
|
+
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
34770
|
+
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.");
|
|
34771
|
+
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
34772
|
+
var paginationSchema = {
|
|
34773
|
+
offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
|
|
34774
|
+
limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
|
|
34775
|
+
};
|
|
34776
|
+
var pageSchema = {
|
|
34777
|
+
page_num: PositiveInt.default(1).describe("1-based page number."),
|
|
34778
|
+
page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
|
|
34779
|
+
};
|
|
34780
|
+
|
|
34637
34781
|
// src/client.ts
|
|
34638
|
-
import { dirname, join as
|
|
34782
|
+
import { dirname, join as join4 } from "path";
|
|
34639
34783
|
import { fileURLToPath } from "url";
|
|
34640
34784
|
|
|
34641
34785
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
@@ -35494,13 +35638,13 @@ async function openEncryptedFrame(sessionKey, frame) {
|
|
|
35494
35638
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
35495
35639
|
import { createServer, Server as HttpServer } from "node:http";
|
|
35496
35640
|
async function electRole(opts) {
|
|
35497
|
-
const
|
|
35641
|
+
const server = createServer();
|
|
35498
35642
|
return new Promise((resolve2, reject) => {
|
|
35499
35643
|
const onError = (e) => {
|
|
35500
|
-
|
|
35644
|
+
server.removeListener("listening", onListening);
|
|
35501
35645
|
if (e.code === "EADDRINUSE") {
|
|
35502
35646
|
try {
|
|
35503
|
-
|
|
35647
|
+
server.close();
|
|
35504
35648
|
} catch {
|
|
35505
35649
|
}
|
|
35506
35650
|
resolve2({ role: "peer" });
|
|
@@ -35509,12 +35653,12 @@ async function electRole(opts) {
|
|
|
35509
35653
|
}
|
|
35510
35654
|
};
|
|
35511
35655
|
const onListening = () => {
|
|
35512
|
-
|
|
35513
|
-
resolve2({ role: "host", server
|
|
35656
|
+
server.removeListener("error", onError);
|
|
35657
|
+
resolve2({ role: "host", server });
|
|
35514
35658
|
};
|
|
35515
|
-
|
|
35516
|
-
|
|
35517
|
-
|
|
35659
|
+
server.once("error", onError);
|
|
35660
|
+
server.once("listening", onListening);
|
|
35661
|
+
server.listen(opts.port, opts.host);
|
|
35518
35662
|
});
|
|
35519
35663
|
}
|
|
35520
35664
|
|
|
@@ -35924,19 +36068,19 @@ async function startPeer(opts) {
|
|
|
35924
36068
|
|
|
35925
36069
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
35926
36070
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
35927
|
-
import { join } from "node:path";
|
|
35928
|
-
import { homedir } from "node:os";
|
|
36071
|
+
import { join as join2 } from "node:path";
|
|
36072
|
+
import { homedir as homedir2 } from "node:os";
|
|
35929
36073
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
35930
36074
|
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
35931
36075
|
function defaultIdentityDir() {
|
|
35932
|
-
return
|
|
36076
|
+
return join2(homedir2(), ".fetchproxy", "identity");
|
|
35933
36077
|
}
|
|
35934
36078
|
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
35935
36079
|
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
35936
36080
|
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
35937
36081
|
}
|
|
35938
36082
|
const safeFile = serverName.replace(/\//g, "_");
|
|
35939
|
-
const path =
|
|
36083
|
+
const path = join2(dir, `${safeFile}.json`);
|
|
35940
36084
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
35941
36085
|
try {
|
|
35942
36086
|
const raw = await readFile(path, "utf8");
|
|
@@ -35999,6 +36143,19 @@ function classifyFetchError(error51) {
|
|
|
35999
36143
|
return "other";
|
|
36000
36144
|
}
|
|
36001
36145
|
|
|
36146
|
+
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
36147
|
+
function classifyBridgeError(err) {
|
|
36148
|
+
if (err instanceof FetchproxyTimeoutError)
|
|
36149
|
+
return "timeout";
|
|
36150
|
+
if (err instanceof FetchproxyBridgeDownError)
|
|
36151
|
+
return "bridge_down";
|
|
36152
|
+
if (err instanceof FetchproxyHttpError)
|
|
36153
|
+
return "http";
|
|
36154
|
+
if (err instanceof FetchproxyProtocolError)
|
|
36155
|
+
return "protocol";
|
|
36156
|
+
return "other";
|
|
36157
|
+
}
|
|
36158
|
+
|
|
36002
36159
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
36003
36160
|
var FetchproxyProtocolError = class extends Error {
|
|
36004
36161
|
constructor(message) {
|
|
@@ -36028,7 +36185,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36028
36185
|
const retryAttempted = args.retryAttempted ?? false;
|
|
36029
36186
|
const op = args.op ?? "fetch";
|
|
36030
36187
|
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). `;
|
|
36031
|
-
const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}
|
|
36188
|
+
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.`;
|
|
36032
36189
|
super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
|
|
36033
36190
|
this.name = "FetchproxyBridgeDownError";
|
|
36034
36191
|
this.originalError = args.originalError;
|
|
@@ -36050,6 +36207,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36050
36207
|
port;
|
|
36051
36208
|
/** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
|
|
36052
36209
|
elapsedMs;
|
|
36210
|
+
/**
|
|
36211
|
+
* 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
|
|
36212
|
+
* fired for this timeout (a cold-start `timeout` symptom followed by
|
|
36213
|
+
* a warm-and-retry that also timed out). False when the retry was
|
|
36214
|
+
* disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
|
|
36215
|
+
* the first attempt.
|
|
36216
|
+
*/
|
|
36217
|
+
retryAttempted;
|
|
36053
36218
|
constructor(args) {
|
|
36054
36219
|
super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
|
|
36055
36220
|
this.name = "FetchproxyTimeoutError";
|
|
@@ -36058,6 +36223,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36058
36223
|
this.role = args.role ?? null;
|
|
36059
36224
|
this.port = args.port ?? 0;
|
|
36060
36225
|
this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
|
|
36226
|
+
this.retryAttempted = args.retryAttempted ?? false;
|
|
36061
36227
|
}
|
|
36062
36228
|
};
|
|
36063
36229
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
@@ -36131,6 +36297,25 @@ var FetchproxyServer = class {
|
|
|
36131
36297
|
// for "we're connecting right now" so two parallel first-calls don't
|
|
36132
36298
|
// race the port bind.
|
|
36133
36299
|
connectingPromise = null;
|
|
36300
|
+
// 0.8.1+ (#67): server-initiated keep-alive ping. Active when
|
|
36301
|
+
// `keepAliveIntervalMs` is set AND we've seen recent activity
|
|
36302
|
+
// (fetch/capture success or failure, or markActive()) within
|
|
36303
|
+
// `keepAliveMaxIdleMs`. The interval handle is created lazily on
|
|
36304
|
+
// first activity and torn down on close() / extension disconnect.
|
|
36305
|
+
keepAliveTimer = null;
|
|
36306
|
+
lastActiveAt = null;
|
|
36307
|
+
// 0.10.0+ (#73): observability counters surfaced via
|
|
36308
|
+
// bridgeHealth().keepAlive / .swEviction. Monotonic across the process
|
|
36309
|
+
// lifetime so a downstream healthcheck tool can verify the keep-alive
|
|
36310
|
+
// is actually preventing SW eviction. `lastPingAt` and `totalPings`
|
|
36311
|
+
// are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
|
|
36312
|
+
// `lastEvictionDetectedAt` are stamped from the lazy-revive code path
|
|
36313
|
+
// in fetch() / captureRequestHeader().
|
|
36314
|
+
lastPingAt = null;
|
|
36315
|
+
totalPings = 0;
|
|
36316
|
+
lazyReviveAttempts = 0;
|
|
36317
|
+
lazyReviveSuccesses = 0;
|
|
36318
|
+
lastEvictionDetectedAt = null;
|
|
36134
36319
|
constructor(opts) {
|
|
36135
36320
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
36136
36321
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -36183,6 +36368,22 @@ var FetchproxyServer = class {
|
|
|
36183
36368
|
// the legacy hang-forever / fail-once-on-SW-eviction behavior.
|
|
36184
36369
|
fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
|
|
36185
36370
|
bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
|
|
36371
|
+
// 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
|
|
36372
|
+
// wave showed every Pattern A consumer was opting into this same
|
|
36373
|
+
// value. Pass `0` to disable; the existing `<= 0` guards in
|
|
36374
|
+
// `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
|
|
36375
|
+
//
|
|
36376
|
+
// #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
|
|
36377
|
+
// Chrome's ~30s SW-eviction window — slack that timer drift, a
|
|
36378
|
+
// busy host event loop (CPU-bound response parsing between calls),
|
|
36379
|
+
// and the ping's own round-trip latency routinely ate, so the SW
|
|
36380
|
+
// evicted before the next ping landed and the next call cold-
|
|
36381
|
+
// started. 20s restores real margin. (The extension
|
|
36382
|
+
// `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
|
|
36383
|
+
// period, firing *at* the edge — it can't rescue a sub-30s race;
|
|
36384
|
+
// the server ping is the real defense.)
|
|
36385
|
+
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
36386
|
+
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
36186
36387
|
identityDir: opts.identityDir,
|
|
36187
36388
|
onPairCode: opts.onPairCode
|
|
36188
36389
|
};
|
|
@@ -36283,7 +36484,10 @@ var FetchproxyServer = class {
|
|
|
36283
36484
|
onPairCode: this.opts.onPairCode
|
|
36284
36485
|
});
|
|
36285
36486
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36286
|
-
this.hostHandle.onExtensionDisconnect(() =>
|
|
36487
|
+
this.hostHandle.onExtensionDisconnect(() => {
|
|
36488
|
+
this.stopKeepalive();
|
|
36489
|
+
this.rejectAllPending();
|
|
36490
|
+
});
|
|
36287
36491
|
this.hostHandle.onPendingPair((code) => {
|
|
36288
36492
|
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36289
36493
|
});
|
|
@@ -36307,7 +36511,10 @@ var FetchproxyServer = class {
|
|
|
36307
36511
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36308
36512
|
});
|
|
36309
36513
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36310
|
-
this.peerHandle.onRenegotiate(() =>
|
|
36514
|
+
this.peerHandle.onRenegotiate(() => {
|
|
36515
|
+
this.stopKeepalive();
|
|
36516
|
+
this.rejectAllPending();
|
|
36517
|
+
});
|
|
36311
36518
|
this.peerHandle.onPendingPair((code) => {
|
|
36312
36519
|
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36313
36520
|
});
|
|
@@ -36348,13 +36555,18 @@ var FetchproxyServer = class {
|
|
|
36348
36555
|
}
|
|
36349
36556
|
const first = await this._fetchOnceWithTimeout(init);
|
|
36350
36557
|
const reviveMs = this.opts.bridgeReviveDelayMs;
|
|
36351
|
-
|
|
36352
|
-
if (
|
|
36558
|
+
const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
|
|
36559
|
+
if (isColdStartSymptom) {
|
|
36560
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
36561
|
+
}
|
|
36562
|
+
if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
|
|
36563
|
+
this.lazyReviveAttempts += 1;
|
|
36353
36564
|
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36354
36565
|
const second = await this._fetchOnceWithTimeout(init);
|
|
36355
|
-
if (second.ok)
|
|
36566
|
+
if (second.ok) {
|
|
36567
|
+
this.lazyReviveSuccesses += 1;
|
|
36356
36568
|
this.recordSuccess();
|
|
36357
|
-
else
|
|
36569
|
+
} else
|
|
36358
36570
|
this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
|
|
36359
36571
|
return { ...second, retryAttempted: true };
|
|
36360
36572
|
}
|
|
@@ -36376,6 +36588,9 @@ var FetchproxyServer = class {
|
|
|
36376
36588
|
* call (addresses #23 ask 4).
|
|
36377
36589
|
*/
|
|
36378
36590
|
bridgeHealth() {
|
|
36591
|
+
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
36592
|
+
const maxIdleMs = this.opts.keepAliveMaxIdleMs;
|
|
36593
|
+
const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
|
|
36379
36594
|
return {
|
|
36380
36595
|
role: this.role,
|
|
36381
36596
|
port: this.opts.port,
|
|
@@ -36386,17 +36601,85 @@ var FetchproxyServer = class {
|
|
|
36386
36601
|
lastFailureAt: this.lastFailureAt,
|
|
36387
36602
|
lastFailureReason: this.lastFailureReason,
|
|
36388
36603
|
consecutiveFailures: this.consecutiveFailures,
|
|
36389
|
-
lastExtensionMessageAt: this.lastExtensionMessageAt
|
|
36604
|
+
lastExtensionMessageAt: this.lastExtensionMessageAt,
|
|
36605
|
+
keepAlive: {
|
|
36606
|
+
enabled: intervalMs > 0,
|
|
36607
|
+
intervalMs,
|
|
36608
|
+
maxIdleMs,
|
|
36609
|
+
lastPingAt: this.lastPingAt,
|
|
36610
|
+
totalPings: this.totalPings,
|
|
36611
|
+
idleSinceMs
|
|
36612
|
+
},
|
|
36613
|
+
swEviction: {
|
|
36614
|
+
lazyReviveAttempts: this.lazyReviveAttempts,
|
|
36615
|
+
lazyReviveSuccesses: this.lazyReviveSuccesses,
|
|
36616
|
+
lastEvictionDetectedAt: this.lastEvictionDetectedAt
|
|
36617
|
+
}
|
|
36390
36618
|
};
|
|
36391
36619
|
}
|
|
36392
36620
|
recordSuccess() {
|
|
36393
36621
|
this.lastSuccessAt = Date.now();
|
|
36394
36622
|
this.consecutiveFailures = 0;
|
|
36623
|
+
this.noteActivityForKeepalive();
|
|
36395
36624
|
}
|
|
36396
36625
|
recordFailure(reason) {
|
|
36397
36626
|
this.lastFailureAt = Date.now();
|
|
36398
36627
|
this.lastFailureReason = reason;
|
|
36399
36628
|
this.consecutiveFailures += 1;
|
|
36629
|
+
this.noteActivityForKeepalive();
|
|
36630
|
+
}
|
|
36631
|
+
/**
|
|
36632
|
+
* 0.8.1+ (#67): caller-side hint that work is happening or about to
|
|
36633
|
+
* happen — bumps the keep-alive idle gate so the server keeps pinging
|
|
36634
|
+
* the extension. Useful for MCPs that do a chain of side-effectful
|
|
36635
|
+
* work between bridge calls and don't want the SW to evict in the
|
|
36636
|
+
* gap (e.g. server-side parsing of a previous response that takes
|
|
36637
|
+
* tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
|
|
36638
|
+
*/
|
|
36639
|
+
markActive() {
|
|
36640
|
+
this.noteActivityForKeepalive();
|
|
36641
|
+
}
|
|
36642
|
+
noteActivityForKeepalive() {
|
|
36643
|
+
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
36644
|
+
if (intervalMs <= 0)
|
|
36645
|
+
return;
|
|
36646
|
+
this.lastActiveAt = Date.now();
|
|
36647
|
+
this.startKeepaliveIfIdle();
|
|
36648
|
+
}
|
|
36649
|
+
startKeepaliveIfIdle() {
|
|
36650
|
+
if (this.keepAliveTimer !== null)
|
|
36651
|
+
return;
|
|
36652
|
+
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
36653
|
+
if (intervalMs <= 0)
|
|
36654
|
+
return;
|
|
36655
|
+
this.keepAliveTimer = setInterval(() => {
|
|
36656
|
+
const now = Date.now();
|
|
36657
|
+
if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
|
|
36658
|
+
this.stopKeepalive();
|
|
36659
|
+
return;
|
|
36660
|
+
}
|
|
36661
|
+
this.totalPings += 1;
|
|
36662
|
+
this.lastPingAt = now;
|
|
36663
|
+
void this.sendKeepalivePing();
|
|
36664
|
+
}, intervalMs);
|
|
36665
|
+
}
|
|
36666
|
+
async sendKeepalivePing() {
|
|
36667
|
+
try {
|
|
36668
|
+
const inner = { type: "ping" };
|
|
36669
|
+
if (this.hostHandle) {
|
|
36670
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
36671
|
+
} else if (this.peerHandle) {
|
|
36672
|
+
await this.peerHandle.sendInner(inner);
|
|
36673
|
+
}
|
|
36674
|
+
} catch (e) {
|
|
36675
|
+
console.error("[fetchproxy] keepalive ping send failed:", e);
|
|
36676
|
+
}
|
|
36677
|
+
}
|
|
36678
|
+
stopKeepalive() {
|
|
36679
|
+
if (this.keepAliveTimer !== null) {
|
|
36680
|
+
clearInterval(this.keepAliveTimer);
|
|
36681
|
+
this.keepAliveTimer = null;
|
|
36682
|
+
}
|
|
36400
36683
|
}
|
|
36401
36684
|
/**
|
|
36402
36685
|
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
@@ -36455,7 +36738,8 @@ var FetchproxyServer = class {
|
|
|
36455
36738
|
timeoutMs: this.opts.fetchTimeoutMs ?? 0,
|
|
36456
36739
|
role: this.role,
|
|
36457
36740
|
port: this.opts.port,
|
|
36458
|
-
elapsedMs: result.elapsedMs
|
|
36741
|
+
elapsedMs: result.elapsedMs,
|
|
36742
|
+
retryAttempted
|
|
36459
36743
|
});
|
|
36460
36744
|
}
|
|
36461
36745
|
if (result.kind === "content_script_unreachable") {
|
|
@@ -36586,6 +36870,108 @@ var FetchproxyServer = class {
|
|
|
36586
36870
|
const response = await this.get(path, this.applyJsonDefaults(opts));
|
|
36587
36871
|
return response.body;
|
|
36588
36872
|
}
|
|
36873
|
+
/**
|
|
36874
|
+
* 0.11.0+: method-generic JSON convenience helper. Generalizes the
|
|
36875
|
+
* `fetchJson<T>(path, { method, headers, body })` that
|
|
36876
|
+
* zillow/redfin/compass/homes hand-rolled char-for-char in their
|
|
36877
|
+
* `src/client.ts`:
|
|
36878
|
+
*
|
|
36879
|
+
* - sets `Accept: application/json`;
|
|
36880
|
+
* - adds `Content-Type: application/json` only for a non-GET request
|
|
36881
|
+
* that carries a `body` (and only if the caller didn't set one);
|
|
36882
|
+
* - `JSON.stringify`s the body (GET / no-body sends nothing);
|
|
36883
|
+
* - treats a `204` or an empty body as `data: null` (no parse);
|
|
36884
|
+
* - otherwise `JSON.parse`s the body.
|
|
36885
|
+
*
|
|
36886
|
+
* Scope is serialization + header defaults + 204-handling +
|
|
36887
|
+
* JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
|
|
36888
|
+
* or look for a sign-in interstitial — those guards differ per site
|
|
36889
|
+
* (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
|
|
36890
|
+
* returns BOTH the parsed `data` and the raw `FetchResult` and leaves
|
|
36891
|
+
* the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
|
|
36892
|
+
* over `result`.
|
|
36893
|
+
*
|
|
36894
|
+
* Bridge-level failures (no signed-in tab, SW down, timeout) still
|
|
36895
|
+
* throw the typed errors via `request()`, exactly like the verb
|
|
36896
|
+
* helpers — only successful round-trips (any HTTP status) return.
|
|
36897
|
+
*/
|
|
36898
|
+
async requestJson(method, path, opts = {}) {
|
|
36899
|
+
const isGet = method.toUpperCase() === "GET";
|
|
36900
|
+
const sendBody = !isGet && opts.body !== void 0;
|
|
36901
|
+
const headers = {
|
|
36902
|
+
Accept: "application/json",
|
|
36903
|
+
...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
|
|
36904
|
+
...opts.headers ?? {}
|
|
36905
|
+
};
|
|
36906
|
+
const response = await this.request(method, path, {
|
|
36907
|
+
headers,
|
|
36908
|
+
body: sendBody ? JSON.stringify(opts.body) : void 0,
|
|
36909
|
+
...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
|
|
36910
|
+
...opts.domain !== void 0 ? { domain: opts.domain } : {}
|
|
36911
|
+
});
|
|
36912
|
+
const result = {
|
|
36913
|
+
ok: true,
|
|
36914
|
+
status: response.status,
|
|
36915
|
+
url: response.url,
|
|
36916
|
+
body: response.body
|
|
36917
|
+
};
|
|
36918
|
+
if (response.status === 204 || response.body === "") {
|
|
36919
|
+
return { data: null, result };
|
|
36920
|
+
}
|
|
36921
|
+
let data;
|
|
36922
|
+
try {
|
|
36923
|
+
data = JSON.parse(response.body);
|
|
36924
|
+
} catch (e) {
|
|
36925
|
+
throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
36926
|
+
}
|
|
36927
|
+
return { data, result };
|
|
36928
|
+
}
|
|
36929
|
+
/**
|
|
36930
|
+
* 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
|
|
36931
|
+
* the elapsed round-trip, classify any thrown error, and project the
|
|
36932
|
+
* post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
|
|
36933
|
+
*
|
|
36934
|
+
* This is the transport half of the probe loop zillow/redfin/homes
|
|
36935
|
+
* had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
|
|
36936
|
+
* supplies its own probe call (`(path) => client.fetchHtml(path)`)
|
|
36937
|
+
* and probe path (e.g. `'/robots.txt'`); the tool registration and
|
|
36938
|
+
* the site-specific plain-English hint text STAY in the consumer.
|
|
36939
|
+
*
|
|
36940
|
+
* `bridgeHealth()` is read AFTER the probe so its freshness counters
|
|
36941
|
+
* (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
|
|
36942
|
+
* round-trip rather than a stale pre-probe snapshot.
|
|
36943
|
+
*/
|
|
36944
|
+
async runProbe(fetchFn, probePath) {
|
|
36945
|
+
const start = Date.now();
|
|
36946
|
+
let ok = false;
|
|
36947
|
+
let error51;
|
|
36948
|
+
try {
|
|
36949
|
+
await fetchFn(probePath);
|
|
36950
|
+
ok = true;
|
|
36951
|
+
} catch (e) {
|
|
36952
|
+
error51 = {
|
|
36953
|
+
kind: classifyBridgeError(e),
|
|
36954
|
+
message: e instanceof Error ? e.message : String(e)
|
|
36955
|
+
};
|
|
36956
|
+
}
|
|
36957
|
+
const elapsed_ms = Date.now() - start;
|
|
36958
|
+
const health = this.bridgeHealth();
|
|
36959
|
+
return {
|
|
36960
|
+
ok,
|
|
36961
|
+
elapsed_ms,
|
|
36962
|
+
bridge: {
|
|
36963
|
+
role: health.role,
|
|
36964
|
+
port: health.port,
|
|
36965
|
+
server_version: health.serverVersion,
|
|
36966
|
+
fetch_timeout_ms: health.fetchTimeoutMs,
|
|
36967
|
+
last_success_at: health.lastSuccessAt,
|
|
36968
|
+
last_failure_at: health.lastFailureAt,
|
|
36969
|
+
last_failure_reason: health.lastFailureReason,
|
|
36970
|
+
consecutive_failures: health.consecutiveFailures
|
|
36971
|
+
},
|
|
36972
|
+
...error51 ? { error: error51 } : {}
|
|
36973
|
+
};
|
|
36974
|
+
}
|
|
36589
36975
|
/**
|
|
36590
36976
|
* Snapshot the user's non-HttpOnly cookies for the chosen domain.
|
|
36591
36977
|
*
|
|
@@ -36754,11 +37140,14 @@ var FetchproxyServer = class {
|
|
|
36754
37140
|
this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
|
|
36755
37141
|
throw err;
|
|
36756
37142
|
}
|
|
37143
|
+
this.lastEvictionDetectedAt = Date.now();
|
|
36757
37144
|
const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
|
|
36758
37145
|
if (reviveMs > 0) {
|
|
37146
|
+
this.lazyReviveAttempts += 1;
|
|
36759
37147
|
await new Promise((r) => setTimeout(r, reviveMs));
|
|
36760
37148
|
try {
|
|
36761
37149
|
const result = await this._captureRequestHeaderOnce(callOpts);
|
|
37150
|
+
this.lazyReviveSuccesses += 1;
|
|
36762
37151
|
this.recordSuccess();
|
|
36763
37152
|
return result;
|
|
36764
37153
|
} catch (retryErr) {
|
|
@@ -37051,6 +37440,7 @@ var FetchproxyServer = class {
|
|
|
37051
37440
|
* twice in a row.
|
|
37052
37441
|
*/
|
|
37053
37442
|
async close() {
|
|
37443
|
+
this.stopKeepalive();
|
|
37054
37444
|
this.rejectAllPending();
|
|
37055
37445
|
if (this.connectingPromise) {
|
|
37056
37446
|
await this.connectingPromise.catch(() => void 0);
|
|
@@ -37066,19 +37456,6 @@ var FetchproxyServer = class {
|
|
|
37066
37456
|
}
|
|
37067
37457
|
};
|
|
37068
37458
|
|
|
37069
|
-
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
37070
|
-
function classifyBridgeError(err) {
|
|
37071
|
-
if (err instanceof FetchproxyTimeoutError)
|
|
37072
|
-
return "timeout";
|
|
37073
|
-
if (err instanceof FetchproxyBridgeDownError)
|
|
37074
|
-
return "bridge_down";
|
|
37075
|
-
if (err instanceof FetchproxyHttpError)
|
|
37076
|
-
return "http";
|
|
37077
|
-
if (err instanceof FetchproxyProtocolError)
|
|
37078
|
-
return "protocol";
|
|
37079
|
-
return "other";
|
|
37080
|
-
}
|
|
37081
|
-
|
|
37082
37459
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
37083
37460
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
37084
37461
|
async function bootstrap(opts) {
|
|
@@ -37110,7 +37487,7 @@ async function bootstrap(opts) {
|
|
|
37110
37487
|
const sessionStorageKeys = new Set(opts.declare.sessionStorage);
|
|
37111
37488
|
for (const p of sessionStoragePointers)
|
|
37112
37489
|
sessionStorageKeys.add(p.storageKey);
|
|
37113
|
-
const
|
|
37490
|
+
const server = factory({
|
|
37114
37491
|
serverName: opts.serverName,
|
|
37115
37492
|
version: opts.version,
|
|
37116
37493
|
domains: [...opts.domains],
|
|
@@ -37145,10 +37522,10 @@ async function bootstrap(opts) {
|
|
|
37145
37522
|
if (opts.storageSubdomain !== void 0)
|
|
37146
37523
|
storageDomainOpts.subdomain = opts.storageSubdomain;
|
|
37147
37524
|
try {
|
|
37148
|
-
await
|
|
37525
|
+
await server.listen();
|
|
37149
37526
|
const cookies = {};
|
|
37150
37527
|
if (opts.declare.cookies.length > 0) {
|
|
37151
|
-
const joined = await
|
|
37528
|
+
const joined = await server.readCookies({
|
|
37152
37529
|
keys: opts.declare.cookies,
|
|
37153
37530
|
...storageDomainOpts
|
|
37154
37531
|
});
|
|
@@ -37168,7 +37545,7 @@ async function bootstrap(opts) {
|
|
|
37168
37545
|
for (const p of localStoragePointers) {
|
|
37169
37546
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
37170
37547
|
}
|
|
37171
|
-
localStorage = await
|
|
37548
|
+
localStorage = await server.readLocalStorage({
|
|
37172
37549
|
keys: allKeys,
|
|
37173
37550
|
...storageDomainOpts,
|
|
37174
37551
|
...localStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37181,7 +37558,7 @@ async function bootstrap(opts) {
|
|
|
37181
37558
|
for (const p of sessionStoragePointers) {
|
|
37182
37559
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
37183
37560
|
}
|
|
37184
|
-
sessionStorage = await
|
|
37561
|
+
sessionStorage = await server.readSessionStorage({
|
|
37185
37562
|
keys: allKeys,
|
|
37186
37563
|
...storageDomainOpts,
|
|
37187
37564
|
...sessionStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -37197,14 +37574,14 @@ async function bootstrap(opts) {
|
|
|
37197
37574
|
opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37198
37575
|
}
|
|
37199
37576
|
}
|
|
37200
|
-
capturedHeaders[h.headerName] = await
|
|
37577
|
+
capturedHeaders[h.headerName] = await server.captureRequestHeader({
|
|
37201
37578
|
urlPattern: h.urlPattern,
|
|
37202
37579
|
headerName: h.headerName
|
|
37203
37580
|
});
|
|
37204
37581
|
}
|
|
37205
37582
|
const indexedDbBucket = {};
|
|
37206
37583
|
for (const d of indexedDb) {
|
|
37207
|
-
const values = await
|
|
37584
|
+
const values = await server.readIndexedDb({
|
|
37208
37585
|
database: d.database,
|
|
37209
37586
|
store: d.store,
|
|
37210
37587
|
keys: [...d.keys],
|
|
@@ -37221,7 +37598,7 @@ async function bootstrap(opts) {
|
|
|
37221
37598
|
};
|
|
37222
37599
|
} finally {
|
|
37223
37600
|
try {
|
|
37224
|
-
await
|
|
37601
|
+
await server.close();
|
|
37225
37602
|
} catch {
|
|
37226
37603
|
}
|
|
37227
37604
|
}
|
|
@@ -37286,8 +37663,8 @@ async function loginWithPassword(username, password) {
|
|
|
37286
37663
|
|
|
37287
37664
|
// src/config.ts
|
|
37288
37665
|
import { createHash } from "node:crypto";
|
|
37289
|
-
import { homedir as
|
|
37290
|
-
import { join as
|
|
37666
|
+
import { homedir as homedir3 } from "node:os";
|
|
37667
|
+
import { join as join3 } from "node:path";
|
|
37291
37668
|
function readCacheIdentity() {
|
|
37292
37669
|
const explicit = process.env.OFW_CACHE_IDENTITY;
|
|
37293
37670
|
if (typeof explicit === "string" && explicit.trim().length > 0) return explicit.trim();
|
|
@@ -37298,31 +37675,29 @@ function readCacheIdentity() {
|
|
|
37298
37675
|
function getCacheDir() {
|
|
37299
37676
|
const override = process.env.OFW_CACHE_DIR;
|
|
37300
37677
|
if (override && override.trim().length > 0) return override.trim();
|
|
37301
|
-
return
|
|
37678
|
+
return join3(homedir3(), ".cache", "ofw-mcp");
|
|
37302
37679
|
}
|
|
37303
37680
|
function getCacheDbPath() {
|
|
37304
37681
|
const identity = readCacheIdentity();
|
|
37305
37682
|
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
37306
|
-
return
|
|
37683
|
+
return join3(getCacheDir(), `${hash2}.db`);
|
|
37307
37684
|
}
|
|
37308
37685
|
function getAttachmentsDir() {
|
|
37309
37686
|
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
37310
37687
|
if (override && override.trim().length > 0) return override.trim();
|
|
37311
|
-
return
|
|
37688
|
+
return join3(homedir3(), "Downloads", "ofw-mcp");
|
|
37312
37689
|
}
|
|
37313
|
-
function
|
|
37314
|
-
|
|
37315
|
-
if (typeof raw !== "string") return false;
|
|
37316
|
-
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
|
|
37690
|
+
function parseBoolEnv2(name) {
|
|
37691
|
+
return parseBoolEnv(name);
|
|
37317
37692
|
}
|
|
37318
37693
|
function getDefaultInlineAttachments() {
|
|
37319
|
-
return
|
|
37694
|
+
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
37320
37695
|
}
|
|
37321
37696
|
|
|
37322
37697
|
// package.json
|
|
37323
37698
|
var package_default = {
|
|
37324
37699
|
name: "ofw-mcp",
|
|
37325
|
-
version: "2.
|
|
37700
|
+
version: "2.3.1",
|
|
37326
37701
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
37327
37702
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
37328
37703
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37346,11 +37721,12 @@ var package_default = {
|
|
|
37346
37721
|
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`,
|
|
37347
37722
|
dev: "node --env-file=.env dist/index.js",
|
|
37348
37723
|
test: "vitest run",
|
|
37724
|
+
"test:coverage": "vitest run --coverage",
|
|
37349
37725
|
"test:watch": "vitest"
|
|
37350
37726
|
},
|
|
37351
37727
|
dependencies: {
|
|
37352
|
-
"@
|
|
37353
|
-
"@fetchproxy/
|
|
37728
|
+
"@chrischall/mcp-utils": "^0.4.0",
|
|
37729
|
+
"@fetchproxy/bootstrap": "^0.11.0",
|
|
37354
37730
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37355
37731
|
dotenv: "^17.4.2",
|
|
37356
37732
|
zod: "^4.4.3"
|
|
@@ -37366,16 +37742,10 @@ var package_default = {
|
|
|
37366
37742
|
|
|
37367
37743
|
// src/auth.ts
|
|
37368
37744
|
function readEnv(key) {
|
|
37369
|
-
|
|
37370
|
-
if (typeof raw !== "string") return void 0;
|
|
37371
|
-
const trimmed = raw.trim();
|
|
37372
|
-
if (trimmed.length === 0) return void 0;
|
|
37373
|
-
if (trimmed === "undefined" || trimmed === "null") return void 0;
|
|
37374
|
-
if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
|
|
37375
|
-
return trimmed;
|
|
37745
|
+
return readEnvVar(key);
|
|
37376
37746
|
}
|
|
37377
37747
|
function fetchproxyDisabled() {
|
|
37378
|
-
return
|
|
37748
|
+
return parseBoolEnv2("OFW_DISABLE_FETCHPROXY");
|
|
37379
37749
|
}
|
|
37380
37750
|
async function resolveAuth() {
|
|
37381
37751
|
const username = readEnv("OFW_USERNAME");
|
|
@@ -37435,12 +37805,8 @@ async function resolveAuth() {
|
|
|
37435
37805
|
}
|
|
37436
37806
|
|
|
37437
37807
|
// src/client.ts
|
|
37438
|
-
|
|
37439
|
-
|
|
37440
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37441
|
-
config2({ path: join3(__dirname, "..", ".env"), override: false, quiet: true });
|
|
37442
|
-
} catch {
|
|
37443
|
-
}
|
|
37808
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37809
|
+
await loadDotenvSafely({ path: join4(__dirname, "..", ".env") });
|
|
37444
37810
|
function parseContentDispositionFilename(cd) {
|
|
37445
37811
|
const extMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(cd);
|
|
37446
37812
|
if (extMatch) {
|
|
@@ -37455,7 +37821,7 @@ function parseContentDispositionFilename(cd) {
|
|
|
37455
37821
|
return m ? m[1] : null;
|
|
37456
37822
|
}
|
|
37457
37823
|
function debugLogEnabled() {
|
|
37458
|
-
return
|
|
37824
|
+
return parseBoolEnv2("OFW_DEBUG_LOG");
|
|
37459
37825
|
}
|
|
37460
37826
|
function redactHeaders(h) {
|
|
37461
37827
|
const out = { ...h };
|
|
@@ -37584,13 +37950,8 @@ var OFWClient = class {
|
|
|
37584
37950
|
var client = new OFWClient();
|
|
37585
37951
|
|
|
37586
37952
|
// src/tools/_shared.ts
|
|
37587
|
-
|
|
37588
|
-
|
|
37589
|
-
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
37590
|
-
}
|
|
37591
|
-
function textResponse(text) {
|
|
37592
|
-
return { content: [{ type: "text", text }] };
|
|
37593
|
-
}
|
|
37953
|
+
var jsonResponse = textResult;
|
|
37954
|
+
var textResponse = rawTextResult;
|
|
37594
37955
|
function mapRecipients(items) {
|
|
37595
37956
|
return (items ?? []).map((r) => ({
|
|
37596
37957
|
userId: r.user?.id ?? 0,
|
|
@@ -37598,10 +37959,7 @@ function mapRecipients(items) {
|
|
|
37598
37959
|
viewedAt: r.viewed?.dateTime ?? null
|
|
37599
37960
|
}));
|
|
37600
37961
|
}
|
|
37601
|
-
|
|
37602
|
-
const expanded = p.startsWith("~/") ? join4(process.env.HOME ?? "", p.slice(2)) : p;
|
|
37603
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
37604
|
-
}
|
|
37962
|
+
var expandPath2 = expandPath;
|
|
37605
37963
|
async function postMessageAndRefetch(client2, payload) {
|
|
37606
37964
|
const raw = await client2.request(
|
|
37607
37965
|
"POST",
|
|
@@ -37615,15 +37973,15 @@ async function postMessageAndRefetch(client2, payload) {
|
|
|
37615
37973
|
}
|
|
37616
37974
|
|
|
37617
37975
|
// src/tools/user.ts
|
|
37618
|
-
function registerUserTools(
|
|
37619
|
-
|
|
37976
|
+
function registerUserTools(server, client2) {
|
|
37977
|
+
server.registerTool("ofw_get_profile", {
|
|
37620
37978
|
description: "Get current user and co-parent profile information from OurFamilyWizard",
|
|
37621
37979
|
annotations: { readOnlyHint: true }
|
|
37622
37980
|
}, async () => {
|
|
37623
37981
|
const data = await client2.request("GET", "/pub/v2/profiles");
|
|
37624
37982
|
return jsonResponse(data);
|
|
37625
37983
|
});
|
|
37626
|
-
|
|
37984
|
+
server.registerTool("ofw_get_notifications", {
|
|
37627
37985
|
description: "Get OurFamilyWizard dashboard summary: unread message count, upcoming events, outstanding expenses. Note: updates your last-seen status.",
|
|
37628
37986
|
annotations: { readOnlyHint: false }
|
|
37629
37987
|
}, async () => {
|
|
@@ -38164,15 +38522,15 @@ function listDataHintsAtFiles(listData) {
|
|
|
38164
38522
|
if (Array.isArray(ld.files)) return ld.files.length > 0;
|
|
38165
38523
|
return false;
|
|
38166
38524
|
}
|
|
38167
|
-
function registerMessageTools(
|
|
38168
|
-
|
|
38525
|
+
function registerMessageTools(server, client2) {
|
|
38526
|
+
server.registerTool("ofw_list_message_folders", {
|
|
38169
38527
|
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.",
|
|
38170
38528
|
annotations: { readOnlyHint: true }
|
|
38171
38529
|
}, async () => {
|
|
38172
38530
|
const data = await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true");
|
|
38173
38531
|
return jsonResponse(data);
|
|
38174
38532
|
});
|
|
38175
|
-
|
|
38533
|
+
server.registerTool("ofw_list_messages", {
|
|
38176
38534
|
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.",
|
|
38177
38535
|
annotations: { readOnlyHint: true },
|
|
38178
38536
|
inputSchema: {
|
|
@@ -38208,7 +38566,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38208
38566
|
}
|
|
38209
38567
|
return jsonResponse(payload);
|
|
38210
38568
|
});
|
|
38211
|
-
|
|
38569
|
+
server.registerTool("ofw_get_message", {
|
|
38212
38570
|
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.',
|
|
38213
38571
|
annotations: { readOnlyHint: false },
|
|
38214
38572
|
inputSchema: {
|
|
@@ -38273,7 +38631,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38273
38631
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
38274
38632
|
return jsonResponse({ ...row, attachments });
|
|
38275
38633
|
});
|
|
38276
|
-
|
|
38634
|
+
server.registerTool("ofw_send_message", {
|
|
38277
38635
|
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.",
|
|
38278
38636
|
annotations: { destructiveHint: true },
|
|
38279
38637
|
inputSchema: {
|
|
@@ -38383,7 +38741,7 @@ function registerMessageTools(server2, client2) {
|
|
|
38383
38741
|
|
|
38384
38742
|
${text}` : text);
|
|
38385
38743
|
});
|
|
38386
|
-
|
|
38744
|
+
server.registerTool("ofw_list_drafts", {
|
|
38387
38745
|
description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
|
|
38388
38746
|
annotations: { readOnlyHint: true },
|
|
38389
38747
|
inputSchema: {
|
|
@@ -38397,7 +38755,7 @@ ${text}` : text);
|
|
|
38397
38755
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
38398
38756
|
return jsonResponse(payload);
|
|
38399
38757
|
});
|
|
38400
|
-
|
|
38758
|
+
server.registerTool("ofw_save_draft", {
|
|
38401
38759
|
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.",
|
|
38402
38760
|
annotations: { readOnlyHint: false },
|
|
38403
38761
|
inputSchema: {
|
|
@@ -38459,7 +38817,7 @@ ${text}` : text);
|
|
|
38459
38817
|
|
|
38460
38818
|
${text}` : text);
|
|
38461
38819
|
});
|
|
38462
|
-
|
|
38820
|
+
server.registerTool("ofw_delete_draft", {
|
|
38463
38821
|
description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
|
|
38464
38822
|
annotations: { destructiveHint: true },
|
|
38465
38823
|
inputSchema: {
|
|
@@ -38470,7 +38828,7 @@ ${text}` : text);
|
|
|
38470
38828
|
deleteDraft(args.messageId);
|
|
38471
38829
|
return data ? jsonResponse(data) : textResponse("Draft deleted.");
|
|
38472
38830
|
});
|
|
38473
|
-
|
|
38831
|
+
server.registerTool("ofw_get_unread_sent", {
|
|
38474
38832
|
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.",
|
|
38475
38833
|
annotations: { readOnlyHint: true },
|
|
38476
38834
|
inputSchema: {
|
|
@@ -38496,7 +38854,7 @@ ${text}` : text);
|
|
|
38496
38854
|
}
|
|
38497
38855
|
return jsonResponse(unread);
|
|
38498
38856
|
});
|
|
38499
|
-
|
|
38857
|
+
server.registerTool("ofw_upload_attachment", {
|
|
38500
38858
|
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.`,
|
|
38501
38859
|
annotations: { destructiveHint: false },
|
|
38502
38860
|
inputSchema: {
|
|
@@ -38506,14 +38864,13 @@ ${text}` : text);
|
|
|
38506
38864
|
description: external_exports.string().describe("Description shown in OFW My Files (default: filename)").optional()
|
|
38507
38865
|
}
|
|
38508
38866
|
}, async (args) => {
|
|
38509
|
-
const abs =
|
|
38867
|
+
const abs = expandPath2(args.path);
|
|
38510
38868
|
const stat = statSync(abs);
|
|
38511
38869
|
if (!stat.isFile()) throw new Error(`Not a file: ${abs}`);
|
|
38512
|
-
const buf = readFileSync(abs);
|
|
38513
38870
|
const fileName = basename(abs);
|
|
38514
38871
|
const mime = mimeFromName(fileName);
|
|
38515
38872
|
const form = new FormData();
|
|
38516
|
-
form.append("file",
|
|
38873
|
+
form.append("file", await fileBlob(abs, { type: mime }), fileName);
|
|
38517
38874
|
form.append("source", "message");
|
|
38518
38875
|
form.append("description", args.description ?? fileName);
|
|
38519
38876
|
form.append("label", args.label ?? fileName);
|
|
@@ -38525,7 +38882,7 @@ ${text}` : text);
|
|
|
38525
38882
|
fileName: meta3.fileName ?? fileName,
|
|
38526
38883
|
label: meta3.label ?? args.label ?? fileName,
|
|
38527
38884
|
mimeType: meta3.fileType ?? mime,
|
|
38528
|
-
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes :
|
|
38885
|
+
sizeBytes: typeof meta3.sizeInBytes === "number" ? meta3.sizeInBytes : stat.size,
|
|
38529
38886
|
metadata: meta3,
|
|
38530
38887
|
messageId: 0
|
|
38531
38888
|
});
|
|
@@ -38533,12 +38890,12 @@ ${text}` : text);
|
|
|
38533
38890
|
fileId: meta3.fileId,
|
|
38534
38891
|
fileName: meta3.fileName ?? fileName,
|
|
38535
38892
|
mimeType: meta3.fileType ?? mime,
|
|
38536
|
-
sizeBytes: meta3.sizeInBytes ??
|
|
38893
|
+
sizeBytes: meta3.sizeInBytes ?? stat.size,
|
|
38537
38894
|
shareClass: meta3.shareClass ?? args.shareClass ?? "PRIVATE",
|
|
38538
38895
|
note: "Pass this fileId to ofw_send_message or ofw_save_draft in myFileIDs to attach it."
|
|
38539
38896
|
});
|
|
38540
38897
|
});
|
|
38541
|
-
|
|
38898
|
+
server.registerTool("ofw_download_attachment", {
|
|
38542
38899
|
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).',
|
|
38543
38900
|
annotations: { readOnlyHint: false },
|
|
38544
38901
|
inputSchema: {
|
|
@@ -38592,7 +38949,7 @@ ${text}` : text);
|
|
|
38592
38949
|
let dest;
|
|
38593
38950
|
if (args.saveTo) {
|
|
38594
38951
|
const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
|
|
38595
|
-
const abs =
|
|
38952
|
+
const abs = expandPath2(args.saveTo);
|
|
38596
38953
|
dest = isDirArg ? join5(abs, `${fileId}-${cached2.fileName}`) : abs;
|
|
38597
38954
|
} else {
|
|
38598
38955
|
dest = join5(getAttachmentsDir(), `${fileId}-${cached2.fileName}`);
|
|
@@ -38619,7 +38976,7 @@ ${text}` : text);
|
|
|
38619
38976
|
fileName: response.suggestedFileName ?? cached2.fileName
|
|
38620
38977
|
});
|
|
38621
38978
|
});
|
|
38622
|
-
|
|
38979
|
+
server.registerTool("ofw_sync_messages", {
|
|
38623
38980
|
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).",
|
|
38624
38981
|
annotations: { readOnlyHint: false },
|
|
38625
38982
|
inputSchema: {
|
|
@@ -38643,8 +39000,8 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
38643
39000
|
}
|
|
38644
39001
|
|
|
38645
39002
|
// src/tools/calendar.ts
|
|
38646
|
-
function registerCalendarTools(
|
|
38647
|
-
|
|
39003
|
+
function registerCalendarTools(server, client2) {
|
|
39004
|
+
server.registerTool("ofw_list_events", {
|
|
38648
39005
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
38649
39006
|
annotations: { readOnlyHint: true },
|
|
38650
39007
|
inputSchema: {
|
|
@@ -38660,7 +39017,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38660
39017
|
);
|
|
38661
39018
|
return jsonResponse(data);
|
|
38662
39019
|
});
|
|
38663
|
-
|
|
39020
|
+
server.registerTool("ofw_create_event", {
|
|
38664
39021
|
description: "Create a calendar event in OurFamilyWizard",
|
|
38665
39022
|
annotations: { destructiveHint: false },
|
|
38666
39023
|
inputSchema: {
|
|
@@ -38680,7 +39037,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38680
39037
|
const data = await client2.request("POST", "/pub/v1/calendar/events", args);
|
|
38681
39038
|
return jsonResponse(data);
|
|
38682
39039
|
});
|
|
38683
|
-
|
|
39040
|
+
server.registerTool("ofw_update_event", {
|
|
38684
39041
|
description: "Update an existing OurFamilyWizard calendar event",
|
|
38685
39042
|
annotations: { destructiveHint: true },
|
|
38686
39043
|
inputSchema: {
|
|
@@ -38698,7 +39055,7 @@ function registerCalendarTools(server2, client2) {
|
|
|
38698
39055
|
const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
38699
39056
|
return jsonResponse(data);
|
|
38700
39057
|
});
|
|
38701
|
-
|
|
39058
|
+
server.registerTool("ofw_delete_event", {
|
|
38702
39059
|
description: "Delete an OurFamilyWizard calendar event",
|
|
38703
39060
|
annotations: { destructiveHint: true },
|
|
38704
39061
|
inputSchema: {
|
|
@@ -38711,15 +39068,15 @@ function registerCalendarTools(server2, client2) {
|
|
|
38711
39068
|
}
|
|
38712
39069
|
|
|
38713
39070
|
// src/tools/expenses.ts
|
|
38714
|
-
function registerExpenseTools(
|
|
38715
|
-
|
|
39071
|
+
function registerExpenseTools(server, client2) {
|
|
39072
|
+
server.registerTool("ofw_get_expense_totals", {
|
|
38716
39073
|
description: "Get OurFamilyWizard expense summary totals (owed/paid)",
|
|
38717
39074
|
annotations: { readOnlyHint: true }
|
|
38718
39075
|
}, async () => {
|
|
38719
39076
|
const data = await client2.request("GET", "/pub/v2/expense/expenses/totals");
|
|
38720
39077
|
return jsonResponse(data);
|
|
38721
39078
|
});
|
|
38722
|
-
|
|
39079
|
+
server.registerTool("ofw_list_expenses", {
|
|
38723
39080
|
description: "List OurFamilyWizard expenses with pagination",
|
|
38724
39081
|
annotations: { readOnlyHint: true },
|
|
38725
39082
|
inputSchema: {
|
|
@@ -38732,7 +39089,7 @@ function registerExpenseTools(server2, client2) {
|
|
|
38732
39089
|
const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
38733
39090
|
return jsonResponse(data);
|
|
38734
39091
|
});
|
|
38735
|
-
|
|
39092
|
+
server.registerTool("ofw_create_expense", {
|
|
38736
39093
|
description: "Log a new expense in OurFamilyWizard",
|
|
38737
39094
|
annotations: { destructiveHint: false },
|
|
38738
39095
|
inputSchema: {
|
|
@@ -38746,8 +39103,8 @@ function registerExpenseTools(server2, client2) {
|
|
|
38746
39103
|
}
|
|
38747
39104
|
|
|
38748
39105
|
// src/tools/journal.ts
|
|
38749
|
-
function registerJournalTools(
|
|
38750
|
-
|
|
39106
|
+
function registerJournalTools(server, client2) {
|
|
39107
|
+
server.registerTool("ofw_list_journal_entries", {
|
|
38751
39108
|
description: "List OurFamilyWizard journal entries",
|
|
38752
39109
|
annotations: { readOnlyHint: true },
|
|
38753
39110
|
inputSchema: {
|
|
@@ -38760,7 +39117,7 @@ function registerJournalTools(server2, client2) {
|
|
|
38760
39117
|
const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
|
|
38761
39118
|
return jsonResponse(data);
|
|
38762
39119
|
});
|
|
38763
|
-
|
|
39120
|
+
server.registerTool("ofw_create_journal_entry", {
|
|
38764
39121
|
description: "Create a new journal entry in OurFamilyWizard",
|
|
38765
39122
|
annotations: { destructiveHint: false },
|
|
38766
39123
|
inputSchema: {
|
|
@@ -38784,12 +39141,17 @@ process.emit = function(event, ...args) {
|
|
|
38784
39141
|
}
|
|
38785
39142
|
return originalEmit(event, ...args);
|
|
38786
39143
|
};
|
|
38787
|
-
|
|
38788
|
-
|
|
38789
|
-
|
|
38790
|
-
|
|
38791
|
-
|
|
38792
|
-
|
|
38793
|
-
|
|
38794
|
-
|
|
38795
|
-
|
|
39144
|
+
await runMcp({
|
|
39145
|
+
name: "ofw",
|
|
39146
|
+
version: "2.3.1",
|
|
39147
|
+
// x-release-please-version
|
|
39148
|
+
deps: client,
|
|
39149
|
+
tools: [
|
|
39150
|
+
registerUserTools,
|
|
39151
|
+
registerMessageTools,
|
|
39152
|
+
registerCalendarTools,
|
|
39153
|
+
registerExpenseTools,
|
|
39154
|
+
registerJournalTools
|
|
39155
|
+
],
|
|
39156
|
+
banner: "[ofw-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion."
|
|
39157
|
+
});
|