@chrischall/tripadvisor-mcp 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/SKILL.md +3 -2
- package/dist/bundle.js +319 -121
- package/dist/client.js +9 -48
- package/dist/projection.js +78 -0
- package/dist/tools/location.js +17 -0
- package/dist/tools/search.js +56 -11
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for the TripAdvisor Terra API — location search, details, photos, and reviews",
|
|
10
|
-
"version": "0.1
|
|
10
|
+
"version": "0.2.1"
|
|
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.1
|
|
18
|
+
"version": "0.2.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -26,9 +26,10 @@ Get a key at [tripadvisor.com/developers](https://www.tripadvisor.com/developers
|
|
|
26
26
|
|
|
27
27
|
| Tool | What it does |
|
|
28
28
|
| --- | --- |
|
|
29
|
-
| `ta_search_locations` | Search locations by name (optionally scoped by category, country/geo/postal code) — paginated |
|
|
30
|
-
| `ta_search_nearby` | Find locations near a lat/lon
|
|
29
|
+
| `ta_search_locations` | Search locations by name (optionally scoped by category, country/geo/postal code) — paginated; `compact:true` for slim summaries |
|
|
30
|
+
| `ta_search_nearby` | Find locations near a lat/lon+radius, a `location_id`+radius, or inside a sw/ne bounding box (category, min rating, sort) — `compact:true` supported |
|
|
31
31
|
| `ta_get_location_details` | Full details: names, descriptions, address, coordinates, traveler ratings, phone, listing URLs |
|
|
32
|
+
| `ta_get_locations` | Batch — details for **multiple** location ids in one call (cheaper than N detail calls); `compact:true` supported |
|
|
32
33
|
| `ta_get_location_photos` | Photos with multi-size image URLs, source, and dimensions — paginated |
|
|
33
34
|
| `ta_get_location_reviews` | Traveler reviews — paginated |
|
|
34
35
|
| `ta_web_healthcheck` | Diagnose the optional tripadvisor.com browser-bridge connection (see below) |
|
package/SKILL.md
CHANGED
|
@@ -52,9 +52,10 @@ npm install && npm run build
|
|
|
52
52
|
|
|
53
53
|
| Tool | Use for |
|
|
54
54
|
| --- | --- |
|
|
55
|
-
| `ta_search_locations` | Find places by name — `query` required; filter with `category` (`RESTAURANT`/`ATTRACTION`/`HOTEL`), `country_code`, `geo_name`, `postal_code`, `locale`; page with `page`/`size` (max 20). Returns matches with a location `id`. |
|
|
56
|
-
| `ta_search_nearby` | Find places near
|
|
55
|
+
| `ta_search_locations` | Find places by name — `query` required; filter with `category` (`RESTAURANT`/`ATTRACTION`/`HOTEL`), `country_code`, `geo_name`, `postal_code`, `locale`; page with `page`/`size` (max 20). `compact:true` → slim summaries. Returns matches with a location `id`. |
|
|
56
|
+
| `ta_search_nearby` | Find places near a center — supply **one** of: `lat`+`lon`+`radius`, `location_id`+`radius`, or a `sw_lat`/`sw_lon`/`ne_lat`/`ne_lon` box (`unit` `MI`/`KM`); filter `category`, `min_rating`, `sort` (`distance`/`rating`), `include_photo`. `compact:true` supported. |
|
|
57
57
|
| `ta_get_location_details` | Full listing for a location `id`: names, descriptions, address, coordinates, traveler ratings, phone, URLs. Optional `locale`. |
|
|
58
|
+
| `ta_get_locations` | Batch details for **multiple** ids (`ids: [..]`, 1–50) in one call — cheaper than repeated details; unknown ids are omitted. `compact:true` supported. |
|
|
58
59
|
| `ta_get_location_photos` | Photos (multi-size URLs, source, dimensions). Page with `page`/`size`. |
|
|
59
60
|
| `ta_get_location_reviews` | Traveler reviews. Page with `page`/`size`. |
|
|
60
61
|
| `ta_web_healthcheck` | Diagnose the optional tripadvisor.com browser-bridge connection (fetchproxy Transporter). Reports bridge role/port/timing and an actionable hint if it's not connected. |
|
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
|
|
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 =
|
|
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 :
|
|
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(
|
|
1128
|
-
return unescapeJsonPointer(decodeURIComponent(
|
|
1127
|
+
function unescapeFragment(str2) {
|
|
1128
|
+
return unescapeJsonPointer(decodeURIComponent(str2));
|
|
1129
1129
|
}
|
|
1130
1130
|
exports.unescapeFragment = unescapeFragment;
|
|
1131
|
-
function escapeFragment(
|
|
1132
|
-
return encodeURIComponent(escapeJsonPointer(
|
|
1131
|
+
function escapeFragment(str2) {
|
|
1132
|
+
return encodeURIComponent(escapeJsonPointer(str2));
|
|
1133
1133
|
}
|
|
1134
1134
|
exports.escapeFragment = escapeFragment;
|
|
1135
|
-
function escapeJsonPointer(
|
|
1136
|
-
if (typeof
|
|
1137
|
-
return `${
|
|
1138
|
-
return
|
|
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(
|
|
1142
|
-
return
|
|
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(
|
|
2168
|
-
return
|
|
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,
|
|
2705
|
-
if (
|
|
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(
|
|
3232
|
+
function findToken(str2, token) {
|
|
3233
3233
|
let ind = 0;
|
|
3234
|
-
for (let i = 0; i <
|
|
3235
|
-
if (
|
|
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 = (
|
|
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(
|
|
4768
|
-
const len =
|
|
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 =
|
|
4774
|
+
value = str2.charCodeAt(pos++);
|
|
4775
4775
|
if (value >= 55296 && value <= 56319 && pos < len) {
|
|
4776
|
-
value =
|
|
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(
|
|
6660
|
-
const matches = DATE.exec(
|
|
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(
|
|
6680
|
-
const matches = TIME.exec(
|
|
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(
|
|
6727
|
-
const dateTime =
|
|
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(
|
|
6753
|
-
return NOT_URI_FRAGMENT.test(
|
|
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(
|
|
6756
|
+
function byte(str2) {
|
|
6757
6757
|
BYTE.lastIndex = 0;
|
|
6758
|
-
return BYTE.test(
|
|
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(
|
|
6773
|
-
if (Z_ANCHOR.test(
|
|
6772
|
+
function regex(str2) {
|
|
6773
|
+
if (Z_ANCHOR.test(str2))
|
|
6774
6774
|
return false;
|
|
6775
6775
|
try {
|
|
6776
|
-
new RegExp(
|
|
6776
|
+
new RegExp(str2);
|
|
6777
6777
|
return true;
|
|
6778
6778
|
} catch (e) {
|
|
6779
6779
|
return false;
|
|
@@ -14992,14 +14992,14 @@ function promiseAllObject(promisesObj) {
|
|
|
14992
14992
|
}
|
|
14993
14993
|
function randomString(length = 10) {
|
|
14994
14994
|
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
14995
|
-
let
|
|
14995
|
+
let str2 = "";
|
|
14996
14996
|
for (let i = 0; i < length; i++) {
|
|
14997
|
-
|
|
14997
|
+
str2 += chars[Math.floor(Math.random() * chars.length)];
|
|
14998
14998
|
}
|
|
14999
|
-
return
|
|
14999
|
+
return str2;
|
|
15000
15000
|
}
|
|
15001
|
-
function esc(
|
|
15002
|
-
return JSON.stringify(
|
|
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(
|
|
15117
|
-
return
|
|
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);
|
|
@@ -34718,12 +34718,16 @@ var API_KEY_RE = new RegExp([
|
|
|
34718
34718
|
// webhook signing secret (Stripe-style)
|
|
34719
34719
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
34720
34720
|
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
34721
|
+
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
34722
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34723
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34724
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34721
34725
|
function redactSecrets(text) {
|
|
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]");
|
|
34726
|
+
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(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
34723
34727
|
}
|
|
34724
34728
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
34725
|
-
const
|
|
34726
|
-
const redacted = redactSecrets(
|
|
34729
|
+
const str2 = text === null || text === void 0 ? "" : String(text);
|
|
34730
|
+
const redacted = redactSecrets(str2);
|
|
34727
34731
|
if (redacted.length <= max)
|
|
34728
34732
|
return redacted;
|
|
34729
34733
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
@@ -34795,6 +34799,100 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
34795
34799
|
return false;
|
|
34796
34800
|
}
|
|
34797
34801
|
}
|
|
34802
|
+
function readIntEnv(key, opts = {}) {
|
|
34803
|
+
const raw = readEnvVar(key, opts.env ? { env: opts.env } : {});
|
|
34804
|
+
if (raw === void 0)
|
|
34805
|
+
return opts.default;
|
|
34806
|
+
if (!/^-?\d+$/.test(raw))
|
|
34807
|
+
return opts.default;
|
|
34808
|
+
const n = Number(raw);
|
|
34809
|
+
if (!Number.isSafeInteger(n))
|
|
34810
|
+
return opts.default;
|
|
34811
|
+
const min = opts.min ?? 0;
|
|
34812
|
+
if (n < min)
|
|
34813
|
+
return opts.default;
|
|
34814
|
+
if (opts.max !== void 0 && n > opts.max)
|
|
34815
|
+
return opts.default;
|
|
34816
|
+
return n;
|
|
34817
|
+
}
|
|
34818
|
+
function readTtlMsEnv(key, defaultMs, opts = {}) {
|
|
34819
|
+
const seconds = readIntEnv(key, { ...opts.env ? { env: opts.env } : {}, min: 0 });
|
|
34820
|
+
return seconds === void 0 ? defaultMs : seconds * 1e3;
|
|
34821
|
+
}
|
|
34822
|
+
|
|
34823
|
+
// node_modules/@chrischall/mcp-utils/dist/http/response-cache.js
|
|
34824
|
+
var RESPONSE_CACHE_MAX_ENTRIES = 256;
|
|
34825
|
+
function createResponseCache(opts) {
|
|
34826
|
+
const now = opts.now ?? Date.now;
|
|
34827
|
+
const maxEntries = opts.maxEntries ?? RESPONSE_CACHE_MAX_ENTRIES;
|
|
34828
|
+
const store = /* @__PURE__ */ new Map();
|
|
34829
|
+
const ttlFor = (tier) => opts.ttlMs[tier] ?? 0;
|
|
34830
|
+
function evictForInsert() {
|
|
34831
|
+
if (store.size < maxEntries)
|
|
34832
|
+
return;
|
|
34833
|
+
const t = now();
|
|
34834
|
+
for (const [key, entry] of store) {
|
|
34835
|
+
if (entry.expiresAt <= t)
|
|
34836
|
+
store.delete(key);
|
|
34837
|
+
}
|
|
34838
|
+
while (store.size >= maxEntries) {
|
|
34839
|
+
const oldest = store.keys().next();
|
|
34840
|
+
if (oldest.done)
|
|
34841
|
+
break;
|
|
34842
|
+
store.delete(oldest.value);
|
|
34843
|
+
}
|
|
34844
|
+
}
|
|
34845
|
+
function get(key) {
|
|
34846
|
+
const entry = store.get(key);
|
|
34847
|
+
if (!entry)
|
|
34848
|
+
return void 0;
|
|
34849
|
+
if (entry.expiresAt <= now()) {
|
|
34850
|
+
store.delete(key);
|
|
34851
|
+
return void 0;
|
|
34852
|
+
}
|
|
34853
|
+
return entry.value;
|
|
34854
|
+
}
|
|
34855
|
+
function set2(key, value, tier = "dynamic") {
|
|
34856
|
+
const ttl = ttlFor(tier);
|
|
34857
|
+
if (ttl <= 0)
|
|
34858
|
+
return;
|
|
34859
|
+
if (!store.has(key))
|
|
34860
|
+
evictForInsert();
|
|
34861
|
+
else
|
|
34862
|
+
store.delete(key);
|
|
34863
|
+
store.set(key, { expiresAt: now() + ttl, value });
|
|
34864
|
+
}
|
|
34865
|
+
return {
|
|
34866
|
+
get,
|
|
34867
|
+
set: set2,
|
|
34868
|
+
async fetchThrough(key, load, tier = "dynamic") {
|
|
34869
|
+
const hit = get(key);
|
|
34870
|
+
if (hit !== void 0)
|
|
34871
|
+
return hit;
|
|
34872
|
+
const value = await load();
|
|
34873
|
+
set2(key, value, tier);
|
|
34874
|
+
return value;
|
|
34875
|
+
},
|
|
34876
|
+
clear() {
|
|
34877
|
+
store.clear();
|
|
34878
|
+
},
|
|
34879
|
+
get size() {
|
|
34880
|
+
return store.size;
|
|
34881
|
+
}
|
|
34882
|
+
};
|
|
34883
|
+
}
|
|
34884
|
+
|
|
34885
|
+
// node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js
|
|
34886
|
+
function parseRetryAfterMs(header, opts = {}) {
|
|
34887
|
+
const defaultMs = opts.defaultMs ?? 2e3;
|
|
34888
|
+
const capMs = opts.capMs ?? 3e4;
|
|
34889
|
+
if (header == null)
|
|
34890
|
+
return defaultMs;
|
|
34891
|
+
const trimmed = header.trim();
|
|
34892
|
+
if (!/^\d+$/.test(trimmed))
|
|
34893
|
+
return defaultMs;
|
|
34894
|
+
return Math.min(Number(trimmed) * 1e3, capMs);
|
|
34895
|
+
}
|
|
34798
34896
|
|
|
34799
34897
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
34800
34898
|
function buildQueryString(params) {
|
|
@@ -34841,7 +34939,7 @@ var pageSchema = {
|
|
|
34841
34939
|
};
|
|
34842
34940
|
|
|
34843
34941
|
// src/version.ts
|
|
34844
|
-
var VERSION = "0.1
|
|
34942
|
+
var VERSION = "0.2.1";
|
|
34845
34943
|
|
|
34846
34944
|
// src/client.ts
|
|
34847
34945
|
import { dirname, join } from "node:path";
|
|
@@ -34853,33 +34951,24 @@ var SERVICE = "TripAdvisor Terra API";
|
|
|
34853
34951
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
34854
34952
|
var DEFAULT_CACHE_TTL_MS = 3e5;
|
|
34855
34953
|
var DEFAULT_STATIC_CACHE_TTL_MS = 36e5;
|
|
34856
|
-
var CACHE_MAX_ENTRIES = 256;
|
|
34857
34954
|
var MAX_RETRY_AFTER_MS = 1e4;
|
|
34858
|
-
function readCacheTtlMs(envVar, defaultMs) {
|
|
34859
|
-
const raw = readEnvVar(envVar);
|
|
34860
|
-
if (raw === void 0) return defaultMs;
|
|
34861
|
-
const secs = Number(raw);
|
|
34862
|
-
return Number.isFinite(secs) && secs >= 0 ? secs * 1e3 : defaultMs;
|
|
34863
|
-
}
|
|
34864
34955
|
var TripAdvisorClient = class {
|
|
34865
34956
|
apiKey;
|
|
34866
34957
|
configError;
|
|
34867
34958
|
fetchImpl;
|
|
34868
34959
|
sleep;
|
|
34869
|
-
|
|
34870
|
-
staticCacheTtlMs;
|
|
34871
|
-
now;
|
|
34872
|
-
cache = /* @__PURE__ */ new Map();
|
|
34960
|
+
cache;
|
|
34873
34961
|
/**
|
|
34874
34962
|
* Defer the config error so the server still boots (and answers the host's
|
|
34875
34963
|
* install-time tools/list probe) when TRIPADVISOR_API_KEY isn't set yet. The
|
|
34876
34964
|
* error is re-raised at request time via requireKey().
|
|
34877
34965
|
*/
|
|
34878
34966
|
constructor(opts = {}) {
|
|
34879
|
-
|
|
34967
|
+
const now = opts.now ?? Date.now;
|
|
34880
34968
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
34881
|
-
|
|
34882
|
-
|
|
34969
|
+
const cacheTtlMs = opts.cacheTtlMs ?? readTtlMsEnv("TRIPADVISOR_CACHE_TTL", DEFAULT_CACHE_TTL_MS);
|
|
34970
|
+
const staticCacheTtlMs = opts.staticCacheTtlMs ?? readTtlMsEnv("TRIPADVISOR_STATIC_CACHE_TTL", DEFAULT_STATIC_CACHE_TTL_MS);
|
|
34971
|
+
this.cache = createResponseCache({ ttlMs: { dynamic: cacheTtlMs, static: staticCacheTtlMs }, now });
|
|
34883
34972
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
34884
34973
|
const key = readEnvVar("TRIPADVISOR_API_KEY");
|
|
34885
34974
|
if (!key) {
|
|
@@ -34905,25 +34994,8 @@ var TripAdvisorClient = class {
|
|
|
34905
34994
|
* (TRIPADVISOR_CACHE_TTL) is for searches.
|
|
34906
34995
|
*/
|
|
34907
34996
|
async get(path, opts = {}) {
|
|
34908
|
-
const
|
|
34909
|
-
|
|
34910
|
-
const hit = this.cache.get(path);
|
|
34911
|
-
if (hit && hit.expiresAt > this.now()) return hit.value;
|
|
34912
|
-
}
|
|
34913
|
-
const value = await this.request(path);
|
|
34914
|
-
if (ttl > 0) {
|
|
34915
|
-
if (this.cache.size >= CACHE_MAX_ENTRIES) {
|
|
34916
|
-
const t = this.now();
|
|
34917
|
-
for (const [k, v] of this.cache) if (v.expiresAt <= t) this.cache.delete(k);
|
|
34918
|
-
while (this.cache.size >= CACHE_MAX_ENTRIES) {
|
|
34919
|
-
const oldest = this.cache.keys().next().value;
|
|
34920
|
-
if (oldest === void 0) break;
|
|
34921
|
-
this.cache.delete(oldest);
|
|
34922
|
-
}
|
|
34923
|
-
}
|
|
34924
|
-
this.cache.set(path, { expiresAt: this.now() + ttl, value });
|
|
34925
|
-
}
|
|
34926
|
-
return value;
|
|
34997
|
+
const tier = opts.cache === "static" ? "static" : "dynamic";
|
|
34998
|
+
return this.cache.fetchThrough(path, () => this.request(path), tier);
|
|
34927
34999
|
}
|
|
34928
35000
|
async request(path, isRetry = false) {
|
|
34929
35001
|
const key = this.requireKey();
|
|
@@ -34932,8 +35004,7 @@ var TripAdvisorClient = class {
|
|
|
34932
35004
|
if (res.ok) return await res.json();
|
|
34933
35005
|
const text = await res.text();
|
|
34934
35006
|
if (res.status === 429 && !isRetry) {
|
|
34935
|
-
const
|
|
34936
|
-
const delayMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1e3, MAX_RETRY_AFTER_MS) : 1e3;
|
|
35007
|
+
const delayMs = parseRetryAfterMs(res.headers.get("retry-after"), { defaultMs: 1e3, capMs: MAX_RETRY_AFTER_MS });
|
|
34937
35008
|
await this.sleep(delayMs);
|
|
34938
35009
|
return this.request(path, true);
|
|
34939
35010
|
}
|
|
@@ -34970,12 +35041,74 @@ function qs(params) {
|
|
|
34970
35041
|
return buildQueryString(params);
|
|
34971
35042
|
}
|
|
34972
35043
|
|
|
35044
|
+
// src/projection.ts
|
|
35045
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
35046
|
+
var numOf = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
35047
|
+
function primaryValue(arr) {
|
|
35048
|
+
if (!Array.isArray(arr) || arr.length === 0) return void 0;
|
|
35049
|
+
const primary = arr.find((e) => e && typeof e === "object" && e.primary === true);
|
|
35050
|
+
const pick2 = primary ?? arr[0];
|
|
35051
|
+
return str(pick2?.value);
|
|
35052
|
+
}
|
|
35053
|
+
function categoryFromUrl(url2) {
|
|
35054
|
+
if (!url2) return void 0;
|
|
35055
|
+
const m = /\/(Attraction|Hotel|Restaurant)_Review/.exec(url2);
|
|
35056
|
+
return m ? m[1].toUpperCase() : void 0;
|
|
35057
|
+
}
|
|
35058
|
+
function assign(o, k, v) {
|
|
35059
|
+
if (v !== void 0) o[k] = v;
|
|
35060
|
+
}
|
|
35061
|
+
function compactLocation(loc) {
|
|
35062
|
+
const addr = Array.isArray(loc.addresses) ? loc.addresses[0] : void 0;
|
|
35063
|
+
const url2 = str(loc.urls?.tripadvisor && loc.urls.tripadvisor.main);
|
|
35064
|
+
const overall = loc.traveler_ratings?.overall ?? {};
|
|
35065
|
+
const out = {};
|
|
35066
|
+
assign(out, "id", numOf(loc.id));
|
|
35067
|
+
assign(out, "name", primaryValue(loc.names));
|
|
35068
|
+
assign(out, "category", categoryFromUrl(url2));
|
|
35069
|
+
assign(out, "geo", str(loc.geo));
|
|
35070
|
+
assign(out, "city", str(addr?.city));
|
|
35071
|
+
assign(out, "state", str(addr?.state));
|
|
35072
|
+
assign(out, "rating", numOf(overall.rating));
|
|
35073
|
+
assign(out, "review_count", numOf(overall.count));
|
|
35074
|
+
assign(out, "url", url2);
|
|
35075
|
+
return out;
|
|
35076
|
+
}
|
|
35077
|
+
function compactList(raw) {
|
|
35078
|
+
const env = raw;
|
|
35079
|
+
const rows = env?.data;
|
|
35080
|
+
if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === "object" && "location" in r)) {
|
|
35081
|
+
console.error("[tripadvisor] compact: unexpected response shape, returning raw payload");
|
|
35082
|
+
return raw;
|
|
35083
|
+
}
|
|
35084
|
+
const data = rows.map((r) => {
|
|
35085
|
+
const row = r;
|
|
35086
|
+
const item = compactLocation(row.location);
|
|
35087
|
+
assign(item, "distance_miles", numOf(row.distance_miles));
|
|
35088
|
+
assign(item, "distance_kilometers", numOf(row.distance_kilometers));
|
|
35089
|
+
return item;
|
|
35090
|
+
});
|
|
35091
|
+
return env.pagination !== void 0 ? { data, pagination: env.pagination } : { data };
|
|
35092
|
+
}
|
|
35093
|
+
function compactLocationList(raw) {
|
|
35094
|
+
const env = raw;
|
|
35095
|
+
const rows = env?.data;
|
|
35096
|
+
if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === "object" && "id" in r)) {
|
|
35097
|
+
console.error("[tripadvisor] compact: unexpected response shape, returning raw payload");
|
|
35098
|
+
return raw;
|
|
35099
|
+
}
|
|
35100
|
+
return { data: rows.map((r) => compactLocation(r)) };
|
|
35101
|
+
}
|
|
35102
|
+
|
|
34973
35103
|
// src/tools/search.ts
|
|
35104
|
+
var compactParam = {
|
|
35105
|
+
compact: external_exports.boolean().optional().describe("Return a slim summary per result (id, name, category, city, rating, review_count, url) instead of full records")
|
|
35106
|
+
};
|
|
34974
35107
|
function registerSearchTools(server) {
|
|
34975
35108
|
server.registerTool(
|
|
34976
35109
|
"ta_search_locations",
|
|
34977
35110
|
{
|
|
34978
|
-
description: "Search TripAdvisor locations (restaurants, attractions, hotels) by name. Returns matches with a location id for the detail tools, plus pagination.",
|
|
35111
|
+
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.",
|
|
34979
35112
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
34980
35113
|
inputSchema: {
|
|
34981
35114
|
query: external_exports.string().min(1).max(500).describe("Text to search location names for"),
|
|
@@ -34984,47 +35117,98 @@ function registerSearchTools(server) {
|
|
|
34984
35117
|
geo_name: external_exports.string().optional().describe("City, town, or country name to scope the search"),
|
|
34985
35118
|
postal_code: external_exports.string().optional().describe("Postal/ZIP code (takes precedence over geo_name)"),
|
|
34986
35119
|
locale: LocaleList,
|
|
34987
|
-
...pageParams
|
|
35120
|
+
...pageParams,
|
|
35121
|
+
...compactParam
|
|
34988
35122
|
}
|
|
34989
35123
|
},
|
|
34990
|
-
async ({ query, category, country_code, geo_name, postal_code, locale, page, size }) => {
|
|
35124
|
+
async ({ query, category, country_code, geo_name, postal_code, locale, page, size, compact }) => {
|
|
34991
35125
|
const data = await client.get(
|
|
34992
35126
|
`/locations/search${qs({ query, category, country_code, geo_name, postal_code, locale, page, size })}`,
|
|
34993
35127
|
{ cache: "dynamic" }
|
|
34994
35128
|
);
|
|
34995
|
-
return textResult(data);
|
|
35129
|
+
return textResult(compact ? compactList(data) : data);
|
|
34996
35130
|
}
|
|
34997
35131
|
);
|
|
34998
35132
|
server.registerTool(
|
|
34999
35133
|
"ta_search_nearby",
|
|
35000
35134
|
{
|
|
35001
|
-
description: "Find TripAdvisor locations near a
|
|
35135
|
+
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.",
|
|
35002
35136
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
35003
35137
|
inputSchema: {
|
|
35004
|
-
|
|
35005
|
-
|
|
35006
|
-
|
|
35138
|
+
// Center — supply exactly one of: lat+lon, location_id, or the sw/ne box.
|
|
35139
|
+
lat: external_exports.number().min(-90).max(90).optional().describe("Center latitude (with lon+radius)"),
|
|
35140
|
+
lon: external_exports.number().min(-180).max(180).optional().describe("Center longitude (with lat+radius)"),
|
|
35141
|
+
location_id: external_exports.number().int().positive().optional().describe("Reference location as center (with radius)"),
|
|
35142
|
+
radius: external_exports.number().positive().optional().describe("Search radius (required with lat/lon or location_id; must be > 0)"),
|
|
35007
35143
|
unit: external_exports.enum(["MI", "KM"]).optional().describe("Radius unit (default MI)"),
|
|
35144
|
+
sw_lat: external_exports.number().min(-90).max(90).optional().describe("Bounding box SW latitude"),
|
|
35145
|
+
sw_lon: external_exports.number().min(-180).max(180).optional().describe("Bounding box SW longitude"),
|
|
35146
|
+
ne_lat: external_exports.number().min(-90).max(90).optional().describe("Bounding box NE latitude"),
|
|
35147
|
+
ne_lon: external_exports.number().min(-180).max(180).optional().describe("Bounding box NE longitude"),
|
|
35008
35148
|
category: Category.optional().describe("Restrict to one category"),
|
|
35009
35149
|
min_rating: external_exports.number().min(1).max(5).optional().describe("Minimum traveler rating (1.0\u20135.0)"),
|
|
35010
35150
|
include_photo: external_exports.boolean().optional().describe("Include a photo per result"),
|
|
35011
35151
|
sort: external_exports.enum(["distance", "rating"]).optional().describe("Sort order (default distance)"),
|
|
35012
35152
|
locale: LocaleList,
|
|
35013
|
-
...pageParams
|
|
35153
|
+
...pageParams,
|
|
35154
|
+
...compactParam
|
|
35014
35155
|
}
|
|
35015
35156
|
},
|
|
35016
|
-
async (
|
|
35157
|
+
async (args) => {
|
|
35158
|
+
const { lat, lon, location_id, radius, sw_lat, sw_lon, ne_lat, ne_lon, compact, ...rest } = args;
|
|
35159
|
+
const boxParts = [sw_lat, sw_lon, ne_lat, ne_lon];
|
|
35160
|
+
const boxGiven = boxParts.filter((v) => v !== void 0).length;
|
|
35161
|
+
if (boxGiven > 0 && boxGiven < 4) {
|
|
35162
|
+
throw new McpToolError("ta_search_nearby bounding box needs all four of sw_lat, sw_lon, ne_lat, ne_lon.", {
|
|
35163
|
+
hint: "Provide all four box corners, or use lat+lon+radius or location_id+radius instead."
|
|
35164
|
+
});
|
|
35165
|
+
}
|
|
35166
|
+
if (lat === void 0 !== (lon === void 0)) {
|
|
35167
|
+
throw new McpToolError("ta_search_nearby needs both lat and lon together (or neither).", {
|
|
35168
|
+
hint: "Provide lat AND lon with a radius, or use location_id+radius or a sw/ne box."
|
|
35169
|
+
});
|
|
35170
|
+
}
|
|
35171
|
+
const hasLatLon = lat !== void 0 && lon !== void 0;
|
|
35172
|
+
const hasBox = boxGiven === 4;
|
|
35173
|
+
const centers = [hasLatLon, location_id !== void 0, hasBox].filter(Boolean).length;
|
|
35174
|
+
if (centers !== 1) {
|
|
35175
|
+
throw new McpToolError(
|
|
35176
|
+
"ta_search_nearby needs exactly one center: lat+lon (+radius), location_id (+radius), or the sw/ne bounding box.",
|
|
35177
|
+
{ 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." }
|
|
35178
|
+
);
|
|
35179
|
+
}
|
|
35180
|
+
if ((hasLatLon || location_id !== void 0) && radius === void 0) {
|
|
35181
|
+
throw new McpToolError("lat/lon and location_id center modes require radius.", {
|
|
35182
|
+
hint: "Add radius (with optional unit MI/KM), or switch to a sw/ne bounding box."
|
|
35183
|
+
});
|
|
35184
|
+
}
|
|
35017
35185
|
const data = await client.get(
|
|
35018
|
-
`/locations/nearby${qs({ lat, lon,
|
|
35186
|
+
`/locations/nearby${qs({ lat, lon, location_id, radius, sw_lat, sw_lon, ne_lat, ne_lon, ...rest })}`,
|
|
35019
35187
|
{ cache: "dynamic" }
|
|
35020
35188
|
);
|
|
35021
|
-
return textResult(data);
|
|
35189
|
+
return textResult(compact ? compactList(data) : data);
|
|
35022
35190
|
}
|
|
35023
35191
|
);
|
|
35024
35192
|
}
|
|
35025
35193
|
|
|
35026
35194
|
// src/tools/location.ts
|
|
35027
35195
|
function registerLocationTools(server) {
|
|
35196
|
+
server.registerTool(
|
|
35197
|
+
"ta_get_locations",
|
|
35198
|
+
{
|
|
35199
|
+
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.",
|
|
35200
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
35201
|
+
inputSchema: {
|
|
35202
|
+
ids: external_exports.array(LocationId).min(1).max(50).describe("Location IDs to fetch (1\u201350)"),
|
|
35203
|
+
locale: LocaleList,
|
|
35204
|
+
compact: external_exports.boolean().optional().describe("Return a slim summary per location instead of full records")
|
|
35205
|
+
}
|
|
35206
|
+
},
|
|
35207
|
+
async ({ ids, locale, compact }) => {
|
|
35208
|
+
const data = await client.get(`/locations${qs({ id: ids, locale })}`, { cache: "static" });
|
|
35209
|
+
return textResult(compact ? compactLocationList(data) : data);
|
|
35210
|
+
}
|
|
35211
|
+
);
|
|
35028
35212
|
server.registerTool(
|
|
35029
35213
|
"ta_get_location_details",
|
|
35030
35214
|
{
|
|
@@ -38398,7 +38582,7 @@ function healthcheckHint(args) {
|
|
|
38398
38582
|
return `Unexpected error \u2014 see the error.message field for details.`;
|
|
38399
38583
|
}
|
|
38400
38584
|
function registerBridgeHealthcheckTool(args) {
|
|
38401
|
-
const { server, prefix, probePath, hostLabel, transport, probeFn } = args;
|
|
38585
|
+
const { server, prefix, probePath, hostLabel, transport, probeFn, classifyThrown, hints } = args;
|
|
38402
38586
|
const probeUrl = `https://${hostLabel}${probePath}`;
|
|
38403
38587
|
server.registerTool(`${prefix}_healthcheck`, {
|
|
38404
38588
|
title: "Verify the fetchproxy bridge end-to-end",
|
|
@@ -38431,16 +38615,39 @@ function registerBridgeHealthcheckTool(args) {
|
|
|
38431
38615
|
} : { url: probeUrl, elapsed_ms: probeResult.elapsed_ms };
|
|
38432
38616
|
let error51;
|
|
38433
38617
|
let bridgeHint;
|
|
38618
|
+
let customHint;
|
|
38619
|
+
let customDetail;
|
|
38434
38620
|
if (probeResult.error) {
|
|
38435
|
-
|
|
38621
|
+
let kind = probeResult.error.kind === "other" ? "unknown" : probeResult.error.kind;
|
|
38436
38622
|
bridgeHint = thrown instanceof FetchproxyBridgeDownError ? thrown.hint : void 0;
|
|
38623
|
+
if (thrown !== void 0 && classifyThrown) {
|
|
38624
|
+
const custom2 = classifyThrown(thrown);
|
|
38625
|
+
if (custom2) {
|
|
38626
|
+
kind = custom2.kind;
|
|
38627
|
+
customHint = custom2.hint;
|
|
38628
|
+
customDetail = custom2.detail;
|
|
38629
|
+
}
|
|
38630
|
+
}
|
|
38437
38631
|
error51 = {
|
|
38438
38632
|
kind,
|
|
38439
38633
|
message: probeResult.error.message,
|
|
38440
|
-
...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {}
|
|
38634
|
+
...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
|
|
38635
|
+
...customDetail !== void 0 ? { detail: customDetail } : {}
|
|
38441
38636
|
};
|
|
38442
38637
|
}
|
|
38443
38638
|
const lastExtensionMessageAt = transport.status().lastExtensionMessageAt;
|
|
38639
|
+
const arm = ok ? "ok" : error51?.kind === "bridge_down" ? "bridge_down" : probeResult.bridge.role === null ? "no_role" : error51?.kind === "timeout" ? "timeout" : error51?.kind === "protocol" || error51?.kind === "http" ? "protocol" : "unknown";
|
|
38640
|
+
const defaultHint = healthcheckHint({
|
|
38641
|
+
ok,
|
|
38642
|
+
role: probeResult.bridge.role,
|
|
38643
|
+
// FIX: the real configured port from bridgeHealth(), not a literal 37149.
|
|
38644
|
+
port: probeResult.bridge.port,
|
|
38645
|
+
hostLabel,
|
|
38646
|
+
prefix,
|
|
38647
|
+
probePath,
|
|
38648
|
+
errorKind: error51?.kind,
|
|
38649
|
+
bridgeHint: error51?.kind === "bridge_down" ? bridgeHint : void 0
|
|
38650
|
+
});
|
|
38444
38651
|
const result = {
|
|
38445
38652
|
ok,
|
|
38446
38653
|
bridge: {
|
|
@@ -38449,17 +38656,8 @@ function registerBridgeHealthcheckTool(args) {
|
|
|
38449
38656
|
},
|
|
38450
38657
|
probe,
|
|
38451
38658
|
...error51 ? { error: error51 } : {},
|
|
38452
|
-
|
|
38453
|
-
|
|
38454
|
-
role: probeResult.bridge.role,
|
|
38455
|
-
// FIX: the real configured port from bridgeHealth(), not a literal 37149.
|
|
38456
|
-
port: probeResult.bridge.port,
|
|
38457
|
-
hostLabel,
|
|
38458
|
-
prefix,
|
|
38459
|
-
probePath,
|
|
38460
|
-
errorKind: error51?.kind,
|
|
38461
|
-
bridgeHint: error51?.kind === "bridge_down" ? bridgeHint : void 0
|
|
38462
|
-
})
|
|
38659
|
+
// Precedence: classifyThrown's hint > per-arm override > default ladder.
|
|
38660
|
+
hint: customHint ?? hints?.[arm] ?? defaultHint
|
|
38463
38661
|
};
|
|
38464
38662
|
return {
|
|
38465
38663
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
@@ -38529,19 +38727,19 @@ function parseLocationDetail(html) {
|
|
|
38529
38727
|
const rating = node.aggregateRating ?? {};
|
|
38530
38728
|
const geo = node.geo ?? {};
|
|
38531
38729
|
const detail = { name: node.name };
|
|
38532
|
-
const
|
|
38730
|
+
const assign2 = (key, value) => {
|
|
38533
38731
|
if (value !== void 0) detail[key] = value;
|
|
38534
38732
|
};
|
|
38535
|
-
|
|
38536
|
-
|
|
38537
|
-
|
|
38538
|
-
|
|
38539
|
-
|
|
38540
|
-
|
|
38541
|
-
|
|
38542
|
-
|
|
38543
|
-
|
|
38544
|
-
|
|
38733
|
+
assign2("type", typeof node["@type"] === "string" ? node["@type"] : void 0);
|
|
38734
|
+
assign2("url", typeof node.url === "string" ? node.url : void 0);
|
|
38735
|
+
assign2("rating", num(rating.ratingValue));
|
|
38736
|
+
assign2("review_count", num(rating.reviewCount));
|
|
38737
|
+
assign2("best_rating", num(rating.bestRating));
|
|
38738
|
+
assign2("telephone", typeof node.telephone === "string" ? node.telephone : void 0);
|
|
38739
|
+
assign2("image", typeof node.image === "string" ? node.image : void 0);
|
|
38740
|
+
assign2("latitude", num(geo.latitude));
|
|
38741
|
+
assign2("longitude", num(geo.longitude));
|
|
38742
|
+
assign2("same_as", typeof node.sameAs === "string" ? node.sameAs : void 0);
|
|
38545
38743
|
if (node.address && typeof node.address === "object") {
|
|
38546
38744
|
const addr = {};
|
|
38547
38745
|
for (const [k, v] of Object.entries(node.address)) {
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { loadDotenvSafely, readEnvVar, formatApiError, truncateErrorMessage, McpToolError } from '@chrischall/mcp-utils';
|
|
3
|
+
import { loadDotenvSafely, readEnvVar, readTtlMsEnv, createResponseCache, parseRetryAfterMs, formatApiError, truncateErrorMessage, McpToolError, } from '@chrischall/mcp-utils';
|
|
4
4
|
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. the
|
|
5
5
|
// .mcpb bundle). loadDotenvSafely never lets .env override a host-provided value.
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -15,40 +15,25 @@ const DEFAULT_CACHE_TTL_MS = 300_000;
|
|
|
15
15
|
// Location details/photos/reviews change slowly — 1 hour by default.
|
|
16
16
|
// Override with TRIPADVISOR_STATIC_CACHE_TTL (seconds; 0 = off).
|
|
17
17
|
const DEFAULT_STATIC_CACHE_TTL_MS = 3_600_000;
|
|
18
|
-
// Bound the cache so a long-lived server doesn't grow unbounded across many
|
|
19
|
-
// distinct paths; oldest entries are evicted first.
|
|
20
|
-
const CACHE_MAX_ENTRIES = 256;
|
|
21
18
|
// Cap a server-supplied Retry-After so one bad header can't stall a tool call.
|
|
22
19
|
const MAX_RETRY_AFTER_MS = 10_000;
|
|
23
|
-
/** Resolve a cache TTL (ms) from an env var holding seconds. A blank or
|
|
24
|
-
* non-numeric value falls back to `defaultMs`; a valid `0` disables caching. */
|
|
25
|
-
function readCacheTtlMs(envVar, defaultMs) {
|
|
26
|
-
const raw = readEnvVar(envVar);
|
|
27
|
-
if (raw === undefined)
|
|
28
|
-
return defaultMs;
|
|
29
|
-
const secs = Number(raw);
|
|
30
|
-
return Number.isFinite(secs) && secs >= 0 ? secs * 1000 : defaultMs;
|
|
31
|
-
}
|
|
32
20
|
export class TripAdvisorClient {
|
|
33
21
|
apiKey;
|
|
34
22
|
configError;
|
|
35
23
|
fetchImpl;
|
|
36
24
|
sleep;
|
|
37
|
-
|
|
38
|
-
staticCacheTtlMs;
|
|
39
|
-
now;
|
|
40
|
-
cache = new Map();
|
|
25
|
+
cache;
|
|
41
26
|
/**
|
|
42
27
|
* Defer the config error so the server still boots (and answers the host's
|
|
43
28
|
* install-time tools/list probe) when TRIPADVISOR_API_KEY isn't set yet. The
|
|
44
29
|
* error is re-raised at request time via requireKey().
|
|
45
30
|
*/
|
|
46
31
|
constructor(opts = {}) {
|
|
47
|
-
|
|
32
|
+
const now = opts.now ?? Date.now;
|
|
48
33
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
const cacheTtlMs = opts.cacheTtlMs ?? readTtlMsEnv('TRIPADVISOR_CACHE_TTL', DEFAULT_CACHE_TTL_MS);
|
|
35
|
+
const staticCacheTtlMs = opts.staticCacheTtlMs ?? readTtlMsEnv('TRIPADVISOR_STATIC_CACHE_TTL', DEFAULT_STATIC_CACHE_TTL_MS);
|
|
36
|
+
this.cache = createResponseCache({ ttlMs: { dynamic: cacheTtlMs, static: staticCacheTtlMs }, now });
|
|
52
37
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
53
38
|
const key = readEnvVar('TRIPADVISOR_API_KEY');
|
|
54
39
|
if (!key) {
|
|
@@ -76,31 +61,8 @@ export class TripAdvisorClient {
|
|
|
76
61
|
* (TRIPADVISOR_CACHE_TTL) is for searches.
|
|
77
62
|
*/
|
|
78
63
|
async get(path, opts = {}) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const hit = this.cache.get(path);
|
|
82
|
-
if (hit && hit.expiresAt > this.now())
|
|
83
|
-
return hit.value;
|
|
84
|
-
}
|
|
85
|
-
const value = await this.request(path);
|
|
86
|
-
if (ttl > 0) {
|
|
87
|
-
if (this.cache.size >= CACHE_MAX_ENTRIES) {
|
|
88
|
-
// Evict expired entries first; if still full, drop the oldest (Map
|
|
89
|
-
// preserves insertion order, so the first key is the oldest).
|
|
90
|
-
const t = this.now();
|
|
91
|
-
for (const [k, v] of this.cache)
|
|
92
|
-
if (v.expiresAt <= t)
|
|
93
|
-
this.cache.delete(k);
|
|
94
|
-
while (this.cache.size >= CACHE_MAX_ENTRIES) {
|
|
95
|
-
const oldest = this.cache.keys().next().value;
|
|
96
|
-
if (oldest === undefined)
|
|
97
|
-
break;
|
|
98
|
-
this.cache.delete(oldest);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
this.cache.set(path, { expiresAt: this.now() + ttl, value });
|
|
102
|
-
}
|
|
103
|
-
return value;
|
|
64
|
+
const tier = opts.cache === 'static' ? 'static' : 'dynamic';
|
|
65
|
+
return this.cache.fetchThrough(path, () => this.request(path), tier);
|
|
104
66
|
}
|
|
105
67
|
async request(path, isRetry = false) {
|
|
106
68
|
const key = this.requireKey();
|
|
@@ -112,8 +74,7 @@ export class TripAdvisorClient {
|
|
|
112
74
|
return (await res.json());
|
|
113
75
|
const text = await res.text();
|
|
114
76
|
if (res.status === 429 && !isRetry) {
|
|
115
|
-
const
|
|
116
|
-
const delayMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1000, MAX_RETRY_AFTER_MS) : 1_000;
|
|
77
|
+
const delayMs = parseRetryAfterMs(res.headers.get('retry-after'), { defaultMs: 1_000, capMs: MAX_RETRY_AFTER_MS });
|
|
117
78
|
await this.sleep(delayMs);
|
|
118
79
|
return this.request(path, true);
|
|
119
80
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Opt-in compact projection for the verbose Terra list responses. Pure and
|
|
2
|
+
// unit-tested against captured shapes (docs/TRIPADVISOR-API.md). Keyed only off
|
|
3
|
+
// fields observed live; on shape drift it warns and returns the RAW payload so
|
|
4
|
+
// an undocumented API change degrades instead of silently emitting empties.
|
|
5
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
6
|
+
const numOf = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
7
|
+
/** Primary-language value from a Terra `names`/`descriptions` array (or the first entry). */
|
|
8
|
+
function primaryValue(arr) {
|
|
9
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
const primary = arr.find((e) => e && typeof e === 'object' && e.primary === true);
|
|
12
|
+
const pick = (primary ?? arr[0]);
|
|
13
|
+
return str(pick?.value);
|
|
14
|
+
}
|
|
15
|
+
/** ATTRACTION | HOTEL | RESTAURANT from the `/{Type}_Review-…` listing URL. */
|
|
16
|
+
function categoryFromUrl(url) {
|
|
17
|
+
if (!url)
|
|
18
|
+
return undefined;
|
|
19
|
+
const m = /\/(Attraction|Hotel|Restaurant)_Review/.exec(url);
|
|
20
|
+
return m ? m[1].toUpperCase() : undefined;
|
|
21
|
+
}
|
|
22
|
+
function assign(o, k, v) {
|
|
23
|
+
if (v !== undefined)
|
|
24
|
+
o[k] = v;
|
|
25
|
+
}
|
|
26
|
+
/** Project one Terra location object to a {@link CompactLocation}. */
|
|
27
|
+
export function compactLocation(loc) {
|
|
28
|
+
const addr = (Array.isArray(loc.addresses) ? loc.addresses[0] : undefined);
|
|
29
|
+
const url = str(loc.urls?.tripadvisor && loc.urls.tripadvisor.main);
|
|
30
|
+
const overall = (loc.traveler_ratings?.overall ?? {});
|
|
31
|
+
const out = {};
|
|
32
|
+
assign(out, 'id', numOf(loc.id));
|
|
33
|
+
assign(out, 'name', primaryValue(loc.names));
|
|
34
|
+
assign(out, 'category', categoryFromUrl(url));
|
|
35
|
+
assign(out, 'geo', str(loc.geo));
|
|
36
|
+
assign(out, 'city', str(addr?.city));
|
|
37
|
+
assign(out, 'state', str(addr?.state));
|
|
38
|
+
assign(out, 'rating', numOf(overall.rating));
|
|
39
|
+
assign(out, 'review_count', numOf(overall.count));
|
|
40
|
+
assign(out, 'url', url);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Project a Terra list envelope (`{data:[{location,…}], pagination}`) to compact
|
|
45
|
+
* items, preserving `pagination` and any per-item `distance_*` (nearby). If the
|
|
46
|
+
* shape isn't the expected `data:[{location}]`, warn to stderr and return the raw
|
|
47
|
+
* payload unchanged — undocumented APIs drift, so degrade rather than break.
|
|
48
|
+
*/
|
|
49
|
+
export function compactList(raw) {
|
|
50
|
+
const env = raw;
|
|
51
|
+
const rows = env?.data;
|
|
52
|
+
if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === 'object' && 'location' in r)) {
|
|
53
|
+
console.error('[tripadvisor] compact: unexpected response shape, returning raw payload');
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
const data = rows.map((r) => {
|
|
57
|
+
const row = r;
|
|
58
|
+
const item = compactLocation(row.location);
|
|
59
|
+
assign(item, 'distance_miles', numOf(row.distance_miles));
|
|
60
|
+
assign(item, 'distance_kilometers', numOf(row.distance_kilometers));
|
|
61
|
+
return item;
|
|
62
|
+
});
|
|
63
|
+
return env.pagination !== undefined ? { data, pagination: env.pagination } : { data };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compact projection for the multi-get envelope (`{data:[<Location>]}` — the
|
|
67
|
+
* items ARE locations, not `{location}` wrappers). Same drift-fallback contract
|
|
68
|
+
* as {@link compactList}.
|
|
69
|
+
*/
|
|
70
|
+
export function compactLocationList(raw) {
|
|
71
|
+
const env = raw;
|
|
72
|
+
const rows = env?.data;
|
|
73
|
+
if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === 'object' && 'id' in r)) {
|
|
74
|
+
console.error('[tripadvisor] compact: unexpected response shape, returning raw payload');
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
77
|
+
return { data: rows.map((r) => compactLocation(r)) };
|
|
78
|
+
}
|
package/dist/tools/location.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
2
3
|
import { client } from '../client.js';
|
|
3
4
|
import { LocationId, LocaleList, pageParams, qs } from './shared.js';
|
|
5
|
+
import { compactLocationList } from '../projection.js';
|
|
4
6
|
export function registerLocationTools(server) {
|
|
7
|
+
server.registerTool('ta_get_locations', {
|
|
8
|
+
description: 'Get details for MULTIPLE locations in one call (batch). Pass an array of location ids — cheaper than repeated ta_get_location_details. Unknown or unlicensed ids are silently omitted. Pass compact:true for slim summaries.',
|
|
9
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
10
|
+
inputSchema: {
|
|
11
|
+
ids: z.array(LocationId).min(1).max(50).describe('Location IDs to fetch (1–50)'),
|
|
12
|
+
locale: LocaleList,
|
|
13
|
+
compact: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Return a slim summary per location instead of full records'),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ ids, locale, compact }) => {
|
|
19
|
+
const data = await client.get(`/locations${qs({ id: ids, locale })}`, { cache: 'static' });
|
|
20
|
+
return textResult(compact ? compactLocationList(data) : data);
|
|
21
|
+
});
|
|
5
22
|
server.registerTool('ta_get_location_details', {
|
|
6
23
|
description: 'Get full details for a TripAdvisor location: names, descriptions, address, coordinates, traveler ratings, phone, category, and listing URLs.',
|
|
7
24
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
package/dist/tools/search.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textResult } from '@chrischall/mcp-utils';
|
|
2
|
+
import { textResult, McpToolError } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from '../client.js';
|
|
4
4
|
import { Category, LocaleList, pageParams, qs } from './shared.js';
|
|
5
|
+
import { compactList } from '../projection.js';
|
|
6
|
+
/** `compact` arg shared by the list-returning search tools. */
|
|
7
|
+
const compactParam = {
|
|
8
|
+
compact: z
|
|
9
|
+
.boolean()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Return a slim summary per result (id, name, category, city, rating, review_count, url) instead of full records'),
|
|
12
|
+
};
|
|
5
13
|
export function registerSearchTools(server) {
|
|
6
14
|
server.registerTool('ta_search_locations', {
|
|
7
|
-
description: 'Search TripAdvisor locations (restaurants, attractions, hotels) by name. Returns matches with a location id for the detail tools, plus pagination.',
|
|
15
|
+
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.',
|
|
8
16
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
9
17
|
inputSchema: {
|
|
10
18
|
query: z.string().min(1).max(500).describe('Text to search location names for'),
|
|
@@ -14,28 +22,65 @@ export function registerSearchTools(server) {
|
|
|
14
22
|
postal_code: z.string().optional().describe('Postal/ZIP code (takes precedence over geo_name)'),
|
|
15
23
|
locale: LocaleList,
|
|
16
24
|
...pageParams,
|
|
25
|
+
...compactParam,
|
|
17
26
|
},
|
|
18
|
-
}, async ({ query, category, country_code, geo_name, postal_code, locale, page, size }) => {
|
|
27
|
+
}, async ({ query, category, country_code, geo_name, postal_code, locale, page, size, compact }) => {
|
|
19
28
|
const data = await client.get(`/locations/search${qs({ query, category, country_code, geo_name, postal_code, locale, page, size })}`, { cache: 'dynamic' });
|
|
20
|
-
return textResult(data);
|
|
29
|
+
return textResult(compact ? compactList(data) : data);
|
|
21
30
|
});
|
|
22
31
|
server.registerTool('ta_search_nearby', {
|
|
23
|
-
description: 'Find TripAdvisor locations near a
|
|
32
|
+
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.',
|
|
24
33
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
25
34
|
inputSchema: {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
// Center — supply exactly one of: lat+lon, location_id, or the sw/ne box.
|
|
36
|
+
lat: z.number().min(-90).max(90).optional().describe('Center latitude (with lon+radius)'),
|
|
37
|
+
lon: z.number().min(-180).max(180).optional().describe('Center longitude (with lat+radius)'),
|
|
38
|
+
location_id: z.number().int().positive().optional().describe('Reference location as center (with radius)'),
|
|
39
|
+
radius: z.number().positive().optional().describe('Search radius (required with lat/lon or location_id; must be > 0)'),
|
|
29
40
|
unit: z.enum(['MI', 'KM']).optional().describe('Radius unit (default MI)'),
|
|
41
|
+
sw_lat: z.number().min(-90).max(90).optional().describe('Bounding box SW latitude'),
|
|
42
|
+
sw_lon: z.number().min(-180).max(180).optional().describe('Bounding box SW longitude'),
|
|
43
|
+
ne_lat: z.number().min(-90).max(90).optional().describe('Bounding box NE latitude'),
|
|
44
|
+
ne_lon: z.number().min(-180).max(180).optional().describe('Bounding box NE longitude'),
|
|
30
45
|
category: Category.optional().describe('Restrict to one category'),
|
|
31
46
|
min_rating: z.number().min(1).max(5).optional().describe('Minimum traveler rating (1.0–5.0)'),
|
|
32
47
|
include_photo: z.boolean().optional().describe('Include a photo per result'),
|
|
33
48
|
sort: z.enum(['distance', 'rating']).optional().describe('Sort order (default distance)'),
|
|
34
49
|
locale: LocaleList,
|
|
35
50
|
...pageParams,
|
|
51
|
+
...compactParam,
|
|
36
52
|
},
|
|
37
|
-
}, async (
|
|
38
|
-
const
|
|
39
|
-
|
|
53
|
+
}, async (args) => {
|
|
54
|
+
const { lat, lon, location_id, radius, sw_lat, sw_lon, ne_lat, ne_lon, compact, ...rest } = args;
|
|
55
|
+
const boxParts = [sw_lat, sw_lon, ne_lat, ne_lon];
|
|
56
|
+
const boxGiven = boxParts.filter((v) => v !== undefined).length;
|
|
57
|
+
// A partial box (1–3 of 4) is never valid: it can't form a center on its
|
|
58
|
+
// own, and if it rode alongside a lat/lon or location_id center it would
|
|
59
|
+
// bleed into the Terra URL and 400. Reject it before the center count.
|
|
60
|
+
if (boxGiven > 0 && boxGiven < 4) {
|
|
61
|
+
throw new McpToolError('ta_search_nearby bounding box needs all four of sw_lat, sw_lon, ne_lat, ne_lon.', {
|
|
62
|
+
hint: 'Provide all four box corners, or use lat+lon+radius or location_id+radius instead.',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Same defect class: a lone lat or lon can't form a center and would bleed
|
|
66
|
+
// into the URL alongside another center — require them together or not at all.
|
|
67
|
+
if ((lat === undefined) !== (lon === undefined)) {
|
|
68
|
+
throw new McpToolError('ta_search_nearby needs both lat and lon together (or neither).', {
|
|
69
|
+
hint: 'Provide lat AND lon with a radius, or use location_id+radius or a sw/ne box.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const hasLatLon = lat !== undefined && lon !== undefined;
|
|
73
|
+
const hasBox = boxGiven === 4;
|
|
74
|
+
const centers = [hasLatLon, location_id !== undefined, hasBox].filter(Boolean).length;
|
|
75
|
+
if (centers !== 1) {
|
|
76
|
+
throw new McpToolError('ta_search_nearby needs exactly one center: lat+lon (+radius), location_id (+radius), or the sw/ne bounding box.', { hint: 'Provide lat AND lon, OR location_id, OR all four of sw_lat/sw_lon/ne_lat/ne_lon — not more than one.' });
|
|
77
|
+
}
|
|
78
|
+
if ((hasLatLon || location_id !== undefined) && radius === undefined) {
|
|
79
|
+
throw new McpToolError('lat/lon and location_id center modes require radius.', {
|
|
80
|
+
hint: 'Add radius (with optional unit MI/KM), or switch to a sw/ne bounding box.',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const data = await client.get(`/locations/nearby${qs({ lat, lon, location_id, radius, sw_lat, sw_lon, ne_lat, ne_lon, ...rest })}`, { cache: 'dynamic' });
|
|
84
|
+
return textResult(compact ? compactList(data) : data);
|
|
40
85
|
});
|
|
41
86
|
}
|
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.1
|
|
2
|
+
export const VERSION = '0.2.1'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/tripadvisor-mcp",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,7 +44,7 @@
|
|
|
44
44
|
"test:coverage": "vitest run --coverage"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@chrischall/mcp-utils": "^0.
|
|
47
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
48
48
|
"@fetchproxy/server": "^1.3.4",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
50
|
"dotenv": "^17.4.0",
|
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.1
|
|
9
|
+
"version": "0.2.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/tripadvisor-mcp",
|
|
14
|
-
"version": "0.1
|
|
14
|
+
"version": "0.2.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|