ofw-mcp 2.9.2 → 2.10.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/bundle.js +810 -174
- package/dist/index.js +1 -1
- package/dist/sync.js +7 -2
- package/dist/tools/_shared.js +25 -0
- package/dist/tools/draft-freshness.js +13 -2
- package/dist/tools/lifecycle.js +38 -12
- package/dist/tools/messages.js +264 -79
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/ofw/SKILL.md +5 -5
package/dist/bundle.js
CHANGED
|
@@ -3651,7 +3651,12 @@ var require_fast_uri = __commonJS({
|
|
|
3651
3651
|
}
|
|
3652
3652
|
function resolve2(baseURI, relativeURI, options) {
|
|
3653
3653
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3654
|
-
const
|
|
3654
|
+
const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
|
|
3655
|
+
const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
|
|
3656
|
+
if (baseMalformed || relativeMalformed) {
|
|
3657
|
+
throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
|
|
3658
|
+
}
|
|
3659
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
|
|
3655
3660
|
schemelessOptions.skipEscape = true;
|
|
3656
3661
|
return serialize(resolved, schemelessOptions);
|
|
3657
3662
|
}
|
|
@@ -3777,6 +3782,7 @@ var require_fast_uri = __commonJS({
|
|
|
3777
3782
|
}
|
|
3778
3783
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3779
3784
|
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
3785
|
+
var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
|
|
3780
3786
|
function getParseError(parsed, matches) {
|
|
3781
3787
|
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3782
3788
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3811,6 +3817,20 @@ var require_fast_uri = __commonJS({
|
|
|
3811
3817
|
parsed.error = "URI authority must not contain a literal backslash.";
|
|
3812
3818
|
malformedAuthorityOrPort = true;
|
|
3813
3819
|
}
|
|
3820
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
|
|
3821
|
+
if (introducerMatch !== null) {
|
|
3822
|
+
const region = introducerMatch[1];
|
|
3823
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, "");
|
|
3824
|
+
if (normalizedRegion.length >= 2) {
|
|
3825
|
+
if (normalizedRegion.slice(0, 2) !== "//") {
|
|
3826
|
+
parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
|
|
3827
|
+
malformedAuthorityOrPort = true;
|
|
3828
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
3829
|
+
parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
|
|
3830
|
+
malformedAuthorityOrPort = true;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3814
3834
|
const matches = uri.match(URI_PARSE);
|
|
3815
3835
|
if (matches) {
|
|
3816
3836
|
parsed.scheme = matches[1];
|
|
@@ -26725,17 +26745,33 @@ function normalizeObjectSchema(schema) {
|
|
|
26725
26745
|
}
|
|
26726
26746
|
return void 0;
|
|
26727
26747
|
}
|
|
26748
|
+
function getDotPath(path) {
|
|
26749
|
+
if (path.length === 0) {
|
|
26750
|
+
return "object root";
|
|
26751
|
+
}
|
|
26752
|
+
return path.reduce((acc, seg, index) => {
|
|
26753
|
+
if (index === 0) {
|
|
26754
|
+
return String(seg);
|
|
26755
|
+
}
|
|
26756
|
+
if (typeof seg === "number") {
|
|
26757
|
+
return `${acc}[${seg}]`;
|
|
26758
|
+
}
|
|
26759
|
+
return `${acc}.${seg}`;
|
|
26760
|
+
}, "");
|
|
26761
|
+
}
|
|
26728
26762
|
function getParseErrorMessage(error51) {
|
|
26729
26763
|
if (error51 && typeof error51 === "object") {
|
|
26764
|
+
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26765
|
+
return error51.issues.map((i) => {
|
|
26766
|
+
if (!i.path?.length) {
|
|
26767
|
+
return i.message;
|
|
26768
|
+
}
|
|
26769
|
+
return `${i.message} at ${getDotPath(i.path)}`;
|
|
26770
|
+
}).join("\n");
|
|
26771
|
+
}
|
|
26730
26772
|
if ("message" in error51 && typeof error51.message === "string") {
|
|
26731
26773
|
return error51.message;
|
|
26732
26774
|
}
|
|
26733
|
-
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26734
|
-
const firstIssue = error51.issues[0];
|
|
26735
|
-
if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
|
|
26736
|
-
return String(firstIssue.message);
|
|
26737
|
-
}
|
|
26738
|
-
}
|
|
26739
26775
|
try {
|
|
26740
26776
|
return JSON.stringify(error51);
|
|
26741
26777
|
} catch {
|
|
@@ -33350,16 +33386,7 @@ var Server = class extends Protocol {
|
|
|
33350
33386
|
if (!methodSchema) {
|
|
33351
33387
|
throw new Error("Schema is missing a method literal");
|
|
33352
33388
|
}
|
|
33353
|
-
|
|
33354
|
-
if (isZ4Schema(methodSchema)) {
|
|
33355
|
-
const v4Schema = methodSchema;
|
|
33356
|
-
const v4Def = v4Schema._zod?.def;
|
|
33357
|
-
methodValue = v4Def?.value ?? v4Schema.value;
|
|
33358
|
-
} else {
|
|
33359
|
-
const v3Schema = methodSchema;
|
|
33360
|
-
const legacyDef = v3Schema._def;
|
|
33361
|
-
methodValue = legacyDef?.value ?? v3Schema.value;
|
|
33362
|
-
}
|
|
33389
|
+
const methodValue = getLiteralValue(methodSchema);
|
|
33363
33390
|
if (typeof methodValue !== "string") {
|
|
33364
33391
|
throw new Error("Schema method literal must be a string");
|
|
33365
33392
|
}
|
|
@@ -34547,8 +34574,17 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
34547
34574
|
import process3 from "node:process";
|
|
34548
34575
|
|
|
34549
34576
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
34577
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
34550
34578
|
var ReadBuffer = class {
|
|
34579
|
+
constructor(options) {
|
|
34580
|
+
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
34581
|
+
}
|
|
34551
34582
|
append(chunk2) {
|
|
34583
|
+
const newSize = (this._buffer?.length ?? 0) + chunk2.length;
|
|
34584
|
+
if (newSize > this._maxBufferSize) {
|
|
34585
|
+
this.clear();
|
|
34586
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
34587
|
+
}
|
|
34552
34588
|
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
34553
34589
|
}
|
|
34554
34590
|
readMessage() {
|
|
@@ -34576,18 +34612,24 @@ function serializeMessage(message) {
|
|
|
34576
34612
|
|
|
34577
34613
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
34578
34614
|
var StdioServerTransport = class {
|
|
34579
|
-
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
34615
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
34580
34616
|
this._stdin = _stdin;
|
|
34581
34617
|
this._stdout = _stdout;
|
|
34582
|
-
this._readBuffer = new ReadBuffer();
|
|
34583
34618
|
this._started = false;
|
|
34584
34619
|
this._ondata = (chunk2) => {
|
|
34585
|
-
|
|
34586
|
-
|
|
34620
|
+
try {
|
|
34621
|
+
this._readBuffer.append(chunk2);
|
|
34622
|
+
this.processReadBuffer();
|
|
34623
|
+
} catch (error51) {
|
|
34624
|
+
this.onerror?.(error51);
|
|
34625
|
+
this.close().catch(() => {
|
|
34626
|
+
});
|
|
34627
|
+
}
|
|
34587
34628
|
};
|
|
34588
34629
|
this._onerror = (error51) => {
|
|
34589
34630
|
this.onerror?.(error51);
|
|
34590
34631
|
};
|
|
34632
|
+
this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
|
|
34591
34633
|
}
|
|
34592
34634
|
/**
|
|
34593
34635
|
* Starts listening for messages on stdin.
|
|
@@ -34914,12 +34956,19 @@ var TokenManager = class {
|
|
|
34914
34956
|
};
|
|
34915
34957
|
|
|
34916
34958
|
// src/client.ts
|
|
34917
|
-
import { dirname, join as
|
|
34959
|
+
import { dirname, join as join4 } from "path";
|
|
34918
34960
|
import { fileURLToPath } from "url";
|
|
34919
34961
|
|
|
34920
34962
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
34921
|
-
var PROTOCOL_VERSION =
|
|
34963
|
+
var PROTOCOL_VERSION = 3;
|
|
34922
34964
|
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
34965
|
+
function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
|
|
34966
|
+
const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
|
|
34967
|
+
out.set(mcpHelloNonce, 0);
|
|
34968
|
+
out.set(extHelloNonce, mcpHelloNonce.length);
|
|
34969
|
+
out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
|
|
34970
|
+
return out;
|
|
34971
|
+
}
|
|
34923
34972
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
34924
34973
|
"fetch",
|
|
34925
34974
|
"read_cookies",
|
|
@@ -34930,7 +34979,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34930
34979
|
"read_indexed_db",
|
|
34931
34980
|
"read_dom",
|
|
34932
34981
|
"download",
|
|
34933
|
-
"graphql"
|
|
34982
|
+
"graphql",
|
|
34983
|
+
"write_cookies"
|
|
34934
34984
|
]);
|
|
34935
34985
|
|
|
34936
34986
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35041,6 +35091,15 @@ function assertHttpUrl(x, label) {
|
|
|
35041
35091
|
throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
|
|
35042
35092
|
}
|
|
35043
35093
|
}
|
|
35094
|
+
function assertCookiePath(x, label) {
|
|
35095
|
+
assertString(x, label);
|
|
35096
|
+
if (!x.startsWith("/") || x.startsWith("//")) {
|
|
35097
|
+
throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
|
|
35098
|
+
}
|
|
35099
|
+
if (x.includes("?") || x.includes("#") || x.includes("\\")) {
|
|
35100
|
+
throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
|
|
35101
|
+
}
|
|
35102
|
+
}
|
|
35044
35103
|
function assertHttpsOriginOnly(x, label) {
|
|
35045
35104
|
assertString(x, label);
|
|
35046
35105
|
let u;
|
|
@@ -35485,13 +35544,43 @@ function validateInnerRequest(raw) {
|
|
|
35485
35544
|
}
|
|
35486
35545
|
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35487
35546
|
assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
|
|
35547
|
+
if (raw.init.path !== void 0)
|
|
35548
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35488
35549
|
for (const k of Object.keys(raw.init)) {
|
|
35489
|
-
if (k !== "origin" && k !== "keys") {
|
|
35550
|
+
if (k !== "origin" && k !== "keys" && k !== "path") {
|
|
35490
35551
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
|
|
35491
35552
|
}
|
|
35492
35553
|
}
|
|
35493
35554
|
return raw;
|
|
35494
35555
|
}
|
|
35556
|
+
if (raw.op === "write_cookies") {
|
|
35557
|
+
assertObject(raw.init, "inner.init");
|
|
35558
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35559
|
+
if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
|
|
35560
|
+
throw new ProtocolError("inner.init.cookies: must be a non-empty array");
|
|
35561
|
+
}
|
|
35562
|
+
for (const [i, entry] of raw.init.cookies.entries()) {
|
|
35563
|
+
assertObject(entry, `inner.init.cookies[${i}]`);
|
|
35564
|
+
assertString(entry.name, `inner.init.cookies[${i}].name`);
|
|
35565
|
+
if (!SCOPE_KEY_RE.test(entry.name)) {
|
|
35566
|
+
throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
|
|
35567
|
+
}
|
|
35568
|
+
assertString(entry.value, `inner.init.cookies[${i}].value`);
|
|
35569
|
+
for (const k of Object.keys(entry)) {
|
|
35570
|
+
if (k !== "name" && k !== "value") {
|
|
35571
|
+
throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35572
|
+
}
|
|
35573
|
+
}
|
|
35574
|
+
}
|
|
35575
|
+
if (raw.init.path !== void 0)
|
|
35576
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35577
|
+
for (const k of Object.keys(raw.init)) {
|
|
35578
|
+
if (k !== "origin" && k !== "cookies" && k !== "path") {
|
|
35579
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
|
|
35580
|
+
}
|
|
35581
|
+
}
|
|
35582
|
+
return raw;
|
|
35583
|
+
}
|
|
35495
35584
|
if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
|
|
35496
35585
|
assertObject(raw.init, "inner.init");
|
|
35497
35586
|
if (raw.init.origin === void 0) {
|
|
@@ -35678,7 +35767,7 @@ function validateInnerRequest(raw) {
|
|
|
35678
35767
|
}
|
|
35679
35768
|
return raw;
|
|
35680
35769
|
}
|
|
35681
|
-
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", "read_dom", "download", "graphql_query"; got ${JSON.stringify(raw.op)}`);
|
|
35770
|
+
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", "read_dom", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
|
|
35682
35771
|
}
|
|
35683
35772
|
function assertNonEmptyKeyArray(value, label) {
|
|
35684
35773
|
if (!Array.isArray(value)) {
|
|
@@ -35739,6 +35828,18 @@ function validateInnerResponse(raw) {
|
|
|
35739
35828
|
}
|
|
35740
35829
|
return raw;
|
|
35741
35830
|
}
|
|
35831
|
+
if (op === "write_cookies") {
|
|
35832
|
+
if (raw.written === void 0) {
|
|
35833
|
+
throw new ProtocolError("inner.written: missing on write_cookies response");
|
|
35834
|
+
}
|
|
35835
|
+
if (!Array.isArray(raw.written)) {
|
|
35836
|
+
throw new ProtocolError("inner.written: must be an array");
|
|
35837
|
+
}
|
|
35838
|
+
for (const [i, name] of raw.written.entries()) {
|
|
35839
|
+
assertString(name, `inner.written[${i}]`);
|
|
35840
|
+
}
|
|
35841
|
+
return raw;
|
|
35842
|
+
}
|
|
35742
35843
|
if (op === "read_local_storage" || op === "read_session_storage") {
|
|
35743
35844
|
if (raw.values === void 0) {
|
|
35744
35845
|
throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
|
|
@@ -36216,6 +36317,148 @@ async function awaitSessionReady(ready, opts) {
|
|
|
36216
36317
|
}
|
|
36217
36318
|
}
|
|
36218
36319
|
|
|
36320
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36321
|
+
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
36322
|
+
import { join as join3 } from "node:path";
|
|
36323
|
+
|
|
36324
|
+
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36325
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36326
|
+
import { join as join2 } from "node:path";
|
|
36327
|
+
import { homedir as homedir2 } from "node:os";
|
|
36328
|
+
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36329
|
+
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36330
|
+
function defaultIdentityDir() {
|
|
36331
|
+
return join2(homedir2(), ".fetchproxy", "identity");
|
|
36332
|
+
}
|
|
36333
|
+
function safeIdentityFileBase(serverName) {
|
|
36334
|
+
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36335
|
+
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36336
|
+
}
|
|
36337
|
+
return serverName.replace(/\//g, "_");
|
|
36338
|
+
}
|
|
36339
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36340
|
+
const safeFile = safeIdentityFileBase(serverName);
|
|
36341
|
+
const path = join2(dir, `${safeFile}.json`);
|
|
36342
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36343
|
+
try {
|
|
36344
|
+
const raw = await readFile(path, "utf8");
|
|
36345
|
+
const j2 = JSON.parse(raw);
|
|
36346
|
+
return {
|
|
36347
|
+
x25519Priv: fromB64(j2.x25519Priv),
|
|
36348
|
+
x25519Pub: fromB64(j2.x25519Pub),
|
|
36349
|
+
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36350
|
+
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36351
|
+
createdAt: j2.createdAt
|
|
36352
|
+
};
|
|
36353
|
+
} catch (e) {
|
|
36354
|
+
if (e.code !== "ENOENT")
|
|
36355
|
+
throw e;
|
|
36356
|
+
}
|
|
36357
|
+
const x = await generateX25519();
|
|
36358
|
+
const ed = await generateEd25519();
|
|
36359
|
+
const id = {
|
|
36360
|
+
x25519Priv: x.privateKey,
|
|
36361
|
+
x25519Pub: x.publicKey,
|
|
36362
|
+
ed25519Priv: ed.privateKey,
|
|
36363
|
+
ed25519Pub: ed.publicKey,
|
|
36364
|
+
createdAt: Date.now()
|
|
36365
|
+
};
|
|
36366
|
+
const j = {
|
|
36367
|
+
x25519Priv: toB64(id.x25519Priv),
|
|
36368
|
+
x25519Pub: toB64(id.x25519Pub),
|
|
36369
|
+
ed25519Priv: toB64(id.ed25519Priv),
|
|
36370
|
+
ed25519Pub: toB64(id.ed25519Pub),
|
|
36371
|
+
createdAt: id.createdAt
|
|
36372
|
+
};
|
|
36373
|
+
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36374
|
+
await chmod(path, 384);
|
|
36375
|
+
return id;
|
|
36376
|
+
}
|
|
36377
|
+
|
|
36378
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36379
|
+
function fileExtensionTrust(args) {
|
|
36380
|
+
return {
|
|
36381
|
+
allowNew: args.allowNew,
|
|
36382
|
+
location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36383
|
+
read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36384
|
+
write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
|
|
36385
|
+
};
|
|
36386
|
+
}
|
|
36387
|
+
var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
|
|
36388
|
+
function allowNewExtensionIdentity(explicit, env = process.env) {
|
|
36389
|
+
if (explicit !== void 0)
|
|
36390
|
+
return explicit;
|
|
36391
|
+
return env[TRUST_NEW_EXTENSION_ENV] === "1";
|
|
36392
|
+
}
|
|
36393
|
+
function decideExtensionTrust(args) {
|
|
36394
|
+
const { pin, hello, allowNew, serverName } = args;
|
|
36395
|
+
if (!pin)
|
|
36396
|
+
return { decision: "first-use" };
|
|
36397
|
+
if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
|
|
36398
|
+
return { decision: "pinned" };
|
|
36399
|
+
}
|
|
36400
|
+
const trustPath = args.location ?? extensionTrustPathHint(serverName);
|
|
36401
|
+
if (allowNew) {
|
|
36402
|
+
return {
|
|
36403
|
+
decision: "replace",
|
|
36404
|
+
message: `[fetchproxy] ${serverName}: accepting a NEW extension identity because ${TRUST_NEW_EXTENSION_ENV}=1 \u2014 re-pinning. Unset it once the browser you expect is connected.`
|
|
36405
|
+
};
|
|
36406
|
+
}
|
|
36407
|
+
return {
|
|
36408
|
+
decision: "refused",
|
|
36409
|
+
message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
|
|
36410
|
+
};
|
|
36411
|
+
}
|
|
36412
|
+
function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
|
|
36413
|
+
return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
36414
|
+
}
|
|
36415
|
+
function extensionTrustPathHint(serverName) {
|
|
36416
|
+
try {
|
|
36417
|
+
return extensionTrustPath(serverName);
|
|
36418
|
+
} catch {
|
|
36419
|
+
return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
|
|
36420
|
+
}
|
|
36421
|
+
}
|
|
36422
|
+
function isPin(x) {
|
|
36423
|
+
if (!x || typeof x !== "object")
|
|
36424
|
+
return false;
|
|
36425
|
+
const r = x;
|
|
36426
|
+
return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
|
|
36427
|
+
}
|
|
36428
|
+
async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
|
|
36429
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36430
|
+
let raw;
|
|
36431
|
+
try {
|
|
36432
|
+
raw = await readFile2(path, "utf8");
|
|
36433
|
+
} catch (e) {
|
|
36434
|
+
if (e.code === "ENOENT")
|
|
36435
|
+
return null;
|
|
36436
|
+
throw e;
|
|
36437
|
+
}
|
|
36438
|
+
let parsed;
|
|
36439
|
+
try {
|
|
36440
|
+
parsed = JSON.parse(raw);
|
|
36441
|
+
} catch {
|
|
36442
|
+
throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
|
|
36443
|
+
}
|
|
36444
|
+
if (!isPin(parsed)) {
|
|
36445
|
+
throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
|
|
36446
|
+
}
|
|
36447
|
+
return {
|
|
36448
|
+
identityX25519Pub: parsed.identityX25519Pub,
|
|
36449
|
+
identityEd25519Pub: parsed.identityEd25519Pub,
|
|
36450
|
+
pinnedAt: parsed.pinnedAt
|
|
36451
|
+
};
|
|
36452
|
+
}
|
|
36453
|
+
async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
|
|
36454
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36455
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
36456
|
+
const tmp = `${path}.tmp`;
|
|
36457
|
+
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
36458
|
+
await chmod2(tmp, 384);
|
|
36459
|
+
await rename(tmp, path);
|
|
36460
|
+
}
|
|
36461
|
+
|
|
36219
36462
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
36220
36463
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
36221
36464
|
var enc2 = new TextEncoder();
|
|
@@ -36269,9 +36512,12 @@ async function startHost(opts) {
|
|
|
36269
36512
|
}
|
|
36270
36513
|
resetSessionPromise();
|
|
36271
36514
|
let extensionHello = null;
|
|
36515
|
+
let extensionClaim = null;
|
|
36272
36516
|
wss.on("connection", (ws) => {
|
|
36273
36517
|
let identified = null;
|
|
36274
36518
|
let peerMcpId = null;
|
|
36519
|
+
let closed = false;
|
|
36520
|
+
let pinOnReady = false;
|
|
36275
36521
|
ws.on("message", async (data) => {
|
|
36276
36522
|
try {
|
|
36277
36523
|
let frame;
|
|
@@ -36283,10 +36529,43 @@ async function startHost(opts) {
|
|
|
36283
36529
|
return;
|
|
36284
36530
|
}
|
|
36285
36531
|
if (frame.type === "hello" && frame.role === "extension") {
|
|
36286
|
-
if (extensionWs) {
|
|
36532
|
+
if (extensionWs || extensionClaim) {
|
|
36287
36533
|
ws.close(1008, "extension already connected");
|
|
36288
36534
|
return;
|
|
36289
36535
|
}
|
|
36536
|
+
extensionClaim = ws;
|
|
36537
|
+
let pin;
|
|
36538
|
+
try {
|
|
36539
|
+
pin = await opts.extensionTrust.read();
|
|
36540
|
+
} catch (e) {
|
|
36541
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36542
|
+
if (extensionClaim === ws)
|
|
36543
|
+
extensionClaim = null;
|
|
36544
|
+
ws.close(1008, "extension pin unreadable");
|
|
36545
|
+
return;
|
|
36546
|
+
}
|
|
36547
|
+
const outcome = decideExtensionTrust({
|
|
36548
|
+
pin,
|
|
36549
|
+
hello: frame,
|
|
36550
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36551
|
+
serverName: opts.ownServerName,
|
|
36552
|
+
location: opts.extensionTrust.location
|
|
36553
|
+
});
|
|
36554
|
+
if (outcome.decision === "refused") {
|
|
36555
|
+
console.warn(outcome.message);
|
|
36556
|
+
if (extensionClaim === ws)
|
|
36557
|
+
extensionClaim = null;
|
|
36558
|
+
ws.close(1008, "extension identity is not the pinned one");
|
|
36559
|
+
return;
|
|
36560
|
+
}
|
|
36561
|
+
if (outcome.decision === "replace")
|
|
36562
|
+
console.warn(outcome.message);
|
|
36563
|
+
if (closed || ws.readyState !== import_websocket.default.OPEN) {
|
|
36564
|
+
if (extensionClaim === ws)
|
|
36565
|
+
extensionClaim = null;
|
|
36566
|
+
return;
|
|
36567
|
+
}
|
|
36568
|
+
pinOnReady = outcome.decision !== "pinned";
|
|
36290
36569
|
identified = "extension";
|
|
36291
36570
|
extensionWs = ws;
|
|
36292
36571
|
extensionHello = frame;
|
|
@@ -36298,6 +36577,8 @@ async function startHost(opts) {
|
|
|
36298
36577
|
console.error("[fetchproxy] onPairCode threw:", e);
|
|
36299
36578
|
}
|
|
36300
36579
|
}
|
|
36580
|
+
for (const slot of peers.values())
|
|
36581
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36301
36582
|
ws.send(JSON.stringify(ownHello));
|
|
36302
36583
|
for (const slot of peers.values()) {
|
|
36303
36584
|
ws.send(JSON.stringify(slot.helloFrame));
|
|
@@ -36333,6 +36614,8 @@ async function startHost(opts) {
|
|
|
36333
36614
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
36334
36615
|
if (extensionWs)
|
|
36335
36616
|
extensionWs.send(JSON.stringify(frame));
|
|
36617
|
+
if (extensionHello)
|
|
36618
|
+
ws.send(JSON.stringify(extensionHello));
|
|
36336
36619
|
return;
|
|
36337
36620
|
}
|
|
36338
36621
|
if (frame.type === "ready") {
|
|
@@ -36344,7 +36627,7 @@ async function startHost(opts) {
|
|
|
36344
36627
|
}
|
|
36345
36628
|
const extEdPub = fromB64(extensionHello.identityEd25519Pub);
|
|
36346
36629
|
const extNonce = fromB64(extensionHello.sessionNonce);
|
|
36347
|
-
const msg =
|
|
36630
|
+
const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
|
|
36348
36631
|
const sig = fromB64(frame.sessionSig);
|
|
36349
36632
|
let sigOk = false;
|
|
36350
36633
|
try {
|
|
@@ -36357,6 +36640,18 @@ async function startHost(opts) {
|
|
|
36357
36640
|
ws.close(1008, "extension session signature invalid");
|
|
36358
36641
|
return;
|
|
36359
36642
|
}
|
|
36643
|
+
if (pinOnReady) {
|
|
36644
|
+
pinOnReady = false;
|
|
36645
|
+
try {
|
|
36646
|
+
await opts.extensionTrust.write({
|
|
36647
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36648
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36649
|
+
pinnedAt: Date.now()
|
|
36650
|
+
});
|
|
36651
|
+
} catch (e) {
|
|
36652
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36653
|
+
}
|
|
36654
|
+
}
|
|
36360
36655
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36361
36656
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36362
36657
|
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36410,6 +36705,9 @@ async function startHost(opts) {
|
|
|
36410
36705
|
}
|
|
36411
36706
|
});
|
|
36412
36707
|
ws.on("close", () => {
|
|
36708
|
+
closed = true;
|
|
36709
|
+
if (extensionClaim === ws)
|
|
36710
|
+
extensionClaim = null;
|
|
36413
36711
|
if (identified === "extension" && extensionWs === ws) {
|
|
36414
36712
|
extensionWs = null;
|
|
36415
36713
|
extensionHello = null;
|
|
@@ -36500,11 +36798,84 @@ async function startPeer(opts) {
|
|
|
36500
36798
|
resolveFirstReady = resolve2;
|
|
36501
36799
|
rejectFirstReady = reject;
|
|
36502
36800
|
});
|
|
36801
|
+
let extensionHello = null;
|
|
36802
|
+
let warnedUnverifiable = false;
|
|
36803
|
+
let cachedPin = void 0;
|
|
36804
|
+
const authenticateExtension = async (sessionSig, extensionSessionPub) => {
|
|
36805
|
+
if (!extensionHello) {
|
|
36806
|
+
if (opts.requireExtensionIdentity) {
|
|
36807
|
+
console.error(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity, so this session cannot be verified \u2014 refusing. Upgrade the MCP holding the bridge port to 1.12.0 or later.`);
|
|
36808
|
+
return false;
|
|
36809
|
+
}
|
|
36810
|
+
if (!warnedUnverifiable) {
|
|
36811
|
+
warnedUnverifiable = true;
|
|
36812
|
+
console.warn(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity (pre-1.12.0), so this peer cannot verify which browser it is talking to. Upgrade the MCP holding the bridge port to close this.`);
|
|
36813
|
+
}
|
|
36814
|
+
return true;
|
|
36815
|
+
}
|
|
36816
|
+
const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
|
|
36817
|
+
let sigOk = false;
|
|
36818
|
+
try {
|
|
36819
|
+
sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
|
|
36820
|
+
} catch {
|
|
36821
|
+
sigOk = false;
|
|
36822
|
+
}
|
|
36823
|
+
if (!sigOk) {
|
|
36824
|
+
console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
|
|
36825
|
+
return false;
|
|
36826
|
+
}
|
|
36827
|
+
if (cachedPin === void 0) {
|
|
36828
|
+
try {
|
|
36829
|
+
cachedPin = await opts.extensionTrust.read();
|
|
36830
|
+
} catch (e) {
|
|
36831
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36832
|
+
return false;
|
|
36833
|
+
}
|
|
36834
|
+
}
|
|
36835
|
+
const pin = cachedPin;
|
|
36836
|
+
const outcome = decideExtensionTrust({
|
|
36837
|
+
pin,
|
|
36838
|
+
hello: extensionHello,
|
|
36839
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36840
|
+
serverName: opts.serverName,
|
|
36841
|
+
location: opts.extensionTrust.location
|
|
36842
|
+
});
|
|
36843
|
+
if (outcome.decision === "refused") {
|
|
36844
|
+
console.warn(outcome.message);
|
|
36845
|
+
return false;
|
|
36846
|
+
}
|
|
36847
|
+
if (outcome.decision === "replace")
|
|
36848
|
+
console.warn(outcome.message);
|
|
36849
|
+
if (outcome.decision !== "pinned") {
|
|
36850
|
+
try {
|
|
36851
|
+
const written = {
|
|
36852
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36853
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36854
|
+
pinnedAt: Date.now()
|
|
36855
|
+
};
|
|
36856
|
+
await opts.extensionTrust.write(written);
|
|
36857
|
+
cachedPin = written;
|
|
36858
|
+
} catch (e) {
|
|
36859
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36860
|
+
}
|
|
36861
|
+
}
|
|
36862
|
+
return true;
|
|
36863
|
+
};
|
|
36503
36864
|
const onMessage = async (data) => {
|
|
36504
36865
|
try {
|
|
36505
36866
|
const raw = JSON.parse(data.toString());
|
|
36506
36867
|
const frame = validateFrame(raw);
|
|
36868
|
+
if (frame.type === "hello" && frame.role === "extension") {
|
|
36869
|
+
extensionHello = frame;
|
|
36870
|
+
return;
|
|
36871
|
+
}
|
|
36507
36872
|
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
36873
|
+
const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
|
|
36874
|
+
if (!authorised) {
|
|
36875
|
+
ws.close(1008, "extension identity refused");
|
|
36876
|
+
rejectFirstReady(new Error("peer: extension identity refused"));
|
|
36877
|
+
return;
|
|
36878
|
+
}
|
|
36508
36879
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36509
36880
|
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36510
36881
|
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36585,57 +36956,6 @@ async function startPeer(opts) {
|
|
|
36585
36956
|
return handle;
|
|
36586
36957
|
}
|
|
36587
36958
|
|
|
36588
|
-
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36589
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36590
|
-
import { join as join2 } from "node:path";
|
|
36591
|
-
import { homedir as homedir2 } from "node:os";
|
|
36592
|
-
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36593
|
-
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36594
|
-
function defaultIdentityDir() {
|
|
36595
|
-
return join2(homedir2(), ".fetchproxy", "identity");
|
|
36596
|
-
}
|
|
36597
|
-
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36598
|
-
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36599
|
-
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36600
|
-
}
|
|
36601
|
-
const safeFile = serverName.replace(/\//g, "_");
|
|
36602
|
-
const path = join2(dir, `${safeFile}.json`);
|
|
36603
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36604
|
-
try {
|
|
36605
|
-
const raw = await readFile(path, "utf8");
|
|
36606
|
-
const j2 = JSON.parse(raw);
|
|
36607
|
-
return {
|
|
36608
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
36609
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
36610
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36611
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36612
|
-
createdAt: j2.createdAt
|
|
36613
|
-
};
|
|
36614
|
-
} catch (e) {
|
|
36615
|
-
if (e.code !== "ENOENT")
|
|
36616
|
-
throw e;
|
|
36617
|
-
}
|
|
36618
|
-
const x = await generateX25519();
|
|
36619
|
-
const ed = await generateEd25519();
|
|
36620
|
-
const id = {
|
|
36621
|
-
x25519Priv: x.privateKey,
|
|
36622
|
-
x25519Pub: x.publicKey,
|
|
36623
|
-
ed25519Priv: ed.privateKey,
|
|
36624
|
-
ed25519Pub: ed.publicKey,
|
|
36625
|
-
createdAt: Date.now()
|
|
36626
|
-
};
|
|
36627
|
-
const j = {
|
|
36628
|
-
x25519Priv: toB64(id.x25519Priv),
|
|
36629
|
-
x25519Pub: toB64(id.x25519Pub),
|
|
36630
|
-
ed25519Priv: toB64(id.ed25519Priv),
|
|
36631
|
-
ed25519Pub: toB64(id.ed25519Pub),
|
|
36632
|
-
createdAt: id.createdAt
|
|
36633
|
-
};
|
|
36634
|
-
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36635
|
-
await chmod(path, 384);
|
|
36636
|
-
return id;
|
|
36637
|
-
}
|
|
36638
|
-
|
|
36639
36959
|
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36640
36960
|
function classifyFetchError(error51) {
|
|
36641
36961
|
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
@@ -36717,6 +37037,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36717
37037
|
this.hint = hint;
|
|
36718
37038
|
}
|
|
36719
37039
|
};
|
|
37040
|
+
var FetchproxyHintedError = class extends FetchproxyProtocolError {
|
|
37041
|
+
/** The extension's raw rejection, unmodified. */
|
|
37042
|
+
originalError;
|
|
37043
|
+
/** What the user should actually do, in prose. */
|
|
37044
|
+
hint;
|
|
37045
|
+
constructor(originalError, hint) {
|
|
37046
|
+
super(`${originalError} \u2014 ${hint}`);
|
|
37047
|
+
this.name = "FetchproxyHintedError";
|
|
37048
|
+
this.originalError = originalError;
|
|
37049
|
+
this.hint = hint;
|
|
37050
|
+
}
|
|
37051
|
+
};
|
|
37052
|
+
var FetchproxyScopeError = class extends FetchproxyHintedError {
|
|
37053
|
+
constructor(originalError) {
|
|
37054
|
+
super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
|
|
37055
|
+
this.name = "FetchproxyScopeError";
|
|
37056
|
+
}
|
|
37057
|
+
};
|
|
37058
|
+
var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
37059
|
+
constructor(originalError) {
|
|
37060
|
+
super(originalError, "open a tab on that host and sign in, then re-run. This is not a version problem and does not need an update.");
|
|
37061
|
+
this.name = "FetchproxyNoTabError";
|
|
37062
|
+
}
|
|
37063
|
+
};
|
|
37064
|
+
var SCOPE_REJECTION = /not in declared/;
|
|
37065
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
37066
|
+
function protocolErrorFrom(error51) {
|
|
37067
|
+
if (SCOPE_REJECTION.test(error51))
|
|
37068
|
+
return new FetchproxyScopeError(error51);
|
|
37069
|
+
if (NO_TAB_REJECTION.test(error51))
|
|
37070
|
+
return new FetchproxyNoTabError(error51);
|
|
37071
|
+
return new FetchproxyProtocolError(error51);
|
|
37072
|
+
}
|
|
36720
37073
|
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36721
37074
|
url;
|
|
36722
37075
|
timeoutMs;
|
|
@@ -36746,6 +37099,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36746
37099
|
}
|
|
36747
37100
|
};
|
|
36748
37101
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
37102
|
+
function normalizeCookiePath(path) {
|
|
37103
|
+
if (path === void 0 || path === "")
|
|
37104
|
+
return void 0;
|
|
37105
|
+
const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
37106
|
+
try {
|
|
37107
|
+
assertCookiePath(trimmed, "path");
|
|
37108
|
+
} catch (e) {
|
|
37109
|
+
throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
|
|
37110
|
+
}
|
|
37111
|
+
return trimmed;
|
|
37112
|
+
}
|
|
36749
37113
|
function assertSubdomainLabel(label) {
|
|
36750
37114
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
36751
37115
|
throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
|
|
@@ -36810,6 +37174,9 @@ var FetchproxyServer = class {
|
|
|
36810
37174
|
// them off from `pending` (fetch) and `pendingReadCookies` (legacy
|
|
36811
37175
|
// string-shape) so the response routing in `onInner` stays linear.
|
|
36812
37176
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
37177
|
+
// 1.12.0+: write-cookies awaiters resolve the list of names actually
|
|
37178
|
+
// written, so a caller can confirm rather than assume.
|
|
37179
|
+
pendingWriteCookies = /* @__PURE__ */ new Map();
|
|
36813
37180
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
36814
37181
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
36815
37182
|
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
@@ -36936,6 +37303,8 @@ var FetchproxyServer = class {
|
|
|
36936
37303
|
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
36937
37304
|
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
36938
37305
|
identityDir: opts.identityDir,
|
|
37306
|
+
allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
|
|
37307
|
+
requireExtensionIdentity: opts.requireExtensionIdentity,
|
|
36939
37308
|
onPairCode: opts.onPairCode
|
|
36940
37309
|
};
|
|
36941
37310
|
}
|
|
@@ -37035,7 +37404,8 @@ var FetchproxyServer = class {
|
|
|
37035
37404
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37036
37405
|
ownDomSelectors: this.opts.domSelectors,
|
|
37037
37406
|
ownGraphqlOps: this.opts.graphqlOps,
|
|
37038
|
-
onPairCode: this.opts.onPairCode
|
|
37407
|
+
onPairCode: this.opts.onPairCode,
|
|
37408
|
+
extensionTrust: this.extensionTrust()
|
|
37039
37409
|
});
|
|
37040
37410
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
37041
37411
|
this.hostHandle.onExtensionDisconnect(() => {
|
|
@@ -37064,7 +37434,9 @@ var FetchproxyServer = class {
|
|
|
37064
37434
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37065
37435
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37066
37436
|
domSelectors: this.opts.domSelectors,
|
|
37067
|
-
graphqlOps: this.opts.graphqlOps
|
|
37437
|
+
graphqlOps: this.opts.graphqlOps,
|
|
37438
|
+
extensionTrust: this.extensionTrust(),
|
|
37439
|
+
requireExtensionIdentity: this.opts.requireExtensionIdentity
|
|
37068
37440
|
});
|
|
37069
37441
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37070
37442
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37203,6 +37575,23 @@ var FetchproxyServer = class {
|
|
|
37203
37575
|
markActive() {
|
|
37204
37576
|
this.noteActivityForKeepalive();
|
|
37205
37577
|
}
|
|
37578
|
+
/**
|
|
37579
|
+
* #208: this MCP's pin on the extension's identity, stored beside its own
|
|
37580
|
+
* identity key and so following `identityDir` wherever the caller put it.
|
|
37581
|
+
*
|
|
37582
|
+
* `allowNewExtensionIdentity` falls back to an environment variable when the
|
|
37583
|
+
* caller expressed no opinion, because the thirteen MCPs that construct this
|
|
37584
|
+
* class are separate packages: an operator whose extension re-install has
|
|
37585
|
+
* just locked all of them out needs one lever that does not require patching
|
|
37586
|
+
* every one of them.
|
|
37587
|
+
*/
|
|
37588
|
+
extensionTrust() {
|
|
37589
|
+
return fileExtensionTrust({
|
|
37590
|
+
serverName: this.opts.serverName,
|
|
37591
|
+
dir: this.opts.identityDir,
|
|
37592
|
+
allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
|
|
37593
|
+
});
|
|
37594
|
+
}
|
|
37206
37595
|
noteActivityForKeepalive() {
|
|
37207
37596
|
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
37208
37597
|
if (intervalMs <= 0)
|
|
@@ -37266,6 +37655,7 @@ var FetchproxyServer = class {
|
|
|
37266
37655
|
this.pending.delete(id);
|
|
37267
37656
|
this.pendingReadCookies.delete(id);
|
|
37268
37657
|
this.pendingStorage.delete(id);
|
|
37658
|
+
this.pendingWriteCookies.delete(id);
|
|
37269
37659
|
this.pendingCapture.delete(id);
|
|
37270
37660
|
this.pendingRedirect.delete(id);
|
|
37271
37661
|
this.pendingDownload.delete(id);
|
|
@@ -37387,7 +37777,7 @@ var FetchproxyServer = class {
|
|
|
37387
37777
|
port: this.opts.port
|
|
37388
37778
|
});
|
|
37389
37779
|
}
|
|
37390
|
-
return
|
|
37780
|
+
return protocolErrorFrom(result.error);
|
|
37391
37781
|
}
|
|
37392
37782
|
/**
|
|
37393
37783
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -37423,10 +37813,20 @@ var FetchproxyServer = class {
|
|
|
37423
37813
|
}
|
|
37424
37814
|
const url2 = isAbsolute2 ? path : `https://${host}${path}`;
|
|
37425
37815
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
37816
|
+
let tabUrl = `https://${host}/`;
|
|
37817
|
+
if (opts.viaTab !== void 0) {
|
|
37818
|
+
try {
|
|
37819
|
+
new URL(opts.viaTab);
|
|
37820
|
+
} catch {
|
|
37821
|
+
throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
|
|
37822
|
+
}
|
|
37823
|
+
assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
|
|
37824
|
+
tabUrl = opts.viaTab;
|
|
37825
|
+
}
|
|
37426
37826
|
const init = {
|
|
37427
37827
|
url: url2,
|
|
37428
37828
|
method,
|
|
37429
|
-
tabUrl
|
|
37829
|
+
tabUrl,
|
|
37430
37830
|
headers: opts.headers,
|
|
37431
37831
|
body: opts.body
|
|
37432
37832
|
};
|
|
@@ -37638,9 +38038,14 @@ var FetchproxyServer = class {
|
|
|
37638
38038
|
let inner;
|
|
37639
38039
|
if (opts.keys !== void 0) {
|
|
37640
38040
|
this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
|
|
38041
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
37641
38042
|
const initV3 = {
|
|
38043
|
+
// Origin stays BARE. The path travels as its own validated field —
|
|
38044
|
+
// `assertHttpsOriginOnly` deliberately refuses a path here so one
|
|
38045
|
+
// cannot be used to re-point the read past the domain gate.
|
|
37642
38046
|
origin: `https://${host}`,
|
|
37643
|
-
keys: [...opts.keys]
|
|
38047
|
+
keys: [...opts.keys],
|
|
38048
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
37644
38049
|
};
|
|
37645
38050
|
inner = { type: "request", id, op: "read_cookies", init: initV3 };
|
|
37646
38051
|
} else {
|
|
@@ -37653,10 +38058,65 @@ var FetchproxyServer = class {
|
|
|
37653
38058
|
await this.sendInnerFrame(inner);
|
|
37654
38059
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37655
38060
|
if (!result.ok) {
|
|
37656
|
-
throw
|
|
38061
|
+
throw protocolErrorFrom(result.error);
|
|
37657
38062
|
}
|
|
37658
38063
|
return result.cookies;
|
|
37659
38064
|
}
|
|
38065
|
+
/**
|
|
38066
|
+
* 1.12.0+: overwrite the value of cookies this MCP already declares.
|
|
38067
|
+
*
|
|
38068
|
+
* The bridge's only write verb, and it exists for one failure class. Sites
|
|
38069
|
+
* that ROTATE a credential cookie hand back a new value on every refresh; if
|
|
38070
|
+
* the MCP refreshes and keeps the result to itself, the copy in the browser's
|
|
38071
|
+
* cookie jar is dead, and the user gets signed out of a tab they never
|
|
38072
|
+
* touched — usually reported to them as "inactivity". Writing the rotated
|
|
38073
|
+
* value back is the only thing that repairs it.
|
|
38074
|
+
*
|
|
38075
|
+
* Requires `'write_cookies'` in capabilities, which the user approves at pair
|
|
38076
|
+
* time as its own line. Every name must ALSO be in declared `cookieKeys`: a
|
|
38077
|
+
* write can never reach a cookie the MCP was not already trusted to read, so
|
|
38078
|
+
* granting it cannot widen which cookies are in play — only what may be done
|
|
38079
|
+
* to the ones already listed.
|
|
38080
|
+
*
|
|
38081
|
+
* The extension refuses the whole request unless every named cookie already
|
|
38082
|
+
* exists; this refreshes a value in place and deliberately cannot author new
|
|
38083
|
+
* cookies. Returns the names actually written.
|
|
38084
|
+
*/
|
|
38085
|
+
async writeCookies(opts) {
|
|
38086
|
+
if (!this.opts.capabilities.includes("write_cookies")) {
|
|
38087
|
+
throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
38088
|
+
}
|
|
38089
|
+
const names = Object.keys(opts.cookies);
|
|
38090
|
+
if (names.length === 0) {
|
|
38091
|
+
throw new Error("FetchproxyServer.writeCookies(): no cookies given");
|
|
38092
|
+
}
|
|
38093
|
+
await this.ensureConnected();
|
|
38094
|
+
this.throwIfPendingPair();
|
|
38095
|
+
if (opts.subdomain !== void 0)
|
|
38096
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38097
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38098
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38099
|
+
this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
|
|
38100
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
38101
|
+
const id = this.nextRequestId++;
|
|
38102
|
+
const inner = {
|
|
38103
|
+
type: "request",
|
|
38104
|
+
id,
|
|
38105
|
+
op: "write_cookies",
|
|
38106
|
+
init: {
|
|
38107
|
+
// Bare origin, same invariant as the read path: a path must never be
|
|
38108
|
+
// able to move the request past the domain gate.
|
|
38109
|
+
origin: `https://${host}`,
|
|
38110
|
+
cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
|
|
38111
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
38112
|
+
}
|
|
38113
|
+
};
|
|
38114
|
+
const pending = new Promise((resolve2, reject) => {
|
|
38115
|
+
this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
|
|
38116
|
+
});
|
|
38117
|
+
await this.sendInnerFrame(inner);
|
|
38118
|
+
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
38119
|
+
}
|
|
37660
38120
|
/**
|
|
37661
38121
|
* 0.3.0+: read declared localStorage keys from the user's signed-in
|
|
37662
38122
|
* tab. Requires `'read_local_storage'` in capabilities AND each key
|
|
@@ -38203,7 +38663,7 @@ var FetchproxyServer = class {
|
|
|
38203
38663
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
38204
38664
|
}
|
|
38205
38665
|
} else {
|
|
38206
|
-
storageCb.reject(
|
|
38666
|
+
storageCb.reject(protocolErrorFrom(inner.error));
|
|
38207
38667
|
}
|
|
38208
38668
|
return;
|
|
38209
38669
|
}
|
|
@@ -38217,7 +38677,7 @@ var FetchproxyServer = class {
|
|
|
38217
38677
|
captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
|
|
38218
38678
|
}
|
|
38219
38679
|
} else {
|
|
38220
|
-
captureCb.reject(
|
|
38680
|
+
captureCb.reject(protocolErrorFrom(inner.error));
|
|
38221
38681
|
}
|
|
38222
38682
|
return;
|
|
38223
38683
|
}
|
|
@@ -38231,7 +38691,7 @@ var FetchproxyServer = class {
|
|
|
38231
38691
|
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
38232
38692
|
}
|
|
38233
38693
|
} else {
|
|
38234
|
-
redirectCb.reject(
|
|
38694
|
+
redirectCb.reject(protocolErrorFrom(inner.error));
|
|
38235
38695
|
}
|
|
38236
38696
|
return;
|
|
38237
38697
|
}
|
|
@@ -38245,7 +38705,7 @@ var FetchproxyServer = class {
|
|
|
38245
38705
|
idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
|
|
38246
38706
|
}
|
|
38247
38707
|
} else {
|
|
38248
|
-
idbCb.reject(
|
|
38708
|
+
idbCb.reject(protocolErrorFrom(inner.error));
|
|
38249
38709
|
}
|
|
38250
38710
|
return;
|
|
38251
38711
|
}
|
|
@@ -38259,7 +38719,7 @@ var FetchproxyServer = class {
|
|
|
38259
38719
|
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
38260
38720
|
}
|
|
38261
38721
|
} else {
|
|
38262
|
-
downloadCb.reject(
|
|
38722
|
+
downloadCb.reject(protocolErrorFrom(inner.error));
|
|
38263
38723
|
}
|
|
38264
38724
|
return;
|
|
38265
38725
|
}
|
|
@@ -38273,7 +38733,17 @@ var FetchproxyServer = class {
|
|
|
38273
38733
|
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38274
38734
|
}
|
|
38275
38735
|
} else {
|
|
38276
|
-
graphqlCb.reject(
|
|
38736
|
+
graphqlCb.reject(protocolErrorFrom(inner.error));
|
|
38737
|
+
}
|
|
38738
|
+
return;
|
|
38739
|
+
}
|
|
38740
|
+
const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
|
|
38741
|
+
if (writeCookiesCb) {
|
|
38742
|
+
this.pendingWriteCookies.delete(inner.id);
|
|
38743
|
+
if (inner.ok && inner.op === "write_cookies") {
|
|
38744
|
+
writeCookiesCb.resolve([...inner.written]);
|
|
38745
|
+
} else {
|
|
38746
|
+
writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
|
|
38277
38747
|
}
|
|
38278
38748
|
return;
|
|
38279
38749
|
}
|
|
@@ -38316,6 +38786,9 @@ var FetchproxyServer = class {
|
|
|
38316
38786
|
for (const { reject } of this.pendingStorage.values())
|
|
38317
38787
|
reject(err);
|
|
38318
38788
|
this.pendingStorage.clear();
|
|
38789
|
+
for (const { reject } of this.pendingWriteCookies.values())
|
|
38790
|
+
reject(err);
|
|
38791
|
+
this.pendingWriteCookies.clear();
|
|
38319
38792
|
for (const { reject } of this.pendingCapture.values())
|
|
38320
38793
|
reject(err);
|
|
38321
38794
|
this.pendingCapture.clear();
|
|
@@ -38385,6 +38858,9 @@ var FetchproxyServer = class {
|
|
|
38385
38858
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
38386
38859
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
38387
38860
|
async function bootstrap(opts) {
|
|
38861
|
+
return runOneLift(opts);
|
|
38862
|
+
}
|
|
38863
|
+
async function runOneLift(opts) {
|
|
38388
38864
|
const envVar = opts.serverName.toUpperCase().replace(/[^A-Z0-9]/g, "_").replace(/^_+/, "") + "_DISABLE_FETCHPROXY";
|
|
38389
38865
|
const envVal = process.env[envVar];
|
|
38390
38866
|
if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
|
|
@@ -38453,7 +38929,9 @@ async function bootstrap(opts) {
|
|
|
38453
38929
|
if (opts.declare.cookies.length > 0) {
|
|
38454
38930
|
const joined = await server.readCookies({
|
|
38455
38931
|
keys: opts.declare.cookies,
|
|
38456
|
-
...storageDomainOpts
|
|
38932
|
+
...storageDomainOpts,
|
|
38933
|
+
// Cookie-only: the other buckets are origin-scoped and ignore path.
|
|
38934
|
+
...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
|
|
38457
38935
|
});
|
|
38458
38936
|
for (const piece of joined.split("; ")) {
|
|
38459
38937
|
if (!piece)
|
|
@@ -38511,12 +38989,18 @@ async function bootstrap(opts) {
|
|
|
38511
38989
|
});
|
|
38512
38990
|
indexedDbBucket[`${d.database}/${d.store}`] = values;
|
|
38513
38991
|
}
|
|
38992
|
+
const absent = (declared, got) => declared.filter((k) => !(k in got));
|
|
38514
38993
|
return {
|
|
38515
38994
|
cookies,
|
|
38516
38995
|
localStorage,
|
|
38517
38996
|
sessionStorage,
|
|
38518
38997
|
capturedHeaders,
|
|
38519
|
-
indexedDb: indexedDbBucket
|
|
38998
|
+
indexedDb: indexedDbBucket,
|
|
38999
|
+
missing: {
|
|
39000
|
+
cookies: absent(opts.declare.cookies, cookies),
|
|
39001
|
+
localStorage: absent(opts.declare.localStorage, localStorage),
|
|
39002
|
+
sessionStorage: absent(opts.declare.sessionStorage, sessionStorage)
|
|
39003
|
+
}
|
|
38520
39004
|
};
|
|
38521
39005
|
} finally {
|
|
38522
39006
|
try {
|
|
@@ -38590,7 +39074,7 @@ async function loginWithPassword(username, password) {
|
|
|
38590
39074
|
// package.json
|
|
38591
39075
|
var package_default = {
|
|
38592
39076
|
name: "ofw-mcp",
|
|
38593
|
-
version: "2.
|
|
39077
|
+
version: "2.10.1",
|
|
38594
39078
|
license: "MIT",
|
|
38595
39079
|
mcpName: "io.github.chrischall/ofw-mcp",
|
|
38596
39080
|
description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
|
|
@@ -38626,14 +39110,14 @@ var package_default = {
|
|
|
38626
39110
|
},
|
|
38627
39111
|
dependencies: {
|
|
38628
39112
|
"@chrischall/mcp-utils": "^0.14.0",
|
|
38629
|
-
"@fetchproxy/bootstrap": "^
|
|
39113
|
+
"@fetchproxy/bootstrap": "^2.0.0",
|
|
38630
39114
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38631
39115
|
dotenv: "^17.4.2",
|
|
38632
39116
|
zod: "^4.4.3"
|
|
38633
39117
|
},
|
|
38634
39118
|
devDependencies: {
|
|
38635
39119
|
"@chrischall/mcp-connector": "^1.1.1",
|
|
38636
|
-
"@cloudflare/vitest-pool-workers": "^0.
|
|
39120
|
+
"@cloudflare/vitest-pool-workers": "^0.19.1",
|
|
38637
39121
|
"@cloudflare/workers-oauth-provider": "^0.8.1",
|
|
38638
39122
|
"@cloudflare/workers-types": "^5.20260708.1",
|
|
38639
39123
|
"@types/node": "^26.0.0",
|
|
@@ -38710,7 +39194,7 @@ async function resolveAuth() {
|
|
|
38710
39194
|
// src/client.ts
|
|
38711
39195
|
try {
|
|
38712
39196
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
38713
|
-
await loadDotenvSafely({ path:
|
|
39197
|
+
await loadDotenvSafely({ path: join4(dir, "..", ".env") });
|
|
38714
39198
|
} catch {
|
|
38715
39199
|
}
|
|
38716
39200
|
function parseContentDispositionFilename(cd) {
|
|
@@ -39099,6 +39583,16 @@ function mapRecipients(items) {
|
|
|
39099
39583
|
function hasRealView(recipients) {
|
|
39100
39584
|
return recipients.some((r) => r.viewedAt !== null && !r.viewedAt.startsWith("1970-01-01"));
|
|
39101
39585
|
}
|
|
39586
|
+
function threadedReplyTo(detail) {
|
|
39587
|
+
return detail.replyToId ?? detail.inReplyTo ?? null;
|
|
39588
|
+
}
|
|
39589
|
+
function reportsThreaded(detail) {
|
|
39590
|
+
return threadedReplyTo(detail) !== null || detail.showContext === true;
|
|
39591
|
+
}
|
|
39592
|
+
function reportsUnthreaded(detail) {
|
|
39593
|
+
if (reportsThreaded(detail)) return false;
|
|
39594
|
+
return detail.inReplyTo !== void 0 || detail.showContext !== void 0;
|
|
39595
|
+
}
|
|
39102
39596
|
function scrapeSaysRead(listData) {
|
|
39103
39597
|
if (typeof listData !== "object" || listData === null) return false;
|
|
39104
39598
|
const ld = listData;
|
|
@@ -39413,7 +39907,12 @@ var DraftListItemSchema = external_exports.looseObject({
|
|
|
39413
39907
|
id: external_exports.number(),
|
|
39414
39908
|
subject: external_exports.string(),
|
|
39415
39909
|
date: external_exports.looseObject({ dateTime: external_exports.string() }),
|
|
39910
|
+
// Both spellings of the threading echo — OFW reports the reply target as
|
|
39911
|
+
// `inReplyTo` (with showContext) on list payloads where `replyToId` is null.
|
|
39912
|
+
// The cached row must derive the SAME value ofw_save_draft derived from the
|
|
39913
|
+
// detail, or the content revision drifts between a save and the next sync.
|
|
39416
39914
|
replyToId: external_exports.number().nullable().optional(),
|
|
39915
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
39417
39916
|
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
39418
39917
|
});
|
|
39419
39918
|
var DraftListResponseSchema = external_exports.looseObject({ data: external_exports.array(DraftListItemSchema).optional() });
|
|
@@ -39476,7 +39975,7 @@ async function syncDrafts(client2, draftsFolderId, store, budget) {
|
|
|
39476
39975
|
subject: detail.subject ?? item.subject ?? "(no subject)",
|
|
39477
39976
|
body: detail.body ?? "",
|
|
39478
39977
|
recipients: mapRecipients(item.recipients),
|
|
39479
|
-
replyToId: item
|
|
39978
|
+
replyToId: threadedReplyTo(item),
|
|
39480
39979
|
modifiedAt: item.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
39481
39980
|
listData: item
|
|
39482
39981
|
});
|
|
@@ -39582,24 +40081,24 @@ async function syncAll(client2, opts, store) {
|
|
|
39582
40081
|
// src/config.ts
|
|
39583
40082
|
import { createHash } from "node:crypto";
|
|
39584
40083
|
import { homedir as homedir3 } from "node:os";
|
|
39585
|
-
import { join as
|
|
40084
|
+
import { join as join5 } from "node:path";
|
|
39586
40085
|
function readCacheIdentity() {
|
|
39587
40086
|
return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
|
|
39588
40087
|
}
|
|
39589
40088
|
function getCacheDir() {
|
|
39590
40089
|
const override = process.env.OFW_CACHE_DIR;
|
|
39591
40090
|
if (override && override.trim().length > 0) return override.trim();
|
|
39592
|
-
return
|
|
40091
|
+
return join5(homedir3(), ".cache", "ofw-mcp");
|
|
39593
40092
|
}
|
|
39594
40093
|
function getCacheDbPath() {
|
|
39595
40094
|
const identity = readCacheIdentity();
|
|
39596
40095
|
const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
|
|
39597
|
-
return
|
|
40096
|
+
return join5(getCacheDir(), `${hash2}.db`);
|
|
39598
40097
|
}
|
|
39599
40098
|
function getAttachmentsDir() {
|
|
39600
40099
|
const override = process.env.OFW_ATTACHMENTS_DIR;
|
|
39601
40100
|
if (override && override.trim().length > 0) return override.trim();
|
|
39602
|
-
return
|
|
40101
|
+
return join5(homedir3(), "Downloads", "ofw-mcp");
|
|
39603
40102
|
}
|
|
39604
40103
|
function getWriteMode() {
|
|
39605
40104
|
const raw = process.env.OFW_WRITE_MODE;
|
|
@@ -39777,8 +40276,18 @@ function draftRevision(d) {
|
|
|
39777
40276
|
var ServerDraftSchema = external_exports.looseObject({
|
|
39778
40277
|
subject: external_exports.string().optional(),
|
|
39779
40278
|
body: external_exports.string().optional(),
|
|
40279
|
+
// BOTH spellings of the threading echo (see ThreadingEcho in _shared.ts):
|
|
40280
|
+
// OFW reports the reply target as `replyToId` on some payloads and as
|
|
40281
|
+
// `inReplyTo` on others. The snapshot derives one value from whichever is
|
|
40282
|
+
// present, so the revision hashed here matches the one ofw_save_draft
|
|
40283
|
+
// computed from the same server state — a one-sided read produced revisions
|
|
40284
|
+
// that disagreed about the same draft.
|
|
39780
40285
|
replyToId: external_exports.number().nullable().optional(),
|
|
40286
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
39781
40287
|
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
40288
|
+
// Attachment fileIds — read so send-by-draft carries the draft's
|
|
40289
|
+
// attachments onto the sent message (see DraftContent.files).
|
|
40290
|
+
files: external_exports.array(external_exports.number()).optional(),
|
|
39782
40291
|
// Read for the LIFECYCLE answer (see tools/lifecycle.ts): which folder OFW
|
|
39783
40292
|
// itself says this id lives in right now. `existsOnServer` alone cannot
|
|
39784
40293
|
// distinguish "still a draft" from "was sent" — a sent draft still exists.
|
|
@@ -39821,8 +40330,9 @@ async function fetchMessageSnapshot(client2, id) {
|
|
|
39821
40330
|
content: {
|
|
39822
40331
|
subject: detail.subject ?? "",
|
|
39823
40332
|
body: detail.body ?? "",
|
|
39824
|
-
replyToId: detail
|
|
39825
|
-
recipients: mapRecipients(detail.recipients)
|
|
40333
|
+
replyToId: threadedReplyTo(detail),
|
|
40334
|
+
recipients: mapRecipients(detail.recipients),
|
|
40335
|
+
...detail.files !== void 0 ? { files: detail.files } : {}
|
|
39826
40336
|
},
|
|
39827
40337
|
folderId: detail.folder?.id === void 0 ? null : String(detail.folder.id),
|
|
39828
40338
|
folderName: detail.folder?.name ?? null,
|
|
@@ -39952,13 +40462,24 @@ async function ensureFolderIdMap(client2, store) {
|
|
|
39952
40462
|
return { map: cached2, requests: 1 };
|
|
39953
40463
|
}
|
|
39954
40464
|
}
|
|
40465
|
+
var STATE_BY_FOLDER_NAME = /* @__PURE__ */ new Map([
|
|
40466
|
+
["drafts", "draft"],
|
|
40467
|
+
["sent", "sent"],
|
|
40468
|
+
["sent messages", "sent"],
|
|
40469
|
+
["inbox", "received"]
|
|
40470
|
+
]);
|
|
39955
40471
|
function classifyState(snapshot, map2) {
|
|
39956
40472
|
if (snapshot === null) return "deleted";
|
|
39957
|
-
const { folderId } = snapshot;
|
|
39958
|
-
if (folderId
|
|
39959
|
-
|
|
39960
|
-
|
|
39961
|
-
|
|
40473
|
+
const { folderId, folderName } = snapshot;
|
|
40474
|
+
if (folderId !== null) {
|
|
40475
|
+
if (map2.drafts !== null && folderId === map2.drafts) return "draft";
|
|
40476
|
+
if (map2.sent !== null && folderId === map2.sent) return "sent";
|
|
40477
|
+
if (map2.inbox !== null && folderId === map2.inbox) return "received";
|
|
40478
|
+
}
|
|
40479
|
+
if (folderName !== null) {
|
|
40480
|
+
const byName = STATE_BY_FOLDER_NAME.get(folderName.trim().toLowerCase());
|
|
40481
|
+
if (byName !== void 0) return byName;
|
|
40482
|
+
}
|
|
39962
40483
|
return "unknown";
|
|
39963
40484
|
}
|
|
39964
40485
|
function probeWouldStamp(cachedDraft, cachedMessage) {
|
|
@@ -41093,20 +41614,32 @@ async function buildInlineDelivery(input) {
|
|
|
41093
41614
|
}
|
|
41094
41615
|
|
|
41095
41616
|
// src/tools/messages.ts
|
|
41096
|
-
import { basename as basename2, join as
|
|
41617
|
+
import { basename as basename2, join as join6 } from "node:path";
|
|
41097
41618
|
var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
|
|
41098
41619
|
var SentDetailSchema = external_exports.looseObject({
|
|
41099
41620
|
subject: external_exports.string().optional(),
|
|
41100
41621
|
body: external_exports.string().optional(),
|
|
41101
41622
|
date: DateSchema.optional(),
|
|
41102
41623
|
from: external_exports.looseObject({ name: external_exports.string().optional() }).optional(),
|
|
41103
|
-
recipients: external_exports.array(ApiRecipientSchema).optional()
|
|
41624
|
+
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
41625
|
+
// The threading echo, in BOTH spellings plus showContext — OFW reports the
|
|
41626
|
+
// reply target inconsistently across payloads (see ThreadingEcho in
|
|
41627
|
+
// _shared.ts). Backs the `threaded` verdict on ofw_send_message.
|
|
41628
|
+
replyToId: external_exports.number().nullable().optional(),
|
|
41629
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
41630
|
+
showContext: external_exports.boolean().optional()
|
|
41104
41631
|
});
|
|
41105
41632
|
var SavedDraftDetailSchema = external_exports.looseObject({
|
|
41106
41633
|
subject: external_exports.string().optional(),
|
|
41107
41634
|
body: external_exports.string().optional(),
|
|
41108
41635
|
date: DateSchema.optional(),
|
|
41636
|
+
// All three threading-echo fields. Reading ONLY `replyToId` here fired a
|
|
41637
|
+
// false "OurFamilyWizard did not thread this draft" warning on nearly every
|
|
41638
|
+
// threaded save, while the same payload's `inReplyTo`/`showContext` showed
|
|
41639
|
+
// the draft WAS threaded — see threadedReplyTo in _shared.ts.
|
|
41109
41640
|
replyToId: external_exports.number().nullable().optional(),
|
|
41641
|
+
inReplyTo: external_exports.number().nullable().optional(),
|
|
41642
|
+
showContext: external_exports.boolean().optional(),
|
|
41110
41643
|
recipients: external_exports.array(ApiRecipientSchema).optional(),
|
|
41111
41644
|
// Read to audit whether requested myFileIDs actually attached (Defect 3).
|
|
41112
41645
|
files: external_exports.array(external_exports.number()).optional()
|
|
@@ -41305,6 +41838,11 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41305
41838
|
if (draftRow !== null) {
|
|
41306
41839
|
const { freshness: freshness2, serverConfirmed, cacheStatus } = await draftsFreshness(cache);
|
|
41307
41840
|
return jsonResponse({
|
|
41841
|
+
// Stable identity FIRST — the id below changes on every edit
|
|
41842
|
+
// (create-then-delete), so callers should key off draftKey. Null when
|
|
41843
|
+
// this draft was never written through this tool (e.g. authored in
|
|
41844
|
+
// the web app).
|
|
41845
|
+
draftKey: (await cache.getDraftLineageById(draftRow.id))?.draftKey ?? null,
|
|
41308
41846
|
id: draftRow.id,
|
|
41309
41847
|
folder: "drafts",
|
|
41310
41848
|
subject: draftRow.subject,
|
|
@@ -41321,13 +41859,9 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41321
41859
|
listData: draftRow.listData,
|
|
41322
41860
|
attachments: [],
|
|
41323
41861
|
// Concurrency token — pass as expectedRevision to ofw_save_draft /
|
|
41324
|
-
// ofw_delete_draft to assert you are
|
|
41862
|
+
// ofw_delete_draft / ofw_send_message to assert you are acting on
|
|
41863
|
+
// THIS version.
|
|
41325
41864
|
revision: draftRevision(draftRow),
|
|
41326
|
-
// Stable logical identity. Survives the create-then-delete id churn of
|
|
41327
|
-
// editing AND the transition to sent — pass it to ofw_status to ask
|
|
41328
|
-
// "what happened to the thing I was working on?". Null when this draft
|
|
41329
|
-
// was never written through this tool (e.g. authored in the web app).
|
|
41330
|
-
draftKey: (await cache.getDraftLineageById(draftRow.id))?.draftKey ?? null,
|
|
41331
41865
|
cacheStatus,
|
|
41332
41866
|
// False = this draft's existence and unsent status are remembered from
|
|
41333
41867
|
// a cache, not confirmed on OFW. Call ofw_check_freshness before
|
|
@@ -41410,16 +41944,19 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41410
41944
|
return jsonResponse({ ...withReadState(row), attachments, freshness });
|
|
41411
41945
|
});
|
|
41412
41946
|
if (allowSend) server.registerTool("ofw_send_message", {
|
|
41413
|
-
description: "Send a message via OurFamilyWizard
|
|
41947
|
+
description: "Send a message via OurFamilyWizard \u2014 the ONE irreversible operation here, so it carries the strongest guard. TO SEND AN EXISTING DRAFT (the safe default): pass draftId (or messageId \u2014 same thing). The tool re-reads the draft from OFW and sends the SERVER'S version, so what goes out is what is on OurFamilyWizard, not what this session remembers \u2014 subject/body act only as explicit overrides. It is guarded exactly like ofw_save_draft: pass expectedRevision to assert which version you are sending; if the draft changed on OFW since you read it \u2014 or no longer exists (it may already have been SENT) \u2014 the send is REFUSED with the current server content echoed back, and nothing goes out. RECIPIENTS: OurFamilyWizard does not persist recipients on drafts, so recipientIds is usually still required at send time (ids from ofw_get_profile). After the send is CONFIRMED (OFW returned the new message id and the re-fetched sent record matches what was posted), the source draft is deleted automatically; pass deleteDraftOnSuccess:false to keep it. On ANY failure or ambiguity the draft is never deleted \u2014 the response carries draftRetained:true with the reason. TO COMPOSE FROM SCRATCH: supply subject/body/recipientIds with no draftId. If replyToId is provided (or inherited from the draft), the cache may rewrite it to the latest reply in the same thread (a note is included when this happens). ATTACHMENTS: when sending by draftId, the server draft's own attachments carry over automatically; myFileIDs (from ofw_upload_attachment) overrides or attaches files on a fresh compose. The response leads with sentMessageId and the stable draftKey, and reports threaded (whether OFW actually linked the reply) and draftDeleted.",
|
|
41414
41948
|
annotations: { destructiveHint: true },
|
|
41415
41949
|
inputSchema: {
|
|
41416
|
-
subject: external_exports.string().describe("Message subject. Required unless messageId
|
|
41417
|
-
body: external_exports.string().describe("Message body text. Required unless messageId
|
|
41418
|
-
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile).
|
|
41419
|
-
replyToId: external_exports.number().describe("ID of the message being replied to").optional(),
|
|
41420
|
-
|
|
41421
|
-
|
|
41422
|
-
|
|
41950
|
+
subject: external_exports.string().describe("Message subject. Required unless draftId/messageId is given (then it overrides the server draft's subject).").optional(),
|
|
41951
|
+
body: external_exports.string().describe("Message body text. Required unless draftId/messageId is given (then it overrides the server draft's body \u2014 omit it to send exactly what is on OurFamilyWizard).").optional(),
|
|
41952
|
+
recipientIds: external_exports.array(external_exports.number()).describe("Array of recipient user IDs (get from ofw_get_profile). Usually required even when sending a draft: OurFamilyWizard does not persist recipients on drafts.").optional(),
|
|
41953
|
+
replyToId: external_exports.number().describe("ID of the message being replied to. Defaults to the draft's stored reply target when sending by draftId.").optional(),
|
|
41954
|
+
draftId: external_exports.number().describe("ID of an existing draft to send. The draft is re-read from OurFamilyWizard and its SERVER content is sent; missing subject/body default from it. Guarded: a draft that changed since you read it, or that was already sent/deleted, refuses rather than sending blind.").optional(),
|
|
41955
|
+
messageId: external_exports.number().describe("Synonym for draftId (if both are passed they must be equal).").optional(),
|
|
41956
|
+
expectedRevision: external_exports.string().describe('With draftId: the `revision` from ofw_list_drafts / ofw_get_message / ofw_check_freshness for that draft. Asserts you are sending THAT version; if the draft changed on OFW since, the send is refused and the current server content returned. Omit and the tool compares the server against the local cache instead \u2014 omitting never means "send whatever is there now".').optional(),
|
|
41957
|
+
deleteDraftOnSuccess: external_exports.boolean().describe("Default true. Delete the source draft after \u2014 and ONLY after \u2014 the send is confirmed (new message id returned and the re-fetched sent record checks out). Set false to keep the draft. On a failed or unverifiable send the draft is ALWAYS kept, regardless of this flag.").optional(),
|
|
41958
|
+
force: external_exports.boolean().describe("Default false. Send even when the draft changed on OurFamilyWizard since you read it, or its current state could not be read. Only use after showing the user the conflict.").optional(),
|
|
41959
|
+
myFileIDs: external_exports.array(external_exports.number()).describe("Attachment file ids (from ofw_upload_attachment) to attach to the message. When sending by draftId, omit it to carry the server draft's own attachments over; passing it overrides them.").optional()
|
|
41423
41960
|
}
|
|
41424
41961
|
}, async (args) => {
|
|
41425
41962
|
if (args.messageId !== void 0 && args.draftId !== void 0 && args.messageId !== args.draftId) {
|
|
@@ -41427,37 +41964,51 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41427
41964
|
}
|
|
41428
41965
|
const draftRef = args.messageId ?? args.draftId;
|
|
41429
41966
|
const cache = cacheProvider();
|
|
41967
|
+
const deleteOnSuccess = args.deleteDraftOnSuccess ?? true;
|
|
41430
41968
|
let subject = args.subject;
|
|
41431
41969
|
let body = args.body;
|
|
41432
41970
|
let recipientIds = args.recipientIds;
|
|
41433
41971
|
let draftReplyToId = null;
|
|
41434
|
-
let
|
|
41435
|
-
let
|
|
41972
|
+
let guardNote = null;
|
|
41973
|
+
let serverDraft;
|
|
41436
41974
|
if (draftRef !== void 0) {
|
|
41437
|
-
|
|
41438
|
-
const
|
|
41439
|
-
if (
|
|
41440
|
-
|
|
41441
|
-
|
|
41442
|
-
|
|
41443
|
-
|
|
41444
|
-
|
|
41975
|
+
const cachedDraft = await cache.getDraft(draftRef);
|
|
41976
|
+
const needsContent = subject === void 0 || body === void 0 || recipientIds === void 0;
|
|
41977
|
+
if (needsContent || deleteOnSuccess) {
|
|
41978
|
+
const guard = await guardDestructiveDraftOp({
|
|
41979
|
+
cache,
|
|
41980
|
+
draftId: draftRef,
|
|
41981
|
+
expectedRevision: args.expectedRevision,
|
|
41982
|
+
force: args.force ?? false,
|
|
41983
|
+
action: "send"
|
|
41984
|
+
});
|
|
41985
|
+
if (!guard.ok) return guard.response;
|
|
41986
|
+
guardNote = guard.note;
|
|
41987
|
+
serverDraft = guard.server;
|
|
41988
|
+
}
|
|
41989
|
+
const base = serverDraft ?? cachedDraft;
|
|
41990
|
+
if (base != null) {
|
|
41991
|
+
subject = subject ?? base.subject;
|
|
41992
|
+
body = body ?? base.body;
|
|
41993
|
+
draftReplyToId = base.replyToId;
|
|
41994
|
+
}
|
|
41995
|
+
if (recipientIds === void 0) {
|
|
41996
|
+
const source = [serverDraft ?? null, cachedDraft].find(
|
|
41997
|
+
(s) => s !== null && s !== void 0 && s.recipients.some((r) => r.userId !== 0)
|
|
41998
|
+
);
|
|
41999
|
+
if (source != null) {
|
|
42000
|
+
recipientIds = [...new Set(source.recipients.map((r) => r.userId).filter((id) => id !== 0))];
|
|
42001
|
+
}
|
|
41445
42002
|
}
|
|
41446
42003
|
}
|
|
41447
42004
|
if (subject === void 0 || body === void 0 || recipientIds === void 0) {
|
|
41448
|
-
if (draftLookupAttempted && !draftFound) {
|
|
41449
|
-
throw new Error(
|
|
41450
|
-
`draft ${draftRef} not found in local cache. Call ofw_sync_messages first, or supply subject/body/recipientIds explicitly.`
|
|
41451
|
-
);
|
|
41452
|
-
}
|
|
41453
42005
|
const missing = [
|
|
41454
42006
|
subject === void 0 ? "subject" : null,
|
|
41455
42007
|
body === void 0 ? "body" : null,
|
|
41456
42008
|
recipientIds === void 0 ? "recipientIds" : null
|
|
41457
42009
|
].filter((n) => n !== null).join(", ");
|
|
41458
|
-
|
|
41459
|
-
|
|
41460
|
-
);
|
|
42010
|
+
const hint = draftRef === void 0 ? "Pass them directly, or pass draftId to send an existing draft." : missing === "recipientIds" ? `Draft ${draftRef} carries no stored recipients \u2014 OurFamilyWizard does not persist recipients on drafts, so they must be supplied at send time. Get the co-parent's user id from ofw_get_profile and pass recipientIds.` : `Draft ${draftRef}'s content was not readable from OurFamilyWizard or the local cache, so it cannot supply the missing fields. Pass them explicitly.`;
|
|
42011
|
+
throw new Error(`ofw_send_message requires ${missing}. ${hint}`);
|
|
41461
42012
|
}
|
|
41462
42013
|
const requestedReplyTo = args.replyToId ?? draftReplyToId ?? null;
|
|
41463
42014
|
let resolvedReplyTo = requestedReplyTo;
|
|
@@ -41471,7 +42022,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41471
42022
|
const parent = await cache.getMessage(resolvedReplyTo);
|
|
41472
42023
|
chainRootId = parent?.chainRootId ?? parent?.id ?? requestedReplyTo;
|
|
41473
42024
|
}
|
|
41474
|
-
const myFileIDs = args.myFileIDs ?? [];
|
|
42025
|
+
const myFileIDs = args.myFileIDs ?? serverDraft?.files ?? [];
|
|
41475
42026
|
const { id: newId, detail, raw } = await postMessageAndRefetch(client2, {
|
|
41476
42027
|
subject,
|
|
41477
42028
|
body,
|
|
@@ -41484,19 +42035,51 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41484
42035
|
let persisted = null;
|
|
41485
42036
|
let verifyNote = null;
|
|
41486
42037
|
let sentDraftKey = null;
|
|
42038
|
+
let threaded = false;
|
|
42039
|
+
let threadNote = null;
|
|
41487
42040
|
if (newId !== null) {
|
|
41488
42041
|
verifyNote = verifyWriteLanded("message", { subject, body }, detail);
|
|
42042
|
+
const echoed = threadedReplyTo(detail);
|
|
42043
|
+
if (resolvedReplyTo === null) {
|
|
42044
|
+
threaded = reportsThreaded(detail);
|
|
42045
|
+
} else if (reportsThreaded(detail)) {
|
|
42046
|
+
threaded = true;
|
|
42047
|
+
if (echoed !== null && echoed !== resolvedReplyTo) {
|
|
42048
|
+
threadNote = `NOTE: the sent message threads to ${echoed}, not the requested ${resolvedReplyTo} \u2014 OurFamilyWizard re-targeted the reply within the thread.`;
|
|
42049
|
+
}
|
|
42050
|
+
} else if (reportsUnthreaded(detail)) {
|
|
42051
|
+
threaded = false;
|
|
42052
|
+
threadNote = `WARNING: the sent message came back UNTHREADED \u2014 replyToId ${resolvedReplyTo} was posted but OurFamilyWizard reports no reply linkage on the sent record, so it went out as a new top-level conversation. Verify on ourfamilywizard.com.`;
|
|
42053
|
+
} else {
|
|
42054
|
+
threaded = true;
|
|
42055
|
+
}
|
|
42056
|
+
const storedRecipients = mapRecipients(detail.recipients);
|
|
42057
|
+
if (Array.isArray(detail.recipients) && detail.recipients.length > 0) {
|
|
42058
|
+
const landed = new Set(storedRecipients.map((r) => r.userId));
|
|
42059
|
+
const missingRecipients = recipientIds.filter((rid) => !landed.has(rid));
|
|
42060
|
+
if (missingRecipients.length > 0) {
|
|
42061
|
+
verifyNote = [
|
|
42062
|
+
verifyNote,
|
|
42063
|
+
`WARNING: the sent record does not list requested recipient id(s) ${missingRecipients.join(", ")}, so the send could not be fully confirmed. Verify on ourfamilywizard.com.`
|
|
42064
|
+
].filter((n) => n !== null).join("\n\n");
|
|
42065
|
+
}
|
|
42066
|
+
}
|
|
41489
42067
|
persisted = {
|
|
41490
42068
|
id: newId,
|
|
41491
42069
|
folder: "sent",
|
|
41492
42070
|
subject: detail.subject ?? subject,
|
|
41493
42071
|
fromUser: detail.from?.name ?? "",
|
|
41494
42072
|
sentAt: detail.date?.dateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
41495
|
-
recipients:
|
|
42073
|
+
recipients: storedRecipients,
|
|
41496
42074
|
body: detail.body ?? body,
|
|
41497
42075
|
fetchedBodyAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41498
|
-
|
|
41499
|
-
|
|
42076
|
+
// Prefer OFW's own echo of where the reply landed; keep what was
|
|
42077
|
+
// posted when OFW echoed nothing (sent rows feed findLatestReplyTip,
|
|
42078
|
+
// and a null would break the chain for a message that IS threaded).
|
|
42079
|
+
// A positively UNTHREADED send stores null — the chain link OFW says
|
|
42080
|
+
// does not exist must not be invented.
|
|
42081
|
+
replyToId: threaded ? echoed ?? resolvedReplyTo : null,
|
|
42082
|
+
chainRootId: threaded ? chainRootId : null,
|
|
41500
42083
|
listData: detail
|
|
41501
42084
|
};
|
|
41502
42085
|
await cache.upsertMessage(persisted);
|
|
@@ -41524,16 +42107,43 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
|
|
|
41524
42107
|
}
|
|
41525
42108
|
}
|
|
41526
42109
|
let unconfirmedNote = null;
|
|
42110
|
+
let draftDeleted = false;
|
|
42111
|
+
let draftRetainedReason = null;
|
|
41527
42112
|
if (newId === null) {
|
|
41528
42113
|
const draftClause = draftRef !== void 0 ? `Draft ${draftRef} was NOT deleted \u2014 check` : "Check";
|
|
41529
42114
|
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.`;
|
|
42115
|
+
if (draftRef !== void 0) {
|
|
42116
|
+
draftRetainedReason = "the send could not be confirmed (OFW returned no message id), so the draft is your only reliable copy of the message";
|
|
42117
|
+
}
|
|
41530
42118
|
} else if (draftRef !== void 0) {
|
|
41531
|
-
|
|
41532
|
-
|
|
42119
|
+
if (verifyNote !== null) {
|
|
42120
|
+
draftRetainedReason = "the sent record could not be fully verified against what was posted (see WARNING above) \u2014 the draft is kept until you confirm the send on ourfamilywizard.com";
|
|
42121
|
+
} else if (!deleteOnSuccess) {
|
|
42122
|
+
draftRetainedReason = "deleteDraftOnSuccess:false \u2014 kept by request";
|
|
42123
|
+
} else {
|
|
42124
|
+
try {
|
|
42125
|
+
await deleteOFWMessages(client2, [draftRef]);
|
|
42126
|
+
await cache.deleteDraft(draftRef);
|
|
42127
|
+
draftDeleted = true;
|
|
42128
|
+
} catch (e) {
|
|
42129
|
+
draftRetainedReason = `the send succeeded but the draft delete failed (${e.message}) \u2014 remove it with ofw_delete_draft once you have verified the sent message`;
|
|
42130
|
+
}
|
|
42131
|
+
}
|
|
41533
42132
|
}
|
|
41534
|
-
const
|
|
42133
|
+
const retainNote = draftRef !== void 0 && newId !== null && !draftDeleted ? `NOTE: draft ${draftRef} was retained: ${draftRetainedReason}.` : null;
|
|
42134
|
+
const responseObj = persisted === null ? draftRef !== void 0 ? { sendConfirmed: false, draftDeleted: false, draftRetained: true, draftRetainedReason, raw } : raw : {
|
|
42135
|
+
sentMessageId: newId,
|
|
42136
|
+
draftKey: sentDraftKey,
|
|
42137
|
+
threaded,
|
|
42138
|
+
...draftRef !== void 0 ? {
|
|
42139
|
+
draftDeleted,
|
|
42140
|
+
...draftDeleted ? {} : { draftRetained: true, draftRetainedReason },
|
|
42141
|
+
previousId: draftRef
|
|
42142
|
+
} : {},
|
|
42143
|
+
...persisted
|
|
42144
|
+
};
|
|
41535
42145
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Message sent successfully.";
|
|
41536
|
-
const notes = [rewriteNote, verifyNote, unconfirmedNote].filter((n) => n !== null).join("\n\n");
|
|
42146
|
+
const notes = [guardNote, rewriteNote, verifyNote, threadNote, unconfirmedNote, retainNote].filter((n) => n !== null).join("\n\n");
|
|
41537
42147
|
return textResponse(notes ? `${notes}
|
|
41538
42148
|
|
|
41539
42149
|
${text}` : text);
|
|
@@ -41553,7 +42163,7 @@ ${text}` : text);
|
|
|
41553
42163
|
} catch (e) {
|
|
41554
42164
|
const reason = e.message;
|
|
41555
42165
|
if (force) {
|
|
41556
|
-
return { ok: true, note: `WARNING: force:true \u2014 proceeded with ${action} on draft ${draftId} even though its current state could not be read from OurFamilyWizard (${reason}). Any newer server-side version was destroyed and is NOT recoverable from this response
|
|
42166
|
+
return { ok: true, note: `WARNING: force:true \u2014 proceeded with ${action} on draft ${draftId} even though its current state could not be read from OurFamilyWizard (${reason}). Any newer server-side version was destroyed and is NOT recoverable from this response.`, server: void 0 };
|
|
41557
42167
|
}
|
|
41558
42168
|
return {
|
|
41559
42169
|
ok: false,
|
|
@@ -41568,7 +42178,7 @@ ${text}` : text);
|
|
|
41568
42178
|
const verdict = checkDraftFreshness({ server: server2, cached: cached2, expectedRevision });
|
|
41569
42179
|
if (verdict.verdict === "FRESH") {
|
|
41570
42180
|
const note = verdict.metadataOnly ? `NOTE: draft ${draftId} was treated as current for this ${action}. Since you read it, OurFamilyWizard normalized connector-authored metadata (${verdict.changedFields.join(", ")}); the subject, body and recipients are unchanged, so this is not a conflict.` : null;
|
|
41571
|
-
return { ok: true, note };
|
|
42181
|
+
return { ok: true, note, server: server2 };
|
|
41572
42182
|
}
|
|
41573
42183
|
if (force) {
|
|
41574
42184
|
console.error(`[ofw-mcp] WARNING: force:true overrode a ${verdict.verdict} verdict on draft ${draftId} (${action}). ${verdict.reason}`);
|
|
@@ -41581,7 +42191,8 @@ ${JSON.stringify(
|
|
|
41581
42191
|
{ overwrittenServerDraft: server2 === null ? null : { ...server2, revision: draftRevision(server2) } },
|
|
41582
42192
|
null,
|
|
41583
42193
|
2
|
|
41584
|
-
)}
|
|
42194
|
+
)}`,
|
|
42195
|
+
server: server2
|
|
41585
42196
|
};
|
|
41586
42197
|
}
|
|
41587
42198
|
return {
|
|
@@ -41596,17 +42207,31 @@ ${JSON.stringify(
|
|
|
41596
42207
|
};
|
|
41597
42208
|
}
|
|
41598
42209
|
server.registerTool("ofw_list_drafts", {
|
|
41599
|
-
description: 'List draft messages
|
|
42210
|
+
description: 'List draft messages, verified against OurFamilyWizard in ONE call: when the local drafts cache is not verified-fresh, a cheap drafts sync runs first by default (verify:true), so the answer is server-confirmed without a second call. Pass verify:false to answer purely from the cache (no OFW requests). Returns an explicit `complete` boolean describing the RESULT SET: true means "these are ALL the drafts on OurFamilyWizard as of freshness.asOf" \u2014 check it before saying "you have N drafts". Each draft carries its `draftKey` (stable across the create-then-delete churn of editing) when one is known. An empty result from a cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY"); pass autoRefresh:true to sync and answer instead.',
|
|
41600
42211
|
annotations: { readOnlyHint: false },
|
|
41601
42212
|
inputSchema: {
|
|
41602
42213
|
page: external_exports.number().int().min(1).describe("Page number (default 1)").optional(),
|
|
41603
42214
|
size: external_exports.number().int().min(1).describe("Drafts per page (default 50)").optional(),
|
|
42215
|
+
verify: external_exports.boolean().describe("Default true: when the drafts cache is not verified-fresh, run a drafts sync first (cheap \u2014 one list page plus one detail per draft) so the response is server-confirmed in one call. Set false to serve straight from the local cache with no OFW requests.").optional(),
|
|
41604
42216
|
autoRefresh: external_exports.boolean().describe(AUTO_REFRESH_DESC).optional()
|
|
41605
42217
|
}
|
|
41606
42218
|
}, async (args) => {
|
|
41607
42219
|
const page = args.page ?? 1;
|
|
41608
42220
|
const size = args.size ?? 50;
|
|
41609
42221
|
const cache = cacheProvider();
|
|
42222
|
+
let autoVerified = false;
|
|
42223
|
+
let verifyNote = null;
|
|
42224
|
+
if (args.verify ?? true) {
|
|
42225
|
+
const { cacheStatus } = await draftsFreshness(cache);
|
|
42226
|
+
if (cacheStatus !== "fresh") {
|
|
42227
|
+
try {
|
|
42228
|
+
await syncAll(client2, { folders: ["drafts"], maxRequests: getSyncMaxRequests() }, cache);
|
|
42229
|
+
autoVerified = await getDraftsCacheStatus(cache) === "fresh";
|
|
42230
|
+
} catch (e) {
|
|
42231
|
+
verifyNote = `The automatic drafts verification could not reach OurFamilyWizard (${e.message}). Answering from the local cache \u2014 the freshness block below labels its age, and an empty result will still be refused rather than reported as an absence.`;
|
|
42232
|
+
}
|
|
42233
|
+
}
|
|
42234
|
+
}
|
|
41610
42235
|
const { value, refreshed, unverifiedEmpty } = await guardedCacheRead({
|
|
41611
42236
|
client: client2,
|
|
41612
42237
|
cache,
|
|
@@ -41637,7 +42262,7 @@ ${JSON.stringify(
|
|
|
41637
42262
|
freshness: value.freshness,
|
|
41638
42263
|
refreshed,
|
|
41639
42264
|
remedy: 'Call ofw_sync_messages(folders:["drafts"]) and retry, re-call with autoRefresh:true, or use ofw_status(includeDraftInventory:true) for a single live answer.',
|
|
41640
|
-
extra: { page, size }
|
|
42265
|
+
extra: { page, size, ...verifyNote !== null ? { verifyNote } : {} }
|
|
41641
42266
|
});
|
|
41642
42267
|
}
|
|
41643
42268
|
const { drafts, total, freshness, serverConfirmed } = value;
|
|
@@ -41656,10 +42281,16 @@ ${JSON.stringify(
|
|
|
41656
42281
|
if (refreshed) {
|
|
41657
42282
|
payload.autoRefreshed = true;
|
|
41658
42283
|
}
|
|
42284
|
+
if (autoVerified) {
|
|
42285
|
+
payload.autoVerified = true;
|
|
42286
|
+
}
|
|
42287
|
+
if (verifyNote !== null) {
|
|
42288
|
+
payload.verifyNote = verifyNote;
|
|
42289
|
+
}
|
|
41659
42290
|
return jsonResponse(payload);
|
|
41660
42291
|
});
|
|
41661
42292
|
if (allowDrafts) server.registerTool("ofw_save_draft", {
|
|
41662
|
-
description: "Save a message as a draft in OurFamilyWizard.
|
|
42293
|
+
description: "Save a message as a draft in OurFamilyWizard. RECIPIENTS: OurFamilyWizard does NOT persist recipients on drafts \u2014 recipientIds are accepted but the saved draft comes back with none (documented OFW behavior, noted once in the response, not warned about; supply recipientIds at send time instead). IDENTITY: the response leads with `draftKey`, the stable identity that survives editing \u2014 key off it, because the `id` changes on EVERY edit (replacing a draft creates a NEW draft and deletes the old one; OFW's update-in-place endpoint silently no-ops, so we never use it). Pass messageId to replace an existing draft; the response.id will be the NEW id, and a transparency NOTE documents the swap and which fields were carried over. THREADING: if replyToId is provided, the cache may rewrite it to the latest reply in the thread (note included). The threading verdict is read from OFW's full echo (replyToId/inReplyTo/showContext) \u2014 a warning appears ONLY when the reply linkage was genuinely dropped or re-targeted, and the response's top-level replyToId/inReplyTo always agree with its listData. Attach files via myFileIDs (from ofw_upload_attachment). After saving, the tool re-fetches the draft from OFW, and the returned `revision` reflects that authoritative state (so it will match on your next edit). SAFETY: because replacing DESTROYS the old draft rather than merging, passing messageId first re-reads that draft from OFW and REFUSES the write if its subject/body/recipients changed since you read it (drafts edited in the OFW web app do not bump any timestamp, so the local cache can be silently behind). A pure replyToId normalization by OFW is NOT treated as a conflict. The refusal returns the current server body under serverBody \u2014 merge your edit into it and retry with expectedRevision.",
|
|
41663
42294
|
annotations: { readOnlyHint: false },
|
|
41664
42295
|
inputSchema: {
|
|
41665
42296
|
subject: external_exports.string().describe("Message subject"),
|
|
@@ -41713,12 +42344,13 @@ ${JSON.stringify(
|
|
|
41713
42344
|
let persisted = null;
|
|
41714
42345
|
let replaceNote = null;
|
|
41715
42346
|
let verifyNote = null;
|
|
42347
|
+
let recipientsNote = null;
|
|
41716
42348
|
let newRevision = null;
|
|
41717
42349
|
let draftKey = null;
|
|
41718
42350
|
const warnings = [];
|
|
41719
42351
|
if (newId !== null) {
|
|
41720
42352
|
verifyNote = verifyWriteLanded("draft", { subject: args.subject, body: args.body }, detail);
|
|
41721
|
-
const effectiveReplyTo = detail
|
|
42353
|
+
const effectiveReplyTo = threadedReplyTo(detail);
|
|
41722
42354
|
const storedRecipients = mapRecipients(detail.recipients);
|
|
41723
42355
|
persisted = {
|
|
41724
42356
|
id: newId,
|
|
@@ -41754,17 +42386,23 @@ ${JSON.stringify(
|
|
|
41754
42386
|
previousId: args.messageId ?? null,
|
|
41755
42387
|
recordedAt: now
|
|
41756
42388
|
});
|
|
41757
|
-
if (resolvedReplyTo !== null && effectiveReplyTo !== resolvedReplyTo) {
|
|
42389
|
+
if (resolvedReplyTo !== null && effectiveReplyTo !== resolvedReplyTo && reportsUnthreaded(detail)) {
|
|
42390
|
+
const rewrittenFrom = requestedReplyTo !== resolvedReplyTo ? ` (rewritten from ${requestedReplyTo})` : "";
|
|
42391
|
+
warnings.push(
|
|
42392
|
+
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId null \u2014 OurFamilyWizard did not thread this draft (its inReplyTo/showContext are empty). The subject and body were saved; only the reply linkage was dropped. If threading matters, verify on ourfamilywizard.com.`
|
|
42393
|
+
);
|
|
42394
|
+
} else if (resolvedReplyTo !== null && effectiveReplyTo !== null && effectiveReplyTo !== resolvedReplyTo) {
|
|
41758
42395
|
const rewrittenFrom = requestedReplyTo !== resolvedReplyTo ? ` (rewritten from ${requestedReplyTo})` : "";
|
|
41759
|
-
const outcome = effectiveReplyTo === null ? "OurFamilyWizard did not thread this draft (its inReplyTo/showContext will be empty). The subject and body were saved; only the reply linkage was dropped." : `OurFamilyWizard re-targeted the reply to message ${effectiveReplyTo} instead. The draft IS threaded \u2014 to that message, not the one requested \u2014 and the inReplyTo in this response reflects where it actually landed.`;
|
|
41760
42396
|
warnings.push(
|
|
41761
|
-
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId ${effectiveReplyTo
|
|
42397
|
+
`replyToId was requested as ${resolvedReplyTo}${rewrittenFrom} but the saved draft came back with replyToId ${effectiveReplyTo} \u2014 OurFamilyWizard re-targeted the reply to message ${effectiveReplyTo} instead. The draft IS threaded \u2014 to that message, not the one requested. If threading matters, verify on ourfamilywizard.com.`
|
|
41762
42398
|
);
|
|
41763
42399
|
}
|
|
41764
|
-
if (args.recipientIds !== void 0 && Array.isArray(detail.recipients)) {
|
|
42400
|
+
if (args.recipientIds !== void 0 && args.recipientIds.length > 0 && Array.isArray(detail.recipients)) {
|
|
41765
42401
|
const requested = [...new Set(args.recipientIds)].sort((a, b) => a - b);
|
|
41766
42402
|
const stored = [...new Set(storedRecipients.map((r) => r.userId))].sort((a, b) => a - b);
|
|
41767
|
-
if (
|
|
42403
|
+
if (stored.length === 0) {
|
|
42404
|
+
recipientsNote = "NOTE: OurFamilyWizard does not persist recipients on drafts \u2014 the recipientIds you passed were accepted but are not stored on the draft (documented OFW behavior, not an error; it also means a draft cannot be sent by accident). Supply recipientIds when you send: ofw_send_message requires them when the draft carries none.";
|
|
42405
|
+
} else if (requested.join(",") !== stored.join(",")) {
|
|
41768
42406
|
warnings.push(
|
|
41769
42407
|
`recipientIds were requested as [${requested.join(", ")}] but the saved draft has [${stored.join(", ")}]. Verify the recipients on ourfamilywizard.com.`
|
|
41770
42408
|
);
|
|
@@ -41783,28 +42421,26 @@ ${JSON.stringify(
|
|
|
41783
42421
|
try {
|
|
41784
42422
|
await deleteOFWMessages(client2, [args.messageId]);
|
|
41785
42423
|
await cache.deleteDraft(args.messageId);
|
|
41786
|
-
replaceNote = `NOTE: ofw_save_draft replaced draft ${args.messageId} via create-then-delete. The new draft id is ${newId}; the old draft has been deleted. (OFW's update-in-place endpoint silently no-ops on subsequent updates, so we never use it.
|
|
42424
|
+
replaceNote = `NOTE: ofw_save_draft replaced draft ${args.messageId} via create-then-delete. The new draft id is ${newId}; the old draft has been deleted. The draftKey is UNCHANGED \u2014 key off it rather than the volatile id, which changes on every edit. (OFW's update-in-place endpoint silently no-ops on subsequent updates, so we never use it.) Fields carried over to the new draft: subject, body, recipients (${persisted.recipients.length}), replyToId (${persisted.replyToId === null ? "none" : persisted.replyToId}), attachments (${myFileIDs.length}).${warnings.length > 0 ? " See warnings above for any field OurFamilyWizard did not carry over." : ""}`;
|
|
41787
42425
|
} catch (e) {
|
|
41788
42426
|
replaceNote = `WARNING: New draft ${newId} was created successfully, but the old draft ${args.messageId} could NOT be deleted: ${e.message}. BOTH drafts now exist on OurFamilyWizard and nothing was lost. Verify ${newId} reads correctly, then remove ${args.messageId} with ofw_delete_draft.`;
|
|
41789
42427
|
}
|
|
41790
42428
|
}
|
|
41791
42429
|
}
|
|
41792
42430
|
const responseObj = persisted !== null ? {
|
|
42431
|
+
draftKey,
|
|
42432
|
+
revision: newRevision,
|
|
41793
42433
|
...persisted,
|
|
41794
42434
|
inReplyTo: persisted.replyToId,
|
|
41795
|
-
revision: newRevision,
|
|
41796
|
-
// The id above is volatile — it changes on every edit. `draftKey` is
|
|
41797
|
-
// not: pass it to ofw_status to resolve the chain's CURRENT id, or to
|
|
41798
|
-
// find out that the draft was sent and when.
|
|
41799
|
-
draftKey,
|
|
41800
42435
|
previousId: args.messageId ?? null,
|
|
41801
42436
|
cacheStatus: "fresh",
|
|
41802
42437
|
serverConfirmed: true,
|
|
41803
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
42438
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
42439
|
+
...recipientsNote !== null ? { recipientsNote } : {}
|
|
41804
42440
|
} : raw;
|
|
41805
42441
|
const text = responseObj ? JSON.stringify(responseObj, null, 2) : "Draft saved.";
|
|
41806
42442
|
const warnNote = warnings.length > 0 ? `WARNING: ${warnings.join("\n\n")}` : null;
|
|
41807
|
-
const notes = [forceNote, rewriteNote, verifyNote, warnNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
42443
|
+
const notes = [forceNote, rewriteNote, verifyNote, warnNote, recipientsNote, replaceNote].filter((n) => n !== null).join("\n\n");
|
|
41808
42444
|
return textResponse(notes ? `${notes}
|
|
41809
42445
|
|
|
41810
42446
|
${text}` : text);
|
|
@@ -41988,9 +42624,9 @@ ${text}` : text);
|
|
|
41988
42624
|
if (args.saveTo) {
|
|
41989
42625
|
const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
|
|
41990
42626
|
const abs = expandPath2(args.saveTo);
|
|
41991
|
-
dest = isDirArg ?
|
|
42627
|
+
dest = isDirArg ? join6(abs, `${fileId}-${safeName}`) : abs;
|
|
41992
42628
|
} else {
|
|
41993
|
-
dest =
|
|
42629
|
+
dest = join6(getAttachmentsDir(), `${fileId}-${safeName}`);
|
|
41994
42630
|
}
|
|
41995
42631
|
const extractOnDisk = args.extract === true;
|
|
41996
42632
|
if (!args.force && cached2.downloadedPath === dest) {
|
|
@@ -43094,7 +43730,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
|
|
|
43094
43730
|
var nodeAttachmentIO = new NodeAttachmentIO();
|
|
43095
43731
|
await runMcp({
|
|
43096
43732
|
name: "ofw",
|
|
43097
|
-
version: "2.
|
|
43733
|
+
version: "2.10.1",
|
|
43098
43734
|
// x-release-please-version
|
|
43099
43735
|
deps: client,
|
|
43100
43736
|
tools: [
|