ofw-mcp 2.3.1 → 2.4.0
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/LICENSE +21 -0
- package/README.md +42 -22
- package/dist/auth-password.js +7 -2
- package/dist/auth.js +3 -3
- package/dist/bundle.js +796 -156
- package/dist/cache.js +19 -1
- package/dist/client.js +66 -50
- package/dist/config.js +26 -0
- package/dist/index.js +1 -1
- package/dist/sync.js +68 -9
- package/dist/tools/_shared.js +47 -3
- package/dist/tools/calendar.js +54 -48
- package/dist/tools/expenses.js +17 -13
- package/dist/tools/journal.js +17 -13
- package/dist/tools/messages.js +313 -242
- package/dist/validate.js +35 -0
- package/package.json +7 -3
- package/server.json +8 -2
package/dist/bundle.js
CHANGED
|
@@ -34681,6 +34681,22 @@ async function runMcp(opts) {
|
|
|
34681
34681
|
return server;
|
|
34682
34682
|
}
|
|
34683
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
|
+
|
|
34684
34700
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34685
34701
|
function textResult(data) {
|
|
34686
34702
|
return {
|
|
@@ -34767,6 +34783,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
|
|
|
34767
34783
|
var NonEmptyString = external_exports.string().min(1);
|
|
34768
34784
|
var IsoDate = external_exports.iso.date();
|
|
34769
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.');
|
|
34770
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.");
|
|
34771
34789
|
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
34772
34790
|
var paginationSchema = {
|
|
@@ -34778,6 +34796,83 @@ var pageSchema = {
|
|
|
34778
34796
|
page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
|
|
34779
34797
|
};
|
|
34780
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
|
+
|
|
34781
34876
|
// src/client.ts
|
|
34782
34877
|
import { dirname, join as join4 } from "path";
|
|
34783
34878
|
import { fileURLToPath } from "url";
|
|
@@ -34791,7 +34886,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34791
34886
|
"read_local_storage",
|
|
34792
34887
|
"read_session_storage",
|
|
34793
34888
|
"capture_request_header",
|
|
34794
|
-
"
|
|
34889
|
+
"capture_redirect",
|
|
34890
|
+
"read_indexed_db",
|
|
34891
|
+
"download"
|
|
34795
34892
|
]);
|
|
34796
34893
|
|
|
34797
34894
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -34938,6 +35035,16 @@ function assertScopeKeyArray(value, label) {
|
|
|
34938
35035
|
seen.add(k);
|
|
34939
35036
|
}
|
|
34940
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
|
+
}
|
|
34941
35048
|
function assertCaptureHeadersArray(value, label) {
|
|
34942
35049
|
if (!Array.isArray(value)) {
|
|
34943
35050
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -34946,14 +35053,25 @@ function assertCaptureHeadersArray(value, label) {
|
|
|
34946
35053
|
for (let i = 0; i < value.length; i++) {
|
|
34947
35054
|
const entry = value[i];
|
|
34948
35055
|
assertObject(entry, `${label}[${i}]`);
|
|
34949
|
-
if (entry.
|
|
34950
|
-
throw new ProtocolError(`${label}[${i}].
|
|
35056
|
+
if (entry.host === void 0) {
|
|
35057
|
+
throw new ProtocolError(`${label}[${i}].host: missing`);
|
|
34951
35058
|
}
|
|
34952
35059
|
if (entry.headerName === void 0) {
|
|
34953
35060
|
throw new ProtocolError(`${label}[${i}].headerName: missing`);
|
|
34954
35061
|
}
|
|
34955
|
-
if (typeof entry.
|
|
34956
|
-
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
|
+
}
|
|
34957
35075
|
}
|
|
34958
35076
|
if (typeof entry.headerName !== "string") {
|
|
34959
35077
|
throw new ProtocolError(`${label}[${i}].headerName: expected string, got ${typeof entry.headerName}`);
|
|
@@ -34961,19 +35079,29 @@ function assertCaptureHeadersArray(value, label) {
|
|
|
34961
35079
|
if (!HEADER_NAME_RE.test(entry.headerName)) {
|
|
34962
35080
|
throw new ProtocolError(`${label}[${i}].headerName: invalid name ${JSON.stringify(entry.headerName)}`);
|
|
34963
35081
|
}
|
|
34964
|
-
|
|
34965
|
-
const key = `${entry.
|
|
35082
|
+
const normalizedPath = entry.path ?? "/*";
|
|
35083
|
+
const key = `${entry.host}\0${normalizedPath}\0${entry.headerName}`;
|
|
34966
35084
|
if (seen.has(key)) {
|
|
34967
|
-
throw new ProtocolError(`${label}: duplicate ${JSON.stringify({
|
|
35085
|
+
throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ host: entry.host, path: normalizedPath, headerName: entry.headerName })}`);
|
|
34968
35086
|
}
|
|
34969
35087
|
seen.add(key);
|
|
34970
35088
|
for (const k of Object.keys(entry)) {
|
|
34971
|
-
if (k !== "
|
|
35089
|
+
if (k !== "host" && k !== "path" && k !== "headerName") {
|
|
34972
35090
|
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
34973
35091
|
}
|
|
34974
35092
|
}
|
|
34975
35093
|
}
|
|
34976
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
|
+
}
|
|
34977
35105
|
function assertStoragePointersArray(value, label, declaredKeys) {
|
|
34978
35106
|
if (!Array.isArray(value)) {
|
|
34979
35107
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -35061,23 +35189,6 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35061
35189
|
}
|
|
35062
35190
|
}
|
|
35063
35191
|
}
|
|
35064
|
-
function assertCaptureUrlPattern(pattern, label) {
|
|
35065
|
-
if (!pattern.startsWith("https://")) {
|
|
35066
|
-
throw new ProtocolError(`${label}: must start with https:// (got ${JSON.stringify(pattern)})`);
|
|
35067
|
-
}
|
|
35068
|
-
const afterScheme = pattern.slice("https://".length);
|
|
35069
|
-
const slash = afterScheme.indexOf("/");
|
|
35070
|
-
const host = slash === -1 ? afterScheme : afterScheme.slice(0, slash);
|
|
35071
|
-
if (host.length === 0) {
|
|
35072
|
-
throw new ProtocolError(`${label}: missing host (got ${JSON.stringify(pattern)})`);
|
|
35073
|
-
}
|
|
35074
|
-
if (host.includes("*")) {
|
|
35075
|
-
throw new ProtocolError(`${label}: wildcards not permitted in host (got ${JSON.stringify(pattern)})`);
|
|
35076
|
-
}
|
|
35077
|
-
if (!HOSTNAME_RE.test(host)) {
|
|
35078
|
-
throw new ProtocolError(`${label}: invalid host ${JSON.stringify(host)} in ${JSON.stringify(pattern)}`);
|
|
35079
|
-
}
|
|
35080
|
-
}
|
|
35081
35192
|
function validateFrame(raw) {
|
|
35082
35193
|
assertObject(raw, "frame");
|
|
35083
35194
|
const t = raw.type;
|
|
@@ -35142,7 +35253,7 @@ function validateHello(raw) {
|
|
|
35142
35253
|
assertScopeKeyArray(raw.sessionStorageKeys, "hello.sessionStorageKeys");
|
|
35143
35254
|
}
|
|
35144
35255
|
if (raw.captureHeaders !== void 0) {
|
|
35145
|
-
|
|
35256
|
+
validateCaptureHeaderDecls(raw.captureHeaders, raw.domains, "hello.captureHeaders");
|
|
35146
35257
|
}
|
|
35147
35258
|
if (raw.indexedDbScopes !== void 0) {
|
|
35148
35259
|
assertIndexedDbScopesArray(raw.indexedDbScopes, "hello.indexedDbScopes");
|
|
@@ -35310,24 +35421,58 @@ function validateInnerRequest(raw) {
|
|
|
35310
35421
|
}
|
|
35311
35422
|
if (raw.op === "capture_request_header") {
|
|
35312
35423
|
assertObject(raw.init, "inner.init");
|
|
35313
|
-
if (raw.init.
|
|
35314
|
-
throw new ProtocolError("inner.init.
|
|
35424
|
+
if (raw.init.host === void 0) {
|
|
35425
|
+
throw new ProtocolError("inner.init.host: missing");
|
|
35315
35426
|
}
|
|
35316
35427
|
if (raw.init.headerName === void 0) {
|
|
35317
35428
|
throw new ProtocolError("inner.init.headerName: missing");
|
|
35318
35429
|
}
|
|
35319
|
-
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
|
+
}
|
|
35320
35440
|
assertString(raw.init.headerName, "inner.init.headerName");
|
|
35321
35441
|
if (raw.init.timeoutMs !== void 0) {
|
|
35322
35442
|
assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
|
|
35323
35443
|
}
|
|
35324
35444
|
for (const k of Object.keys(raw.init)) {
|
|
35325
|
-
if (k !== "
|
|
35445
|
+
if (k !== "host" && k !== "path" && k !== "headerName" && k !== "timeoutMs") {
|
|
35326
35446
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_request_header`);
|
|
35327
35447
|
}
|
|
35328
35448
|
}
|
|
35329
35449
|
return raw;
|
|
35330
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
|
+
}
|
|
35331
35476
|
if (raw.op === "read_indexed_db") {
|
|
35332
35477
|
assertObject(raw.init, "inner.init");
|
|
35333
35478
|
if (raw.init.origin === void 0)
|
|
@@ -35353,7 +35498,32 @@ function validateInnerRequest(raw) {
|
|
|
35353
35498
|
}
|
|
35354
35499
|
return raw;
|
|
35355
35500
|
}
|
|
35356
|
-
|
|
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)}`);
|
|
35357
35527
|
}
|
|
35358
35528
|
function assertNonEmptyKeyArray(value, label) {
|
|
35359
35529
|
if (!Array.isArray(value)) {
|
|
@@ -35424,6 +35594,13 @@ function validateInnerResponse(raw) {
|
|
|
35424
35594
|
assertString(raw.value, "inner.value");
|
|
35425
35595
|
return raw;
|
|
35426
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
|
+
}
|
|
35427
35604
|
if (op === "read_indexed_db") {
|
|
35428
35605
|
if (raw.values === void 0) {
|
|
35429
35606
|
throw new ProtocolError("inner.values: missing on read_indexed_db response");
|
|
@@ -35431,6 +35608,25 @@ function validateInnerResponse(raw) {
|
|
|
35431
35608
|
assertObject(raw.values, "inner.values");
|
|
35432
35609
|
return raw;
|
|
35433
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
|
+
}
|
|
35434
35630
|
throw new ProtocolError(`inner.op: unknown success-response op ${JSON.stringify(raw.op)}`);
|
|
35435
35631
|
}
|
|
35436
35632
|
if (raw.ok === false) {
|
|
@@ -35702,7 +35898,8 @@ async function buildServerHello(opts) {
|
|
|
35702
35898
|
}
|
|
35703
35899
|
if (opts.captureHeaders && opts.captureHeaders.length > 0) {
|
|
35704
35900
|
hello.captureHeaders = opts.captureHeaders.map((d) => ({
|
|
35705
|
-
|
|
35901
|
+
host: d.host,
|
|
35902
|
+
...d.path !== void 0 ? { path: d.path } : {},
|
|
35706
35903
|
headerName: d.headerName
|
|
35707
35904
|
}));
|
|
35708
35905
|
}
|
|
@@ -35940,7 +36137,9 @@ async function startHost(opts) {
|
|
|
35940
36137
|
} catch {
|
|
35941
36138
|
}
|
|
35942
36139
|
}
|
|
35943
|
-
wss.close(() =>
|
|
36140
|
+
wss.close(() => {
|
|
36141
|
+
opts.httpServer.close(() => resolve2());
|
|
36142
|
+
});
|
|
35944
36143
|
}),
|
|
35945
36144
|
sendOwnInner: async (inner) => {
|
|
35946
36145
|
const session = await ownSessionReady;
|
|
@@ -35990,6 +36189,7 @@ async function startPeer(opts) {
|
|
|
35990
36189
|
const innerListeners = [];
|
|
35991
36190
|
const renegotiateListeners = [];
|
|
35992
36191
|
const pendingPairListeners = [];
|
|
36192
|
+
const closeListeners = [];
|
|
35993
36193
|
let session = null;
|
|
35994
36194
|
let pendingPairCode = null;
|
|
35995
36195
|
let resolveFirstReady;
|
|
@@ -36039,6 +36239,7 @@ async function startPeer(opts) {
|
|
|
36039
36239
|
ws.on("message", onMessage);
|
|
36040
36240
|
ws.once("close", () => {
|
|
36041
36241
|
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
36242
|
+
closeListeners.forEach((cb) => cb());
|
|
36042
36243
|
});
|
|
36043
36244
|
sessionPromise.catch(() => {
|
|
36044
36245
|
});
|
|
@@ -36061,6 +36262,9 @@ async function startPeer(opts) {
|
|
|
36061
36262
|
pendingPairListeners.push(cb);
|
|
36062
36263
|
},
|
|
36063
36264
|
pendingPairCode: () => pendingPairCode,
|
|
36265
|
+
onClose: (cb) => {
|
|
36266
|
+
closeListeners.push(cb);
|
|
36267
|
+
},
|
|
36064
36268
|
close: () => ws.close()
|
|
36065
36269
|
};
|
|
36066
36270
|
return handle;
|
|
@@ -36262,6 +36466,12 @@ var FetchproxyServer = class {
|
|
|
36262
36466
|
opts;
|
|
36263
36467
|
hostHandle = null;
|
|
36264
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;
|
|
36265
36475
|
nextRequestId = 1;
|
|
36266
36476
|
// 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
|
|
36267
36477
|
// Replaces the local copies every downstream MCP was rolling on top
|
|
@@ -36287,8 +36497,12 @@ var FetchproxyServer = class {
|
|
|
36287
36497
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
36288
36498
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36289
36499
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36500
|
+
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
36501
|
+
pendingRedirect = /* @__PURE__ */ new Map();
|
|
36290
36502
|
// 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
|
|
36291
36503
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36504
|
+
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
36505
|
+
pendingDownload = /* @__PURE__ */ new Map();
|
|
36292
36506
|
mcpId = null;
|
|
36293
36507
|
identity = null;
|
|
36294
36508
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -36334,6 +36548,14 @@ var FetchproxyServer = class {
|
|
|
36334
36548
|
}
|
|
36335
36549
|
capabilities = [...opts.capabilities];
|
|
36336
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
|
+
}
|
|
36337
36559
|
this.opts = {
|
|
36338
36560
|
port: opts.port ?? 37149,
|
|
36339
36561
|
host: opts.host ?? "127.0.0.1",
|
|
@@ -36345,7 +36567,8 @@ var FetchproxyServer = class {
|
|
|
36345
36567
|
localStorageKeys: [...opts.localStorageKeys ?? []],
|
|
36346
36568
|
sessionStorageKeys: [...opts.sessionStorageKeys ?? []],
|
|
36347
36569
|
captureHeaders: (opts.captureHeaders ?? []).map((d) => ({
|
|
36348
|
-
|
|
36570
|
+
host: d.host,
|
|
36571
|
+
...d.path !== void 0 ? { path: d.path } : {},
|
|
36349
36572
|
headerName: d.headerName
|
|
36350
36573
|
})),
|
|
36351
36574
|
indexedDbScopes: (opts.indexedDbScopes ?? []).map((d) => ({
|
|
@@ -36463,6 +36686,7 @@ var FetchproxyServer = class {
|
|
|
36463
36686
|
async doConnect() {
|
|
36464
36687
|
const identity = this.identity;
|
|
36465
36688
|
const mcpId = this.mcpId;
|
|
36689
|
+
this.closing = false;
|
|
36466
36690
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
36467
36691
|
if (el.role === "host") {
|
|
36468
36692
|
this.role = "host";
|
|
@@ -36522,6 +36746,14 @@ var FetchproxyServer = class {
|
|
|
36522
36746
|
const cb = this.opts.onPairCode;
|
|
36523
36747
|
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36524
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
|
+
});
|
|
36525
36757
|
}
|
|
36526
36758
|
}
|
|
36527
36759
|
pairingErrorMessage(code) {
|
|
@@ -37096,12 +37328,12 @@ var FetchproxyServer = class {
|
|
|
37096
37328
|
/**
|
|
37097
37329
|
* 0.3.0+: snapshot the next outgoing request's named header. Single-
|
|
37098
37330
|
* shot: the extension registers a one-time `webRequest` listener
|
|
37099
|
-
* filtered on `
|
|
37100
|
-
* match, removes itself, and resolves with the
|
|
37101
|
-
* 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).
|
|
37102
37334
|
*
|
|
37103
|
-
* `(
|
|
37104
|
-
* `FetchproxyServerOpts.captureHeaders
|
|
37335
|
+
* `(host, path?, headerName)` must match a declared entry in
|
|
37336
|
+
* `FetchproxyServerOpts.captureHeaders` (omitted path ≡ `/*`).
|
|
37105
37337
|
*/
|
|
37106
37338
|
async captureRequestHeader(opts) {
|
|
37107
37339
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
@@ -37110,24 +37342,25 @@ var FetchproxyServer = class {
|
|
|
37110
37342
|
await this.ensureConnected();
|
|
37111
37343
|
this.throwIfPendingPair();
|
|
37112
37344
|
const decls = this.opts.captureHeaders;
|
|
37345
|
+
const normPath = (p) => p ?? "/*";
|
|
37113
37346
|
let resolved;
|
|
37114
|
-
if (opts?.
|
|
37115
|
-
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);
|
|
37116
37349
|
if (!found) {
|
|
37117
|
-
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`);
|
|
37118
37351
|
}
|
|
37119
37352
|
resolved = found;
|
|
37120
|
-
} else if (opts?.
|
|
37353
|
+
} else if (opts?.host === void 0 && opts?.headerName === void 0) {
|
|
37121
37354
|
if (decls.length === 0) {
|
|
37122
|
-
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");
|
|
37123
37356
|
}
|
|
37124
37357
|
if (decls.length > 1) {
|
|
37125
|
-
const list = decls.map((d) => `${JSON.stringify(d.
|
|
37126
|
-
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`);
|
|
37127
37360
|
}
|
|
37128
37361
|
resolved = decls[0];
|
|
37129
37362
|
} else {
|
|
37130
|
-
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)");
|
|
37131
37364
|
}
|
|
37132
37365
|
const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
|
|
37133
37366
|
try {
|
|
@@ -37161,7 +37394,7 @@ var FetchproxyServer = class {
|
|
|
37161
37394
|
originalError: retryErr.message,
|
|
37162
37395
|
retryAttempted: true,
|
|
37163
37396
|
op: "capture_request_header",
|
|
37164
|
-
url: resolved.
|
|
37397
|
+
url: `https://${resolved.host}${resolved.path ?? "/*"}`,
|
|
37165
37398
|
role: this.role,
|
|
37166
37399
|
port: this.opts.port
|
|
37167
37400
|
});
|
|
@@ -37172,7 +37405,7 @@ var FetchproxyServer = class {
|
|
|
37172
37405
|
originalError: err.message,
|
|
37173
37406
|
retryAttempted: false,
|
|
37174
37407
|
op: "capture_request_header",
|
|
37175
|
-
url: resolved.
|
|
37408
|
+
url: `https://${resolved.host}${resolved.path ?? "/*"}`,
|
|
37176
37409
|
role: this.role,
|
|
37177
37410
|
port: this.opts.port
|
|
37178
37411
|
});
|
|
@@ -37185,7 +37418,8 @@ var FetchproxyServer = class {
|
|
|
37185
37418
|
id,
|
|
37186
37419
|
op: "capture_request_header",
|
|
37187
37420
|
init: {
|
|
37188
|
-
|
|
37421
|
+
host: opts.host,
|
|
37422
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
37189
37423
|
headerName: opts.headerName,
|
|
37190
37424
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
37191
37425
|
}
|
|
@@ -37200,6 +37434,181 @@ var FetchproxyServer = class {
|
|
|
37200
37434
|
}
|
|
37201
37435
|
return pending;
|
|
37202
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
|
+
}
|
|
37203
37612
|
/**
|
|
37204
37613
|
* 0.4.0+: read declared IndexedDB keys from the user's signed-in
|
|
37205
37614
|
* tab. Requires `'read_indexed_db'` in capabilities AND the
|
|
@@ -37347,6 +37756,20 @@ var FetchproxyServer = class {
|
|
|
37347
37756
|
}
|
|
37348
37757
|
return;
|
|
37349
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
|
+
}
|
|
37350
37773
|
const idbCb = this.pendingIdb.get(inner.id);
|
|
37351
37774
|
if (idbCb) {
|
|
37352
37775
|
this.pendingIdb.delete(inner.id);
|
|
@@ -37361,6 +37784,20 @@ var FetchproxyServer = class {
|
|
|
37361
37784
|
}
|
|
37362
37785
|
return;
|
|
37363
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
|
+
}
|
|
37364
37801
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
37365
37802
|
if (cookiesCb) {
|
|
37366
37803
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -37403,9 +37840,15 @@ var FetchproxyServer = class {
|
|
|
37403
37840
|
for (const { reject } of this.pendingCapture.values())
|
|
37404
37841
|
reject(err);
|
|
37405
37842
|
this.pendingCapture.clear();
|
|
37843
|
+
for (const { reject } of this.pendingRedirect.values())
|
|
37844
|
+
reject(err);
|
|
37845
|
+
this.pendingRedirect.clear();
|
|
37406
37846
|
for (const { reject } of this.pendingIdb.values())
|
|
37407
37847
|
reject(err);
|
|
37408
37848
|
this.pendingIdb.clear();
|
|
37849
|
+
for (const { reject } of this.pendingDownload.values())
|
|
37850
|
+
reject(err);
|
|
37851
|
+
this.pendingDownload.clear();
|
|
37409
37852
|
}
|
|
37410
37853
|
/**
|
|
37411
37854
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
@@ -37440,6 +37883,7 @@ var FetchproxyServer = class {
|
|
|
37440
37883
|
* twice in a row.
|
|
37441
37884
|
*/
|
|
37442
37885
|
async close() {
|
|
37886
|
+
this.closing = true;
|
|
37443
37887
|
this.stopKeepalive();
|
|
37444
37888
|
this.rejectAllPending();
|
|
37445
37889
|
if (this.connectingPromise) {
|
|
@@ -37567,15 +38011,11 @@ async function bootstrap(opts) {
|
|
|
37567
38011
|
const capturedHeaders = {};
|
|
37568
38012
|
for (const h of opts.declare.captureHeaders) {
|
|
37569
38013
|
if (opts.onWaiting) {
|
|
37570
|
-
|
|
37571
|
-
const url2 = new URL(h.urlPattern.replace(/\*+/g, "placeholder"));
|
|
37572
|
-
opts.onWaiting(`waiting for next request to ${url2.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37573
|
-
} catch {
|
|
37574
|
-
opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37575
|
-
}
|
|
38014
|
+
opts.onWaiting(`waiting for next request to ${h.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
|
|
37576
38015
|
}
|
|
37577
38016
|
capturedHeaders[h.headerName] = await server.captureRequestHeader({
|
|
37578
|
-
|
|
38017
|
+
host: h.host,
|
|
38018
|
+
...h.path !== void 0 ? { path: h.path } : {},
|
|
37579
38019
|
headerName: h.headerName
|
|
37580
38020
|
});
|
|
37581
38021
|
}
|
|
@@ -37629,8 +38069,7 @@ async function loginWithPassword(username, password) {
|
|
|
37629
38069
|
headers: { ...OFW_PROTOCOL_HEADERS },
|
|
37630
38070
|
redirect: "manual"
|
|
37631
38071
|
});
|
|
37632
|
-
const
|
|
37633
|
-
const sessionCookie = setCookie.split(";")[0];
|
|
38072
|
+
const sessionCookie = initResponse.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
|
|
37634
38073
|
const response = await fetch(`${BASE_URL}/ofw/login`, {
|
|
37635
38074
|
method: "POST",
|
|
37636
38075
|
headers: {
|
|
@@ -37690,6 +38129,16 @@ function getAttachmentsDir() {
|
|
|
37690
38129
|
function parseBoolEnv2(name) {
|
|
37691
38130
|
return parseBoolEnv(name);
|
|
37692
38131
|
}
|
|
38132
|
+
function getWriteMode() {
|
|
38133
|
+
const raw = process.env.OFW_WRITE_MODE;
|
|
38134
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return "all";
|
|
38135
|
+
const mode = raw.trim().toLowerCase();
|
|
38136
|
+
if (mode === "none" || mode === "drafts" || mode === "all") return mode;
|
|
38137
|
+
console.error(
|
|
38138
|
+
`[ofw-mcp] Unrecognized OFW_WRITE_MODE "${raw.trim()}" \u2014 failing closed to "none" (no write tools registered). Valid values: none, drafts, all.`
|
|
38139
|
+
);
|
|
38140
|
+
return "none";
|
|
38141
|
+
}
|
|
37693
38142
|
function getDefaultInlineAttachments() {
|
|
37694
38143
|
return parseBoolEnv2("OFW_INLINE_ATTACHMENTS");
|
|
37695
38144
|
}
|
|
@@ -37697,7 +38146,8 @@ function getDefaultInlineAttachments() {
|
|
|
37697
38146
|
// package.json
|
|
37698
38147
|
var package_default = {
|
|
37699
38148
|
name: "ofw-mcp",
|
|
37700
|
-
version: "2.
|
|
38149
|
+
version: "2.4.0",
|
|
38150
|
+
license: "MIT",
|
|
37701
38151
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
37702
38152
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
37703
38153
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37706,6 +38156,9 @@ var package_default = {
|
|
|
37706
38156
|
url: "git+https://github.com/chrischall/ofw-mcp.git"
|
|
37707
38157
|
},
|
|
37708
38158
|
type: "module",
|
|
38159
|
+
engines: {
|
|
38160
|
+
node: ">=22.5.0"
|
|
38161
|
+
},
|
|
37709
38162
|
bin: {
|
|
37710
38163
|
"ofw-mcp": "dist/index.js"
|
|
37711
38164
|
},
|
|
@@ -37725,8 +38178,8 @@ var package_default = {
|
|
|
37725
38178
|
"test:watch": "vitest"
|
|
37726
38179
|
},
|
|
37727
38180
|
dependencies: {
|
|
37728
|
-
"@chrischall/mcp-utils": "^0.
|
|
37729
|
-
"@fetchproxy/bootstrap": "^
|
|
38181
|
+
"@chrischall/mcp-utils": "^0.9.0",
|
|
38182
|
+
"@fetchproxy/bootstrap": "^1.3.0",
|
|
37730
38183
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37731
38184
|
dotenv: "^17.4.2",
|
|
37732
38185
|
zod: "^4.4.3"
|
|
@@ -37835,12 +38288,39 @@ function getRequestTimeoutMs() {
|
|
|
37835
38288
|
const n = Number(raw.trim());
|
|
37836
38289
|
return Number.isFinite(n) && n > 0 ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37837
38290
|
}
|
|
38291
|
+
var OFW_REFRESH_SENTINEL = "ofw";
|
|
37838
38292
|
var OFWClient = class {
|
|
37839
|
-
token
|
|
37840
|
-
|
|
38293
|
+
// Bearer-token lifecycle is delegated to the shared, race-safe TokenManager
|
|
38294
|
+
// (proactive refresh inside the skew window, single-flight refresh so a burst
|
|
38295
|
+
// of concurrent callers coalesces onto ONE `resolveAuth()`, and a 401-replay
|
|
38296
|
+
// guarded against double-refresh). It is created lazily, seeded with an
|
|
38297
|
+
// already-expired placeholder token so the first request drives the refresh
|
|
38298
|
+
// callback — i.e. the original "log in on first request" behavior.
|
|
38299
|
+
tokenManager;
|
|
38300
|
+
getTokenManager() {
|
|
38301
|
+
if (!this.tokenManager) {
|
|
38302
|
+
this.tokenManager = new TokenManager({
|
|
38303
|
+
initial: { accessToken: "", refreshToken: OFW_REFRESH_SENTINEL, expiresAt: 0 },
|
|
38304
|
+
skewMs: OFW_TOKEN_EXPIRY_SKEW_MS,
|
|
38305
|
+
// Map OFW's mint/refresh onto the refresh callback. `resolveAuth()`
|
|
38306
|
+
// returns a token and a best-effort expiry; when the fetchproxy path
|
|
38307
|
+
// can't supply one we fall back to the same 6h estimate the password
|
|
38308
|
+
// path uses (the 401-replay covers a wrong guess). We re-arm the
|
|
38309
|
+
// sentinel so the manager can refresh again later.
|
|
38310
|
+
refresh: async () => {
|
|
38311
|
+
const { token, expiresAt } = await resolveAuth();
|
|
38312
|
+
return {
|
|
38313
|
+
accessToken: token,
|
|
38314
|
+
refreshToken: OFW_REFRESH_SENTINEL,
|
|
38315
|
+
expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
|
|
38316
|
+
};
|
|
38317
|
+
}
|
|
38318
|
+
});
|
|
38319
|
+
}
|
|
38320
|
+
return this.tokenManager;
|
|
38321
|
+
}
|
|
37841
38322
|
async request(method, path, body) {
|
|
37842
|
-
await this.
|
|
37843
|
-
const response = await this.fetchWithRetry(method, path, body, "application/json", false);
|
|
38323
|
+
const response = await this.fetchAuthed(method, path, body, "application/json");
|
|
37844
38324
|
const text = await response.text();
|
|
37845
38325
|
if (debugLogEnabled()) {
|
|
37846
38326
|
console.error(`[ofw-debug] response body: ${text || "<empty>"}`);
|
|
@@ -37849,23 +38329,45 @@ var OFWClient = class {
|
|
|
37849
38329
|
}
|
|
37850
38330
|
/** Like `request`, but returns the raw bytes plus Content-Type/-Disposition metadata. */
|
|
37851
38331
|
async requestBinary(method, path) {
|
|
37852
|
-
await this.
|
|
37853
|
-
const response = await this.fetchWithRetry(method, path, void 0, "application/octet-stream", false);
|
|
38332
|
+
const response = await this.fetchAuthed(method, path, void 0, "application/octet-stream");
|
|
37854
38333
|
return {
|
|
37855
38334
|
body: Buffer.from(await response.arrayBuffer()),
|
|
37856
38335
|
contentType: response.headers.get("content-type"),
|
|
37857
38336
|
suggestedFileName: parseContentDispositionFilename(response.headers.get("content-disposition") ?? "")
|
|
37858
38337
|
};
|
|
37859
38338
|
}
|
|
37860
|
-
//
|
|
37861
|
-
//
|
|
37862
|
-
//
|
|
37863
|
-
|
|
38339
|
+
// Authenticated fetch for both JSON and binary callers. Auth (proactive
|
|
38340
|
+
// refresh inside the skew window + one 401-replay, guarded against a
|
|
38341
|
+
// double-refresh under concurrency) is delegated to the shared TokenManager's
|
|
38342
|
+
// `withAuth`. The 429 wait-and-replay and the non-2xx → throw remain here.
|
|
38343
|
+
async fetchAuthed(method, path, body, accept) {
|
|
38344
|
+
let attempt = 0;
|
|
38345
|
+
let response = await this.getTokenManager().withAuth(
|
|
38346
|
+
(token) => this.fetchOnce(method, path, body, accept, token, attempt++ > 0)
|
|
38347
|
+
);
|
|
38348
|
+
if (response.status === 429) {
|
|
38349
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
38350
|
+
response = await this.getTokenManager().withAuth(
|
|
38351
|
+
(token) => this.fetchOnce(method, path, body, accept, token, true)
|
|
38352
|
+
);
|
|
38353
|
+
if (response.status === 429) throw new Error("Rate limited by OFW API");
|
|
38354
|
+
}
|
|
38355
|
+
if (!response.ok) {
|
|
38356
|
+
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
38357
|
+
}
|
|
38358
|
+
return response;
|
|
38359
|
+
}
|
|
38360
|
+
// A single OFW API fetch with the bearer token supplied by `withAuth`.
|
|
38361
|
+
// Carries the per-request timeout (AbortController + setTimeout so vitest
|
|
38362
|
+
// fake timers can drive it and we attach a clear error message) and the
|
|
38363
|
+
// OFW_DEBUG_LOG instrumentation. Returns the raw Response — 401/429/non-2xx
|
|
38364
|
+
// handling lives in the callers (`withAuth` and `fetchAuthed`).
|
|
38365
|
+
async fetchOnce(method, path, body, accept, token, isRetry = false) {
|
|
37864
38366
|
const isFormData = body instanceof FormData;
|
|
37865
38367
|
const headers = {
|
|
37866
38368
|
...OFW_PROTOCOL_HEADERS,
|
|
37867
38369
|
Accept: accept,
|
|
37868
|
-
Authorization: `Bearer ${
|
|
38370
|
+
Authorization: `Bearer ${token}`
|
|
37869
38371
|
};
|
|
37870
38372
|
if (body !== void 0 && !isFormData) headers["Content-Type"] = "application/json";
|
|
37871
38373
|
const url2 = `${BASE_URL}${path}`;
|
|
@@ -37907,51 +38409,29 @@ var OFWClient = class {
|
|
|
37907
38409
|
if (debugLogEnabled()) {
|
|
37908
38410
|
console.error(`[ofw-debug] \u2190 ${response.status} ${response.statusText} (${Date.now() - startedAt}ms)`);
|
|
37909
38411
|
}
|
|
37910
|
-
if (response.status === 401 && !isRetry) {
|
|
37911
|
-
this.token = null;
|
|
37912
|
-
this.tokenExpiry = null;
|
|
37913
|
-
await this.ensureAuthenticated();
|
|
37914
|
-
return this.fetchWithRetry(method, path, body, accept, true);
|
|
37915
|
-
}
|
|
37916
|
-
if (response.status === 429) {
|
|
37917
|
-
if (!isRetry) {
|
|
37918
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
37919
|
-
return this.fetchWithRetry(method, path, body, accept, true);
|
|
37920
|
-
}
|
|
37921
|
-
throw new Error("Rate limited by OFW API");
|
|
37922
|
-
}
|
|
37923
|
-
if (!response.ok) {
|
|
37924
|
-
throw new Error(`OFW API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
37925
|
-
}
|
|
37926
38412
|
return response;
|
|
37927
38413
|
}
|
|
37928
|
-
async ensureAuthenticated() {
|
|
37929
|
-
if (!this.isTokenExpiredSoon()) return;
|
|
37930
|
-
await this.login();
|
|
37931
|
-
}
|
|
37932
|
-
// Auth resolution is delegated to `./auth.ts`. This client doesn't care
|
|
37933
|
-
// whether the token came from a password POST or from a one-shot
|
|
37934
|
-
// fetchproxy session-snapshot — it just consumes the result.
|
|
37935
|
-
//
|
|
37936
|
-
// If `expiresAt` is missing (the fetchproxy path on a tab whose
|
|
37937
|
-
// browser didn't persist tokenExpiry), we fall back to the same 6h
|
|
37938
|
-
// estimate the password path uses. The 401-replay path covers us if
|
|
37939
|
-
// the estimate is wrong.
|
|
37940
|
-
async login() {
|
|
37941
|
-
const { token, expiresAt } = await resolveAuth();
|
|
37942
|
-
this.token = token;
|
|
37943
|
-
this.tokenExpiry = expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS);
|
|
37944
|
-
}
|
|
37945
|
-
isTokenExpiredSoon() {
|
|
37946
|
-
if (!this.token || !this.tokenExpiry) return true;
|
|
37947
|
-
return this.tokenExpiry.getTime() - Date.now() < OFW_TOKEN_EXPIRY_SKEW_MS;
|
|
37948
|
-
}
|
|
37949
38414
|
};
|
|
37950
38415
|
var client = new OFWClient();
|
|
37951
38416
|
|
|
38417
|
+
// src/validate.ts
|
|
38418
|
+
function parseOFW(schema, raw, ctx, mode = "lenient") {
|
|
38419
|
+
const result = schema.safeParse(raw);
|
|
38420
|
+
if (result.success) return result.data;
|
|
38421
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
38422
|
+
const message = `OFW response for ${ctx} failed validation: ${issues}`;
|
|
38423
|
+
if (mode === "strict") throw new Error(message);
|
|
38424
|
+
console.error(`[ofw-mcp] WARNING: ${message} \u2014 continuing with the raw response; fields derived from it may be missing or wrong.`);
|
|
38425
|
+
return raw;
|
|
38426
|
+
}
|
|
38427
|
+
|
|
37952
38428
|
// src/tools/_shared.ts
|
|
37953
38429
|
var jsonResponse = textResult;
|
|
37954
38430
|
var textResponse = rawTextResult;
|
|
38431
|
+
var ApiRecipientSchema = external_exports.looseObject({
|
|
38432
|
+
user: external_exports.looseObject({ id: external_exports.number().optional(), name: external_exports.string().optional() }).optional(),
|
|
38433
|
+
viewed: external_exports.looseObject({ dateTime: external_exports.string() }).nullable().optional()
|
|
38434
|
+
});
|
|
37955
38435
|
function mapRecipients(items) {
|
|
37956
38436
|
return (items ?? []).map((r) => ({
|
|
37957
38437
|
userId: r.user?.id ?? 0,
|
|
@@ -37960,15 +38440,36 @@ function mapRecipients(items) {
|
|
|
37960
38440
|
}));
|
|
37961
38441
|
}
|
|
37962
38442
|
var expandPath2 = expandPath;
|
|
37963
|
-
|
|
37964
|
-
const
|
|
37965
|
-
|
|
37966
|
-
"
|
|
37967
|
-
|
|
38443
|
+
function verifyWriteLanded(kind, sent, persisted) {
|
|
38444
|
+
const mismatches = [];
|
|
38445
|
+
if (typeof persisted.subject !== "string" || !persisted.subject.includes(sent.subject)) {
|
|
38446
|
+
mismatches.push("subject");
|
|
38447
|
+
}
|
|
38448
|
+
if (typeof persisted.body !== "string" || !persisted.body.includes(sent.body)) {
|
|
38449
|
+
mismatches.push("body");
|
|
38450
|
+
}
|
|
38451
|
+
if (mismatches.length === 0) return null;
|
|
38452
|
+
return `WARNING: the ${kind} re-fetched from OFW does not contain the ${mismatches.join(" and ")} that was posted \u2014 OFW may have silently dropped or altered the write. Verify the ${kind} on ourfamilywizard.com before relying on it.`;
|
|
38453
|
+
}
|
|
38454
|
+
var PostMessagesResponseSchema = external_exports.looseObject({
|
|
38455
|
+
id: external_exports.number().optional(),
|
|
38456
|
+
entityId: external_exports.number().optional()
|
|
38457
|
+
}).nullable();
|
|
38458
|
+
async function postMessageAndRefetch(client2, payload, detailSchema, ctx) {
|
|
38459
|
+
const raw = parseOFW(
|
|
38460
|
+
PostMessagesResponseSchema,
|
|
38461
|
+
await client2.request("POST", "/pub/v3/messages", payload),
|
|
38462
|
+
`POST /pub/v3/messages (${ctx})`,
|
|
38463
|
+
"strict"
|
|
37968
38464
|
);
|
|
37969
38465
|
const id = typeof raw?.id === "number" ? raw.id : typeof raw?.entityId === "number" ? raw.entityId : null;
|
|
37970
38466
|
if (id === null) return { id: null, detail: null, raw };
|
|
37971
|
-
const detail =
|
|
38467
|
+
const detail = parseOFW(
|
|
38468
|
+
detailSchema,
|
|
38469
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
38470
|
+
`GET /pub/v3/messages/{id} (${ctx})`,
|
|
38471
|
+
"strict"
|
|
38472
|
+
);
|
|
37972
38473
|
return { id, detail, raw };
|
|
37973
38474
|
}
|
|
37974
38475
|
|
|
@@ -37992,7 +38493,7 @@ function registerUserTools(server, client2) {
|
|
|
37992
38493
|
|
|
37993
38494
|
// src/cache.ts
|
|
37994
38495
|
import { DatabaseSync } from "node:sqlite";
|
|
37995
|
-
import { mkdirSync } from "node:fs";
|
|
38496
|
+
import { mkdirSync, chmodSync, existsSync } from "node:fs";
|
|
37996
38497
|
import { dirname as dirname2 } from "node:path";
|
|
37997
38498
|
var instance = null;
|
|
37998
38499
|
var SCHEMA_V1 = `
|
|
@@ -38055,14 +38556,23 @@ function migrate(db) {
|
|
|
38055
38556
|
"INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
|
|
38056
38557
|
).run("schema_version", "2");
|
|
38057
38558
|
}
|
|
38559
|
+
function enforceCachePermissions(dbPath) {
|
|
38560
|
+
chmodSync(dirname2(dbPath), 448);
|
|
38561
|
+
chmodSync(dbPath, 384);
|
|
38562
|
+
for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
38563
|
+
if (existsSync(sibling)) chmodSync(sibling, 384);
|
|
38564
|
+
}
|
|
38565
|
+
}
|
|
38058
38566
|
function openCache() {
|
|
38059
38567
|
if (instance) return instance;
|
|
38060
38568
|
const path = getCacheDbPath();
|
|
38061
38569
|
mkdirSync(dirname2(path), { recursive: true });
|
|
38062
38570
|
const db = new DatabaseSync(path);
|
|
38571
|
+
enforceCachePermissions(path);
|
|
38063
38572
|
db.exec("PRAGMA journal_mode = WAL");
|
|
38064
38573
|
db.exec("PRAGMA foreign_keys = ON");
|
|
38065
38574
|
migrate(db);
|
|
38575
|
+
enforceCachePermissions(path);
|
|
38066
38576
|
instance = { db };
|
|
38067
38577
|
return instance;
|
|
38068
38578
|
}
|
|
@@ -38328,8 +38838,20 @@ function markAttachmentDownloaded(fileId, path) {
|
|
|
38328
38838
|
}
|
|
38329
38839
|
|
|
38330
38840
|
// src/sync.ts
|
|
38841
|
+
var FileMetaSchema = external_exports.looseObject({
|
|
38842
|
+
fileId: external_exports.number(),
|
|
38843
|
+
label: external_exports.string().optional(),
|
|
38844
|
+
fileName: external_exports.string().optional(),
|
|
38845
|
+
fileType: external_exports.string().optional(),
|
|
38846
|
+
// MIME
|
|
38847
|
+
fileSize: external_exports.number().optional()
|
|
38848
|
+
});
|
|
38331
38849
|
async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
38332
|
-
const meta3 =
|
|
38850
|
+
const meta3 = parseOFW(
|
|
38851
|
+
FileMetaSchema,
|
|
38852
|
+
await client2.request("GET", `/pub/v1/myfiles/${fileId}`),
|
|
38853
|
+
"GET /pub/v1/myfiles/{fileId}"
|
|
38854
|
+
);
|
|
38333
38855
|
upsertAttachmentForMessage({
|
|
38334
38856
|
fileId: meta3.fileId ?? fileId,
|
|
38335
38857
|
fileName: meta3.fileName ?? `file-${fileId}`,
|
|
@@ -38343,10 +38865,14 @@ async function fetchAttachmentMeta(client2, fileId, messageId) {
|
|
|
38343
38865
|
async function fetchAttachmentMetaForMessage(client2, messageId, fileIds) {
|
|
38344
38866
|
await Promise.allSettled(fileIds.map((fid) => fetchAttachmentMeta(client2, fid, messageId)));
|
|
38345
38867
|
}
|
|
38868
|
+
var FoldersSchema = external_exports.looseObject({
|
|
38869
|
+
systemFolders: external_exports.array(external_exports.looseObject({ id: external_exports.string(), folderType: external_exports.string() })).optional()
|
|
38870
|
+
});
|
|
38346
38871
|
async function resolveFolderIds(client2) {
|
|
38347
|
-
const data =
|
|
38348
|
-
|
|
38349
|
-
"/pub/v1/messageFolders?includeFolderCounts=true"
|
|
38872
|
+
const data = parseOFW(
|
|
38873
|
+
FoldersSchema,
|
|
38874
|
+
await client2.request("GET", "/pub/v1/messageFolders?includeFolderCounts=true"),
|
|
38875
|
+
"GET /pub/v1/messageFolders"
|
|
38350
38876
|
);
|
|
38351
38877
|
const sys = data.systemFolders ?? [];
|
|
38352
38878
|
const find = (type) => {
|
|
@@ -38362,6 +38888,19 @@ async function resolveFolderIds(client2) {
|
|
|
38362
38888
|
setMeta("drafts_folder_id", ids.drafts);
|
|
38363
38889
|
return ids;
|
|
38364
38890
|
}
|
|
38891
|
+
var ListItemSchema = external_exports.looseObject({
|
|
38892
|
+
id: external_exports.number(),
|
|
38893
|
+
subject: external_exports.string(),
|
|
38894
|
+
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
38895
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
38896
|
+
showNeverViewed: external_exports.boolean(),
|
|
38897
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
38898
|
+
});
|
|
38899
|
+
var ListResponseSchema = external_exports.looseObject({ data: external_exports.array(ListItemSchema).optional() });
|
|
38900
|
+
var DetailResponseSchema = external_exports.looseObject({
|
|
38901
|
+
body: external_exports.string().optional(),
|
|
38902
|
+
files: external_exports.array(external_exports.number()).optional()
|
|
38903
|
+
});
|
|
38365
38904
|
async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
38366
38905
|
let page = 1;
|
|
38367
38906
|
let synced = 0;
|
|
@@ -38369,7 +38908,11 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38369
38908
|
const unread = [];
|
|
38370
38909
|
while (true) {
|
|
38371
38910
|
const path = `/pub/v3/messages?folders=${encodeURIComponent(folderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
38372
|
-
const list =
|
|
38911
|
+
const list = parseOFW(
|
|
38912
|
+
ListResponseSchema,
|
|
38913
|
+
await client2.request("GET", path),
|
|
38914
|
+
`GET /pub/v3/messages?folders={${folder}}`
|
|
38915
|
+
);
|
|
38373
38916
|
const items = list.data ?? [];
|
|
38374
38917
|
if (items.length === 0) break;
|
|
38375
38918
|
let pageHadNewItem = false;
|
|
@@ -38384,7 +38927,11 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38384
38927
|
let fetchedBodyAt = null;
|
|
38385
38928
|
let detailFileIds = [];
|
|
38386
38929
|
if (shouldFetchBody) {
|
|
38387
|
-
const detail =
|
|
38930
|
+
const detail = parseOFW(
|
|
38931
|
+
DetailResponseSchema,
|
|
38932
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
38933
|
+
"GET /pub/v3/messages/{id} (sync)"
|
|
38934
|
+
);
|
|
38388
38935
|
body = detail.body ?? "";
|
|
38389
38936
|
fetchedBodyAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38390
38937
|
if (Array.isArray(detail.files) && detail.files.length > 0) {
|
|
@@ -38426,17 +38973,44 @@ async function syncMessageFolder(client2, folder, folderId, opts) {
|
|
|
38426
38973
|
});
|
|
38427
38974
|
return { synced, unread };
|
|
38428
38975
|
}
|
|
38976
|
+
var DraftListItemSchema = external_exports.looseObject({
|
|
38977
|
+
id: external_exports.number(),
|
|
38978
|
+
subject: external_exports.string(),
|
|
38979
|
+
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
38980
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
38981
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
38982
|
+
});
|
|
38983
|
+
var DraftListResponseSchema = external_exports.looseObject({ data: external_exports.array(DraftListItemSchema).optional() });
|
|
38984
|
+
var DraftDetailSchema = external_exports.looseObject({
|
|
38985
|
+
body: external_exports.string().optional(),
|
|
38986
|
+
subject: external_exports.string().optional()
|
|
38987
|
+
});
|
|
38429
38988
|
async function syncDrafts(client2, draftsFolderId) {
|
|
38430
|
-
const
|
|
38431
|
-
|
|
38432
|
-
|
|
38989
|
+
const items = [];
|
|
38990
|
+
let page = 1;
|
|
38991
|
+
while (true) {
|
|
38992
|
+
const path = `/pub/v3/messages?folders=${encodeURIComponent(draftsFolderId)}&page=${page}&size=50&sort=date&sortDirection=desc`;
|
|
38993
|
+
const list = parseOFW(
|
|
38994
|
+
DraftListResponseSchema,
|
|
38995
|
+
await client2.request("GET", path),
|
|
38996
|
+
"GET /pub/v3/messages?folders={drafts}"
|
|
38997
|
+
);
|
|
38998
|
+
const pageItems = list.data ?? [];
|
|
38999
|
+
items.push(...pageItems);
|
|
39000
|
+
if (pageItems.length < 50) break;
|
|
39001
|
+
page++;
|
|
39002
|
+
}
|
|
38433
39003
|
const seenIds = /* @__PURE__ */ new Set();
|
|
38434
39004
|
let synced = 0;
|
|
38435
39005
|
for (const item of items) {
|
|
38436
39006
|
seenIds.add(item.id);
|
|
38437
39007
|
const modifiedAt = item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
38438
39008
|
const existing = getDraft(item.id);
|
|
38439
|
-
const detail =
|
|
39009
|
+
const detail = parseOFW(
|
|
39010
|
+
DraftDetailSchema,
|
|
39011
|
+
await client2.request("GET", `/pub/v3/messages/${item.id}`),
|
|
39012
|
+
"GET /pub/v3/messages/{id} (drafts sync)"
|
|
39013
|
+
);
|
|
38440
39014
|
const row = {
|
|
38441
39015
|
id: item.id,
|
|
38442
39016
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
@@ -38488,6 +39062,39 @@ async function syncAll(client2, opts) {
|
|
|
38488
39062
|
// src/tools/messages.ts
|
|
38489
39063
|
import { mkdirSync as mkdirSync2, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
38490
39064
|
import { basename, dirname as dirname3, extname, join as join5 } from "node:path";
|
|
39065
|
+
var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
|
|
39066
|
+
var SentDetailSchema = external_exports.looseObject({
|
|
39067
|
+
subject: external_exports.string().optional(),
|
|
39068
|
+
body: external_exports.string().optional(),
|
|
39069
|
+
date: DateSchema.optional(),
|
|
39070
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39071
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39072
|
+
});
|
|
39073
|
+
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
39074
|
+
subject: external_exports.string().optional(),
|
|
39075
|
+
body: external_exports.string().optional(),
|
|
39076
|
+
date: DateSchema.optional(),
|
|
39077
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
39078
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39079
|
+
});
|
|
39080
|
+
var MessageDetailSchema = external_exports.looseObject({
|
|
39081
|
+
id: external_exports.number(),
|
|
39082
|
+
subject: external_exports.string(),
|
|
39083
|
+
body: external_exports.string().optional(),
|
|
39084
|
+
date: DateSchema,
|
|
39085
|
+
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
39086
|
+
files: external_exports.array(external_exports.number()).optional(),
|
|
39087
|
+
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39088
|
+
});
|
|
39089
|
+
var DetailFilesSchema = external_exports.looseObject({ files: external_exports.array(external_exports.number()).optional() });
|
|
39090
|
+
var UploadedFileSchema = external_exports.looseObject({
|
|
39091
|
+
fileId: external_exports.number(),
|
|
39092
|
+
fileName: external_exports.string().optional(),
|
|
39093
|
+
label: external_exports.string().optional(),
|
|
39094
|
+
fileType: external_exports.string().optional(),
|
|
39095
|
+
sizeInBytes: external_exports.number().optional(),
|
|
39096
|
+
shareClass: external_exports.string().optional()
|
|
39097
|
+
});
|
|
38491
39098
|
var MIME_BY_EXT = {
|
|
38492
39099
|
".pdf": "application/pdf",
|
|
38493
39100
|
".png": "image/png",
|
|
@@ -38523,6 +39130,9 @@ function listDataHintsAtFiles(listData) {
|
|
|
38523
39130
|
return false;
|
|
38524
39131
|
}
|
|
38525
39132
|
function registerMessageTools(server, client2) {
|
|
39133
|
+
const writeMode = getWriteMode();
|
|
39134
|
+
const allowSend = writeMode === "all";
|
|
39135
|
+
const allowDrafts = writeMode !== "none";
|
|
38526
39136
|
server.registerTool("ofw_list_message_folders", {
|
|
38527
39137
|
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.",
|
|
38528
39138
|
annotations: { readOnlyHint: true }
|
|
@@ -38535,8 +39145,8 @@ function registerMessageTools(server, client2) {
|
|
|
38535
39145
|
annotations: { readOnlyHint: true },
|
|
38536
39146
|
inputSchema: {
|
|
38537
39147
|
folderId: external_exports.string().describe('Folder name: "inbox", "sent", or "both" (default "both")').optional(),
|
|
38538
|
-
page: external_exports.number().describe("Page number (default 1)").optional(),
|
|
38539
|
-
size: external_exports.number().describe("Messages per page (default 50)").optional(),
|
|
39148
|
+
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
39149
|
+
size: external_exports.number().int().min(1).describe("Messages per page (default 50)").optional(),
|
|
38540
39150
|
since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive)").optional(),
|
|
38541
39151
|
until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive)").optional(),
|
|
38542
39152
|
q: external_exports.string().describe("Substring match on subject AND body (case-insensitive). Use to find messages on a specific topic.").optional()
|
|
@@ -38599,7 +39209,11 @@ function registerMessageTools(server, client2) {
|
|
|
38599
39209
|
let attachments2 = listAttachmentsForMessage(id);
|
|
38600
39210
|
if (attachments2.length === 0 && listDataHintsAtFiles(cached2.listData)) {
|
|
38601
39211
|
try {
|
|
38602
|
-
const detail2 =
|
|
39212
|
+
const detail2 = parseOFW(
|
|
39213
|
+
DetailFilesSchema,
|
|
39214
|
+
await client2.request("GET", `/pub/v3/messages/${id}`),
|
|
39215
|
+
"GET /pub/v3/messages/{id} (attachment backfill)"
|
|
39216
|
+
);
|
|
38603
39217
|
if (Array.isArray(detail2.files) && detail2.files.length > 0) {
|
|
38604
39218
|
await fetchAttachmentMetaForMessage(client2, id, detail2.files);
|
|
38605
39219
|
attachments2 = listAttachmentsForMessage(id);
|
|
@@ -38609,7 +39223,11 @@ function registerMessageTools(server, client2) {
|
|
|
38609
39223
|
}
|
|
38610
39224
|
return jsonResponse({ ...cached2, attachments: attachments2 });
|
|
38611
39225
|
}
|
|
38612
|
-
const detail =
|
|
39226
|
+
const detail = parseOFW(
|
|
39227
|
+
MessageDetailSchema,
|
|
39228
|
+
await client2.request("GET", `/pub/v3/messages/${encodeURIComponent(args.messageId)}`),
|
|
39229
|
+
"GET /pub/v3/messages/{id} (ofw_get_message)"
|
|
39230
|
+
);
|
|
38613
39231
|
const folder = cached2?.folder ?? "inbox";
|
|
38614
39232
|
const row = {
|
|
38615
39233
|
id: detail.id,
|
|
@@ -38631,7 +39249,7 @@ function registerMessageTools(server, client2) {
|
|
|
38631
39249
|
const attachments = listAttachmentsForMessage(detail.id);
|
|
38632
39250
|
return jsonResponse({ ...row, attachments });
|
|
38633
39251
|
});
|
|
38634
|
-
server.registerTool("ofw_send_message", {
|
|
39252
|
+
if (allowSend) server.registerTool("ofw_send_message", {
|
|
38635
39253
|
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.",
|
|
38636
39254
|
annotations: { destructiveHint: true },
|
|
38637
39255
|
inputSchema: {
|
|
@@ -38701,9 +39319,11 @@ function registerMessageTools(server, client2) {
|
|
|
38701
39319
|
draft: false,
|
|
38702
39320
|
includeOriginal: resolvedReplyTo !== null,
|
|
38703
39321
|
replyToId: resolvedReplyTo
|
|
38704
|
-
});
|
|
39322
|
+
}, SentDetailSchema, "ofw_send_message");
|
|
38705
39323
|
let persisted = null;
|
|
39324
|
+
let verifyNote = null;
|
|
38706
39325
|
if (newId !== null) {
|
|
39326
|
+
verifyNote = verifyWriteLanded("message", { subject, body }, detail);
|
|
38707
39327
|
persisted = {
|
|
38708
39328
|
id: newId,
|
|
38709
39329
|
folder: "sent",
|
|
@@ -38731,13 +39351,18 @@ function registerMessageTools(server, client2) {
|
|
|
38731
39351
|
});
|
|
38732
39352
|
}
|
|
38733
39353
|
}
|
|
38734
|
-
|
|
39354
|
+
let unconfirmedNote = null;
|
|
39355
|
+
if (newId === null) {
|
|
39356
|
+
const draftClause = draftRef !== void 0 ? `Draft ${draftRef} was NOT deleted \u2014 check` : "Check";
|
|
39357
|
+
unconfirmedNote = `WARNING: OFW's send response did not include a message id, so the send could not be confirmed. ${draftClause} ourfamilywizard.com to see whether the message went out before retrying.`;
|
|
39358
|
+
} else if (draftRef !== void 0) {
|
|
38735
39359
|
await deleteOFWMessages(client2, [draftRef]);
|
|
38736
39360
|
deleteDraft(draftRef);
|
|
38737
39361
|
}
|
|
38738
39362
|
const responseObj = persisted ?? raw;
|
|
38739
39363
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
38740
|
-
|
|
39364
|
+
const notes = [rewriteNote, verifyNote, unconfirmedNote].filter((n) => n !== null).join("\n\n");
|
|
39365
|
+
return textResponse(notes ? `${notes}
|
|
38741
39366
|
|
|
38742
39367
|
${text}` : text);
|
|
38743
39368
|
});
|
|
@@ -38745,8 +39370,8 @@ ${text}` : text);
|
|
|
38745
39370
|
description: "List draft messages from the local OurFamilyWizard cache. Call ofw_sync_messages first if the cache is empty.",
|
|
38746
39371
|
annotations: { readOnlyHint: true },
|
|
38747
39372
|
inputSchema: {
|
|
38748
|
-
page: external_exports.number().describe("Page number (default 1)").optional(),
|
|
38749
|
-
size: external_exports.number().describe("Drafts per page (default 50)").optional()
|
|
39373
|
+
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
39374
|
+
size: external_exports.number().int().min(1).describe("Drafts per page (default 50)").optional()
|
|
38750
39375
|
}
|
|
38751
39376
|
}, async (args) => {
|
|
38752
39377
|
const page = args.page ?? 1;
|
|
@@ -38755,7 +39380,7 @@ ${text}` : text);
|
|
|
38755
39380
|
const payload = drafts.length === 0 ? { drafts: [], note: "Cache empty. Call ofw_sync_messages to populate." } : { drafts };
|
|
38756
39381
|
return jsonResponse(payload);
|
|
38757
39382
|
});
|
|
38758
|
-
server.registerTool("ofw_save_draft", {
|
|
39383
|
+
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
38759
39384
|
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.",
|
|
38760
39385
|
annotations: { readOnlyHint: false },
|
|
38761
39386
|
inputSchema: {
|
|
@@ -38786,10 +39411,17 @@ ${text}` : text);
|
|
|
38786
39411
|
includeOriginal: resolvedReplyTo !== null,
|
|
38787
39412
|
replyToId: resolvedReplyTo
|
|
38788
39413
|
};
|
|
38789
|
-
const { id: newId, detail, raw } = await postMessageAndRefetch(
|
|
39414
|
+
const { id: newId, detail, raw } = await postMessageAndRefetch(
|
|
39415
|
+
client2,
|
|
39416
|
+
payload,
|
|
39417
|
+
SavedDraftDetailSchema,
|
|
39418
|
+
"ofw_save_draft"
|
|
39419
|
+
);
|
|
38790
39420
|
let persisted = null;
|
|
38791
39421
|
let replaceNote = null;
|
|
39422
|
+
let verifyNote = null;
|
|
38792
39423
|
if (newId !== null) {
|
|
39424
|
+
verifyNote = verifyWriteLanded("draft", { subject: args.subject, body: args.body }, detail);
|
|
38793
39425
|
persisted = {
|
|
38794
39426
|
id: newId,
|
|
38795
39427
|
subject: detail.subject ?? args.subject,
|
|
@@ -38812,12 +39444,12 @@ ${text}` : text);
|
|
|
38812
39444
|
}
|
|
38813
39445
|
const responseObj = persisted ?? raw;
|
|
38814
39446
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
38815
|
-
const notes = [rewriteNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
39447
|
+
const notes = [rewriteNote, verifyNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
38816
39448
|
return textResponse(notes ? `${notes}
|
|
38817
39449
|
|
|
38818
39450
|
${text}` : text);
|
|
38819
39451
|
});
|
|
38820
|
-
server.registerTool("ofw_delete_draft", {
|
|
39452
|
+
if (allowDrafts) server.registerTool("ofw_delete_draft", {
|
|
38821
39453
|
description: "Delete a draft message from OurFamilyWizard. Also removes the draft from the local cache.",
|
|
38822
39454
|
annotations: { destructiveHint: true },
|
|
38823
39455
|
inputSchema: {
|
|
@@ -38832,8 +39464,8 @@ ${text}` : text);
|
|
|
38832
39464
|
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.",
|
|
38833
39465
|
annotations: { readOnlyHint: true },
|
|
38834
39466
|
inputSchema: {
|
|
38835
|
-
page: external_exports.number().describe("Page (default 1)").optional(),
|
|
38836
|
-
size: external_exports.number().describe("Per page (default 50)").optional()
|
|
39467
|
+
page: external_exports.number().int().min(1).describe("Page (default 1)").optional(),
|
|
39468
|
+
size: external_exports.number().int().min(1).describe("Per page (default 50)").optional()
|
|
38837
39469
|
}
|
|
38838
39470
|
}, async (args) => {
|
|
38839
39471
|
const page = args.page ?? 1;
|
|
@@ -38854,7 +39486,7 @@ ${text}` : text);
|
|
|
38854
39486
|
}
|
|
38855
39487
|
return jsonResponse(unread);
|
|
38856
39488
|
});
|
|
38857
|
-
server.registerTool("ofw_upload_attachment", {
|
|
39489
|
+
if (allowDrafts) server.registerTool("ofw_upload_attachment", {
|
|
38858
39490
|
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.`,
|
|
38859
39491
|
annotations: { destructiveHint: false },
|
|
38860
39492
|
inputSchema: {
|
|
@@ -38876,7 +39508,12 @@ ${text}` : text);
|
|
|
38876
39508
|
form.append("label", args.label ?? fileName);
|
|
38877
39509
|
form.append("fileName", fileName);
|
|
38878
39510
|
form.append("shareClass", args.shareClass ?? "PRIVATE");
|
|
38879
|
-
const meta3 =
|
|
39511
|
+
const meta3 = parseOFW(
|
|
39512
|
+
UploadedFileSchema,
|
|
39513
|
+
await client2.request("POST", "/pub/v3/myfiles/multipart", form),
|
|
39514
|
+
"POST /pub/v3/myfiles/multipart (ofw_upload_attachment)",
|
|
39515
|
+
"strict"
|
|
39516
|
+
);
|
|
38880
39517
|
upsertAttachmentForMessage({
|
|
38881
39518
|
fileId: meta3.fileId,
|
|
38882
39519
|
fileName: meta3.fileName ?? fileName,
|
|
@@ -39001,6 +39638,7 @@ async function deleteOFWMessages(client2, ids) {
|
|
|
39001
39638
|
|
|
39002
39639
|
// src/tools/calendar.ts
|
|
39003
39640
|
function registerCalendarTools(server, client2) {
|
|
39641
|
+
const allowWrites = getWriteMode() === "all";
|
|
39004
39642
|
server.registerTool("ofw_list_events", {
|
|
39005
39643
|
description: "List OurFamilyWizard calendar events in a date range",
|
|
39006
39644
|
annotations: { readOnlyHint: true },
|
|
@@ -39017,7 +39655,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39017
39655
|
);
|
|
39018
39656
|
return jsonResponse(data);
|
|
39019
39657
|
});
|
|
39020
|
-
server.registerTool("ofw_create_event", {
|
|
39658
|
+
if (allowWrites) server.registerTool("ofw_create_event", {
|
|
39021
39659
|
description: "Create a calendar event in OurFamilyWizard",
|
|
39022
39660
|
annotations: { destructiveHint: false },
|
|
39023
39661
|
inputSchema: {
|
|
@@ -39037,7 +39675,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39037
39675
|
const data = await client2.request("POST", "/pub/v1/calendar/events", args);
|
|
39038
39676
|
return jsonResponse(data);
|
|
39039
39677
|
});
|
|
39040
|
-
server.registerTool("ofw_update_event", {
|
|
39678
|
+
if (allowWrites) server.registerTool("ofw_update_event", {
|
|
39041
39679
|
description: "Update an existing OurFamilyWizard calendar event",
|
|
39042
39680
|
annotations: { destructiveHint: true },
|
|
39043
39681
|
inputSchema: {
|
|
@@ -39055,7 +39693,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39055
39693
|
const data = await client2.request("PUT", `/pub/v1/calendar/events/${encodeURIComponent(eventId)}`, updateData);
|
|
39056
39694
|
return jsonResponse(data);
|
|
39057
39695
|
});
|
|
39058
|
-
server.registerTool("ofw_delete_event", {
|
|
39696
|
+
if (allowWrites) server.registerTool("ofw_delete_event", {
|
|
39059
39697
|
description: "Delete an OurFamilyWizard calendar event",
|
|
39060
39698
|
annotations: { destructiveHint: true },
|
|
39061
39699
|
inputSchema: {
|
|
@@ -39069,6 +39707,7 @@ function registerCalendarTools(server, client2) {
|
|
|
39069
39707
|
|
|
39070
39708
|
// src/tools/expenses.ts
|
|
39071
39709
|
function registerExpenseTools(server, client2) {
|
|
39710
|
+
const allowWrites = getWriteMode() === "all";
|
|
39072
39711
|
server.registerTool("ofw_get_expense_totals", {
|
|
39073
39712
|
description: "Get OurFamilyWizard expense summary totals (owed/paid)",
|
|
39074
39713
|
annotations: { readOnlyHint: true }
|
|
@@ -39080,8 +39719,8 @@ function registerExpenseTools(server, client2) {
|
|
|
39080
39719
|
description: "List OurFamilyWizard expenses with pagination",
|
|
39081
39720
|
annotations: { readOnlyHint: true },
|
|
39082
39721
|
inputSchema: {
|
|
39083
|
-
start: external_exports.number().describe("Start offset (default 0)").optional(),
|
|
39084
|
-
max: external_exports.number().describe("Max results (default 20)").optional()
|
|
39722
|
+
start: external_exports.number().int().min(0).describe("Start offset (default 0)").optional(),
|
|
39723
|
+
max: external_exports.number().int().min(1).describe("Max results (default 20)").optional()
|
|
39085
39724
|
}
|
|
39086
39725
|
}, async (args) => {
|
|
39087
39726
|
const start = args.start ?? 0;
|
|
@@ -39089,7 +39728,7 @@ function registerExpenseTools(server, client2) {
|
|
|
39089
39728
|
const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
|
|
39090
39729
|
return jsonResponse(data);
|
|
39091
39730
|
});
|
|
39092
|
-
server.registerTool("ofw_create_expense", {
|
|
39731
|
+
if (allowWrites) server.registerTool("ofw_create_expense", {
|
|
39093
39732
|
description: "Log a new expense in OurFamilyWizard",
|
|
39094
39733
|
annotations: { destructiveHint: false },
|
|
39095
39734
|
inputSchema: {
|
|
@@ -39104,12 +39743,13 @@ function registerExpenseTools(server, client2) {
|
|
|
39104
39743
|
|
|
39105
39744
|
// src/tools/journal.ts
|
|
39106
39745
|
function registerJournalTools(server, client2) {
|
|
39746
|
+
const allowWrites = getWriteMode() === "all";
|
|
39107
39747
|
server.registerTool("ofw_list_journal_entries", {
|
|
39108
39748
|
description: "List OurFamilyWizard journal entries",
|
|
39109
39749
|
annotations: { readOnlyHint: true },
|
|
39110
39750
|
inputSchema: {
|
|
39111
|
-
start: external_exports.number().describe("Start offset (default 1)").optional(),
|
|
39112
|
-
max: external_exports.number().describe("Max results (default 10)").optional()
|
|
39751
|
+
start: external_exports.number().int().min(1).describe("Start offset (default 1)").optional(),
|
|
39752
|
+
max: external_exports.number().int().min(1).describe("Max results (default 10)").optional()
|
|
39113
39753
|
}
|
|
39114
39754
|
}, async (args) => {
|
|
39115
39755
|
const start = args.start ?? 1;
|
|
@@ -39117,7 +39757,7 @@ function registerJournalTools(server, client2) {
|
|
|
39117
39757
|
const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
|
|
39118
39758
|
return jsonResponse(data);
|
|
39119
39759
|
});
|
|
39120
|
-
server.registerTool("ofw_create_journal_entry", {
|
|
39760
|
+
if (allowWrites) server.registerTool("ofw_create_journal_entry", {
|
|
39121
39761
|
description: "Create a new journal entry in OurFamilyWizard",
|
|
39122
39762
|
annotations: { destructiveHint: false },
|
|
39123
39763
|
inputSchema: {
|
|
@@ -39143,7 +39783,7 @@ process.emit = function(event, ...args) {
|
|
|
39143
39783
|
};
|
|
39144
39784
|
await runMcp({
|
|
39145
39785
|
name: "ofw",
|
|
39146
|
-
version: "2.
|
|
39786
|
+
version: "2.4.0",
|
|
39147
39787
|
// x-release-please-version
|
|
39148
39788
|
deps: client,
|
|
39149
39789
|
tools: [
|