@chrischall/tripadvisor-mcp 0.3.1 → 0.3.2

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.1"
10
+ "version": "0.3.2"
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.1",
18
+ "version": "0.3.2",
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.1",
4
+ "version": "0.3.2",
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
@@ -34936,7 +34936,7 @@ var pageSchema = {
34936
34936
  };
34937
34937
 
34938
34938
  // src/version.ts
34939
- var VERSION = "0.3.1";
34939
+ var VERSION = "0.3.2";
34940
34940
 
34941
34941
  // src/client.ts
34942
34942
  import { dirname, join } from "node:path";
@@ -35271,7 +35271,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
35271
35271
  "capture_redirect",
35272
35272
  "read_indexed_db",
35273
35273
  "read_dom",
35274
- "download"
35274
+ "download",
35275
+ "graphql"
35275
35276
  ]);
35276
35277
 
35277
35278
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
@@ -35574,6 +35575,7 @@ function assertIndexedDbScopesArray(value, label) {
35574
35575
  }
35575
35576
  var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
35576
35577
  var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
35578
+ var GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
35577
35579
  function assertDomSelectorsArray(value, label) {
35578
35580
  if (!Array.isArray(value)) {
35579
35581
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -35610,6 +35612,37 @@ function assertDomSelectorsArray(value, label) {
35610
35612
  }
35611
35613
  }
35612
35614
  }
35615
+ function assertGraphqlOpsArray(value, label) {
35616
+ if (!Array.isArray(value)) {
35617
+ throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
35618
+ }
35619
+ const seen = /* @__PURE__ */ new Set();
35620
+ for (let i = 0; i < value.length; i++) {
35621
+ const entry = value[i];
35622
+ assertObject(entry, `${label}[${i}]`);
35623
+ if (entry.name === void 0) {
35624
+ throw new ProtocolError(`${label}[${i}].name: missing`);
35625
+ }
35626
+ if (entry.operationName === void 0) {
35627
+ throw new ProtocolError(`${label}[${i}].operationName: missing`);
35628
+ }
35629
+ if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
35630
+ throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
35631
+ }
35632
+ if (typeof entry.operationName !== "string" || !GRAPHQL_OP_NAME_RE.test(entry.operationName)) {
35633
+ throw new ProtocolError(`${label}[${i}].operationName: invalid ${JSON.stringify(entry.operationName)}`);
35634
+ }
35635
+ if (seen.has(entry.name)) {
35636
+ throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
35637
+ }
35638
+ seen.add(entry.name);
35639
+ for (const k of Object.keys(entry)) {
35640
+ if (k !== "name" && k !== "operationName") {
35641
+ throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
35642
+ }
35643
+ }
35644
+ }
35645
+ }
35613
35646
  function validateFrame(raw) {
35614
35647
  assertObject(raw, "frame");
35615
35648
  const t = raw.type;
@@ -35688,6 +35721,9 @@ function validateHello(raw) {
35688
35721
  if (raw.domSelectors !== void 0) {
35689
35722
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
35690
35723
  }
35724
+ if (raw.graphqlOps !== void 0) {
35725
+ assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
35726
+ }
35691
35727
  assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
35692
35728
  assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
35693
35729
  assertBase64(raw.sessionNonce, "hello.sessionNonce");
@@ -35937,6 +35973,28 @@ function validateInnerRequest(raw) {
35937
35973
  }
35938
35974
  return raw;
35939
35975
  }
35976
+ if (raw.op === "graphql_query") {
35977
+ assertObject(raw.init, "inner.init");
35978
+ if (raw.init.name === void 0)
35979
+ throw new ProtocolError("inner.init.name: missing");
35980
+ if (raw.init.variables === void 0) {
35981
+ throw new ProtocolError("inner.init.variables: missing");
35982
+ }
35983
+ assertString(raw.init.name, "inner.init.name");
35984
+ if (raw.init.name.length === 0) {
35985
+ throw new ProtocolError("inner.init.name: must be non-empty");
35986
+ }
35987
+ assertObject(raw.init.variables, "inner.init.variables");
35988
+ if (raw.init.tabUrl !== void 0) {
35989
+ assertString(raw.init.tabUrl, "inner.init.tabUrl");
35990
+ }
35991
+ for (const k of Object.keys(raw.init)) {
35992
+ if (k !== "name" && k !== "variables" && k !== "tabUrl") {
35993
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on graphql_query`);
35994
+ }
35995
+ }
35996
+ return raw;
35997
+ }
35940
35998
  if (raw.op === "download") {
35941
35999
  assertObject(raw.init, "inner.init");
35942
36000
  if (raw.init.url === void 0) {
@@ -35962,7 +36020,7 @@ function validateInnerRequest(raw) {
35962
36020
  }
35963
36021
  return raw;
35964
36022
  }
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)}`);
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)}`);
35966
36024
  }
35967
36025
  function assertNonEmptyKeyArray(value, label) {
35968
36026
  if (!Array.isArray(value)) {
@@ -35993,6 +36051,10 @@ function assertStringMap(value, label) {
35993
36051
  }
35994
36052
  }
35995
36053
  }
36054
+ var KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
36055
+ ...KNOWN_CAPABILITIES,
36056
+ "graphql_query"
36057
+ ]);
35996
36058
  function validateInnerResponse(raw) {
35997
36059
  assertPositiveInt(raw.id, "inner.id");
35998
36060
  if (raw.ok === true) {
@@ -36054,6 +36116,13 @@ function validateInnerResponse(raw) {
36054
36116
  assertStringMap(raw.values, "inner.values");
36055
36117
  return raw;
36056
36118
  }
36119
+ if (op === "graphql_query") {
36120
+ if (raw.data === void 0) {
36121
+ throw new ProtocolError("inner.data: missing on graphql_query response");
36122
+ }
36123
+ assertObject(raw.data, "inner.data");
36124
+ return raw;
36125
+ }
36057
36126
  if (op === "download") {
36058
36127
  assertObject(raw.value, "inner.value");
36059
36128
  assertString(raw.value.path, "inner.value.path");
@@ -36078,7 +36147,7 @@ function validateInnerResponse(raw) {
36078
36147
  if (raw.ok === false) {
36079
36148
  assertString(raw.error, "inner.error");
36080
36149
  if (raw.op !== void 0) {
36081
- if (typeof raw.op !== "string" || !KNOWN_CAPABILITIES.has(raw.op)) {
36150
+ if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
36082
36151
  throw new ProtocolError(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
36083
36152
  }
36084
36153
  }
@@ -36270,11 +36339,33 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
36270
36339
  };
36271
36340
  }
36272
36341
  async function openEncryptedFrame(sessionKey, frame) {
36273
- const iv = fromB64(frame.iv);
36274
- const ct = fromB64(frame.ciphertext);
36275
- const pt = await aesGcmOpen(sessionKey, iv, ct);
36276
- const parsed = JSON.parse(dec.decode(pt));
36277
- return validateInnerFrame(parsed);
36342
+ const result = await openEncryptedFrameDetailed(sessionKey, frame);
36343
+ if (result.stage === "ok")
36344
+ return result.inner;
36345
+ throw result.error instanceof Error ? result.error : new Error(String(result.error));
36346
+ }
36347
+ async function openEncryptedFrameDetailed(sessionKey, frame) {
36348
+ let pt;
36349
+ try {
36350
+ const iv = fromB64(frame.iv);
36351
+ const ct = fromB64(frame.ciphertext);
36352
+ pt = await aesGcmOpen(sessionKey, iv, ct);
36353
+ } catch (error51) {
36354
+ return { stage: "decrypt-failed", error: error51 };
36355
+ }
36356
+ let parsed;
36357
+ try {
36358
+ parsed = JSON.parse(dec.decode(pt));
36359
+ } catch (error51) {
36360
+ return { stage: "validation-failed", error: error51, recoveredId: void 0 };
36361
+ }
36362
+ try {
36363
+ const inner = validateInnerFrame(parsed);
36364
+ return { stage: "ok", inner };
36365
+ } catch (error51) {
36366
+ 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;
36367
+ return { stage: "validation-failed", error: error51, recoveredId };
36368
+ }
36278
36369
  }
36279
36370
 
36280
36371
  // node_modules/@fetchproxy/server/dist/election.js
@@ -36400,6 +36491,12 @@ async function buildServerHello(opts) {
36400
36491
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
36401
36492
  }));
36402
36493
  }
36494
+ if (opts.graphqlOps && opts.graphqlOps.length > 0) {
36495
+ hello.graphqlOps = opts.graphqlOps.map((d) => ({
36496
+ name: d.name,
36497
+ operationName: d.operationName
36498
+ }));
36499
+ }
36403
36500
  return hello;
36404
36501
  }
36405
36502
 
@@ -36490,7 +36587,8 @@ async function startHost(opts) {
36490
36587
  indexedDbScopes: opts.ownIndexedDbScopes,
36491
36588
  localStoragePointers: opts.ownLocalStoragePointers,
36492
36589
  sessionStoragePointers: opts.ownSessionStoragePointers,
36493
- domSelectors: opts.ownDomSelectors
36590
+ domSelectors: opts.ownDomSelectors,
36591
+ graphqlOps: opts.ownGraphqlOps
36494
36592
  });
36495
36593
  const ownSessionNonce = fromB64(ownHello.sessionNonce);
36496
36594
  let extensionWs = null;
@@ -36727,7 +36825,8 @@ async function startPeer(opts) {
36727
36825
  indexedDbScopes: opts.indexedDbScopes,
36728
36826
  domSelectors: opts.domSelectors,
36729
36827
  localStoragePointers: opts.localStoragePointers,
36730
- sessionStoragePointers: opts.sessionStoragePointers
36828
+ sessionStoragePointers: opts.sessionStoragePointers,
36829
+ graphqlOps: opts.graphqlOps
36731
36830
  });
36732
36831
  const sessionNonce = fromB64(hello.sessionNonce);
36733
36832
  ws.send(JSON.stringify(hello));
@@ -36771,10 +36870,20 @@ async function startPeer(opts) {
36771
36870
  return;
36772
36871
  if (!session.acceptInboundSeq(frame.seq))
36773
36872
  return;
36774
- try {
36775
- const inner = await openEncryptedFrame(session.sessionKey, frame);
36776
- innerListeners.forEach((cb) => cb(inner));
36777
- } catch {
36873
+ const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
36874
+ if (result.stage === "ok") {
36875
+ innerListeners.forEach((cb) => cb(result.inner));
36876
+ } else if (result.stage === "decrypt-failed") {
36877
+ } else {
36878
+ console.error("[fetchproxy] peer: received a frame that decrypted OK but failed validation:", result.error);
36879
+ if (result.recoveredId !== void 0) {
36880
+ innerListeners.forEach((cb) => cb({
36881
+ type: "response",
36882
+ id: result.recoveredId,
36883
+ ok: false,
36884
+ error: `malformed response failed protocol validation: ${String(result.error)}`
36885
+ }));
36886
+ }
36778
36887
  }
36779
36888
  }
36780
36889
  } catch (e) {
@@ -37051,6 +37160,10 @@ var FetchproxyServer = class {
37051
37160
  pendingIdb = /* @__PURE__ */ new Map();
37052
37161
  // download awaiters resolve the saved-file metadata (path + size + mime).
37053
37162
  pendingDownload = /* @__PURE__ */ new Map();
37163
+ // 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
37164
+ // shape is operation-specific, so the awaiter resolves `unknown` and the
37165
+ // caller narrows.
37166
+ pendingGraphql = /* @__PURE__ */ new Map();
37054
37167
  mcpId = null;
37055
37168
  identity = null;
37056
37169
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -37138,6 +37251,10 @@ var FetchproxyServer = class {
37138
37251
  selector: d.selector,
37139
37252
  ...d.attribute !== void 0 ? { attribute: d.attribute } : {}
37140
37253
  })),
37254
+ graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
37255
+ name: d.name,
37256
+ operationName: d.operationName
37257
+ })),
37141
37258
  // 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
37142
37259
  // adapter was about to set these to the same numbers anyway; the
37143
37260
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
@@ -37259,6 +37376,7 @@ var FetchproxyServer = class {
37259
37376
  ownLocalStoragePointers: this.opts.localStoragePointers,
37260
37377
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
37261
37378
  ownDomSelectors: this.opts.domSelectors,
37379
+ ownGraphqlOps: this.opts.graphqlOps,
37262
37380
  onPairCode: this.opts.onPairCode
37263
37381
  });
37264
37382
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
@@ -37287,7 +37405,8 @@ var FetchproxyServer = class {
37287
37405
  indexedDbScopes: this.opts.indexedDbScopes,
37288
37406
  localStoragePointers: this.opts.localStoragePointers,
37289
37407
  sessionStoragePointers: this.opts.sessionStoragePointers,
37290
- domSelectors: this.opts.domSelectors
37408
+ domSelectors: this.opts.domSelectors,
37409
+ graphqlOps: this.opts.graphqlOps
37291
37410
  });
37292
37411
  this.peerHandle.onInner((inner) => this.onInner(inner));
37293
37412
  this.peerHandle.onRenegotiate(() => {
@@ -37493,6 +37612,7 @@ var FetchproxyServer = class {
37493
37612
  this.pendingRedirect.delete(id);
37494
37613
  this.pendingDownload.delete(id);
37495
37614
  this.pendingIdb.delete(id);
37615
+ this.pendingGraphql.delete(id);
37496
37616
  }
37497
37617
  throw err;
37498
37618
  }
@@ -38302,6 +38422,52 @@ var FetchproxyServer = class {
38302
38422
  await this.sendInnerFrame(inner);
38303
38423
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
38304
38424
  }
38425
+ /**
38426
+ * 1.x+: run a declared GraphQL operation through the page's own Apollo
38427
+ * client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
38428
+ * Requires `'graphql'` in capabilities AND `name` to match a declared
38429
+ * `graphqlOps` entry. The extension resolves `name` → `operationName` →
38430
+ * the live DocumentNode the page already observed, then invokes
38431
+ * `client.query({ query, variables })` — the site's own request path, so
38432
+ * per-request bot telemetry (Akamai etc.) runs automatically.
38433
+ *
38434
+ * Returns the GraphQL `data` object on success (shape is
38435
+ * operation-specific; the caller narrows). Throws a plain `Error` on
38436
+ * developer mistakes (undeclared capability, undeclared name) and a
38437
+ * descriptive `Error` on the `ok:false` bridge path — which includes the
38438
+ * typed "operation not yet observed on this tab" case (open the site's
38439
+ * page and retry).
38440
+ */
38441
+ async graphqlQuery(opts) {
38442
+ if (!this.opts.capabilities.includes("graphql")) {
38443
+ throw new Error('FetchproxyServer.graphqlQuery(): MCP did not declare "graphql" in capabilities');
38444
+ }
38445
+ if (typeof opts.name !== "string" || opts.name.length === 0) {
38446
+ throw new Error("FetchproxyServer.graphqlQuery: opts.name must be a non-empty string");
38447
+ }
38448
+ const declaredNames = this.opts.graphqlOps.map((d) => d.name);
38449
+ if (!declaredNames.includes(opts.name)) {
38450
+ throw new Error(`FetchproxyServer.graphqlQuery: operation ${JSON.stringify(opts.name)} not in declared graphqlOps [${declaredNames.map((n) => JSON.stringify(n)).join(", ")}]`);
38451
+ }
38452
+ await this.ensureConnected();
38453
+ this.throwIfPendingPair();
38454
+ const id = this.nextRequestId++;
38455
+ const inner = {
38456
+ type: "request",
38457
+ id,
38458
+ op: "graphql_query",
38459
+ init: {
38460
+ name: opts.name,
38461
+ variables: opts.variables,
38462
+ ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
38463
+ }
38464
+ };
38465
+ const pending = new Promise((resolve, reject) => {
38466
+ this.pendingGraphql.set(id, { resolve, reject });
38467
+ });
38468
+ await this.sendInnerFrame(inner);
38469
+ return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
38470
+ }
38305
38471
  assertScopeSubset(requested, declared, label) {
38306
38472
  const undeclared = undeclaredKeys(requested, declared);
38307
38473
  if (undeclared.length > 0) {
@@ -38439,6 +38605,20 @@ var FetchproxyServer = class {
38439
38605
  }
38440
38606
  return;
38441
38607
  }
38608
+ const graphqlCb = this.pendingGraphql.get(inner.id);
38609
+ if (graphqlCb) {
38610
+ this.pendingGraphql.delete(inner.id);
38611
+ if (inner.ok) {
38612
+ if (inner.op === "graphql_query") {
38613
+ graphqlCb.resolve(inner.data);
38614
+ } else {
38615
+ graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
38616
+ }
38617
+ } else {
38618
+ graphqlCb.reject(new FetchproxyProtocolError(inner.error));
38619
+ }
38620
+ return;
38621
+ }
38442
38622
  const cookiesCb = this.pendingReadCookies.get(inner.id);
38443
38623
  if (cookiesCb) {
38444
38624
  this.pendingReadCookies.delete(inner.id);
@@ -38490,6 +38670,9 @@ var FetchproxyServer = class {
38490
38670
  for (const { reject } of this.pendingDownload.values())
38491
38671
  reject(err);
38492
38672
  this.pendingDownload.clear();
38673
+ for (const { reject } of this.pendingGraphql.values())
38674
+ reject(err);
38675
+ this.pendingGraphql.clear();
38493
38676
  }
38494
38677
  /**
38495
38678
  * 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.1'; // x-release-please-version
2
+ export const VERSION = '0.3.2'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/tripadvisor-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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.13.0",
48
- "@fetchproxy/server": "^1.3.4",
47
+ "@chrischall/mcp-utils": "^0.14.0",
48
+ "@fetchproxy/server": "^1.7.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.1",
9
+ "version": "0.3.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/tripadvisor-mcp",
14
- "version": "0.3.1",
14
+ "version": "0.3.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },