@chrischall/tripadvisor-mcp 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -108,7 +108,7 @@ var require_code = __commonJS({
108
108
  }
109
109
  exports._ = _;
110
110
  var plus = new _Code("+");
111
- function str(strs, ...args) {
111
+ function str2(strs, ...args) {
112
112
  const expr = [safeStringify(strs[0])];
113
113
  let i = 0;
114
114
  while (i < args.length) {
@@ -119,7 +119,7 @@ var require_code = __commonJS({
119
119
  optimize(expr);
120
120
  return new _Code(expr);
121
121
  }
122
- exports.str = str;
122
+ exports.str = str2;
123
123
  function addCodeArg(code, arg) {
124
124
  if (arg instanceof _Code)
125
125
  code.push(...arg._items);
@@ -162,7 +162,7 @@ var require_code = __commonJS({
162
162
  return;
163
163
  }
164
164
  function strConcat(c1, c2) {
165
- return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`;
165
+ return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
166
166
  }
167
167
  exports.strConcat = strConcat;
168
168
  function interpolate(x) {
@@ -1124,22 +1124,22 @@ var require_util = __commonJS({
1124
1124
  return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
1125
1125
  }
1126
1126
  exports.schemaRefOrVal = schemaRefOrVal;
1127
- function unescapeFragment(str) {
1128
- return unescapeJsonPointer(decodeURIComponent(str));
1127
+ function unescapeFragment(str2) {
1128
+ return unescapeJsonPointer(decodeURIComponent(str2));
1129
1129
  }
1130
1130
  exports.unescapeFragment = unescapeFragment;
1131
- function escapeFragment(str) {
1132
- return encodeURIComponent(escapeJsonPointer(str));
1131
+ function escapeFragment(str2) {
1132
+ return encodeURIComponent(escapeJsonPointer(str2));
1133
1133
  }
1134
1134
  exports.escapeFragment = escapeFragment;
1135
- function escapeJsonPointer(str) {
1136
- if (typeof str == "number")
1137
- return `${str}`;
1138
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
1135
+ function escapeJsonPointer(str2) {
1136
+ if (typeof str2 == "number")
1137
+ return `${str2}`;
1138
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
1139
1139
  }
1140
1140
  exports.escapeJsonPointer = escapeJsonPointer;
1141
- function unescapeJsonPointer(str) {
1142
- return str.replace(/~1/g, "/").replace(/~0/g, "~");
1141
+ function unescapeJsonPointer(str2) {
1142
+ return str2.replace(/~1/g, "/").replace(/~0/g, "~");
1143
1143
  }
1144
1144
  exports.unescapeJsonPointer = unescapeJsonPointer;
1145
1145
  function eachItem(xs, f) {
@@ -2164,8 +2164,8 @@ var require_json_schema_traverse = __commonJS({
2164
2164
  post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
2165
2165
  }
2166
2166
  }
2167
- function escapeJsonPtr(str) {
2168
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
2167
+ function escapeJsonPtr(str2) {
2168
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
2169
2169
  }
2170
2170
  }
2171
2171
  });
@@ -2701,8 +2701,8 @@ var require_validate = __commonJS({
2701
2701
  if (!this.allErrors)
2702
2702
  this.gen.if(cond);
2703
2703
  }
2704
- setParams(obj, assign) {
2705
- if (assign)
2704
+ setParams(obj, assign2) {
2705
+ if (assign2)
2706
2706
  Object.assign(this.params, obj);
2707
2707
  else
2708
2708
  this.params = obj;
@@ -3229,10 +3229,10 @@ var require_utils = __commonJS({
3229
3229
  return { host, isIPV6: false };
3230
3230
  }
3231
3231
  }
3232
- function findToken(str, token) {
3232
+ function findToken(str2, token) {
3233
3233
  let ind = 0;
3234
- for (let i = 0; i < str.length; i++) {
3235
- if (str[i] === token) ind++;
3234
+ for (let i = 0; i < str2.length; i++) {
3235
+ if (str2[i] === token) ind++;
3236
3236
  }
3237
3237
  return ind;
3238
3238
  }
@@ -3969,7 +3969,7 @@ var require_core = __commonJS({
3969
3969
  var util_1 = require_util();
3970
3970
  var $dataRefSchema = require_data();
3971
3971
  var uri_1 = require_uri();
3972
- var defaultRegExp = (str, flags) => new RegExp(str, flags);
3972
+ var defaultRegExp = (str2, flags) => new RegExp(str2, flags);
3973
3973
  defaultRegExp.code = "new RegExp";
3974
3974
  var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
3975
3975
  var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
@@ -4764,16 +4764,16 @@ var require_ucs2length = __commonJS({
4764
4764
  "node_modules/ajv/dist/runtime/ucs2length.js"(exports) {
4765
4765
  "use strict";
4766
4766
  Object.defineProperty(exports, "__esModule", { value: true });
4767
- function ucs2length(str) {
4768
- const len = str.length;
4767
+ function ucs2length(str2) {
4768
+ const len = str2.length;
4769
4769
  let length = 0;
4770
4770
  let pos = 0;
4771
4771
  let value;
4772
4772
  while (pos < len) {
4773
4773
  length++;
4774
- value = str.charCodeAt(pos++);
4774
+ value = str2.charCodeAt(pos++);
4775
4775
  if (value >= 55296 && value <= 56319 && pos < len) {
4776
- value = str.charCodeAt(pos);
4776
+ value = str2.charCodeAt(pos);
4777
4777
  if ((value & 64512) === 56320)
4778
4778
  pos++;
4779
4779
  }
@@ -6656,8 +6656,8 @@ var require_formats = __commonJS({
6656
6656
  }
6657
6657
  var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
6658
6658
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
6659
- function date5(str) {
6660
- const matches = DATE.exec(str);
6659
+ function date5(str2) {
6660
+ const matches = DATE.exec(str2);
6661
6661
  if (!matches)
6662
6662
  return false;
6663
6663
  const year = +matches[1];
@@ -6676,8 +6676,8 @@ var require_formats = __commonJS({
6676
6676
  }
6677
6677
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6678
6678
  function getTime(strictTimeZone) {
6679
- return function time3(str) {
6680
- const matches = TIME.exec(str);
6679
+ return function time3(str2) {
6680
+ const matches = TIME.exec(str2);
6681
6681
  if (!matches)
6682
6682
  return false;
6683
6683
  const hr = +matches[1];
@@ -6723,8 +6723,8 @@ var require_formats = __commonJS({
6723
6723
  var DATE_TIME_SEPARATOR = /t|\s/i;
6724
6724
  function getDateTime(strictTimeZone) {
6725
6725
  const time3 = getTime(strictTimeZone);
6726
- return function date_time(str) {
6727
- const dateTime = str.split(DATE_TIME_SEPARATOR);
6726
+ return function date_time(str2) {
6727
+ const dateTime = str2.split(DATE_TIME_SEPARATOR);
6728
6728
  return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
6729
6729
  };
6730
6730
  }
@@ -6749,13 +6749,13 @@ var require_formats = __commonJS({
6749
6749
  }
6750
6750
  var NOT_URI_FRAGMENT = /\/|:/;
6751
6751
  var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
6752
- function uri(str) {
6753
- return NOT_URI_FRAGMENT.test(str) && URI.test(str);
6752
+ function uri(str2) {
6753
+ return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
6754
6754
  }
6755
6755
  var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
6756
- function byte(str) {
6756
+ function byte(str2) {
6757
6757
  BYTE.lastIndex = 0;
6758
- return BYTE.test(str);
6758
+ return BYTE.test(str2);
6759
6759
  }
6760
6760
  var MIN_INT32 = -(2 ** 31);
6761
6761
  var MAX_INT32 = 2 ** 31 - 1;
@@ -6769,11 +6769,11 @@ var require_formats = __commonJS({
6769
6769
  return true;
6770
6770
  }
6771
6771
  var Z_ANCHOR = /[^\\]\\Z/;
6772
- function regex(str) {
6773
- if (Z_ANCHOR.test(str))
6772
+ function regex(str2) {
6773
+ if (Z_ANCHOR.test(str2))
6774
6774
  return false;
6775
6775
  try {
6776
- new RegExp(str);
6776
+ new RegExp(str2);
6777
6777
  return true;
6778
6778
  } catch (e) {
6779
6779
  return false;
@@ -7237,26 +7237,26 @@ var require_permessage_deflate = __commonJS({
7237
7237
  value = value[0];
7238
7238
  if (key === "client_max_window_bits") {
7239
7239
  if (value !== true) {
7240
- const num = +value;
7241
- if (!Number.isInteger(num) || num < 8 || num > 15) {
7240
+ const num2 = +value;
7241
+ if (!Number.isInteger(num2) || num2 < 8 || num2 > 15) {
7242
7242
  throw new TypeError(
7243
7243
  `Invalid value for parameter "${key}": ${value}`
7244
7244
  );
7245
7245
  }
7246
- value = num;
7246
+ value = num2;
7247
7247
  } else if (!this._isServer) {
7248
7248
  throw new TypeError(
7249
7249
  `Invalid value for parameter "${key}": ${value}`
7250
7250
  );
7251
7251
  }
7252
7252
  } else if (key === "server_max_window_bits") {
7253
- const num = +value;
7254
- if (!Number.isInteger(num) || num < 8 || num > 15) {
7253
+ const num2 = +value;
7254
+ if (!Number.isInteger(num2) || num2 < 8 || num2 > 15) {
7255
7255
  throw new TypeError(
7256
7256
  `Invalid value for parameter "${key}": ${value}`
7257
7257
  );
7258
7258
  }
7259
- value = num;
7259
+ value = num2;
7260
7260
  } else if (key === "client_no_context_takeover" || key === "server_no_context_takeover") {
7261
7261
  if (value !== true) {
7262
7262
  throw new TypeError(
@@ -7969,8 +7969,8 @@ var require_receiver = __commonJS({
7969
7969
  return;
7970
7970
  }
7971
7971
  const buf = this.consume(8);
7972
- const num = buf.readUInt32BE(0);
7973
- if (num > Math.pow(2, 53 - 32) - 1) {
7972
+ const num2 = buf.readUInt32BE(0);
7973
+ if (num2 > Math.pow(2, 53 - 32) - 1) {
7974
7974
  const error51 = this.createError(
7975
7975
  RangeError,
7976
7976
  "Unsupported WebSocket frame: payload length > 2^53 - 1",
@@ -7981,7 +7981,7 @@ var require_receiver = __commonJS({
7981
7981
  cb(error51);
7982
7982
  return;
7983
7983
  }
7984
- this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
7984
+ this._payloadLength = num2 * Math.pow(2, 32) + buf.readUInt32BE(4);
7985
7985
  this.haveLength(cb);
7986
7986
  }
7987
7987
  /**
@@ -14992,14 +14992,14 @@ function promiseAllObject(promisesObj) {
14992
14992
  }
14993
14993
  function randomString(length = 10) {
14994
14994
  const chars = "abcdefghijklmnopqrstuvwxyz";
14995
- let str = "";
14995
+ let str2 = "";
14996
14996
  for (let i = 0; i < length; i++) {
14997
- str += chars[Math.floor(Math.random() * chars.length)];
14997
+ str2 += chars[Math.floor(Math.random() * chars.length)];
14998
14998
  }
14999
- return str;
14999
+ return str2;
15000
15000
  }
15001
- function esc(str) {
15002
- return JSON.stringify(str);
15001
+ function esc(str2) {
15002
+ return JSON.stringify(str2);
15003
15003
  }
15004
15004
  function slugify(input) {
15005
15005
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -15113,8 +15113,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
15113
15113
  "symbol",
15114
15114
  "undefined"
15115
15115
  ]);
15116
- function escapeRegex(str) {
15117
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15116
+ function escapeRegex(str2) {
15117
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15118
15118
  }
15119
15119
  function clone(inst, def, params) {
15120
15120
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -34722,8 +34722,8 @@ function redactSecrets(text) {
34722
34722
  return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34723
34723
  }
34724
34724
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34725
- const str = text === null || text === void 0 ? "" : String(text);
34726
- const redacted = redactSecrets(str);
34725
+ const str2 = text === null || text === void 0 ? "" : String(text);
34726
+ const redacted = redactSecrets(str2);
34727
34727
  if (redacted.length <= max)
34728
34728
  return redacted;
34729
34729
  return `${redacted.slice(0, max)}\u2026 [truncated]`;
@@ -34841,15 +34841,15 @@ var pageSchema = {
34841
34841
  };
34842
34842
 
34843
34843
  // src/version.ts
34844
- var VERSION = "0.0.0";
34844
+ var VERSION = "0.2.0";
34845
34845
 
34846
34846
  // src/client.ts
34847
34847
  import { dirname, join } from "node:path";
34848
34848
  import { fileURLToPath } from "node:url";
34849
34849
  var __dirname = dirname(fileURLToPath(import.meta.url));
34850
34850
  await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
34851
- var BASE_URL = "https://api.content.tripadvisor.com/api/v1";
34852
- var SERVICE = "TripAdvisor Content API";
34851
+ var BASE_URL = "https://terra.tripadvisor.com/api";
34852
+ var SERVICE = "TripAdvisor Terra API";
34853
34853
  var REQUEST_TIMEOUT_MS = 3e4;
34854
34854
  var DEFAULT_CACHE_TTL_MS = 3e5;
34855
34855
  var DEFAULT_STATIC_CACHE_TTL_MS = 36e5;
@@ -34864,7 +34864,6 @@ function readCacheTtlMs(envVar, defaultMs) {
34864
34864
  var TripAdvisorClient = class {
34865
34865
  apiKey;
34866
34866
  configError;
34867
- referer;
34868
34867
  fetchImpl;
34869
34868
  sleep;
34870
34869
  cacheTtlMs;
@@ -34882,12 +34881,11 @@ var TripAdvisorClient = class {
34882
34881
  this.cacheTtlMs = opts.cacheTtlMs ?? readCacheTtlMs("TRIPADVISOR_CACHE_TTL", DEFAULT_CACHE_TTL_MS);
34883
34882
  this.staticCacheTtlMs = opts.staticCacheTtlMs ?? readCacheTtlMs("TRIPADVISOR_STATIC_CACHE_TTL", DEFAULT_STATIC_CACHE_TTL_MS);
34884
34883
  this.fetchImpl = opts.fetchImpl ?? fetch;
34885
- this.referer = readEnvVar("TRIPADVISOR_REFERER");
34886
34884
  const key = readEnvVar("TRIPADVISOR_API_KEY");
34887
34885
  if (!key) {
34888
34886
  this.apiKey = null;
34889
34887
  this.configError = new McpToolError("TRIPADVISOR_API_KEY environment variable is required", {
34890
- hint: "Create a Content API key at https://www.tripadvisor.com/developers and set TRIPADVISOR_API_KEY in your MCP host env or .env (free tier: 5,000 calls/month)."
34888
+ hint: "Create a Terra API key at https://www.tripadvisor.com/developers and set TRIPADVISOR_API_KEY in your MCP host env or .env (free Discover tier: 10,000 calls/day)."
34891
34889
  });
34892
34890
  } else {
34893
34891
  this.apiKey = key;
@@ -34929,11 +34927,8 @@ var TripAdvisorClient = class {
34929
34927
  }
34930
34928
  async request(path, isRetry = false) {
34931
34929
  const key = this.requireKey();
34932
- const sep = path.includes("?") ? "&" : "?";
34933
- const url2 = `${BASE_URL}${path}${sep}key=${encodeURIComponent(key)}`;
34934
- const headers = { Accept: "application/json" };
34935
- if (this.referer) headers.Referer = this.referer;
34936
- const res = await this.fetchImpl(url2, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
34930
+ const headers = { "X-API-Key": key, Accept: "application/json" };
34931
+ const res = await this.fetchImpl(`${BASE_URL}${path}`, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
34937
34932
  if (res.ok) return await res.json();
34938
34933
  const text = await res.text();
34939
34934
  if (res.status === 429 && !isRetry) {
@@ -34942,22 +34937,20 @@ var TripAdvisorClient = class {
34942
34937
  await this.sleep(delayMs);
34943
34938
  return this.request(path, true);
34944
34939
  }
34945
- if (res.status === 401) {
34946
- throw new McpToolError(`${SERVICE} returned 401 Unauthorized \u2014 TRIPADVISOR_API_KEY is missing or invalid.`, {
34947
- hint: "Check the key in your MCP host env or .env; create one at https://www.tripadvisor.com/developers"
34948
- });
34949
- }
34950
- if (res.status === 403) {
34940
+ if (res.status === 401 || res.status === 403) {
34951
34941
  throw new McpToolError(
34952
- `${SERVICE} returned 403 Forbidden \u2014 the key is valid but blocked, usually by its domain/IP restriction (set at key creation) or an unapproved application.`,
34953
- {
34954
- hint: "If the key is domain-restricted, set TRIPADVISOR_REFERER to a matching https://domain; if IP-restricted, call from a listed address."
34955
- }
34942
+ `${SERVICE} returned ${res.status} \u2014 TRIPADVISOR_API_KEY is missing, invalid, or not authorized for Terra. A legacy Content API key does NOT work here (and a Terra key does not work on the legacy endpoint).`,
34943
+ { hint: "Confirm the key on the Terra dashboard at https://www.tripadvisor.com/developers and that its plan is active." }
34956
34944
  );
34957
34945
  }
34958
34946
  if (res.status === 429) {
34959
- throw new McpToolError(`${SERVICE} rate limit or monthly quota exceeded (429).`, {
34960
- hint: "The free tier is 5,000 calls/month \u2014 check usage at https://www.tripadvisor.com/developers. Cached reads (TRIPADVISOR_CACHE_TTL) stretch the quota."
34947
+ throw new McpToolError(`${SERVICE} rate limit or daily quota exceeded (429).`, {
34948
+ hint: "The Discover tier allows 10 QPS and 10,000 calls/day \u2014 check usage at https://www.tripadvisor.com/developers. Cached reads (TRIPADVISOR_CACHE_TTL) stretch the quota."
34949
+ });
34950
+ }
34951
+ if (res.status === 400) {
34952
+ throw new McpToolError(`${SERVICE} rejected the request (400): ${truncateErrorMessage(text)}`, {
34953
+ hint: "Check the parameters against docs/TRIPADVISOR-API.md (e.g. category must be RESTAURANT/ATTRACTION/HOTEL; nearby needs lat+lon+radius or a bounding box)."
34961
34954
  });
34962
34955
  }
34963
34956
  throw new McpToolError(formatApiError(res.status, "GET", path, text, { service: SERVICE }));
@@ -34967,102 +34960,212 @@ var client = new TripAdvisorClient();
34967
34960
 
34968
34961
  // src/tools/shared.ts
34969
34962
  var LocationId = external_exports.number().int().positive().describe("TripAdvisor location ID (from a search tool)");
34970
- var LatLong = external_exports.string().regex(/^-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?$/, 'must be a "lat,long" pair like "42.3455,-71.10767"');
34971
- var Category = external_exports.enum(["hotels", "attractions", "restaurants", "geos"]);
34972
- var searchFilterParams = {
34973
- category: Category.optional().describe("Restrict results to one property type"),
34974
- phone: external_exports.string().optional().describe('Phone number filter (spaces/dashes ok, no leading "+")'),
34975
- address: external_exports.string().optional().describe("Address filter"),
34976
- radius: external_exports.number().positive().optional().describe("Search radius around latLong (must be > 0)"),
34977
- radiusUnit: external_exports.enum(["km", "mi", "m"]).optional().describe("Unit for radius"),
34978
- language: external_exports.string().optional().describe("Result language code (default: en)")
34963
+ var Category = external_exports.enum(["RESTAURANT", "ATTRACTION", "HOTEL"]);
34964
+ var LocaleList = external_exports.array(external_exports.string()).optional().describe('Preferred locales for localized fields, in priority order (e.g. ["en","es"])');
34965
+ var pageParams = {
34966
+ page: external_exports.number().int().min(1).optional().describe("Page index (1-based)"),
34967
+ size: external_exports.number().int().min(1).max(20).optional().describe("Results per page (max 20)")
34979
34968
  };
34980
34969
  function qs(params) {
34981
34970
  return buildQueryString(params);
34982
34971
  }
34983
34972
 
34973
+ // src/projection.ts
34974
+ var str = (v) => typeof v === "string" ? v : void 0;
34975
+ var numOf = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
34976
+ function primaryValue(arr) {
34977
+ if (!Array.isArray(arr) || arr.length === 0) return void 0;
34978
+ const primary = arr.find((e) => e && typeof e === "object" && e.primary === true);
34979
+ const pick2 = primary ?? arr[0];
34980
+ return str(pick2?.value);
34981
+ }
34982
+ function categoryFromUrl(url2) {
34983
+ if (!url2) return void 0;
34984
+ const m = /\/(Attraction|Hotel|Restaurant)_Review/.exec(url2);
34985
+ return m ? m[1].toUpperCase() : void 0;
34986
+ }
34987
+ function assign(o, k, v) {
34988
+ if (v !== void 0) o[k] = v;
34989
+ }
34990
+ function compactLocation(loc) {
34991
+ const addr = Array.isArray(loc.addresses) ? loc.addresses[0] : void 0;
34992
+ const url2 = str(loc.urls?.tripadvisor && loc.urls.tripadvisor.main);
34993
+ const overall = loc.traveler_ratings?.overall ?? {};
34994
+ const out = {};
34995
+ assign(out, "id", numOf(loc.id));
34996
+ assign(out, "name", primaryValue(loc.names));
34997
+ assign(out, "category", categoryFromUrl(url2));
34998
+ assign(out, "geo", str(loc.geo));
34999
+ assign(out, "city", str(addr?.city));
35000
+ assign(out, "state", str(addr?.state));
35001
+ assign(out, "rating", numOf(overall.rating));
35002
+ assign(out, "review_count", numOf(overall.count));
35003
+ assign(out, "url", url2);
35004
+ return out;
35005
+ }
35006
+ function compactList(raw) {
35007
+ const env = raw;
35008
+ const rows = env?.data;
35009
+ if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === "object" && "location" in r)) {
35010
+ console.error("[tripadvisor] compact: unexpected response shape, returning raw payload");
35011
+ return raw;
35012
+ }
35013
+ const data = rows.map((r) => {
35014
+ const row = r;
35015
+ const item = compactLocation(row.location);
35016
+ assign(item, "distance_miles", numOf(row.distance_miles));
35017
+ assign(item, "distance_kilometers", numOf(row.distance_kilometers));
35018
+ return item;
35019
+ });
35020
+ return env.pagination !== void 0 ? { data, pagination: env.pagination } : { data };
35021
+ }
35022
+ function compactLocationList(raw) {
35023
+ const env = raw;
35024
+ const rows = env?.data;
35025
+ if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === "object" && "id" in r)) {
35026
+ console.error("[tripadvisor] compact: unexpected response shape, returning raw payload");
35027
+ return raw;
35028
+ }
35029
+ return { data: rows.map((r) => compactLocation(r)) };
35030
+ }
35031
+
34984
35032
  // src/tools/search.ts
35033
+ var compactParam = {
35034
+ compact: external_exports.boolean().optional().describe("Return a slim summary per result (id, name, category, city, rating, review_count, url) instead of full records")
35035
+ };
34985
35036
  function registerSearchTools(server) {
34986
35037
  server.registerTool(
34987
35038
  "ta_search_locations",
34988
35039
  {
34989
- description: "Search TripAdvisor locations (hotels, restaurants, attractions, geos) by name. Returns up to 10 matches with location_id for the detail tools.",
35040
+ description: "Search TripAdvisor locations (restaurants, attractions, hotels) by name. Returns matches with a location id for the detail tools, plus pagination. Pass compact:true for slim summaries.",
34990
35041
  annotations: { readOnlyHint: true, openWorldHint: true },
34991
35042
  inputSchema: {
34992
- searchQuery: external_exports.string().min(1).describe("Text to search location names for"),
34993
- latLong: LatLong.optional().describe('Center point to scope the search, e.g. "42.3455,-71.10767"'),
34994
- ...searchFilterParams
35043
+ query: external_exports.string().min(1).max(500).describe("Text to search location names for"),
35044
+ category: Category.optional().describe("Restrict to one category"),
35045
+ country_code: external_exports.string().length(2).optional().describe('Alpha-2 country code (e.g. "US")'),
35046
+ geo_name: external_exports.string().optional().describe("City, town, or country name to scope the search"),
35047
+ postal_code: external_exports.string().optional().describe("Postal/ZIP code (takes precedence over geo_name)"),
35048
+ locale: LocaleList,
35049
+ ...pageParams,
35050
+ ...compactParam
34995
35051
  }
34996
35052
  },
34997
- async ({ searchQuery, latLong, category, phone, address, radius, radiusUnit, language }) => {
35053
+ async ({ query, category, country_code, geo_name, postal_code, locale, page, size, compact }) => {
34998
35054
  const data = await client.get(
34999
- `/location/search${qs({ searchQuery, category, phone, address, latLong, radius, radiusUnit, language })}`,
35055
+ `/locations/search${qs({ query, category, country_code, geo_name, postal_code, locale, page, size })}`,
35000
35056
  { cache: "dynamic" }
35001
35057
  );
35002
- return textResult(data);
35058
+ return textResult(compact ? compactList(data) : data);
35003
35059
  }
35004
35060
  );
35005
35061
  server.registerTool(
35006
35062
  "ta_search_nearby",
35007
35063
  {
35008
- description: "Find TripAdvisor locations near a latitude/longitude. Returns up to 10 locations with location_id for the detail tools.",
35064
+ description: "Find TripAdvisor locations near a point within a radius, or inside a bounding box. Center by lat+lon+radius, by a reference location_id+radius, or by a sw/ne bounding box. Returns matches with distance and a location id. Pass compact:true for slim summaries.",
35009
35065
  annotations: { readOnlyHint: true, openWorldHint: true },
35010
35066
  inputSchema: {
35011
- latLong: LatLong.describe('Center point, e.g. "42.3455,-71.10767"'),
35012
- ...searchFilterParams
35067
+ // Center — supply exactly one of: lat+lon, location_id, or the sw/ne box.
35068
+ lat: external_exports.number().min(-90).max(90).optional().describe("Center latitude (with lon+radius)"),
35069
+ lon: external_exports.number().min(-180).max(180).optional().describe("Center longitude (with lat+radius)"),
35070
+ location_id: external_exports.number().int().positive().optional().describe("Reference location as center (with radius)"),
35071
+ radius: external_exports.number().positive().optional().describe("Search radius (required with lat/lon or location_id; must be > 0)"),
35072
+ unit: external_exports.enum(["MI", "KM"]).optional().describe("Radius unit (default MI)"),
35073
+ sw_lat: external_exports.number().min(-90).max(90).optional().describe("Bounding box SW latitude"),
35074
+ sw_lon: external_exports.number().min(-180).max(180).optional().describe("Bounding box SW longitude"),
35075
+ ne_lat: external_exports.number().min(-90).max(90).optional().describe("Bounding box NE latitude"),
35076
+ ne_lon: external_exports.number().min(-180).max(180).optional().describe("Bounding box NE longitude"),
35077
+ category: Category.optional().describe("Restrict to one category"),
35078
+ min_rating: external_exports.number().min(1).max(5).optional().describe("Minimum traveler rating (1.0\u20135.0)"),
35079
+ include_photo: external_exports.boolean().optional().describe("Include a photo per result"),
35080
+ sort: external_exports.enum(["distance", "rating"]).optional().describe("Sort order (default distance)"),
35081
+ locale: LocaleList,
35082
+ ...pageParams,
35083
+ ...compactParam
35013
35084
  }
35014
35085
  },
35015
- async ({ latLong, category, phone, address, radius, radiusUnit, language }) => {
35086
+ async (args) => {
35087
+ const { lat, lon, location_id, radius, sw_lat, sw_lon, ne_lat, ne_lon, compact, ...rest } = args;
35088
+ const boxParts = [sw_lat, sw_lon, ne_lat, ne_lon];
35089
+ const boxGiven = boxParts.filter((v) => v !== void 0).length;
35090
+ if (boxGiven > 0 && boxGiven < 4) {
35091
+ throw new McpToolError("ta_search_nearby bounding box needs all four of sw_lat, sw_lon, ne_lat, ne_lon.", {
35092
+ hint: "Provide all four box corners, or use lat+lon+radius or location_id+radius instead."
35093
+ });
35094
+ }
35095
+ if (lat === void 0 !== (lon === void 0)) {
35096
+ throw new McpToolError("ta_search_nearby needs both lat and lon together (or neither).", {
35097
+ hint: "Provide lat AND lon with a radius, or use location_id+radius or a sw/ne box."
35098
+ });
35099
+ }
35100
+ const hasLatLon = lat !== void 0 && lon !== void 0;
35101
+ const hasBox = boxGiven === 4;
35102
+ const centers = [hasLatLon, location_id !== void 0, hasBox].filter(Boolean).length;
35103
+ if (centers !== 1) {
35104
+ throw new McpToolError(
35105
+ "ta_search_nearby needs exactly one center: lat+lon (+radius), location_id (+radius), or the sw/ne bounding box.",
35106
+ { hint: "Provide lat AND lon, OR location_id, OR all four of sw_lat/sw_lon/ne_lat/ne_lon \u2014 not more than one." }
35107
+ );
35108
+ }
35109
+ if ((hasLatLon || location_id !== void 0) && radius === void 0) {
35110
+ throw new McpToolError("lat/lon and location_id center modes require radius.", {
35111
+ hint: "Add radius (with optional unit MI/KM), or switch to a sw/ne bounding box."
35112
+ });
35113
+ }
35016
35114
  const data = await client.get(
35017
- `/location/nearby_search${qs({ latLong, category, phone, address, radius, radiusUnit, language })}`,
35115
+ `/locations/nearby${qs({ lat, lon, location_id, radius, sw_lat, sw_lon, ne_lat, ne_lon, ...rest })}`,
35018
35116
  { cache: "dynamic" }
35019
35117
  );
35020
- return textResult(data);
35118
+ return textResult(compact ? compactList(data) : data);
35021
35119
  }
35022
35120
  );
35023
35121
  }
35024
35122
 
35025
35123
  // src/tools/location.ts
35026
- var PhotoSource = external_exports.string().regex(
35027
- /^(Expert|Management|Traveler)(,(Expert|Management|Traveler))*$/,
35028
- "must be a comma-separated list of: Expert, Management, Traveler"
35029
- );
35030
35124
  function registerLocationTools(server) {
35125
+ server.registerTool(
35126
+ "ta_get_locations",
35127
+ {
35128
+ description: "Get details for MULTIPLE locations in one call (batch). Pass an array of location ids \u2014 cheaper than repeated ta_get_location_details. Unknown or unlicensed ids are silently omitted. Pass compact:true for slim summaries.",
35129
+ annotations: { readOnlyHint: true, openWorldHint: true },
35130
+ inputSchema: {
35131
+ ids: external_exports.array(LocationId).min(1).max(50).describe("Location IDs to fetch (1\u201350)"),
35132
+ locale: LocaleList,
35133
+ compact: external_exports.boolean().optional().describe("Return a slim summary per location instead of full records")
35134
+ }
35135
+ },
35136
+ async ({ ids, locale, compact }) => {
35137
+ const data = await client.get(`/locations${qs({ id: ids, locale })}`, { cache: "static" });
35138
+ return textResult(compact ? compactLocationList(data) : data);
35139
+ }
35140
+ );
35031
35141
  server.registerTool(
35032
35142
  "ta_get_location_details",
35033
35143
  {
35034
- description: "Get full details for a TripAdvisor location: name, address, coordinates, rating, ranking, subratings, awards, review count, amenities, hours, and listing URLs.",
35144
+ description: "Get full details for a TripAdvisor location: names, descriptions, address, coordinates, traveler ratings, phone, category, and listing URLs.",
35035
35145
  annotations: { readOnlyHint: true, openWorldHint: true },
35036
35146
  inputSchema: {
35037
35147
  locationId: LocationId,
35038
- language: external_exports.string().optional().describe("Result language code (default: en)"),
35039
- currency: external_exports.string().optional().describe("ISO 4217 currency code for prices (default: USD)")
35148
+ locale: LocaleList
35040
35149
  }
35041
35150
  },
35042
- async ({ locationId, language, currency }) => {
35043
- const data = await client.get(`/location/${locationId}/details${qs({ language, currency })}`, {
35044
- cache: "static"
35045
- });
35151
+ async ({ locationId, locale }) => {
35152
+ const data = await client.get(`/locations/${locationId}${qs({ locale })}`, { cache: "static" });
35046
35153
  return textResult(data);
35047
35154
  }
35048
35155
  );
35049
35156
  server.registerTool(
35050
35157
  "ta_get_location_photos",
35051
35158
  {
35052
- description: "Get photos for a TripAdvisor location (multi-size image URLs, captions, sources).",
35159
+ description: "Get photos for a TripAdvisor location (multi-size image URLs, source, dimensions), with pagination.",
35053
35160
  annotations: { readOnlyHint: true, openWorldHint: true },
35054
35161
  inputSchema: {
35055
35162
  locationId: LocationId,
35056
- language: external_exports.string().optional().describe("Caption language code (default: en)"),
35057
- limit: external_exports.number().int().positive().optional().describe("Number of photos to return"),
35058
- offset: external_exports.number().int().min(0).optional().describe("Index of the first photo"),
35059
- source: PhotoSource.optional().describe(
35060
- "Comma-separated photo sources to allow: Expert, Management, Traveler (default: all)"
35061
- )
35163
+ locale: LocaleList,
35164
+ ...pageParams
35062
35165
  }
35063
35166
  },
35064
- async ({ locationId, language, limit, offset, source }) => {
35065
- const data = await client.get(`/location/${locationId}/photos${qs({ language, limit, offset, source })}`, {
35167
+ async ({ locationId, locale, page, size }) => {
35168
+ const data = await client.get(`/locations/${locationId}/photos${qs({ locale, page, size })}`, {
35066
35169
  cache: "static"
35067
35170
  });
35068
35171
  return textResult(data);
@@ -35071,17 +35174,16 @@ function registerLocationTools(server) {
35071
35174
  server.registerTool(
35072
35175
  "ta_get_location_reviews",
35073
35176
  {
35074
- description: "Get the most recent reviews for a TripAdvisor location (up to 5 per call; page with offset).",
35177
+ description: "Get traveler reviews for a TripAdvisor location, with pagination.",
35075
35178
  annotations: { readOnlyHint: true, openWorldHint: true },
35076
35179
  inputSchema: {
35077
35180
  locationId: LocationId,
35078
- language: external_exports.string().optional().describe("Review language code (default: en)"),
35079
- limit: external_exports.number().int().positive().optional().describe("Number of reviews to return"),
35080
- offset: external_exports.number().int().min(0).optional().describe("Index of the first review")
35181
+ locale: LocaleList,
35182
+ ...pageParams
35081
35183
  }
35082
35184
  },
35083
- async ({ locationId, language, limit, offset }) => {
35084
- const data = await client.get(`/location/${locationId}/reviews${qs({ language, limit, offset })}`, {
35185
+ async ({ locationId, locale, page, size }) => {
35186
+ const data = await client.get(`/locations/${locationId}/reviews${qs({ locale, page, size })}`, {
35085
35187
  cache: "static"
35086
35188
  });
35087
35189
  return textResult(data);
@@ -38513,6 +38615,56 @@ function createTripAdvisorTransport(createServer2) {
38513
38615
  });
38514
38616
  }
38515
38617
 
38618
+ // src/web/parse.ts
38619
+ function locationDetailPath(locationId) {
38620
+ return `/Attraction_Review-g1-d${locationId}-Reviews-a-a.html`;
38621
+ }
38622
+ function ldJsonBlocks(html) {
38623
+ const out = [];
38624
+ for (const m of html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
38625
+ try {
38626
+ out.push(JSON.parse(m[1].trim()));
38627
+ } catch {
38628
+ }
38629
+ }
38630
+ return out;
38631
+ }
38632
+ function num(v) {
38633
+ if (v === void 0 || v === null) return void 0;
38634
+ const n = typeof v === "number" ? v : Number(v);
38635
+ return Number.isFinite(n) ? n : void 0;
38636
+ }
38637
+ function parseLocationDetail(html) {
38638
+ const node = ldJsonBlocks(html).find(
38639
+ (b) => typeof b === "object" && b !== null && typeof b.name === "string" && "aggregateRating" in b
38640
+ );
38641
+ if (!node) return null;
38642
+ const rating = node.aggregateRating ?? {};
38643
+ const geo = node.geo ?? {};
38644
+ const detail = { name: node.name };
38645
+ const assign2 = (key, value) => {
38646
+ if (value !== void 0) detail[key] = value;
38647
+ };
38648
+ assign2("type", typeof node["@type"] === "string" ? node["@type"] : void 0);
38649
+ assign2("url", typeof node.url === "string" ? node.url : void 0);
38650
+ assign2("rating", num(rating.ratingValue));
38651
+ assign2("review_count", num(rating.reviewCount));
38652
+ assign2("best_rating", num(rating.bestRating));
38653
+ assign2("telephone", typeof node.telephone === "string" ? node.telephone : void 0);
38654
+ assign2("image", typeof node.image === "string" ? node.image : void 0);
38655
+ assign2("latitude", num(geo.latitude));
38656
+ assign2("longitude", num(geo.longitude));
38657
+ assign2("same_as", typeof node.sameAs === "string" ? node.sameAs : void 0);
38658
+ if (node.address && typeof node.address === "object") {
38659
+ const addr = {};
38660
+ for (const [k, v] of Object.entries(node.address)) {
38661
+ if (k !== "@type" && typeof v === "string") addr[k] = v;
38662
+ }
38663
+ if (Object.keys(addr).length) detail.address = addr;
38664
+ }
38665
+ return detail;
38666
+ }
38667
+
38516
38668
  // src/web/client.ts
38517
38669
  var TripAdvisorWebClient = class {
38518
38670
  constructor(injected = {}) {
@@ -38599,6 +38751,10 @@ var TripAdvisorWebClient = class {
38599
38751
  });
38600
38752
  }
38601
38753
  }
38754
+ /** Fetch a location detail page's HTML by numeric d-id (canonicalized by TripAdvisor). */
38755
+ async getLocationHtml(locationId) {
38756
+ return this.getHtml(locationDetailPath(locationId));
38757
+ }
38602
38758
  assertNotWalled(status, body, path) {
38603
38759
  const wall = classifyBotWall(body, status);
38604
38760
  if (wall.blocked) {
@@ -38626,6 +38782,26 @@ function registerWebTools(server) {
38626
38782
  },
38627
38783
  probeFn: (path) => webClient.getHtml(path)
38628
38784
  });
38785
+ server.registerTool(
38786
+ "ta_web_get_location",
38787
+ {
38788
+ description: "Get a TripAdvisor location's core details (name, rating, review count, address, coordinates, phone, photo, listing URL) by location ID, read from the public page via the browser bridge. Works without an API key \u2014 use this when ta_get_location_details is unavailable or its key is blocked. Covers attractions, hotels, and restaurants. Does not return individual review text.",
38789
+ annotations: { readOnlyHint: true, openWorldHint: true },
38790
+ inputSchema: {
38791
+ locationId: LocationId
38792
+ }
38793
+ },
38794
+ async ({ locationId }) => {
38795
+ const html = await webClient.getLocationHtml(locationId);
38796
+ const detail = parseLocationDetail(html);
38797
+ if (!detail) {
38798
+ throw new McpToolError(`Could not parse location ${locationId} from its TripAdvisor page.`, {
38799
+ hint: "The page may be a bot-challenge shell or the id may be wrong \u2014 run ta_web_healthcheck and confirm a signed-in www.tripadvisor.com tab is open, then retry."
38800
+ });
38801
+ }
38802
+ return textResult({ location_id: locationId, ...detail });
38803
+ }
38804
+ );
38629
38805
  }
38630
38806
 
38631
38807
  // src/index.ts