hotelzero 1.0.0 → 1.1.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/README.md +4 -1
- package/dist/browser.d.ts +5 -0
- package/dist/browser.js +39 -2
- package/dist/index.js +32 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,13 +100,16 @@ Get detailed information about a specific hotel including full amenity list, des
|
|
|
100
100
|
| `checkOut` | string | Yes | Check-out date (YYYY-MM-DD) |
|
|
101
101
|
| `guests` | number | No | Number of guests (default: 2) |
|
|
102
102
|
| `rooms` | number | No | Number of rooms (default: 1) |
|
|
103
|
+
| `currency` | string | No | Currency code (USD, EUR, GBP, JPY, etc.) Default: USD |
|
|
104
|
+
| `sortBy` | enum | No | Sort results: `popularity`, `price_lowest`, `price_highest`, `rating`, `distance` |
|
|
103
105
|
|
|
104
106
|
### Rating & Price
|
|
105
107
|
|
|
106
108
|
| Filter | Type | Description |
|
|
107
109
|
|--------|------|-------------|
|
|
108
110
|
| `minRating` | number | Minimum review score: 6=Pleasant, 7=Good, 8=Very Good, 9=Wonderful |
|
|
109
|
-
| `
|
|
111
|
+
| `minPrice` | number | Minimum price per night |
|
|
112
|
+
| `maxPrice` | number | Maximum price per night |
|
|
110
113
|
|
|
111
114
|
### Property Type
|
|
112
115
|
|
package/dist/browser.d.ts
CHANGED
|
@@ -4,9 +4,12 @@ export interface HotelSearchParams {
|
|
|
4
4
|
checkOut: string;
|
|
5
5
|
guests: number;
|
|
6
6
|
rooms: number;
|
|
7
|
+
currency?: string;
|
|
8
|
+
sortBy?: "popularity" | "price_lowest" | "price_highest" | "rating" | "distance";
|
|
7
9
|
}
|
|
8
10
|
export interface HotelFilters {
|
|
9
11
|
minRating?: number;
|
|
12
|
+
minPrice?: number;
|
|
10
13
|
maxPrice?: number;
|
|
11
14
|
propertyType?: "hotel" | "apartment" | "resort" | "villa" | "vacation_home" | "hostel" | "bnb" | "guesthouse" | "homestay" | "motel" | "inn" | "lodge" | "chalet" | "campground" | "glamping" | "boat" | "capsule" | "ryokan" | "riad" | "country_house" | "farm_stay";
|
|
12
15
|
starRating?: 1 | 2 | 3 | 4 | 5;
|
|
@@ -99,6 +102,8 @@ export interface HotelResult {
|
|
|
99
102
|
amenities: string[];
|
|
100
103
|
highlights: string[];
|
|
101
104
|
link: string;
|
|
105
|
+
thumbnailUrl: string | null;
|
|
106
|
+
availability: string | null;
|
|
102
107
|
matchScore?: number;
|
|
103
108
|
matchReasons?: string[];
|
|
104
109
|
}
|
package/dist/browser.js
CHANGED
|
@@ -191,14 +191,28 @@ export class HotelBrowser {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
buildBookingUrl(params, filters) {
|
|
194
|
-
const { destination, checkIn, checkOut, guests, rooms } = params;
|
|
194
|
+
const { destination, checkIn, checkOut, guests, rooms, currency, sortBy } = params;
|
|
195
195
|
const url = new URL("https://www.booking.com/searchresults.html");
|
|
196
196
|
url.searchParams.set("ss", destination);
|
|
197
197
|
url.searchParams.set("checkin", checkIn);
|
|
198
198
|
url.searchParams.set("checkout", checkOut);
|
|
199
199
|
url.searchParams.set("group_adults", guests.toString());
|
|
200
200
|
url.searchParams.set("no_rooms", rooms.toString());
|
|
201
|
-
url.searchParams.set("selected_currency", "USD");
|
|
201
|
+
url.searchParams.set("selected_currency", currency || "USD");
|
|
202
|
+
// Sort order
|
|
203
|
+
if (sortBy) {
|
|
204
|
+
const sortMap = {
|
|
205
|
+
popularity: "popularity",
|
|
206
|
+
price_lowest: "price",
|
|
207
|
+
price_highest: "price",
|
|
208
|
+
rating: "review_score_and_price",
|
|
209
|
+
distance: "distance_from_search",
|
|
210
|
+
};
|
|
211
|
+
url.searchParams.set("order", sortMap[sortBy] || "popularity");
|
|
212
|
+
if (sortBy === "price_highest") {
|
|
213
|
+
url.searchParams.set("sort_order", "desc");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
202
216
|
if (!filters)
|
|
203
217
|
return url.toString();
|
|
204
218
|
const nfltParts = [];
|
|
@@ -557,6 +571,24 @@ export class HotelBrowser {
|
|
|
557
571
|
highlights.push("Free Cancellation");
|
|
558
572
|
if (cardText.includes("no prepayment"))
|
|
559
573
|
highlights.push("No Prepayment");
|
|
574
|
+
// Extract thumbnail image URL
|
|
575
|
+
const imgEl = card.querySelector('img[data-testid="image"]');
|
|
576
|
+
const thumbnailUrl = imgEl?.src || null;
|
|
577
|
+
// Extract availability status (e.g., "Only 2 rooms left", "Last booked 5 minutes ago")
|
|
578
|
+
let availability = null;
|
|
579
|
+
const availabilityPatterns = [
|
|
580
|
+
/only\s*\d+\s*(rooms?|left)/i,
|
|
581
|
+
/last\s*(booked|reserved)\s*\d+\s*(minutes?|hours?)\s*ago/i,
|
|
582
|
+
/in\s*high\s*demand/i,
|
|
583
|
+
/selling\s*fast/i,
|
|
584
|
+
];
|
|
585
|
+
for (const pattern of availabilityPatterns) {
|
|
586
|
+
const match = cardText.match(pattern);
|
|
587
|
+
if (match) {
|
|
588
|
+
availability = match[0];
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
560
592
|
results.push({
|
|
561
593
|
name,
|
|
562
594
|
price,
|
|
@@ -569,6 +601,8 @@ export class HotelBrowser {
|
|
|
569
601
|
amenities,
|
|
570
602
|
highlights,
|
|
571
603
|
link,
|
|
604
|
+
thumbnailUrl,
|
|
605
|
+
availability,
|
|
572
606
|
});
|
|
573
607
|
});
|
|
574
608
|
return results;
|
|
@@ -682,6 +716,9 @@ export class HotelBrowser {
|
|
|
682
716
|
if (filters.minRating && hotel.rating && hotel.rating < filters.minRating) {
|
|
683
717
|
return false;
|
|
684
718
|
}
|
|
719
|
+
if (filters.minPrice && hotel.price && hotel.price < filters.minPrice) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
685
722
|
if (filters.maxPrice && hotel.price && hotel.price > filters.maxPrice) {
|
|
686
723
|
return false;
|
|
687
724
|
}
|
package/dist/index.js
CHANGED
|
@@ -37,9 +37,13 @@ const FindHotelsSchema = z.object({
|
|
|
37
37
|
checkOut: z.string().describe("Check-out date (YYYY-MM-DD)"),
|
|
38
38
|
guests: z.number().default(2).describe("Number of guests"),
|
|
39
39
|
rooms: z.number().default(1).describe("Number of rooms"),
|
|
40
|
+
// Currency & Sorting
|
|
41
|
+
currency: z.string().optional().describe("Currency code (USD, EUR, GBP, JPY, etc.)"),
|
|
42
|
+
sortBy: z.enum(["popularity", "price_lowest", "price_highest", "rating", "distance"]).optional().describe("Sort results by"),
|
|
40
43
|
// Rating & Price
|
|
41
44
|
minRating: z.number().optional().describe("Minimum rating (6=Pleasant, 7=Good, 8=Very Good, 9=Wonderful)"),
|
|
42
|
-
|
|
45
|
+
minPrice: z.number().optional().describe("Minimum price per night"),
|
|
46
|
+
maxPrice: z.number().optional().describe("Maximum price per night"),
|
|
43
47
|
// Property & Star Rating
|
|
44
48
|
propertyType: PropertyTypeEnum.optional().describe("Type of property (hotel, resort, apartment, villa, etc.)"),
|
|
45
49
|
starRating: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional().describe("Star rating (1-5)"),
|
|
@@ -136,6 +140,9 @@ function formatHotelResult(hotel, index) {
|
|
|
136
140
|
if (hotel.distanceToCenter) {
|
|
137
141
|
lines.push(` Location: ${hotel.distanceToCenter}`);
|
|
138
142
|
}
|
|
143
|
+
if (hotel.availability) {
|
|
144
|
+
lines.push(` Availability: ${hotel.availability}`);
|
|
145
|
+
}
|
|
139
146
|
if (hotel.amenities.length > 0) {
|
|
140
147
|
lines.push(` Amenities: ${hotel.amenities.join(", ")}`);
|
|
141
148
|
}
|
|
@@ -145,6 +152,9 @@ function formatHotelResult(hotel, index) {
|
|
|
145
152
|
lines.push(` Why it matches: ${hotel.matchReasons.join(", ")}`);
|
|
146
153
|
}
|
|
147
154
|
}
|
|
155
|
+
if (hotel.thumbnailUrl) {
|
|
156
|
+
lines.push(` Image: ${hotel.thumbnailUrl}`);
|
|
157
|
+
}
|
|
148
158
|
if (hotel.link) {
|
|
149
159
|
// Shorten link for readability
|
|
150
160
|
const shortLink = hotel.link.split("?")[0];
|
|
@@ -154,8 +164,8 @@ function formatHotelResult(hotel, index) {
|
|
|
154
164
|
}
|
|
155
165
|
// Create MCP server
|
|
156
166
|
const server = new Server({
|
|
157
|
-
name: "
|
|
158
|
-
version: "
|
|
167
|
+
name: "hotelzero",
|
|
168
|
+
version: "1.1.0",
|
|
159
169
|
}, {
|
|
160
170
|
capabilities: {
|
|
161
171
|
tools: {},
|
|
@@ -173,7 +183,15 @@ const findHotelsInputSchema = {
|
|
|
173
183
|
rooms: { type: "number", description: "Number of rooms", default: 1 },
|
|
174
184
|
// Rating & Price
|
|
175
185
|
minRating: { type: "number", description: "Minimum rating: 6=Pleasant, 7=Good, 8=Very Good, 9=Wonderful" },
|
|
176
|
-
|
|
186
|
+
minPrice: { type: "number", description: "Minimum price per night" },
|
|
187
|
+
maxPrice: { type: "number", description: "Maximum price per night" },
|
|
188
|
+
// Currency & Sorting
|
|
189
|
+
currency: { type: "string", description: "Currency code (USD, EUR, GBP, JPY, etc.)", default: "USD" },
|
|
190
|
+
sortBy: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Sort results by",
|
|
193
|
+
enum: ["popularity", "price_lowest", "price_highest", "rating", "distance"]
|
|
194
|
+
},
|
|
177
195
|
// Property Type
|
|
178
196
|
propertyType: {
|
|
179
197
|
type: "string",
|
|
@@ -333,11 +351,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
333
351
|
checkOut: parsed.checkOut,
|
|
334
352
|
guests: parsed.guests,
|
|
335
353
|
rooms: parsed.rooms,
|
|
354
|
+
currency: parsed.currency,
|
|
355
|
+
sortBy: parsed.sortBy,
|
|
336
356
|
};
|
|
337
357
|
// Build filters object from all parsed parameters
|
|
338
358
|
const filters = {};
|
|
339
359
|
// Copy all filter properties
|
|
340
|
-
const filterKeys = Object.keys(parsed).filter(k => !['destination', 'checkIn', 'checkOut', 'guests', 'rooms'].includes(k));
|
|
360
|
+
const filterKeys = Object.keys(parsed).filter(k => !['destination', 'checkIn', 'checkOut', 'guests', 'rooms', 'currency', 'sortBy'].includes(k));
|
|
341
361
|
for (const key of filterKeys) {
|
|
342
362
|
const value = parsed[key];
|
|
343
363
|
if (value !== undefined && value !== null) {
|
|
@@ -389,6 +409,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
389
409
|
activeFilters.push(parsed.hotelChain);
|
|
390
410
|
if (parsed.minRating)
|
|
391
411
|
activeFilters.push(`rating ≥${parsed.minRating}`);
|
|
412
|
+
if (parsed.minPrice)
|
|
413
|
+
activeFilters.push(`≥$${parsed.minPrice}/night`);
|
|
392
414
|
if (parsed.maxPrice)
|
|
393
415
|
activeFilters.push(`≤$${parsed.maxPrice}/night`);
|
|
394
416
|
if (parsed.allInclusive)
|
|
@@ -399,6 +421,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
399
421
|
activeFilters.push("diving");
|
|
400
422
|
if (parsed.skiing)
|
|
401
423
|
activeFilters.push("skiing");
|
|
424
|
+
if (parsed.currency && parsed.currency !== "USD")
|
|
425
|
+
activeFilters.push(`currency: ${parsed.currency}`);
|
|
426
|
+
if (parsed.sortBy)
|
|
427
|
+
activeFilters.push(`sorted by: ${parsed.sortBy.replace("_", " ")}`);
|
|
402
428
|
const filtersLine = activeFilters.length > 0
|
|
403
429
|
? `Filters: ${activeFilters.join(", ")}\n\n`
|
|
404
430
|
: "\n";
|
|
@@ -486,7 +512,7 @@ process.on("SIGTERM", async () => {
|
|
|
486
512
|
async function main() {
|
|
487
513
|
const transport = new StdioServerTransport();
|
|
488
514
|
await server.connect(transport);
|
|
489
|
-
console.error("HotelZero v1.
|
|
515
|
+
console.error("HotelZero v1.1.0 running on stdio");
|
|
490
516
|
}
|
|
491
517
|
main().catch((error) => {
|
|
492
518
|
console.error("Fatal error:", error);
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hotelzero",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for searching hotels on Booking.com with 80+ filters",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"hotelzero": "
|
|
9
|
+
"hotelzero": "dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|