@chrischall/tripadvisor-mcp 0.3.2 → 0.3.4

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.
@@ -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.2"
10
+ "version": "0.3.4"
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.2",
18
+ "version": "0.3.4",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tripadvisor-mcp",
3
3
  "displayName": "TripAdvisor",
4
- "version": "0.3.2",
4
+ "version": "0.3.4",
5
5
  "description": "MCP server for the TripAdvisor Terra API — location search, details, photos, and reviews",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -3651,7 +3651,12 @@ var require_fast_uri = __commonJS({
3651
3651
  }
3652
3652
  function resolve(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
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
- let methodValue;
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
- this._readBuffer.append(chunk2);
34586
- this.processReadBuffer();
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.
@@ -34635,53 +34677,6 @@ var StdioServerTransport = class {
34635
34677
  }
34636
34678
  };
34637
34679
 
34638
- // node_modules/@chrischall/mcp-utils/dist/server/index.js
34639
- async function createMcpServer(opts) {
34640
- const server = new McpServer({ name: opts.name, version: opts.version });
34641
- if (opts.banner !== void 0) {
34642
- console.error(opts.banner);
34643
- }
34644
- const deps = opts.deps;
34645
- for (const register of opts.tools) {
34646
- await register(server, deps);
34647
- }
34648
- return server;
34649
- }
34650
- function withGracefulShutdown(server, opts = {}) {
34651
- const shouldExit = opts.exit ?? true;
34652
- let shuttingDown = false;
34653
- const handler = (signal) => {
34654
- if (shuttingDown)
34655
- return;
34656
- shuttingDown = true;
34657
- void (async () => {
34658
- try {
34659
- if (opts.onSignal)
34660
- await opts.onSignal(signal);
34661
- await server.close();
34662
- } catch (err) {
34663
- console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34664
- } finally {
34665
- if (shouldExit)
34666
- process.exit(0);
34667
- }
34668
- })();
34669
- };
34670
- process.on("SIGINT", () => handler("SIGINT"));
34671
- process.on("SIGTERM", () => handler("SIGTERM"));
34672
- }
34673
- async function runMcp(opts) {
34674
- const server = await createMcpServer(opts);
34675
- const shutdown = opts.shutdown ?? true;
34676
- if (shutdown !== false) {
34677
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34678
- }
34679
- const spec = opts.transport ?? "stdio";
34680
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34681
- await server.connect(transport);
34682
- return server;
34683
- }
34684
-
34685
34680
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34686
34681
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
34687
34682
  var McpToolError = class extends Error {
@@ -34741,6 +34736,81 @@ function textResult(data) {
34741
34736
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34742
34737
  };
34743
34738
  }
34739
+ function errorResult(message) {
34740
+ return {
34741
+ content: [{ type: "text", text: redactSecrets(message) }],
34742
+ isError: true
34743
+ };
34744
+ }
34745
+
34746
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
34747
+ function hintResultOrRethrow(err) {
34748
+ if (err instanceof McpToolError && err.hint) {
34749
+ return errorResult(`${err.message}
34750
+
34751
+ Hint: ${err.hint}`);
34752
+ }
34753
+ throw err;
34754
+ }
34755
+ function surfaceToolHints(server) {
34756
+ const register = server.registerTool.bind(server);
34757
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
34758
+ let result;
34759
+ try {
34760
+ result = cb(...args);
34761
+ } catch (err) {
34762
+ return hintResultOrRethrow(err);
34763
+ }
34764
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
34765
+ });
34766
+ }
34767
+ async function createMcpServer(opts) {
34768
+ const server = new McpServer({ name: opts.name, version: opts.version });
34769
+ if (opts.surfaceHints !== false)
34770
+ surfaceToolHints(server);
34771
+ if (opts.banner !== void 0) {
34772
+ console.error(opts.banner);
34773
+ }
34774
+ const deps = opts.deps;
34775
+ for (const register of opts.tools) {
34776
+ await register(server, deps);
34777
+ }
34778
+ return server;
34779
+ }
34780
+ function withGracefulShutdown(server, opts = {}) {
34781
+ const shouldExit = opts.exit ?? true;
34782
+ let shuttingDown = false;
34783
+ const handler = (signal) => {
34784
+ if (shuttingDown)
34785
+ return;
34786
+ shuttingDown = true;
34787
+ void (async () => {
34788
+ try {
34789
+ if (opts.onSignal)
34790
+ await opts.onSignal(signal);
34791
+ await server.close();
34792
+ } catch (err) {
34793
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34794
+ } finally {
34795
+ if (shouldExit)
34796
+ process.exit(0);
34797
+ }
34798
+ })();
34799
+ };
34800
+ process.on("SIGINT", () => handler("SIGINT"));
34801
+ process.on("SIGTERM", () => handler("SIGTERM"));
34802
+ }
34803
+ async function runMcp(opts) {
34804
+ const server = await createMcpServer(opts);
34805
+ const shutdown = opts.shutdown ?? true;
34806
+ if (shutdown !== false) {
34807
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34808
+ }
34809
+ const spec = opts.transport ?? "stdio";
34810
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34811
+ await server.connect(transport);
34812
+ return server;
34813
+ }
34744
34814
 
34745
34815
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
34746
34816
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
@@ -34936,7 +35006,7 @@ var pageSchema = {
34936
35006
  };
34937
35007
 
34938
35008
  // src/version.ts
34939
- var VERSION = "0.3.2";
35009
+ var VERSION = "0.3.4";
34940
35010
 
34941
35011
  // src/client.ts
34942
35012
  import { dirname, join } from "node:path";
@@ -35260,8 +35330,15 @@ function registerLocationTools(server) {
35260
35330
  }
35261
35331
 
35262
35332
  // node_modules/@fetchproxy/protocol/dist/frames.js
35263
- var PROTOCOL_VERSION = 2;
35333
+ var PROTOCOL_VERSION = 3;
35264
35334
  var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
35335
+ function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
35336
+ const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
35337
+ out.set(mcpHelloNonce, 0);
35338
+ out.set(extHelloNonce, mcpHelloNonce.length);
35339
+ out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
35340
+ return out;
35341
+ }
35265
35342
  var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
35266
35343
  "fetch",
35267
35344
  "read_cookies",
@@ -35272,7 +35349,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
35272
35349
  "read_indexed_db",
35273
35350
  "read_dom",
35274
35351
  "download",
35275
- "graphql"
35352
+ "graphql",
35353
+ "write_cookies"
35276
35354
  ]);
35277
35355
 
35278
35356
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
@@ -35383,6 +35461,15 @@ function assertHttpUrl(x, label) {
35383
35461
  throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
35384
35462
  }
35385
35463
  }
35464
+ function assertCookiePath(x, label) {
35465
+ assertString(x, label);
35466
+ if (!x.startsWith("/") || x.startsWith("//")) {
35467
+ throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
35468
+ }
35469
+ if (x.includes("?") || x.includes("#") || x.includes("\\")) {
35470
+ throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
35471
+ }
35472
+ }
35386
35473
  function assertHttpsOriginOnly(x, label) {
35387
35474
  assertString(x, label);
35388
35475
  let u;
@@ -35827,13 +35914,43 @@ function validateInnerRequest(raw) {
35827
35914
  }
35828
35915
  assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35829
35916
  assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
35917
+ if (raw.init.path !== void 0)
35918
+ assertCookiePath(raw.init.path, "inner.init.path");
35830
35919
  for (const k of Object.keys(raw.init)) {
35831
- if (k !== "origin" && k !== "keys") {
35920
+ if (k !== "origin" && k !== "keys" && k !== "path") {
35832
35921
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
35833
35922
  }
35834
35923
  }
35835
35924
  return raw;
35836
35925
  }
35926
+ if (raw.op === "write_cookies") {
35927
+ assertObject(raw.init, "inner.init");
35928
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35929
+ if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
35930
+ throw new ProtocolError("inner.init.cookies: must be a non-empty array");
35931
+ }
35932
+ for (const [i, entry] of raw.init.cookies.entries()) {
35933
+ assertObject(entry, `inner.init.cookies[${i}]`);
35934
+ assertString(entry.name, `inner.init.cookies[${i}].name`);
35935
+ if (!SCOPE_KEY_RE.test(entry.name)) {
35936
+ throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
35937
+ }
35938
+ assertString(entry.value, `inner.init.cookies[${i}].value`);
35939
+ for (const k of Object.keys(entry)) {
35940
+ if (k !== "name" && k !== "value") {
35941
+ throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
35942
+ }
35943
+ }
35944
+ }
35945
+ if (raw.init.path !== void 0)
35946
+ assertCookiePath(raw.init.path, "inner.init.path");
35947
+ for (const k of Object.keys(raw.init)) {
35948
+ if (k !== "origin" && k !== "cookies" && k !== "path") {
35949
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
35950
+ }
35951
+ }
35952
+ return raw;
35953
+ }
35837
35954
  if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
35838
35955
  assertObject(raw.init, "inner.init");
35839
35956
  if (raw.init.origin === void 0) {
@@ -36020,7 +36137,7 @@ function validateInnerRequest(raw) {
36020
36137
  }
36021
36138
  return raw;
36022
36139
  }
36023
- 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)}`);
36140
+ 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)}`);
36024
36141
  }
36025
36142
  function assertNonEmptyKeyArray(value, label) {
36026
36143
  if (!Array.isArray(value)) {
@@ -36081,6 +36198,18 @@ function validateInnerResponse(raw) {
36081
36198
  }
36082
36199
  return raw;
36083
36200
  }
36201
+ if (op === "write_cookies") {
36202
+ if (raw.written === void 0) {
36203
+ throw new ProtocolError("inner.written: missing on write_cookies response");
36204
+ }
36205
+ if (!Array.isArray(raw.written)) {
36206
+ throw new ProtocolError("inner.written: must be an array");
36207
+ }
36208
+ for (const [i, name] of raw.written.entries()) {
36209
+ assertString(name, `inner.written[${i}]`);
36210
+ }
36211
+ return raw;
36212
+ }
36084
36213
  if (op === "read_local_storage" || op === "read_session_storage") {
36085
36214
  if (raw.values === void 0) {
36086
36215
  throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
@@ -36558,6 +36687,148 @@ async function awaitSessionReady(ready, opts) {
36558
36687
  }
36559
36688
  }
36560
36689
 
36690
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
36691
+ import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
36692
+ import { join as join3 } from "node:path";
36693
+
36694
+ // node_modules/@fetchproxy/server/dist/identity.js
36695
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36696
+ import { join as join2 } from "node:path";
36697
+ import { homedir } from "node:os";
36698
+ var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36699
+ var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36700
+ function defaultIdentityDir() {
36701
+ return join2(homedir(), ".fetchproxy", "identity");
36702
+ }
36703
+ function safeIdentityFileBase(serverName) {
36704
+ if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
36705
+ throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
36706
+ }
36707
+ return serverName.replace(/\//g, "_");
36708
+ }
36709
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36710
+ const safeFile = safeIdentityFileBase(serverName);
36711
+ const path = join2(dir, `${safeFile}.json`);
36712
+ await mkdir(dir, { recursive: true, mode: 448 });
36713
+ try {
36714
+ const raw = await readFile(path, "utf8");
36715
+ const j2 = JSON.parse(raw);
36716
+ return {
36717
+ x25519Priv: fromB64(j2.x25519Priv),
36718
+ x25519Pub: fromB64(j2.x25519Pub),
36719
+ ed25519Priv: fromB64(j2.ed25519Priv),
36720
+ ed25519Pub: fromB64(j2.ed25519Pub),
36721
+ createdAt: j2.createdAt
36722
+ };
36723
+ } catch (e) {
36724
+ if (e.code !== "ENOENT")
36725
+ throw e;
36726
+ }
36727
+ const x = await generateX25519();
36728
+ const ed = await generateEd25519();
36729
+ const id = {
36730
+ x25519Priv: x.privateKey,
36731
+ x25519Pub: x.publicKey,
36732
+ ed25519Priv: ed.privateKey,
36733
+ ed25519Pub: ed.publicKey,
36734
+ createdAt: Date.now()
36735
+ };
36736
+ const j = {
36737
+ x25519Priv: toB64(id.x25519Priv),
36738
+ x25519Pub: toB64(id.x25519Pub),
36739
+ ed25519Priv: toB64(id.ed25519Priv),
36740
+ ed25519Pub: toB64(id.ed25519Pub),
36741
+ createdAt: id.createdAt
36742
+ };
36743
+ await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
36744
+ await chmod(path, 384);
36745
+ return id;
36746
+ }
36747
+
36748
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
36749
+ function fileExtensionTrust(args) {
36750
+ return {
36751
+ allowNew: args.allowNew,
36752
+ location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
36753
+ read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
36754
+ write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
36755
+ };
36756
+ }
36757
+ var TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
36758
+ function allowNewExtensionIdentity(explicit, env = process.env) {
36759
+ if (explicit !== void 0)
36760
+ return explicit;
36761
+ return env[TRUST_NEW_EXTENSION_ENV] === "1";
36762
+ }
36763
+ function decideExtensionTrust(args) {
36764
+ const { pin, hello, allowNew, serverName } = args;
36765
+ if (!pin)
36766
+ return { decision: "first-use" };
36767
+ if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
36768
+ return { decision: "pinned" };
36769
+ }
36770
+ const trustPath = args.location ?? extensionTrustPathHint(serverName);
36771
+ if (allowNew) {
36772
+ return {
36773
+ decision: "replace",
36774
+ 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.`
36775
+ };
36776
+ }
36777
+ return {
36778
+ decision: "refused",
36779
+ 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.`
36780
+ };
36781
+ }
36782
+ function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
36783
+ return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36784
+ }
36785
+ function extensionTrustPathHint(serverName) {
36786
+ try {
36787
+ return extensionTrustPath(serverName);
36788
+ } catch {
36789
+ return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
36790
+ }
36791
+ }
36792
+ function isPin(x) {
36793
+ if (!x || typeof x !== "object")
36794
+ return false;
36795
+ const r = x;
36796
+ return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
36797
+ }
36798
+ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
36799
+ const path = extensionTrustPath(serverName, dir);
36800
+ let raw;
36801
+ try {
36802
+ raw = await readFile2(path, "utf8");
36803
+ } catch (e) {
36804
+ if (e.code === "ENOENT")
36805
+ return null;
36806
+ throw e;
36807
+ }
36808
+ let parsed;
36809
+ try {
36810
+ parsed = JSON.parse(raw);
36811
+ } catch {
36812
+ throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
36813
+ }
36814
+ if (!isPin(parsed)) {
36815
+ throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
36816
+ }
36817
+ return {
36818
+ identityX25519Pub: parsed.identityX25519Pub,
36819
+ identityEd25519Pub: parsed.identityEd25519Pub,
36820
+ pinnedAt: parsed.pinnedAt
36821
+ };
36822
+ }
36823
+ async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
36824
+ const path = extensionTrustPath(serverName, dir);
36825
+ await mkdir2(dir, { recursive: true, mode: 448 });
36826
+ const tmp = `${path}.tmp`;
36827
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
36828
+ await chmod2(tmp, 384);
36829
+ await rename(tmp, path);
36830
+ }
36831
+
36561
36832
  // node_modules/@fetchproxy/server/dist/host.js
36562
36833
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
36563
36834
  var enc2 = new TextEncoder();
@@ -36611,9 +36882,12 @@ async function startHost(opts) {
36611
36882
  }
36612
36883
  resetSessionPromise();
36613
36884
  let extensionHello = null;
36885
+ let extensionClaim = null;
36614
36886
  wss.on("connection", (ws) => {
36615
36887
  let identified = null;
36616
36888
  let peerMcpId = null;
36889
+ let closed = false;
36890
+ let pinOnReady = false;
36617
36891
  ws.on("message", async (data) => {
36618
36892
  try {
36619
36893
  let frame;
@@ -36625,10 +36899,43 @@ async function startHost(opts) {
36625
36899
  return;
36626
36900
  }
36627
36901
  if (frame.type === "hello" && frame.role === "extension") {
36628
- if (extensionWs) {
36902
+ if (extensionWs || extensionClaim) {
36629
36903
  ws.close(1008, "extension already connected");
36630
36904
  return;
36631
36905
  }
36906
+ extensionClaim = ws;
36907
+ let pin;
36908
+ try {
36909
+ pin = await opts.extensionTrust.read();
36910
+ } catch (e) {
36911
+ console.error(`[fetchproxy] ${String(e)}`);
36912
+ if (extensionClaim === ws)
36913
+ extensionClaim = null;
36914
+ ws.close(1008, "extension pin unreadable");
36915
+ return;
36916
+ }
36917
+ const outcome = decideExtensionTrust({
36918
+ pin,
36919
+ hello: frame,
36920
+ allowNew: opts.extensionTrust.allowNew,
36921
+ serverName: opts.ownServerName,
36922
+ location: opts.extensionTrust.location
36923
+ });
36924
+ if (outcome.decision === "refused") {
36925
+ console.warn(outcome.message);
36926
+ if (extensionClaim === ws)
36927
+ extensionClaim = null;
36928
+ ws.close(1008, "extension identity is not the pinned one");
36929
+ return;
36930
+ }
36931
+ if (outcome.decision === "replace")
36932
+ console.warn(outcome.message);
36933
+ if (closed || ws.readyState !== import_websocket.default.OPEN) {
36934
+ if (extensionClaim === ws)
36935
+ extensionClaim = null;
36936
+ return;
36937
+ }
36938
+ pinOnReady = outcome.decision !== "pinned";
36632
36939
  identified = "extension";
36633
36940
  extensionWs = ws;
36634
36941
  extensionHello = frame;
@@ -36640,6 +36947,8 @@ async function startHost(opts) {
36640
36947
  console.error("[fetchproxy] onPairCode threw:", e);
36641
36948
  }
36642
36949
  }
36950
+ for (const slot of peers.values())
36951
+ slot.ws.send(JSON.stringify(frame));
36643
36952
  ws.send(JSON.stringify(ownHello));
36644
36953
  for (const slot of peers.values()) {
36645
36954
  ws.send(JSON.stringify(slot.helloFrame));
@@ -36675,6 +36984,8 @@ async function startHost(opts) {
36675
36984
  peers.set(frame.mcpId, { ws, helloFrame: frame });
36676
36985
  if (extensionWs)
36677
36986
  extensionWs.send(JSON.stringify(frame));
36987
+ if (extensionHello)
36988
+ ws.send(JSON.stringify(extensionHello));
36678
36989
  return;
36679
36990
  }
36680
36991
  if (frame.type === "ready") {
@@ -36686,7 +36997,7 @@ async function startHost(opts) {
36686
36997
  }
36687
36998
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
36688
36999
  const extNonce = fromB64(extensionHello.sessionNonce);
36689
- const msg = concatBytes(ownSessionNonce, extNonce);
37000
+ const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
36690
37001
  const sig = fromB64(frame.sessionSig);
36691
37002
  let sigOk = false;
36692
37003
  try {
@@ -36699,6 +37010,18 @@ async function startHost(opts) {
36699
37010
  ws.close(1008, "extension session signature invalid");
36700
37011
  return;
36701
37012
  }
37013
+ if (pinOnReady) {
37014
+ pinOnReady = false;
37015
+ try {
37016
+ await opts.extensionTrust.write({
37017
+ identityX25519Pub: extensionHello.identityX25519Pub,
37018
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
37019
+ pinnedAt: Date.now()
37020
+ });
37021
+ } catch (e) {
37022
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
37023
+ }
37024
+ }
36702
37025
  const extPub = fromB64(frame.extensionSessionPub);
36703
37026
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
36704
37027
  const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
@@ -36752,6 +37075,9 @@ async function startHost(opts) {
36752
37075
  }
36753
37076
  });
36754
37077
  ws.on("close", () => {
37078
+ closed = true;
37079
+ if (extensionClaim === ws)
37080
+ extensionClaim = null;
36755
37081
  if (identified === "extension" && extensionWs === ws) {
36756
37082
  extensionWs = null;
36757
37083
  extensionHello = null;
@@ -36842,11 +37168,84 @@ async function startPeer(opts) {
36842
37168
  resolveFirstReady = resolve;
36843
37169
  rejectFirstReady = reject;
36844
37170
  });
37171
+ let extensionHello = null;
37172
+ let warnedUnverifiable = false;
37173
+ let cachedPin = void 0;
37174
+ const authenticateExtension = async (sessionSig, extensionSessionPub) => {
37175
+ if (!extensionHello) {
37176
+ if (opts.requireExtensionIdentity) {
37177
+ 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.`);
37178
+ return false;
37179
+ }
37180
+ if (!warnedUnverifiable) {
37181
+ warnedUnverifiable = true;
37182
+ 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.`);
37183
+ }
37184
+ return true;
37185
+ }
37186
+ const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
37187
+ let sigOk = false;
37188
+ try {
37189
+ sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
37190
+ } catch {
37191
+ sigOk = false;
37192
+ }
37193
+ if (!sigOk) {
37194
+ console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
37195
+ return false;
37196
+ }
37197
+ if (cachedPin === void 0) {
37198
+ try {
37199
+ cachedPin = await opts.extensionTrust.read();
37200
+ } catch (e) {
37201
+ console.error(`[fetchproxy] ${String(e)}`);
37202
+ return false;
37203
+ }
37204
+ }
37205
+ const pin = cachedPin;
37206
+ const outcome = decideExtensionTrust({
37207
+ pin,
37208
+ hello: extensionHello,
37209
+ allowNew: opts.extensionTrust.allowNew,
37210
+ serverName: opts.serverName,
37211
+ location: opts.extensionTrust.location
37212
+ });
37213
+ if (outcome.decision === "refused") {
37214
+ console.warn(outcome.message);
37215
+ return false;
37216
+ }
37217
+ if (outcome.decision === "replace")
37218
+ console.warn(outcome.message);
37219
+ if (outcome.decision !== "pinned") {
37220
+ try {
37221
+ const written = {
37222
+ identityX25519Pub: extensionHello.identityX25519Pub,
37223
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
37224
+ pinnedAt: Date.now()
37225
+ };
37226
+ await opts.extensionTrust.write(written);
37227
+ cachedPin = written;
37228
+ } catch (e) {
37229
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
37230
+ }
37231
+ }
37232
+ return true;
37233
+ };
36845
37234
  const onMessage = async (data) => {
36846
37235
  try {
36847
37236
  const raw = JSON.parse(data.toString());
36848
37237
  const frame = validateFrame(raw);
37238
+ if (frame.type === "hello" && frame.role === "extension") {
37239
+ extensionHello = frame;
37240
+ return;
37241
+ }
36849
37242
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
37243
+ const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
37244
+ if (!authorised) {
37245
+ ws.close(1008, "extension identity refused");
37246
+ rejectFirstReady(new Error("peer: extension identity refused"));
37247
+ return;
37248
+ }
36850
37249
  const extPub = fromB64(frame.extensionSessionPub);
36851
37250
  const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
36852
37251
  const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
@@ -36927,57 +37326,6 @@ async function startPeer(opts) {
36927
37326
  return handle;
36928
37327
  }
36929
37328
 
36930
- // node_modules/@fetchproxy/server/dist/identity.js
36931
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36932
- import { join as join2 } from "node:path";
36933
- import { homedir } from "node:os";
36934
- var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36935
- var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36936
- function defaultIdentityDir() {
36937
- return join2(homedir(), ".fetchproxy", "identity");
36938
- }
36939
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36940
- if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
36941
- throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
36942
- }
36943
- const safeFile = serverName.replace(/\//g, "_");
36944
- const path = join2(dir, `${safeFile}.json`);
36945
- await mkdir(dir, { recursive: true, mode: 448 });
36946
- try {
36947
- const raw = await readFile(path, "utf8");
36948
- const j2 = JSON.parse(raw);
36949
- return {
36950
- x25519Priv: fromB64(j2.x25519Priv),
36951
- x25519Pub: fromB64(j2.x25519Pub),
36952
- ed25519Priv: fromB64(j2.ed25519Priv),
36953
- ed25519Pub: fromB64(j2.ed25519Pub),
36954
- createdAt: j2.createdAt
36955
- };
36956
- } catch (e) {
36957
- if (e.code !== "ENOENT")
36958
- throw e;
36959
- }
36960
- const x = await generateX25519();
36961
- const ed = await generateEd25519();
36962
- const id = {
36963
- x25519Priv: x.privateKey,
36964
- x25519Pub: x.publicKey,
36965
- ed25519Priv: ed.privateKey,
36966
- ed25519Pub: ed.publicKey,
36967
- createdAt: Date.now()
36968
- };
36969
- const j = {
36970
- x25519Priv: toB64(id.x25519Priv),
36971
- x25519Pub: toB64(id.x25519Pub),
36972
- ed25519Priv: toB64(id.ed25519Priv),
36973
- ed25519Pub: toB64(id.ed25519Pub),
36974
- createdAt: id.createdAt
36975
- };
36976
- await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
36977
- await chmod(path, 384);
36978
- return id;
36979
- }
36980
-
36981
37329
  // node_modules/@fetchproxy/server/dist/error-kind.js
36982
37330
  function classifyFetchError(error51) {
36983
37331
  if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
@@ -37018,6 +37366,17 @@ function classifyBridgeError(err) {
37018
37366
  }
37019
37367
 
37020
37368
  // node_modules/@fetchproxy/server/dist/ws-server.js
37369
+ function envWsPort() {
37370
+ const raw = process.env.FETCHPROXY_WS_PORT;
37371
+ if (raw === void 0 || raw.trim() === "")
37372
+ return void 0;
37373
+ if (!/^\d+$/.test(raw.trim()))
37374
+ return void 0;
37375
+ const port = Number(raw.trim());
37376
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
37377
+ return void 0;
37378
+ return port;
37379
+ }
37021
37380
  var FetchproxyProtocolError = class extends Error {
37022
37381
  constructor(message) {
37023
37382
  super(message);
@@ -37059,6 +37418,39 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
37059
37418
  this.hint = hint;
37060
37419
  }
37061
37420
  };
37421
+ var FetchproxyHintedError = class extends FetchproxyProtocolError {
37422
+ /** The extension's raw rejection, unmodified. */
37423
+ originalError;
37424
+ /** What the user should actually do, in prose. */
37425
+ hint;
37426
+ constructor(originalError, hint) {
37427
+ super(`${originalError} \u2014 ${hint}`);
37428
+ this.name = "FetchproxyHintedError";
37429
+ this.originalError = originalError;
37430
+ this.hint = hint;
37431
+ }
37432
+ };
37433
+ var FetchproxyScopeError = class extends FetchproxyHintedError {
37434
+ constructor(originalError) {
37435
+ 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.");
37436
+ this.name = "FetchproxyScopeError";
37437
+ }
37438
+ };
37439
+ var FetchproxyNoTabError = class extends FetchproxyHintedError {
37440
+ constructor(originalError) {
37441
+ 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.");
37442
+ this.name = "FetchproxyNoTabError";
37443
+ }
37444
+ };
37445
+ var SCOPE_REJECTION = /not in declared/;
37446
+ var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
37447
+ function protocolErrorFrom(error51) {
37448
+ if (SCOPE_REJECTION.test(error51))
37449
+ return new FetchproxyScopeError(error51);
37450
+ if (NO_TAB_REJECTION.test(error51))
37451
+ return new FetchproxyNoTabError(error51);
37452
+ return new FetchproxyProtocolError(error51);
37453
+ }
37062
37454
  var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
37063
37455
  url;
37064
37456
  timeoutMs;
@@ -37088,6 +37480,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
37088
37480
  }
37089
37481
  };
37090
37482
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
37483
+ function normalizeCookiePath(path) {
37484
+ if (path === void 0 || path === "")
37485
+ return void 0;
37486
+ const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
37487
+ try {
37488
+ assertCookiePath(trimmed, "path");
37489
+ } catch (e) {
37490
+ throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
37491
+ }
37492
+ return trimmed;
37493
+ }
37091
37494
  function assertSubdomainLabel(label) {
37092
37495
  if (!SUBDOMAIN_LABEL_RE.test(label)) {
37093
37496
  throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
@@ -37152,6 +37555,9 @@ var FetchproxyServer = class {
37152
37555
  // them off from `pending` (fetch) and `pendingReadCookies` (legacy
37153
37556
  // string-shape) so the response routing in `onInner` stays linear.
37154
37557
  pendingStorage = /* @__PURE__ */ new Map();
37558
+ // 1.12.0+: write-cookies awaiters resolve the list of names actually
37559
+ // written, so a caller can confirm rather than assume.
37560
+ pendingWriteCookies = /* @__PURE__ */ new Map();
37155
37561
  // 0.3.0+: capture-header awaiters resolve a single string.
37156
37562
  pendingCapture = /* @__PURE__ */ new Map();
37157
37563
  // capture_redirect awaiters resolve the captured redirect URL string.
@@ -37218,7 +37624,7 @@ var FetchproxyServer = class {
37218
37624
  }
37219
37625
  }
37220
37626
  this.opts = {
37221
- port: opts.port ?? 37149,
37627
+ port: opts.port ?? envWsPort() ?? 37149,
37222
37628
  host: opts.host ?? "127.0.0.1",
37223
37629
  serverName: opts.serverName,
37224
37630
  version: opts.version,
@@ -37278,6 +37684,8 @@ var FetchproxyServer = class {
37278
37684
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
37279
37685
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
37280
37686
  identityDir: opts.identityDir,
37687
+ allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
37688
+ requireExtensionIdentity: opts.requireExtensionIdentity,
37281
37689
  onPairCode: opts.onPairCode
37282
37690
  };
37283
37691
  }
@@ -37377,7 +37785,8 @@ var FetchproxyServer = class {
37377
37785
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
37378
37786
  ownDomSelectors: this.opts.domSelectors,
37379
37787
  ownGraphqlOps: this.opts.graphqlOps,
37380
- onPairCode: this.opts.onPairCode
37788
+ onPairCode: this.opts.onPairCode,
37789
+ extensionTrust: this.extensionTrust()
37381
37790
  });
37382
37791
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
37383
37792
  this.hostHandle.onExtensionDisconnect(() => {
@@ -37406,7 +37815,9 @@ var FetchproxyServer = class {
37406
37815
  localStoragePointers: this.opts.localStoragePointers,
37407
37816
  sessionStoragePointers: this.opts.sessionStoragePointers,
37408
37817
  domSelectors: this.opts.domSelectors,
37409
- graphqlOps: this.opts.graphqlOps
37818
+ graphqlOps: this.opts.graphqlOps,
37819
+ extensionTrust: this.extensionTrust(),
37820
+ requireExtensionIdentity: this.opts.requireExtensionIdentity
37410
37821
  });
37411
37822
  this.peerHandle.onInner((inner) => this.onInner(inner));
37412
37823
  this.peerHandle.onRenegotiate(() => {
@@ -37545,6 +37956,23 @@ var FetchproxyServer = class {
37545
37956
  markActive() {
37546
37957
  this.noteActivityForKeepalive();
37547
37958
  }
37959
+ /**
37960
+ * #208: this MCP's pin on the extension's identity, stored beside its own
37961
+ * identity key and so following `identityDir` wherever the caller put it.
37962
+ *
37963
+ * `allowNewExtensionIdentity` falls back to an environment variable when the
37964
+ * caller expressed no opinion, because the thirteen MCPs that construct this
37965
+ * class are separate packages: an operator whose extension re-install has
37966
+ * just locked all of them out needs one lever that does not require patching
37967
+ * every one of them.
37968
+ */
37969
+ extensionTrust() {
37970
+ return fileExtensionTrust({
37971
+ serverName: this.opts.serverName,
37972
+ dir: this.opts.identityDir,
37973
+ allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
37974
+ });
37975
+ }
37548
37976
  noteActivityForKeepalive() {
37549
37977
  const intervalMs = this.opts.keepAliveIntervalMs;
37550
37978
  if (intervalMs <= 0)
@@ -37608,6 +38036,7 @@ var FetchproxyServer = class {
37608
38036
  this.pending.delete(id);
37609
38037
  this.pendingReadCookies.delete(id);
37610
38038
  this.pendingStorage.delete(id);
38039
+ this.pendingWriteCookies.delete(id);
37611
38040
  this.pendingCapture.delete(id);
37612
38041
  this.pendingRedirect.delete(id);
37613
38042
  this.pendingDownload.delete(id);
@@ -37729,7 +38158,7 @@ var FetchproxyServer = class {
37729
38158
  port: this.opts.port
37730
38159
  });
37731
38160
  }
37732
- return new FetchproxyProtocolError(result.error);
38161
+ return protocolErrorFrom(result.error);
37733
38162
  }
37734
38163
  /**
37735
38164
  * Convenience wrapper around `fetch()`. Builds the URL from a path
@@ -37765,10 +38194,20 @@ var FetchproxyServer = class {
37765
38194
  }
37766
38195
  const url2 = isAbsolute ? path : `https://${host}${path}`;
37767
38196
  assertUrlInDomains("request url", url2, this.opts.domains);
38197
+ let tabUrl = `https://${host}/`;
38198
+ if (opts.viaTab !== void 0) {
38199
+ try {
38200
+ new URL(opts.viaTab);
38201
+ } catch {
38202
+ throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
38203
+ }
38204
+ assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
38205
+ tabUrl = opts.viaTab;
38206
+ }
37768
38207
  const init = {
37769
38208
  url: url2,
37770
38209
  method,
37771
- tabUrl: `https://${host}/`,
38210
+ tabUrl,
37772
38211
  headers: opts.headers,
37773
38212
  body: opts.body
37774
38213
  };
@@ -37980,9 +38419,14 @@ var FetchproxyServer = class {
37980
38419
  let inner;
37981
38420
  if (opts.keys !== void 0) {
37982
38421
  this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
38422
+ const cookiePath = normalizeCookiePath(opts.path);
37983
38423
  const initV3 = {
38424
+ // Origin stays BARE. The path travels as its own validated field —
38425
+ // `assertHttpsOriginOnly` deliberately refuses a path here so one
38426
+ // cannot be used to re-point the read past the domain gate.
37984
38427
  origin: `https://${host}`,
37985
- keys: [...opts.keys]
38428
+ keys: [...opts.keys],
38429
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
37986
38430
  };
37987
38431
  inner = { type: "request", id, op: "read_cookies", init: initV3 };
37988
38432
  } else {
@@ -37995,10 +38439,65 @@ var FetchproxyServer = class {
37995
38439
  await this.sendInnerFrame(inner);
37996
38440
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37997
38441
  if (!result.ok) {
37998
- throw new FetchproxyProtocolError(result.error);
38442
+ throw protocolErrorFrom(result.error);
37999
38443
  }
38000
38444
  return result.cookies;
38001
38445
  }
38446
+ /**
38447
+ * 1.12.0+: overwrite the value of cookies this MCP already declares.
38448
+ *
38449
+ * The bridge's only write verb, and it exists for one failure class. Sites
38450
+ * that ROTATE a credential cookie hand back a new value on every refresh; if
38451
+ * the MCP refreshes and keeps the result to itself, the copy in the browser's
38452
+ * cookie jar is dead, and the user gets signed out of a tab they never
38453
+ * touched — usually reported to them as "inactivity". Writing the rotated
38454
+ * value back is the only thing that repairs it.
38455
+ *
38456
+ * Requires `'write_cookies'` in capabilities, which the user approves at pair
38457
+ * time as its own line. Every name must ALSO be in declared `cookieKeys`: a
38458
+ * write can never reach a cookie the MCP was not already trusted to read, so
38459
+ * granting it cannot widen which cookies are in play — only what may be done
38460
+ * to the ones already listed.
38461
+ *
38462
+ * The extension refuses the whole request unless every named cookie already
38463
+ * exists; this refreshes a value in place and deliberately cannot author new
38464
+ * cookies. Returns the names actually written.
38465
+ */
38466
+ async writeCookies(opts) {
38467
+ if (!this.opts.capabilities.includes("write_cookies")) {
38468
+ throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
38469
+ }
38470
+ const names = Object.keys(opts.cookies);
38471
+ if (names.length === 0) {
38472
+ throw new Error("FetchproxyServer.writeCookies(): no cookies given");
38473
+ }
38474
+ await this.ensureConnected();
38475
+ this.throwIfPendingPair();
38476
+ if (opts.subdomain !== void 0)
38477
+ assertSubdomainLabel(opts.subdomain);
38478
+ const baseDomain = this.resolveBaseDomain(opts.domain);
38479
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
38480
+ this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
38481
+ const cookiePath = normalizeCookiePath(opts.path);
38482
+ const id = this.nextRequestId++;
38483
+ const inner = {
38484
+ type: "request",
38485
+ id,
38486
+ op: "write_cookies",
38487
+ init: {
38488
+ // Bare origin, same invariant as the read path: a path must never be
38489
+ // able to move the request past the domain gate.
38490
+ origin: `https://${host}`,
38491
+ cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
38492
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
38493
+ }
38494
+ };
38495
+ const pending = new Promise((resolve, reject) => {
38496
+ this.pendingWriteCookies.set(id, { resolve, reject });
38497
+ });
38498
+ await this.sendInnerFrame(inner);
38499
+ return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
38500
+ }
38002
38501
  /**
38003
38502
  * 0.3.0+: read declared localStorage keys from the user's signed-in
38004
38503
  * tab. Requires `'read_local_storage'` in capabilities AND each key
@@ -38545,7 +39044,7 @@ var FetchproxyServer = class {
38545
39044
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
38546
39045
  }
38547
39046
  } else {
38548
- storageCb.reject(new FetchproxyProtocolError(inner.error));
39047
+ storageCb.reject(protocolErrorFrom(inner.error));
38549
39048
  }
38550
39049
  return;
38551
39050
  }
@@ -38559,7 +39058,7 @@ var FetchproxyServer = class {
38559
39058
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
38560
39059
  }
38561
39060
  } else {
38562
- captureCb.reject(new FetchproxyProtocolError(inner.error));
39061
+ captureCb.reject(protocolErrorFrom(inner.error));
38563
39062
  }
38564
39063
  return;
38565
39064
  }
@@ -38573,7 +39072,7 @@ var FetchproxyServer = class {
38573
39072
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
38574
39073
  }
38575
39074
  } else {
38576
- redirectCb.reject(new FetchproxyProtocolError(inner.error));
39075
+ redirectCb.reject(protocolErrorFrom(inner.error));
38577
39076
  }
38578
39077
  return;
38579
39078
  }
@@ -38587,7 +39086,7 @@ var FetchproxyServer = class {
38587
39086
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
38588
39087
  }
38589
39088
  } else {
38590
- idbCb.reject(new FetchproxyProtocolError(inner.error));
39089
+ idbCb.reject(protocolErrorFrom(inner.error));
38591
39090
  }
38592
39091
  return;
38593
39092
  }
@@ -38601,7 +39100,7 @@ var FetchproxyServer = class {
38601
39100
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
38602
39101
  }
38603
39102
  } else {
38604
- downloadCb.reject(new FetchproxyProtocolError(inner.error));
39103
+ downloadCb.reject(protocolErrorFrom(inner.error));
38605
39104
  }
38606
39105
  return;
38607
39106
  }
@@ -38615,7 +39114,17 @@ var FetchproxyServer = class {
38615
39114
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
38616
39115
  }
38617
39116
  } else {
38618
- graphqlCb.reject(new FetchproxyProtocolError(inner.error));
39117
+ graphqlCb.reject(protocolErrorFrom(inner.error));
39118
+ }
39119
+ return;
39120
+ }
39121
+ const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
39122
+ if (writeCookiesCb) {
39123
+ this.pendingWriteCookies.delete(inner.id);
39124
+ if (inner.ok && inner.op === "write_cookies") {
39125
+ writeCookiesCb.resolve([...inner.written]);
39126
+ } else {
39127
+ writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
38619
39128
  }
38620
39129
  return;
38621
39130
  }
@@ -38658,6 +39167,9 @@ var FetchproxyServer = class {
38658
39167
  for (const { reject } of this.pendingStorage.values())
38659
39168
  reject(err);
38660
39169
  this.pendingStorage.clear();
39170
+ for (const { reject } of this.pendingWriteCookies.values())
39171
+ reject(err);
39172
+ this.pendingWriteCookies.clear();
38661
39173
  for (const { reject } of this.pendingCapture.values())
38662
39174
  reject(err);
38663
39175
  this.pendingCapture.clear();
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'; // x-release-please-version
2
+ export const VERSION = '0.3.4'; // x-release-please-version
package/mint.yaml ADDED
@@ -0,0 +1,56 @@
1
+ version: 1
2
+ name: TripAdvisor
3
+ slug: tripadvisor
4
+ summary: >-
5
+ TripAdvisor location search, details, photos, and reviews for Claude via
6
+ the Terra API
7
+ #
8
+ # Hosting note (a comment, not user-facing summary text): this is a
9
+ # BROWSER-BRIDGE MCP — it reaches its site through the user's signed-in
10
+ # tab via the fetchproxy bridge. A bridged registration also needs runtime
11
+ # `fly-shared`, `bridge: true` and a `bridgePortEnv`, which are registration
12
+ # fields this manifest has no schema for (set them over the control API).
13
+ # `state.dataDir` below is required for `bridge`.
14
+ env:
15
+ - name: TRIPADVISOR_API_KEY
16
+ secret: true
17
+ required: true
18
+ help: >-
19
+ Your TripAdvisor Terra API key (tripadvisor.com/developers)
20
+ - name: TRIPADVISOR_REQUEST_TIMEOUT_MS
21
+ required: false
22
+ help: >-
23
+ Per-request timeout in milliseconds. Raise it if calls time out on slow
24
+ upstream responses.
25
+ - name: TRIPADVISOR_WS_PORT
26
+ required: false
27
+ help: >-
28
+ Concentrator port for the fetchproxy browser bridge (defaults to the
29
+ fleet-shared 37149). The runner injects a per-registration port for a
30
+ hosted bridged child, so this is the name to give bridgePortEnv at
31
+ registration.
32
+ - name: TRIPADVISOR_CACHE_TTL
33
+ required: false
34
+ help: >-
35
+ Seconds to cache search responses (default 300; 0 disables).
36
+ - name: TRIPADVISOR_STATIC_CACHE_TTL
37
+ required: false
38
+ help: >-
39
+ Seconds to cache details, photos and reviews (default 3600; 0 disables).
40
+ - name: TRIPADVISOR_DEBUG_LOG
41
+ required: false
42
+ help: >-
43
+ Set to 1 to log browser-bridge requests to stderr.
44
+ state:
45
+ dataDir: true
46
+ reason: >-
47
+ The fetchproxy identity lives at $HOME/.fetchproxy/identity/<name>.json
48
+ and the pair code derives from it. Without a persistent $HOME every cold
49
+ start mints a fresh identity and re-prompts pairing in the browser; the
50
+ API refuses bridge without it.
51
+ egress:
52
+ allow:
53
+ # Only hosts the SERVER process actually fetches. Hosts that appear
54
+ # solely in a URL this server BUILDS and returns are excluded.
55
+ - terra.tripadvisor.com
56
+ - tripadvisor.com
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/tripadvisor-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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>",
@@ -33,19 +33,21 @@
33
33
  ".claude-plugin",
34
34
  "skills",
35
35
  ".mcp.json",
36
- "server.json"
36
+ "server.json",
37
+ "mint.yaml"
37
38
  ],
38
39
  "scripts": {
39
40
  "build": "tsc && npm run bundle",
40
41
  "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
41
42
  "dev": "node dist/index.js",
42
- "test": "vitest run",
43
+ "test": "npm run typecheck && vitest run",
43
44
  "test:watch": "vitest",
44
- "test:coverage": "vitest run --coverage"
45
+ "test:coverage": "npm run typecheck && vitest run --coverage",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit"
45
47
  },
46
48
  "dependencies": {
47
- "@chrischall/mcp-utils": "^0.14.0",
48
- "@fetchproxy/server": "^1.7.0",
49
+ "@chrischall/mcp-utils": "^0.15.0",
50
+ "@fetchproxy/server": "^2.0.0",
49
51
  "@modelcontextprotocol/sdk": "^1.29.0",
50
52
  "dotenv": "^17.4.0",
51
53
  "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.2",
9
+ "version": "0.3.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/tripadvisor-mcp",
14
- "version": "0.3.2",
14
+ "version": "0.3.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: tripadvisor-mcp
2
+ name: tripadvisor
3
3
  description: TripAdvisor travel data via the Terra API through MCP. Use when the user asks to find hotels, restaurants, or attractions, look up a place's TripAdvisor rating/reviews/photos, compare places to stay or eat, or find what's near a location. Triggers on phrases like "find a hotel in", "best restaurants near", "TripAdvisor reviews for", "what's the rating of", "things to do in", or "attractions near me". Requires the @chrischall/tripadvisor-mcp package installed and the tripadvisor server registered (see Setup), plus a TripAdvisor Terra API key.
4
4
  ---
5
5