@chrischall/tripadvisor-mcp 0.3.1 → 0.3.3
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 +737 -101
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for the TripAdvisor Terra API — location search, details, photos, and reviews",
|
|
10
|
-
"version": "0.3.
|
|
10
|
+
"version": "0.3.3"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "TripAdvisor",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "TripAdvisor travel data via the Terra API — search hotels, restaurants, and attractions, with details, photos, and reviews",
|
|
18
|
-
"version": "0.3.
|
|
18
|
+
"version": "0.3.3",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -26725,17 +26725,33 @@ function normalizeObjectSchema(schema) {
|
|
|
26725
26725
|
}
|
|
26726
26726
|
return void 0;
|
|
26727
26727
|
}
|
|
26728
|
+
function getDotPath(path) {
|
|
26729
|
+
if (path.length === 0) {
|
|
26730
|
+
return "object root";
|
|
26731
|
+
}
|
|
26732
|
+
return path.reduce((acc, seg, index) => {
|
|
26733
|
+
if (index === 0) {
|
|
26734
|
+
return String(seg);
|
|
26735
|
+
}
|
|
26736
|
+
if (typeof seg === "number") {
|
|
26737
|
+
return `${acc}[${seg}]`;
|
|
26738
|
+
}
|
|
26739
|
+
return `${acc}.${seg}`;
|
|
26740
|
+
}, "");
|
|
26741
|
+
}
|
|
26728
26742
|
function getParseErrorMessage(error51) {
|
|
26729
26743
|
if (error51 && typeof error51 === "object") {
|
|
26744
|
+
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26745
|
+
return error51.issues.map((i) => {
|
|
26746
|
+
if (!i.path?.length) {
|
|
26747
|
+
return i.message;
|
|
26748
|
+
}
|
|
26749
|
+
return `${i.message} at ${getDotPath(i.path)}`;
|
|
26750
|
+
}).join("\n");
|
|
26751
|
+
}
|
|
26730
26752
|
if ("message" in error51 && typeof error51.message === "string") {
|
|
26731
26753
|
return error51.message;
|
|
26732
26754
|
}
|
|
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
26755
|
try {
|
|
26740
26756
|
return JSON.stringify(error51);
|
|
26741
26757
|
} catch {
|
|
@@ -33350,16 +33366,7 @@ var Server = class extends Protocol {
|
|
|
33350
33366
|
if (!methodSchema) {
|
|
33351
33367
|
throw new Error("Schema is missing a method literal");
|
|
33352
33368
|
}
|
|
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
|
-
}
|
|
33369
|
+
const methodValue = getLiteralValue(methodSchema);
|
|
33363
33370
|
if (typeof methodValue !== "string") {
|
|
33364
33371
|
throw new Error("Schema method literal must be a string");
|
|
33365
33372
|
}
|
|
@@ -34547,8 +34554,17 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
34547
34554
|
import process3 from "node:process";
|
|
34548
34555
|
|
|
34549
34556
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
34557
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
34550
34558
|
var ReadBuffer = class {
|
|
34559
|
+
constructor(options) {
|
|
34560
|
+
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
34561
|
+
}
|
|
34551
34562
|
append(chunk2) {
|
|
34563
|
+
const newSize = (this._buffer?.length ?? 0) + chunk2.length;
|
|
34564
|
+
if (newSize > this._maxBufferSize) {
|
|
34565
|
+
this.clear();
|
|
34566
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
34567
|
+
}
|
|
34552
34568
|
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
34553
34569
|
}
|
|
34554
34570
|
readMessage() {
|
|
@@ -34576,18 +34592,24 @@ function serializeMessage(message) {
|
|
|
34576
34592
|
|
|
34577
34593
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
34578
34594
|
var StdioServerTransport = class {
|
|
34579
|
-
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
34595
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
34580
34596
|
this._stdin = _stdin;
|
|
34581
34597
|
this._stdout = _stdout;
|
|
34582
|
-
this._readBuffer = new ReadBuffer();
|
|
34583
34598
|
this._started = false;
|
|
34584
34599
|
this._ondata = (chunk2) => {
|
|
34585
|
-
|
|
34586
|
-
|
|
34600
|
+
try {
|
|
34601
|
+
this._readBuffer.append(chunk2);
|
|
34602
|
+
this.processReadBuffer();
|
|
34603
|
+
} catch (error51) {
|
|
34604
|
+
this.onerror?.(error51);
|
|
34605
|
+
this.close().catch(() => {
|
|
34606
|
+
});
|
|
34607
|
+
}
|
|
34587
34608
|
};
|
|
34588
34609
|
this._onerror = (error51) => {
|
|
34589
34610
|
this.onerror?.(error51);
|
|
34590
34611
|
};
|
|
34612
|
+
this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
|
|
34591
34613
|
}
|
|
34592
34614
|
/**
|
|
34593
34615
|
* Starts listening for messages on stdin.
|
|
@@ -34936,7 +34958,7 @@ var pageSchema = {
|
|
|
34936
34958
|
};
|
|
34937
34959
|
|
|
34938
34960
|
// src/version.ts
|
|
34939
|
-
var VERSION = "0.3.
|
|
34961
|
+
var VERSION = "0.3.3";
|
|
34940
34962
|
|
|
34941
34963
|
// src/client.ts
|
|
34942
34964
|
import { dirname, join } from "node:path";
|
|
@@ -35260,8 +35282,15 @@ function registerLocationTools(server) {
|
|
|
35260
35282
|
}
|
|
35261
35283
|
|
|
35262
35284
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
35263
|
-
var PROTOCOL_VERSION =
|
|
35285
|
+
var PROTOCOL_VERSION = 3;
|
|
35264
35286
|
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
35287
|
+
function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
|
|
35288
|
+
const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
|
|
35289
|
+
out.set(mcpHelloNonce, 0);
|
|
35290
|
+
out.set(extHelloNonce, mcpHelloNonce.length);
|
|
35291
|
+
out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
|
|
35292
|
+
return out;
|
|
35293
|
+
}
|
|
35265
35294
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
35266
35295
|
"fetch",
|
|
35267
35296
|
"read_cookies",
|
|
@@ -35271,7 +35300,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
35271
35300
|
"capture_redirect",
|
|
35272
35301
|
"read_indexed_db",
|
|
35273
35302
|
"read_dom",
|
|
35274
|
-
"download"
|
|
35303
|
+
"download",
|
|
35304
|
+
"graphql",
|
|
35305
|
+
"write_cookies"
|
|
35275
35306
|
]);
|
|
35276
35307
|
|
|
35277
35308
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35382,6 +35413,15 @@ function assertHttpUrl(x, label) {
|
|
|
35382
35413
|
throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
|
|
35383
35414
|
}
|
|
35384
35415
|
}
|
|
35416
|
+
function assertCookiePath(x, label) {
|
|
35417
|
+
assertString(x, label);
|
|
35418
|
+
if (!x.startsWith("/") || x.startsWith("//")) {
|
|
35419
|
+
throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
|
|
35420
|
+
}
|
|
35421
|
+
if (x.includes("?") || x.includes("#") || x.includes("\\")) {
|
|
35422
|
+
throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
|
|
35423
|
+
}
|
|
35424
|
+
}
|
|
35385
35425
|
function assertHttpsOriginOnly(x, label) {
|
|
35386
35426
|
assertString(x, label);
|
|
35387
35427
|
let u;
|
|
@@ -35574,6 +35614,7 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35574
35614
|
}
|
|
35575
35615
|
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35576
35616
|
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35617
|
+
var GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
|
|
35577
35618
|
function assertDomSelectorsArray(value, label) {
|
|
35578
35619
|
if (!Array.isArray(value)) {
|
|
35579
35620
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -35610,6 +35651,37 @@ function assertDomSelectorsArray(value, label) {
|
|
|
35610
35651
|
}
|
|
35611
35652
|
}
|
|
35612
35653
|
}
|
|
35654
|
+
function assertGraphqlOpsArray(value, label) {
|
|
35655
|
+
if (!Array.isArray(value)) {
|
|
35656
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35657
|
+
}
|
|
35658
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35659
|
+
for (let i = 0; i < value.length; i++) {
|
|
35660
|
+
const entry = value[i];
|
|
35661
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35662
|
+
if (entry.name === void 0) {
|
|
35663
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35664
|
+
}
|
|
35665
|
+
if (entry.operationName === void 0) {
|
|
35666
|
+
throw new ProtocolError(`${label}[${i}].operationName: missing`);
|
|
35667
|
+
}
|
|
35668
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35669
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35670
|
+
}
|
|
35671
|
+
if (typeof entry.operationName !== "string" || !GRAPHQL_OP_NAME_RE.test(entry.operationName)) {
|
|
35672
|
+
throw new ProtocolError(`${label}[${i}].operationName: invalid ${JSON.stringify(entry.operationName)}`);
|
|
35673
|
+
}
|
|
35674
|
+
if (seen.has(entry.name)) {
|
|
35675
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35676
|
+
}
|
|
35677
|
+
seen.add(entry.name);
|
|
35678
|
+
for (const k of Object.keys(entry)) {
|
|
35679
|
+
if (k !== "name" && k !== "operationName") {
|
|
35680
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35681
|
+
}
|
|
35682
|
+
}
|
|
35683
|
+
}
|
|
35684
|
+
}
|
|
35613
35685
|
function validateFrame(raw) {
|
|
35614
35686
|
assertObject(raw, "frame");
|
|
35615
35687
|
const t = raw.type;
|
|
@@ -35688,6 +35760,9 @@ function validateHello(raw) {
|
|
|
35688
35760
|
if (raw.domSelectors !== void 0) {
|
|
35689
35761
|
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35690
35762
|
}
|
|
35763
|
+
if (raw.graphqlOps !== void 0) {
|
|
35764
|
+
assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
|
|
35765
|
+
}
|
|
35691
35766
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35692
35767
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35693
35768
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35791,13 +35866,43 @@ function validateInnerRequest(raw) {
|
|
|
35791
35866
|
}
|
|
35792
35867
|
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35793
35868
|
assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
|
|
35869
|
+
if (raw.init.path !== void 0)
|
|
35870
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35794
35871
|
for (const k of Object.keys(raw.init)) {
|
|
35795
|
-
if (k !== "origin" && k !== "keys") {
|
|
35872
|
+
if (k !== "origin" && k !== "keys" && k !== "path") {
|
|
35796
35873
|
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
|
|
35797
35874
|
}
|
|
35798
35875
|
}
|
|
35799
35876
|
return raw;
|
|
35800
35877
|
}
|
|
35878
|
+
if (raw.op === "write_cookies") {
|
|
35879
|
+
assertObject(raw.init, "inner.init");
|
|
35880
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35881
|
+
if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
|
|
35882
|
+
throw new ProtocolError("inner.init.cookies: must be a non-empty array");
|
|
35883
|
+
}
|
|
35884
|
+
for (const [i, entry] of raw.init.cookies.entries()) {
|
|
35885
|
+
assertObject(entry, `inner.init.cookies[${i}]`);
|
|
35886
|
+
assertString(entry.name, `inner.init.cookies[${i}].name`);
|
|
35887
|
+
if (!SCOPE_KEY_RE.test(entry.name)) {
|
|
35888
|
+
throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
|
|
35889
|
+
}
|
|
35890
|
+
assertString(entry.value, `inner.init.cookies[${i}].value`);
|
|
35891
|
+
for (const k of Object.keys(entry)) {
|
|
35892
|
+
if (k !== "name" && k !== "value") {
|
|
35893
|
+
throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35894
|
+
}
|
|
35895
|
+
}
|
|
35896
|
+
}
|
|
35897
|
+
if (raw.init.path !== void 0)
|
|
35898
|
+
assertCookiePath(raw.init.path, "inner.init.path");
|
|
35899
|
+
for (const k of Object.keys(raw.init)) {
|
|
35900
|
+
if (k !== "origin" && k !== "cookies" && k !== "path") {
|
|
35901
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
|
|
35902
|
+
}
|
|
35903
|
+
}
|
|
35904
|
+
return raw;
|
|
35905
|
+
}
|
|
35801
35906
|
if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
|
|
35802
35907
|
assertObject(raw.init, "inner.init");
|
|
35803
35908
|
if (raw.init.origin === void 0) {
|
|
@@ -35937,6 +36042,28 @@ function validateInnerRequest(raw) {
|
|
|
35937
36042
|
}
|
|
35938
36043
|
return raw;
|
|
35939
36044
|
}
|
|
36045
|
+
if (raw.op === "graphql_query") {
|
|
36046
|
+
assertObject(raw.init, "inner.init");
|
|
36047
|
+
if (raw.init.name === void 0)
|
|
36048
|
+
throw new ProtocolError("inner.init.name: missing");
|
|
36049
|
+
if (raw.init.variables === void 0) {
|
|
36050
|
+
throw new ProtocolError("inner.init.variables: missing");
|
|
36051
|
+
}
|
|
36052
|
+
assertString(raw.init.name, "inner.init.name");
|
|
36053
|
+
if (raw.init.name.length === 0) {
|
|
36054
|
+
throw new ProtocolError("inner.init.name: must be non-empty");
|
|
36055
|
+
}
|
|
36056
|
+
assertObject(raw.init.variables, "inner.init.variables");
|
|
36057
|
+
if (raw.init.tabUrl !== void 0) {
|
|
36058
|
+
assertString(raw.init.tabUrl, "inner.init.tabUrl");
|
|
36059
|
+
}
|
|
36060
|
+
for (const k of Object.keys(raw.init)) {
|
|
36061
|
+
if (k !== "name" && k !== "variables" && k !== "tabUrl") {
|
|
36062
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on graphql_query`);
|
|
36063
|
+
}
|
|
36064
|
+
}
|
|
36065
|
+
return raw;
|
|
36066
|
+
}
|
|
35940
36067
|
if (raw.op === "download") {
|
|
35941
36068
|
assertObject(raw.init, "inner.init");
|
|
35942
36069
|
if (raw.init.url === void 0) {
|
|
@@ -35962,7 +36089,7 @@ function validateInnerRequest(raw) {
|
|
|
35962
36089
|
}
|
|
35963
36090
|
return raw;
|
|
35964
36091
|
}
|
|
35965
|
-
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"; got ${JSON.stringify(raw.op)}`);
|
|
36092
|
+
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)}`);
|
|
35966
36093
|
}
|
|
35967
36094
|
function assertNonEmptyKeyArray(value, label) {
|
|
35968
36095
|
if (!Array.isArray(value)) {
|
|
@@ -35993,6 +36120,10 @@ function assertStringMap(value, label) {
|
|
|
35993
36120
|
}
|
|
35994
36121
|
}
|
|
35995
36122
|
}
|
|
36123
|
+
var KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
|
|
36124
|
+
...KNOWN_CAPABILITIES,
|
|
36125
|
+
"graphql_query"
|
|
36126
|
+
]);
|
|
35996
36127
|
function validateInnerResponse(raw) {
|
|
35997
36128
|
assertPositiveInt(raw.id, "inner.id");
|
|
35998
36129
|
if (raw.ok === true) {
|
|
@@ -36019,6 +36150,18 @@ function validateInnerResponse(raw) {
|
|
|
36019
36150
|
}
|
|
36020
36151
|
return raw;
|
|
36021
36152
|
}
|
|
36153
|
+
if (op === "write_cookies") {
|
|
36154
|
+
if (raw.written === void 0) {
|
|
36155
|
+
throw new ProtocolError("inner.written: missing on write_cookies response");
|
|
36156
|
+
}
|
|
36157
|
+
if (!Array.isArray(raw.written)) {
|
|
36158
|
+
throw new ProtocolError("inner.written: must be an array");
|
|
36159
|
+
}
|
|
36160
|
+
for (const [i, name] of raw.written.entries()) {
|
|
36161
|
+
assertString(name, `inner.written[${i}]`);
|
|
36162
|
+
}
|
|
36163
|
+
return raw;
|
|
36164
|
+
}
|
|
36022
36165
|
if (op === "read_local_storage" || op === "read_session_storage") {
|
|
36023
36166
|
if (raw.values === void 0) {
|
|
36024
36167
|
throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
|
|
@@ -36054,6 +36197,13 @@ function validateInnerResponse(raw) {
|
|
|
36054
36197
|
assertStringMap(raw.values, "inner.values");
|
|
36055
36198
|
return raw;
|
|
36056
36199
|
}
|
|
36200
|
+
if (op === "graphql_query") {
|
|
36201
|
+
if (raw.data === void 0) {
|
|
36202
|
+
throw new ProtocolError("inner.data: missing on graphql_query response");
|
|
36203
|
+
}
|
|
36204
|
+
assertObject(raw.data, "inner.data");
|
|
36205
|
+
return raw;
|
|
36206
|
+
}
|
|
36057
36207
|
if (op === "download") {
|
|
36058
36208
|
assertObject(raw.value, "inner.value");
|
|
36059
36209
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -36078,7 +36228,7 @@ function validateInnerResponse(raw) {
|
|
|
36078
36228
|
if (raw.ok === false) {
|
|
36079
36229
|
assertString(raw.error, "inner.error");
|
|
36080
36230
|
if (raw.op !== void 0) {
|
|
36081
|
-
if (typeof raw.op !== "string" || !
|
|
36231
|
+
if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
|
|
36082
36232
|
throw new ProtocolError(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
|
|
36083
36233
|
}
|
|
36084
36234
|
}
|
|
@@ -36270,11 +36420,33 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
|
|
|
36270
36420
|
};
|
|
36271
36421
|
}
|
|
36272
36422
|
async function openEncryptedFrame(sessionKey, frame) {
|
|
36273
|
-
const
|
|
36274
|
-
|
|
36275
|
-
|
|
36276
|
-
|
|
36277
|
-
|
|
36423
|
+
const result = await openEncryptedFrameDetailed(sessionKey, frame);
|
|
36424
|
+
if (result.stage === "ok")
|
|
36425
|
+
return result.inner;
|
|
36426
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error));
|
|
36427
|
+
}
|
|
36428
|
+
async function openEncryptedFrameDetailed(sessionKey, frame) {
|
|
36429
|
+
let pt;
|
|
36430
|
+
try {
|
|
36431
|
+
const iv = fromB64(frame.iv);
|
|
36432
|
+
const ct = fromB64(frame.ciphertext);
|
|
36433
|
+
pt = await aesGcmOpen(sessionKey, iv, ct);
|
|
36434
|
+
} catch (error51) {
|
|
36435
|
+
return { stage: "decrypt-failed", error: error51 };
|
|
36436
|
+
}
|
|
36437
|
+
let parsed;
|
|
36438
|
+
try {
|
|
36439
|
+
parsed = JSON.parse(dec.decode(pt));
|
|
36440
|
+
} catch (error51) {
|
|
36441
|
+
return { stage: "validation-failed", error: error51, recoveredId: void 0 };
|
|
36442
|
+
}
|
|
36443
|
+
try {
|
|
36444
|
+
const inner = validateInnerFrame(parsed);
|
|
36445
|
+
return { stage: "ok", inner };
|
|
36446
|
+
} catch (error51) {
|
|
36447
|
+
const recoveredId = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.id === "number" && Number.isInteger(parsed.id) && parsed.id > 0 ? parsed.id : void 0;
|
|
36448
|
+
return { stage: "validation-failed", error: error51, recoveredId };
|
|
36449
|
+
}
|
|
36278
36450
|
}
|
|
36279
36451
|
|
|
36280
36452
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
@@ -36400,6 +36572,12 @@ async function buildServerHello(opts) {
|
|
|
36400
36572
|
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36401
36573
|
}));
|
|
36402
36574
|
}
|
|
36575
|
+
if (opts.graphqlOps && opts.graphqlOps.length > 0) {
|
|
36576
|
+
hello.graphqlOps = opts.graphqlOps.map((d) => ({
|
|
36577
|
+
name: d.name,
|
|
36578
|
+
operationName: d.operationName
|
|
36579
|
+
}));
|
|
36580
|
+
}
|
|
36403
36581
|
return hello;
|
|
36404
36582
|
}
|
|
36405
36583
|
|
|
@@ -36461,6 +36639,148 @@ async function awaitSessionReady(ready, opts) {
|
|
|
36461
36639
|
}
|
|
36462
36640
|
}
|
|
36463
36641
|
|
|
36642
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36643
|
+
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
36644
|
+
import { join as join3 } from "node:path";
|
|
36645
|
+
|
|
36646
|
+
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36647
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36648
|
+
import { join as join2 } from "node:path";
|
|
36649
|
+
import { homedir } from "node:os";
|
|
36650
|
+
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36651
|
+
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36652
|
+
function defaultIdentityDir() {
|
|
36653
|
+
return join2(homedir(), ".fetchproxy", "identity");
|
|
36654
|
+
}
|
|
36655
|
+
function safeIdentityFileBase(serverName) {
|
|
36656
|
+
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36657
|
+
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36658
|
+
}
|
|
36659
|
+
return serverName.replace(/\//g, "_");
|
|
36660
|
+
}
|
|
36661
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36662
|
+
const safeFile = safeIdentityFileBase(serverName);
|
|
36663
|
+
const path = join2(dir, `${safeFile}.json`);
|
|
36664
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36665
|
+
try {
|
|
36666
|
+
const raw = await readFile(path, "utf8");
|
|
36667
|
+
const j2 = JSON.parse(raw);
|
|
36668
|
+
return {
|
|
36669
|
+
x25519Priv: fromB64(j2.x25519Priv),
|
|
36670
|
+
x25519Pub: fromB64(j2.x25519Pub),
|
|
36671
|
+
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36672
|
+
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36673
|
+
createdAt: j2.createdAt
|
|
36674
|
+
};
|
|
36675
|
+
} catch (e) {
|
|
36676
|
+
if (e.code !== "ENOENT")
|
|
36677
|
+
throw e;
|
|
36678
|
+
}
|
|
36679
|
+
const x = await generateX25519();
|
|
36680
|
+
const ed = await generateEd25519();
|
|
36681
|
+
const id = {
|
|
36682
|
+
x25519Priv: x.privateKey,
|
|
36683
|
+
x25519Pub: x.publicKey,
|
|
36684
|
+
ed25519Priv: ed.privateKey,
|
|
36685
|
+
ed25519Pub: ed.publicKey,
|
|
36686
|
+
createdAt: Date.now()
|
|
36687
|
+
};
|
|
36688
|
+
const j = {
|
|
36689
|
+
x25519Priv: toB64(id.x25519Priv),
|
|
36690
|
+
x25519Pub: toB64(id.x25519Pub),
|
|
36691
|
+
ed25519Priv: toB64(id.ed25519Priv),
|
|
36692
|
+
ed25519Pub: toB64(id.ed25519Pub),
|
|
36693
|
+
createdAt: id.createdAt
|
|
36694
|
+
};
|
|
36695
|
+
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36696
|
+
await chmod(path, 384);
|
|
36697
|
+
return id;
|
|
36698
|
+
}
|
|
36699
|
+
|
|
36700
|
+
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
36701
|
+
function fileExtensionTrust(args) {
|
|
36702
|
+
return {
|
|
36703
|
+
allowNew: args.allowNew,
|
|
36704
|
+
location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36705
|
+
read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
|
|
36706
|
+
write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
|
|
36707
|
+
};
|
|
36708
|
+
}
|
|
36709
|
+
var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
|
|
36710
|
+
function allowNewExtensionIdentity(explicit, env = process.env) {
|
|
36711
|
+
if (explicit !== void 0)
|
|
36712
|
+
return explicit;
|
|
36713
|
+
return env[TRUST_NEW_EXTENSION_ENV] === "1";
|
|
36714
|
+
}
|
|
36715
|
+
function decideExtensionTrust(args) {
|
|
36716
|
+
const { pin, hello, allowNew, serverName } = args;
|
|
36717
|
+
if (!pin)
|
|
36718
|
+
return { decision: "first-use" };
|
|
36719
|
+
if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
|
|
36720
|
+
return { decision: "pinned" };
|
|
36721
|
+
}
|
|
36722
|
+
const trustPath = args.location ?? extensionTrustPathHint(serverName);
|
|
36723
|
+
if (allowNew) {
|
|
36724
|
+
return {
|
|
36725
|
+
decision: "replace",
|
|
36726
|
+
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.`
|
|
36727
|
+
};
|
|
36728
|
+
}
|
|
36729
|
+
return {
|
|
36730
|
+
decision: "refused",
|
|
36731
|
+
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.`
|
|
36732
|
+
};
|
|
36733
|
+
}
|
|
36734
|
+
function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
|
|
36735
|
+
return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
36736
|
+
}
|
|
36737
|
+
function extensionTrustPathHint(serverName) {
|
|
36738
|
+
try {
|
|
36739
|
+
return extensionTrustPath(serverName);
|
|
36740
|
+
} catch {
|
|
36741
|
+
return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
|
|
36742
|
+
}
|
|
36743
|
+
}
|
|
36744
|
+
function isPin(x) {
|
|
36745
|
+
if (!x || typeof x !== "object")
|
|
36746
|
+
return false;
|
|
36747
|
+
const r = x;
|
|
36748
|
+
return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
|
|
36749
|
+
}
|
|
36750
|
+
async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
|
|
36751
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36752
|
+
let raw;
|
|
36753
|
+
try {
|
|
36754
|
+
raw = await readFile2(path, "utf8");
|
|
36755
|
+
} catch (e) {
|
|
36756
|
+
if (e.code === "ENOENT")
|
|
36757
|
+
return null;
|
|
36758
|
+
throw e;
|
|
36759
|
+
}
|
|
36760
|
+
let parsed;
|
|
36761
|
+
try {
|
|
36762
|
+
parsed = JSON.parse(raw);
|
|
36763
|
+
} catch {
|
|
36764
|
+
throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
|
|
36765
|
+
}
|
|
36766
|
+
if (!isPin(parsed)) {
|
|
36767
|
+
throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
|
|
36768
|
+
}
|
|
36769
|
+
return {
|
|
36770
|
+
identityX25519Pub: parsed.identityX25519Pub,
|
|
36771
|
+
identityEd25519Pub: parsed.identityEd25519Pub,
|
|
36772
|
+
pinnedAt: parsed.pinnedAt
|
|
36773
|
+
};
|
|
36774
|
+
}
|
|
36775
|
+
async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
|
|
36776
|
+
const path = extensionTrustPath(serverName, dir);
|
|
36777
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
36778
|
+
const tmp = `${path}.tmp`;
|
|
36779
|
+
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
36780
|
+
await chmod2(tmp, 384);
|
|
36781
|
+
await rename(tmp, path);
|
|
36782
|
+
}
|
|
36783
|
+
|
|
36464
36784
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
36465
36785
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
36466
36786
|
var enc2 = new TextEncoder();
|
|
@@ -36490,7 +36810,8 @@ async function startHost(opts) {
|
|
|
36490
36810
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36491
36811
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36492
36812
|
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36493
|
-
domSelectors: opts.ownDomSelectors
|
|
36813
|
+
domSelectors: opts.ownDomSelectors,
|
|
36814
|
+
graphqlOps: opts.ownGraphqlOps
|
|
36494
36815
|
});
|
|
36495
36816
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36496
36817
|
let extensionWs = null;
|
|
@@ -36513,9 +36834,12 @@ async function startHost(opts) {
|
|
|
36513
36834
|
}
|
|
36514
36835
|
resetSessionPromise();
|
|
36515
36836
|
let extensionHello = null;
|
|
36837
|
+
let extensionClaim = null;
|
|
36516
36838
|
wss.on("connection", (ws) => {
|
|
36517
36839
|
let identified = null;
|
|
36518
36840
|
let peerMcpId = null;
|
|
36841
|
+
let closed = false;
|
|
36842
|
+
let pinOnReady = false;
|
|
36519
36843
|
ws.on("message", async (data) => {
|
|
36520
36844
|
try {
|
|
36521
36845
|
let frame;
|
|
@@ -36527,10 +36851,43 @@ async function startHost(opts) {
|
|
|
36527
36851
|
return;
|
|
36528
36852
|
}
|
|
36529
36853
|
if (frame.type === "hello" && frame.role === "extension") {
|
|
36530
|
-
if (extensionWs) {
|
|
36854
|
+
if (extensionWs || extensionClaim) {
|
|
36531
36855
|
ws.close(1008, "extension already connected");
|
|
36532
36856
|
return;
|
|
36533
36857
|
}
|
|
36858
|
+
extensionClaim = ws;
|
|
36859
|
+
let pin;
|
|
36860
|
+
try {
|
|
36861
|
+
pin = await opts.extensionTrust.read();
|
|
36862
|
+
} catch (e) {
|
|
36863
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
36864
|
+
if (extensionClaim === ws)
|
|
36865
|
+
extensionClaim = null;
|
|
36866
|
+
ws.close(1008, "extension pin unreadable");
|
|
36867
|
+
return;
|
|
36868
|
+
}
|
|
36869
|
+
const outcome = decideExtensionTrust({
|
|
36870
|
+
pin,
|
|
36871
|
+
hello: frame,
|
|
36872
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
36873
|
+
serverName: opts.ownServerName,
|
|
36874
|
+
location: opts.extensionTrust.location
|
|
36875
|
+
});
|
|
36876
|
+
if (outcome.decision === "refused") {
|
|
36877
|
+
console.warn(outcome.message);
|
|
36878
|
+
if (extensionClaim === ws)
|
|
36879
|
+
extensionClaim = null;
|
|
36880
|
+
ws.close(1008, "extension identity is not the pinned one");
|
|
36881
|
+
return;
|
|
36882
|
+
}
|
|
36883
|
+
if (outcome.decision === "replace")
|
|
36884
|
+
console.warn(outcome.message);
|
|
36885
|
+
if (closed || ws.readyState !== import_websocket.default.OPEN) {
|
|
36886
|
+
if (extensionClaim === ws)
|
|
36887
|
+
extensionClaim = null;
|
|
36888
|
+
return;
|
|
36889
|
+
}
|
|
36890
|
+
pinOnReady = outcome.decision !== "pinned";
|
|
36534
36891
|
identified = "extension";
|
|
36535
36892
|
extensionWs = ws;
|
|
36536
36893
|
extensionHello = frame;
|
|
@@ -36542,6 +36899,8 @@ async function startHost(opts) {
|
|
|
36542
36899
|
console.error("[fetchproxy] onPairCode threw:", e);
|
|
36543
36900
|
}
|
|
36544
36901
|
}
|
|
36902
|
+
for (const slot of peers.values())
|
|
36903
|
+
slot.ws.send(JSON.stringify(frame));
|
|
36545
36904
|
ws.send(JSON.stringify(ownHello));
|
|
36546
36905
|
for (const slot of peers.values()) {
|
|
36547
36906
|
ws.send(JSON.stringify(slot.helloFrame));
|
|
@@ -36577,6 +36936,8 @@ async function startHost(opts) {
|
|
|
36577
36936
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
36578
36937
|
if (extensionWs)
|
|
36579
36938
|
extensionWs.send(JSON.stringify(frame));
|
|
36939
|
+
if (extensionHello)
|
|
36940
|
+
ws.send(JSON.stringify(extensionHello));
|
|
36580
36941
|
return;
|
|
36581
36942
|
}
|
|
36582
36943
|
if (frame.type === "ready") {
|
|
@@ -36588,7 +36949,7 @@ async function startHost(opts) {
|
|
|
36588
36949
|
}
|
|
36589
36950
|
const extEdPub = fromB64(extensionHello.identityEd25519Pub);
|
|
36590
36951
|
const extNonce = fromB64(extensionHello.sessionNonce);
|
|
36591
|
-
const msg =
|
|
36952
|
+
const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
|
|
36592
36953
|
const sig = fromB64(frame.sessionSig);
|
|
36593
36954
|
let sigOk = false;
|
|
36594
36955
|
try {
|
|
@@ -36601,6 +36962,18 @@ async function startHost(opts) {
|
|
|
36601
36962
|
ws.close(1008, "extension session signature invalid");
|
|
36602
36963
|
return;
|
|
36603
36964
|
}
|
|
36965
|
+
if (pinOnReady) {
|
|
36966
|
+
pinOnReady = false;
|
|
36967
|
+
try {
|
|
36968
|
+
await opts.extensionTrust.write({
|
|
36969
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
36970
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
36971
|
+
pinnedAt: Date.now()
|
|
36972
|
+
});
|
|
36973
|
+
} catch (e) {
|
|
36974
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
36975
|
+
}
|
|
36976
|
+
}
|
|
36604
36977
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36605
36978
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
36606
36979
|
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36654,6 +37027,9 @@ async function startHost(opts) {
|
|
|
36654
37027
|
}
|
|
36655
37028
|
});
|
|
36656
37029
|
ws.on("close", () => {
|
|
37030
|
+
closed = true;
|
|
37031
|
+
if (extensionClaim === ws)
|
|
37032
|
+
extensionClaim = null;
|
|
36657
37033
|
if (identified === "extension" && extensionWs === ws) {
|
|
36658
37034
|
extensionWs = null;
|
|
36659
37035
|
extensionHello = null;
|
|
@@ -36727,7 +37103,8 @@ async function startPeer(opts) {
|
|
|
36727
37103
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36728
37104
|
domSelectors: opts.domSelectors,
|
|
36729
37105
|
localStoragePointers: opts.localStoragePointers,
|
|
36730
|
-
sessionStoragePointers: opts.sessionStoragePointers
|
|
37106
|
+
sessionStoragePointers: opts.sessionStoragePointers,
|
|
37107
|
+
graphqlOps: opts.graphqlOps
|
|
36731
37108
|
});
|
|
36732
37109
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
36733
37110
|
ws.send(JSON.stringify(hello));
|
|
@@ -36743,11 +37120,84 @@ async function startPeer(opts) {
|
|
|
36743
37120
|
resolveFirstReady = resolve;
|
|
36744
37121
|
rejectFirstReady = reject;
|
|
36745
37122
|
});
|
|
37123
|
+
let extensionHello = null;
|
|
37124
|
+
let warnedUnverifiable = false;
|
|
37125
|
+
let cachedPin = void 0;
|
|
37126
|
+
const authenticateExtension = async (sessionSig, extensionSessionPub) => {
|
|
37127
|
+
if (!extensionHello) {
|
|
37128
|
+
if (opts.requireExtensionIdentity) {
|
|
37129
|
+
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.`);
|
|
37130
|
+
return false;
|
|
37131
|
+
}
|
|
37132
|
+
if (!warnedUnverifiable) {
|
|
37133
|
+
warnedUnverifiable = true;
|
|
37134
|
+
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.`);
|
|
37135
|
+
}
|
|
37136
|
+
return true;
|
|
37137
|
+
}
|
|
37138
|
+
const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
|
|
37139
|
+
let sigOk = false;
|
|
37140
|
+
try {
|
|
37141
|
+
sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
|
|
37142
|
+
} catch {
|
|
37143
|
+
sigOk = false;
|
|
37144
|
+
}
|
|
37145
|
+
if (!sigOk) {
|
|
37146
|
+
console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
|
|
37147
|
+
return false;
|
|
37148
|
+
}
|
|
37149
|
+
if (cachedPin === void 0) {
|
|
37150
|
+
try {
|
|
37151
|
+
cachedPin = await opts.extensionTrust.read();
|
|
37152
|
+
} catch (e) {
|
|
37153
|
+
console.error(`[fetchproxy] ${String(e)}`);
|
|
37154
|
+
return false;
|
|
37155
|
+
}
|
|
37156
|
+
}
|
|
37157
|
+
const pin = cachedPin;
|
|
37158
|
+
const outcome = decideExtensionTrust({
|
|
37159
|
+
pin,
|
|
37160
|
+
hello: extensionHello,
|
|
37161
|
+
allowNew: opts.extensionTrust.allowNew,
|
|
37162
|
+
serverName: opts.serverName,
|
|
37163
|
+
location: opts.extensionTrust.location
|
|
37164
|
+
});
|
|
37165
|
+
if (outcome.decision === "refused") {
|
|
37166
|
+
console.warn(outcome.message);
|
|
37167
|
+
return false;
|
|
37168
|
+
}
|
|
37169
|
+
if (outcome.decision === "replace")
|
|
37170
|
+
console.warn(outcome.message);
|
|
37171
|
+
if (outcome.decision !== "pinned") {
|
|
37172
|
+
try {
|
|
37173
|
+
const written = {
|
|
37174
|
+
identityX25519Pub: extensionHello.identityX25519Pub,
|
|
37175
|
+
identityEd25519Pub: extensionHello.identityEd25519Pub,
|
|
37176
|
+
pinnedAt: Date.now()
|
|
37177
|
+
};
|
|
37178
|
+
await opts.extensionTrust.write(written);
|
|
37179
|
+
cachedPin = written;
|
|
37180
|
+
} catch (e) {
|
|
37181
|
+
console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
|
|
37182
|
+
}
|
|
37183
|
+
}
|
|
37184
|
+
return true;
|
|
37185
|
+
};
|
|
36746
37186
|
const onMessage = async (data) => {
|
|
36747
37187
|
try {
|
|
36748
37188
|
const raw = JSON.parse(data.toString());
|
|
36749
37189
|
const frame = validateFrame(raw);
|
|
37190
|
+
if (frame.type === "hello" && frame.role === "extension") {
|
|
37191
|
+
extensionHello = frame;
|
|
37192
|
+
return;
|
|
37193
|
+
}
|
|
36750
37194
|
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
37195
|
+
const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
|
|
37196
|
+
if (!authorised) {
|
|
37197
|
+
ws.close(1008, "extension identity refused");
|
|
37198
|
+
rejectFirstReady(new Error("peer: extension identity refused"));
|
|
37199
|
+
return;
|
|
37200
|
+
}
|
|
36751
37201
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
36752
37202
|
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
36753
37203
|
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
@@ -36771,10 +37221,20 @@ async function startPeer(opts) {
|
|
|
36771
37221
|
return;
|
|
36772
37222
|
if (!session.acceptInboundSeq(frame.seq))
|
|
36773
37223
|
return;
|
|
36774
|
-
|
|
36775
|
-
|
|
36776
|
-
innerListeners.forEach((cb) => cb(inner));
|
|
36777
|
-
}
|
|
37224
|
+
const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
|
|
37225
|
+
if (result.stage === "ok") {
|
|
37226
|
+
innerListeners.forEach((cb) => cb(result.inner));
|
|
37227
|
+
} else if (result.stage === "decrypt-failed") {
|
|
37228
|
+
} else {
|
|
37229
|
+
console.error("[fetchproxy] peer: received a frame that decrypted OK but failed validation:", result.error);
|
|
37230
|
+
if (result.recoveredId !== void 0) {
|
|
37231
|
+
innerListeners.forEach((cb) => cb({
|
|
37232
|
+
type: "response",
|
|
37233
|
+
id: result.recoveredId,
|
|
37234
|
+
ok: false,
|
|
37235
|
+
error: `malformed response failed protocol validation: ${String(result.error)}`
|
|
37236
|
+
}));
|
|
37237
|
+
}
|
|
36778
37238
|
}
|
|
36779
37239
|
}
|
|
36780
37240
|
} catch (e) {
|
|
@@ -36818,57 +37278,6 @@ async function startPeer(opts) {
|
|
|
36818
37278
|
return handle;
|
|
36819
37279
|
}
|
|
36820
37280
|
|
|
36821
|
-
// node_modules/@fetchproxy/server/dist/identity.js
|
|
36822
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
36823
|
-
import { join as join2 } from "node:path";
|
|
36824
|
-
import { homedir } from "node:os";
|
|
36825
|
-
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
36826
|
-
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
36827
|
-
function defaultIdentityDir() {
|
|
36828
|
-
return join2(homedir(), ".fetchproxy", "identity");
|
|
36829
|
-
}
|
|
36830
|
-
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
36831
|
-
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
36832
|
-
throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
|
|
36833
|
-
}
|
|
36834
|
-
const safeFile = serverName.replace(/\//g, "_");
|
|
36835
|
-
const path = join2(dir, `${safeFile}.json`);
|
|
36836
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
36837
|
-
try {
|
|
36838
|
-
const raw = await readFile(path, "utf8");
|
|
36839
|
-
const j2 = JSON.parse(raw);
|
|
36840
|
-
return {
|
|
36841
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
36842
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
36843
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
36844
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
36845
|
-
createdAt: j2.createdAt
|
|
36846
|
-
};
|
|
36847
|
-
} catch (e) {
|
|
36848
|
-
if (e.code !== "ENOENT")
|
|
36849
|
-
throw e;
|
|
36850
|
-
}
|
|
36851
|
-
const x = await generateX25519();
|
|
36852
|
-
const ed = await generateEd25519();
|
|
36853
|
-
const id = {
|
|
36854
|
-
x25519Priv: x.privateKey,
|
|
36855
|
-
x25519Pub: x.publicKey,
|
|
36856
|
-
ed25519Priv: ed.privateKey,
|
|
36857
|
-
ed25519Pub: ed.publicKey,
|
|
36858
|
-
createdAt: Date.now()
|
|
36859
|
-
};
|
|
36860
|
-
const j = {
|
|
36861
|
-
x25519Priv: toB64(id.x25519Priv),
|
|
36862
|
-
x25519Pub: toB64(id.x25519Pub),
|
|
36863
|
-
ed25519Priv: toB64(id.ed25519Priv),
|
|
36864
|
-
ed25519Pub: toB64(id.ed25519Pub),
|
|
36865
|
-
createdAt: id.createdAt
|
|
36866
|
-
};
|
|
36867
|
-
await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
|
|
36868
|
-
await chmod(path, 384);
|
|
36869
|
-
return id;
|
|
36870
|
-
}
|
|
36871
|
-
|
|
36872
37281
|
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
36873
37282
|
function classifyFetchError(error51) {
|
|
36874
37283
|
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
@@ -36950,6 +37359,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
|
|
|
36950
37359
|
this.hint = hint;
|
|
36951
37360
|
}
|
|
36952
37361
|
};
|
|
37362
|
+
var FetchproxyHintedError = class extends FetchproxyProtocolError {
|
|
37363
|
+
/** The extension's raw rejection, unmodified. */
|
|
37364
|
+
originalError;
|
|
37365
|
+
/** What the user should actually do, in prose. */
|
|
37366
|
+
hint;
|
|
37367
|
+
constructor(originalError, hint) {
|
|
37368
|
+
super(`${originalError} \u2014 ${hint}`);
|
|
37369
|
+
this.name = "FetchproxyHintedError";
|
|
37370
|
+
this.originalError = originalError;
|
|
37371
|
+
this.hint = hint;
|
|
37372
|
+
}
|
|
37373
|
+
};
|
|
37374
|
+
var FetchproxyScopeError = class extends FetchproxyHintedError {
|
|
37375
|
+
constructor(originalError) {
|
|
37376
|
+
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.");
|
|
37377
|
+
this.name = "FetchproxyScopeError";
|
|
37378
|
+
}
|
|
37379
|
+
};
|
|
37380
|
+
var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
37381
|
+
constructor(originalError) {
|
|
37382
|
+
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.");
|
|
37383
|
+
this.name = "FetchproxyNoTabError";
|
|
37384
|
+
}
|
|
37385
|
+
};
|
|
37386
|
+
var SCOPE_REJECTION = /not in declared/;
|
|
37387
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
37388
|
+
function protocolErrorFrom(error51) {
|
|
37389
|
+
if (SCOPE_REJECTION.test(error51))
|
|
37390
|
+
return new FetchproxyScopeError(error51);
|
|
37391
|
+
if (NO_TAB_REJECTION.test(error51))
|
|
37392
|
+
return new FetchproxyNoTabError(error51);
|
|
37393
|
+
return new FetchproxyProtocolError(error51);
|
|
37394
|
+
}
|
|
36953
37395
|
var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
36954
37396
|
url;
|
|
36955
37397
|
timeoutMs;
|
|
@@ -36979,6 +37421,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
|
|
|
36979
37421
|
}
|
|
36980
37422
|
};
|
|
36981
37423
|
var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
37424
|
+
function normalizeCookiePath(path) {
|
|
37425
|
+
if (path === void 0 || path === "")
|
|
37426
|
+
return void 0;
|
|
37427
|
+
const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
37428
|
+
try {
|
|
37429
|
+
assertCookiePath(trimmed, "path");
|
|
37430
|
+
} catch (e) {
|
|
37431
|
+
throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
|
|
37432
|
+
}
|
|
37433
|
+
return trimmed;
|
|
37434
|
+
}
|
|
36982
37435
|
function assertSubdomainLabel(label) {
|
|
36983
37436
|
if (!SUBDOMAIN_LABEL_RE.test(label)) {
|
|
36984
37437
|
throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
|
|
@@ -37043,6 +37496,9 @@ var FetchproxyServer = class {
|
|
|
37043
37496
|
// them off from `pending` (fetch) and `pendingReadCookies` (legacy
|
|
37044
37497
|
// string-shape) so the response routing in `onInner` stays linear.
|
|
37045
37498
|
pendingStorage = /* @__PURE__ */ new Map();
|
|
37499
|
+
// 1.12.0+: write-cookies awaiters resolve the list of names actually
|
|
37500
|
+
// written, so a caller can confirm rather than assume.
|
|
37501
|
+
pendingWriteCookies = /* @__PURE__ */ new Map();
|
|
37046
37502
|
// 0.3.0+: capture-header awaiters resolve a single string.
|
|
37047
37503
|
pendingCapture = /* @__PURE__ */ new Map();
|
|
37048
37504
|
// capture_redirect awaiters resolve the captured redirect URL string.
|
|
@@ -37051,6 +37507,10 @@ var FetchproxyServer = class {
|
|
|
37051
37507
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
37052
37508
|
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
37053
37509
|
pendingDownload = /* @__PURE__ */ new Map();
|
|
37510
|
+
// 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
|
|
37511
|
+
// shape is operation-specific, so the awaiter resolves `unknown` and the
|
|
37512
|
+
// caller narrows.
|
|
37513
|
+
pendingGraphql = /* @__PURE__ */ new Map();
|
|
37054
37514
|
mcpId = null;
|
|
37055
37515
|
identity = null;
|
|
37056
37516
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -37138,6 +37598,10 @@ var FetchproxyServer = class {
|
|
|
37138
37598
|
selector: d.selector,
|
|
37139
37599
|
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
37140
37600
|
})),
|
|
37601
|
+
graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
|
|
37602
|
+
name: d.name,
|
|
37603
|
+
operationName: d.operationName
|
|
37604
|
+
})),
|
|
37141
37605
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
37142
37606
|
// adapter was about to set these to the same numbers anyway; the
|
|
37143
37607
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -37161,6 +37625,8 @@ var FetchproxyServer = class {
|
|
|
37161
37625
|
keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
|
|
37162
37626
|
keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
|
|
37163
37627
|
identityDir: opts.identityDir,
|
|
37628
|
+
allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
|
|
37629
|
+
requireExtensionIdentity: opts.requireExtensionIdentity,
|
|
37164
37630
|
onPairCode: opts.onPairCode
|
|
37165
37631
|
};
|
|
37166
37632
|
}
|
|
@@ -37259,7 +37725,9 @@ var FetchproxyServer = class {
|
|
|
37259
37725
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
37260
37726
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37261
37727
|
ownDomSelectors: this.opts.domSelectors,
|
|
37262
|
-
|
|
37728
|
+
ownGraphqlOps: this.opts.graphqlOps,
|
|
37729
|
+
onPairCode: this.opts.onPairCode,
|
|
37730
|
+
extensionTrust: this.extensionTrust()
|
|
37263
37731
|
});
|
|
37264
37732
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
37265
37733
|
this.hostHandle.onExtensionDisconnect(() => {
|
|
@@ -37287,7 +37755,10 @@ var FetchproxyServer = class {
|
|
|
37287
37755
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
37288
37756
|
localStoragePointers: this.opts.localStoragePointers,
|
|
37289
37757
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
37290
|
-
domSelectors: this.opts.domSelectors
|
|
37758
|
+
domSelectors: this.opts.domSelectors,
|
|
37759
|
+
graphqlOps: this.opts.graphqlOps,
|
|
37760
|
+
extensionTrust: this.extensionTrust(),
|
|
37761
|
+
requireExtensionIdentity: this.opts.requireExtensionIdentity
|
|
37291
37762
|
});
|
|
37292
37763
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
37293
37764
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37426,6 +37897,23 @@ var FetchproxyServer = class {
|
|
|
37426
37897
|
markActive() {
|
|
37427
37898
|
this.noteActivityForKeepalive();
|
|
37428
37899
|
}
|
|
37900
|
+
/**
|
|
37901
|
+
* #208: this MCP's pin on the extension's identity, stored beside its own
|
|
37902
|
+
* identity key and so following `identityDir` wherever the caller put it.
|
|
37903
|
+
*
|
|
37904
|
+
* `allowNewExtensionIdentity` falls back to an environment variable when the
|
|
37905
|
+
* caller expressed no opinion, because the thirteen MCPs that construct this
|
|
37906
|
+
* class are separate packages: an operator whose extension re-install has
|
|
37907
|
+
* just locked all of them out needs one lever that does not require patching
|
|
37908
|
+
* every one of them.
|
|
37909
|
+
*/
|
|
37910
|
+
extensionTrust() {
|
|
37911
|
+
return fileExtensionTrust({
|
|
37912
|
+
serverName: this.opts.serverName,
|
|
37913
|
+
dir: this.opts.identityDir,
|
|
37914
|
+
allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
|
|
37915
|
+
});
|
|
37916
|
+
}
|
|
37429
37917
|
noteActivityForKeepalive() {
|
|
37430
37918
|
const intervalMs = this.opts.keepAliveIntervalMs;
|
|
37431
37919
|
if (intervalMs <= 0)
|
|
@@ -37489,10 +37977,12 @@ var FetchproxyServer = class {
|
|
|
37489
37977
|
this.pending.delete(id);
|
|
37490
37978
|
this.pendingReadCookies.delete(id);
|
|
37491
37979
|
this.pendingStorage.delete(id);
|
|
37980
|
+
this.pendingWriteCookies.delete(id);
|
|
37492
37981
|
this.pendingCapture.delete(id);
|
|
37493
37982
|
this.pendingRedirect.delete(id);
|
|
37494
37983
|
this.pendingDownload.delete(id);
|
|
37495
37984
|
this.pendingIdb.delete(id);
|
|
37985
|
+
this.pendingGraphql.delete(id);
|
|
37496
37986
|
}
|
|
37497
37987
|
throw err;
|
|
37498
37988
|
}
|
|
@@ -37609,7 +38099,7 @@ var FetchproxyServer = class {
|
|
|
37609
38099
|
port: this.opts.port
|
|
37610
38100
|
});
|
|
37611
38101
|
}
|
|
37612
|
-
return
|
|
38102
|
+
return protocolErrorFrom(result.error);
|
|
37613
38103
|
}
|
|
37614
38104
|
/**
|
|
37615
38105
|
* Convenience wrapper around `fetch()`. Builds the URL from a path
|
|
@@ -37645,10 +38135,20 @@ var FetchproxyServer = class {
|
|
|
37645
38135
|
}
|
|
37646
38136
|
const url2 = isAbsolute ? path : `https://${host}${path}`;
|
|
37647
38137
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
38138
|
+
let tabUrl = `https://${host}/`;
|
|
38139
|
+
if (opts.viaTab !== void 0) {
|
|
38140
|
+
try {
|
|
38141
|
+
new URL(opts.viaTab);
|
|
38142
|
+
} catch {
|
|
38143
|
+
throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
|
|
38144
|
+
}
|
|
38145
|
+
assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
|
|
38146
|
+
tabUrl = opts.viaTab;
|
|
38147
|
+
}
|
|
37648
38148
|
const init = {
|
|
37649
38149
|
url: url2,
|
|
37650
38150
|
method,
|
|
37651
|
-
tabUrl
|
|
38151
|
+
tabUrl,
|
|
37652
38152
|
headers: opts.headers,
|
|
37653
38153
|
body: opts.body
|
|
37654
38154
|
};
|
|
@@ -37860,9 +38360,14 @@ var FetchproxyServer = class {
|
|
|
37860
38360
|
let inner;
|
|
37861
38361
|
if (opts.keys !== void 0) {
|
|
37862
38362
|
this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
|
|
38363
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
37863
38364
|
const initV3 = {
|
|
38365
|
+
// Origin stays BARE. The path travels as its own validated field —
|
|
38366
|
+
// `assertHttpsOriginOnly` deliberately refuses a path here so one
|
|
38367
|
+
// cannot be used to re-point the read past the domain gate.
|
|
37864
38368
|
origin: `https://${host}`,
|
|
37865
|
-
keys: [...opts.keys]
|
|
38369
|
+
keys: [...opts.keys],
|
|
38370
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
37866
38371
|
};
|
|
37867
38372
|
inner = { type: "request", id, op: "read_cookies", init: initV3 };
|
|
37868
38373
|
} else {
|
|
@@ -37875,10 +38380,65 @@ var FetchproxyServer = class {
|
|
|
37875
38380
|
await this.sendInnerFrame(inner);
|
|
37876
38381
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37877
38382
|
if (!result.ok) {
|
|
37878
|
-
throw
|
|
38383
|
+
throw protocolErrorFrom(result.error);
|
|
37879
38384
|
}
|
|
37880
38385
|
return result.cookies;
|
|
37881
38386
|
}
|
|
38387
|
+
/**
|
|
38388
|
+
* 1.12.0+: overwrite the value of cookies this MCP already declares.
|
|
38389
|
+
*
|
|
38390
|
+
* The bridge's only write verb, and it exists for one failure class. Sites
|
|
38391
|
+
* that ROTATE a credential cookie hand back a new value on every refresh; if
|
|
38392
|
+
* the MCP refreshes and keeps the result to itself, the copy in the browser's
|
|
38393
|
+
* cookie jar is dead, and the user gets signed out of a tab they never
|
|
38394
|
+
* touched — usually reported to them as "inactivity". Writing the rotated
|
|
38395
|
+
* value back is the only thing that repairs it.
|
|
38396
|
+
*
|
|
38397
|
+
* Requires `'write_cookies'` in capabilities, which the user approves at pair
|
|
38398
|
+
* time as its own line. Every name must ALSO be in declared `cookieKeys`: a
|
|
38399
|
+
* write can never reach a cookie the MCP was not already trusted to read, so
|
|
38400
|
+
* granting it cannot widen which cookies are in play — only what may be done
|
|
38401
|
+
* to the ones already listed.
|
|
38402
|
+
*
|
|
38403
|
+
* The extension refuses the whole request unless every named cookie already
|
|
38404
|
+
* exists; this refreshes a value in place and deliberately cannot author new
|
|
38405
|
+
* cookies. Returns the names actually written.
|
|
38406
|
+
*/
|
|
38407
|
+
async writeCookies(opts) {
|
|
38408
|
+
if (!this.opts.capabilities.includes("write_cookies")) {
|
|
38409
|
+
throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
38410
|
+
}
|
|
38411
|
+
const names = Object.keys(opts.cookies);
|
|
38412
|
+
if (names.length === 0) {
|
|
38413
|
+
throw new Error("FetchproxyServer.writeCookies(): no cookies given");
|
|
38414
|
+
}
|
|
38415
|
+
await this.ensureConnected();
|
|
38416
|
+
this.throwIfPendingPair();
|
|
38417
|
+
if (opts.subdomain !== void 0)
|
|
38418
|
+
assertSubdomainLabel(opts.subdomain);
|
|
38419
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
38420
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
38421
|
+
this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
|
|
38422
|
+
const cookiePath = normalizeCookiePath(opts.path);
|
|
38423
|
+
const id = this.nextRequestId++;
|
|
38424
|
+
const inner = {
|
|
38425
|
+
type: "request",
|
|
38426
|
+
id,
|
|
38427
|
+
op: "write_cookies",
|
|
38428
|
+
init: {
|
|
38429
|
+
// Bare origin, same invariant as the read path: a path must never be
|
|
38430
|
+
// able to move the request past the domain gate.
|
|
38431
|
+
origin: `https://${host}`,
|
|
38432
|
+
cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
|
|
38433
|
+
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
38434
|
+
}
|
|
38435
|
+
};
|
|
38436
|
+
const pending = new Promise((resolve, reject) => {
|
|
38437
|
+
this.pendingWriteCookies.set(id, { resolve, reject });
|
|
38438
|
+
});
|
|
38439
|
+
await this.sendInnerFrame(inner);
|
|
38440
|
+
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
38441
|
+
}
|
|
37882
38442
|
/**
|
|
37883
38443
|
* 0.3.0+: read declared localStorage keys from the user's signed-in
|
|
37884
38444
|
* tab. Requires `'read_local_storage'` in capabilities AND each key
|
|
@@ -38302,6 +38862,52 @@ var FetchproxyServer = class {
|
|
|
38302
38862
|
await this.sendInnerFrame(inner);
|
|
38303
38863
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
38304
38864
|
}
|
|
38865
|
+
/**
|
|
38866
|
+
* 1.x+: run a declared GraphQL operation through the page's own Apollo
|
|
38867
|
+
* client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
|
|
38868
|
+
* Requires `'graphql'` in capabilities AND `name` to match a declared
|
|
38869
|
+
* `graphqlOps` entry. The extension resolves `name` → `operationName` →
|
|
38870
|
+
* the live DocumentNode the page already observed, then invokes
|
|
38871
|
+
* `client.query({ query, variables })` — the site's own request path, so
|
|
38872
|
+
* per-request bot telemetry (Akamai etc.) runs automatically.
|
|
38873
|
+
*
|
|
38874
|
+
* Returns the GraphQL `data` object on success (shape is
|
|
38875
|
+
* operation-specific; the caller narrows). Throws a plain `Error` on
|
|
38876
|
+
* developer mistakes (undeclared capability, undeclared name) and a
|
|
38877
|
+
* descriptive `Error` on the `ok:false` bridge path — which includes the
|
|
38878
|
+
* typed "operation not yet observed on this tab" case (open the site's
|
|
38879
|
+
* page and retry).
|
|
38880
|
+
*/
|
|
38881
|
+
async graphqlQuery(opts) {
|
|
38882
|
+
if (!this.opts.capabilities.includes("graphql")) {
|
|
38883
|
+
throw new Error('FetchproxyServer.graphqlQuery(): MCP did not declare "graphql" in capabilities');
|
|
38884
|
+
}
|
|
38885
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
38886
|
+
throw new Error("FetchproxyServer.graphqlQuery: opts.name must be a non-empty string");
|
|
38887
|
+
}
|
|
38888
|
+
const declaredNames = this.opts.graphqlOps.map((d) => d.name);
|
|
38889
|
+
if (!declaredNames.includes(opts.name)) {
|
|
38890
|
+
throw new Error(`FetchproxyServer.graphqlQuery: operation ${JSON.stringify(opts.name)} not in declared graphqlOps [${declaredNames.map((n) => JSON.stringify(n)).join(", ")}]`);
|
|
38891
|
+
}
|
|
38892
|
+
await this.ensureConnected();
|
|
38893
|
+
this.throwIfPendingPair();
|
|
38894
|
+
const id = this.nextRequestId++;
|
|
38895
|
+
const inner = {
|
|
38896
|
+
type: "request",
|
|
38897
|
+
id,
|
|
38898
|
+
op: "graphql_query",
|
|
38899
|
+
init: {
|
|
38900
|
+
name: opts.name,
|
|
38901
|
+
variables: opts.variables,
|
|
38902
|
+
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
38903
|
+
}
|
|
38904
|
+
};
|
|
38905
|
+
const pending = new Promise((resolve, reject) => {
|
|
38906
|
+
this.pendingGraphql.set(id, { resolve, reject });
|
|
38907
|
+
});
|
|
38908
|
+
await this.sendInnerFrame(inner);
|
|
38909
|
+
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
38910
|
+
}
|
|
38305
38911
|
assertScopeSubset(requested, declared, label) {
|
|
38306
38912
|
const undeclared = undeclaredKeys(requested, declared);
|
|
38307
38913
|
if (undeclared.length > 0) {
|
|
@@ -38379,7 +38985,7 @@ var FetchproxyServer = class {
|
|
|
38379
38985
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
38380
38986
|
}
|
|
38381
38987
|
} else {
|
|
38382
|
-
storageCb.reject(
|
|
38988
|
+
storageCb.reject(protocolErrorFrom(inner.error));
|
|
38383
38989
|
}
|
|
38384
38990
|
return;
|
|
38385
38991
|
}
|
|
@@ -38393,7 +38999,7 @@ var FetchproxyServer = class {
|
|
|
38393
38999
|
captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
|
|
38394
39000
|
}
|
|
38395
39001
|
} else {
|
|
38396
|
-
captureCb.reject(
|
|
39002
|
+
captureCb.reject(protocolErrorFrom(inner.error));
|
|
38397
39003
|
}
|
|
38398
39004
|
return;
|
|
38399
39005
|
}
|
|
@@ -38407,7 +39013,7 @@ var FetchproxyServer = class {
|
|
|
38407
39013
|
redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
|
|
38408
39014
|
}
|
|
38409
39015
|
} else {
|
|
38410
|
-
redirectCb.reject(
|
|
39016
|
+
redirectCb.reject(protocolErrorFrom(inner.error));
|
|
38411
39017
|
}
|
|
38412
39018
|
return;
|
|
38413
39019
|
}
|
|
@@ -38421,7 +39027,7 @@ var FetchproxyServer = class {
|
|
|
38421
39027
|
idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
|
|
38422
39028
|
}
|
|
38423
39029
|
} else {
|
|
38424
|
-
idbCb.reject(
|
|
39030
|
+
idbCb.reject(protocolErrorFrom(inner.error));
|
|
38425
39031
|
}
|
|
38426
39032
|
return;
|
|
38427
39033
|
}
|
|
@@ -38435,7 +39041,31 @@ var FetchproxyServer = class {
|
|
|
38435
39041
|
downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
|
|
38436
39042
|
}
|
|
38437
39043
|
} else {
|
|
38438
|
-
downloadCb.reject(
|
|
39044
|
+
downloadCb.reject(protocolErrorFrom(inner.error));
|
|
39045
|
+
}
|
|
39046
|
+
return;
|
|
39047
|
+
}
|
|
39048
|
+
const graphqlCb = this.pendingGraphql.get(inner.id);
|
|
39049
|
+
if (graphqlCb) {
|
|
39050
|
+
this.pendingGraphql.delete(inner.id);
|
|
39051
|
+
if (inner.ok) {
|
|
39052
|
+
if (inner.op === "graphql_query") {
|
|
39053
|
+
graphqlCb.resolve(inner.data);
|
|
39054
|
+
} else {
|
|
39055
|
+
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
39056
|
+
}
|
|
39057
|
+
} else {
|
|
39058
|
+
graphqlCb.reject(protocolErrorFrom(inner.error));
|
|
39059
|
+
}
|
|
39060
|
+
return;
|
|
39061
|
+
}
|
|
39062
|
+
const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
|
|
39063
|
+
if (writeCookiesCb) {
|
|
39064
|
+
this.pendingWriteCookies.delete(inner.id);
|
|
39065
|
+
if (inner.ok && inner.op === "write_cookies") {
|
|
39066
|
+
writeCookiesCb.resolve([...inner.written]);
|
|
39067
|
+
} else {
|
|
39068
|
+
writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
|
|
38439
39069
|
}
|
|
38440
39070
|
return;
|
|
38441
39071
|
}
|
|
@@ -38478,6 +39108,9 @@ var FetchproxyServer = class {
|
|
|
38478
39108
|
for (const { reject } of this.pendingStorage.values())
|
|
38479
39109
|
reject(err);
|
|
38480
39110
|
this.pendingStorage.clear();
|
|
39111
|
+
for (const { reject } of this.pendingWriteCookies.values())
|
|
39112
|
+
reject(err);
|
|
39113
|
+
this.pendingWriteCookies.clear();
|
|
38481
39114
|
for (const { reject } of this.pendingCapture.values())
|
|
38482
39115
|
reject(err);
|
|
38483
39116
|
this.pendingCapture.clear();
|
|
@@ -38490,6 +39123,9 @@ var FetchproxyServer = class {
|
|
|
38490
39123
|
for (const { reject } of this.pendingDownload.values())
|
|
38491
39124
|
reject(err);
|
|
38492
39125
|
this.pendingDownload.clear();
|
|
39126
|
+
for (const { reject } of this.pendingGraphql.values())
|
|
39127
|
+
reject(err);
|
|
39128
|
+
this.pendingGraphql.clear();
|
|
38493
39129
|
}
|
|
38494
39130
|
/**
|
|
38495
39131
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Single source of the server version. release-please bumps the literal below. */
|
|
2
|
-
export const VERSION = '0.3.
|
|
2
|
+
export const VERSION = '0.3.3'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/tripadvisor-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"mcpName": "io.github.chrischall/tripadvisor-mcp",
|
|
5
5
|
"description": "TripAdvisor Terra API MCP server for Claude — search locations, details, photos, and reviews. Developed and maintained by AI (Claude Code).",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"test:coverage": "vitest run --coverage"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@chrischall/mcp-utils": "^0.
|
|
48
|
-
"@fetchproxy/server": "^
|
|
47
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
48
|
+
"@fetchproxy/server": "^2.0.0",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
50
|
"dotenv": "^17.4.0",
|
|
51
51
|
"zod": "^4.4.2"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/tripadvisor-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/tripadvisor-mcp",
|
|
14
|
-
"version": "0.3.
|
|
14
|
+
"version": "0.3.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|