hotelzero 1.0.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 +521 -0
- package/dist/browser.d.ts +119 -0
- package/dist/browser.js +731 -0
- package/dist/debug-filters.d.ts +1 -0
- package/dist/debug-filters.js +72 -0
- package/dist/debug-sponsored.d.ts +1 -0
- package/dist/debug-sponsored.js +87 -0
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +37 -0
- package/dist/extract-filters.d.ts +1 -0
- package/dist/extract-filters.js +96 -0
- package/dist/final-test.d.ts +1 -0
- package/dist/final-test.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +494 -0
- package/dist/test-mcp.d.ts +1 -0
- package/dist/test-mcp.js +61 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +37 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { HotelBrowser } from "./browser.js";
|
|
7
|
+
// Property type enum
|
|
8
|
+
const PropertyTypeEnum = z.enum([
|
|
9
|
+
"hotel", "apartment", "resort", "villa", "vacation_home", "hostel", "bnb",
|
|
10
|
+
"guesthouse", "homestay", "motel", "inn", "lodge", "chalet", "campground",
|
|
11
|
+
"glamping", "boat", "capsule", "ryokan", "riad", "country_house", "farm_stay"
|
|
12
|
+
]);
|
|
13
|
+
// Bed type enum
|
|
14
|
+
const BedTypeEnum = z.enum(["king", "queen", "double", "twin", "single"]);
|
|
15
|
+
// Distance enum
|
|
16
|
+
const DistanceEnum = z.enum(["half_mile", "1_mile", "2_miles"]);
|
|
17
|
+
// Hotel chain enum
|
|
18
|
+
const HotelChainEnum = z.enum([
|
|
19
|
+
"marriott", "hilton", "hyatt", "ihg", "wyndham", "best_western", "accor",
|
|
20
|
+
"choice", "radisson", "ritz_carlton", "four_seasons", "fairmont", "sheraton",
|
|
21
|
+
"westin", "w_hotels", "courtyard", "residence_inn", "hampton", "embassy_suites",
|
|
22
|
+
"doubletree"
|
|
23
|
+
]);
|
|
24
|
+
// Basic search schema
|
|
25
|
+
const SearchHotelsSchema = z.object({
|
|
26
|
+
destination: z.string().describe("City or location (e.g., 'San Juan, Puerto Rico')"),
|
|
27
|
+
checkIn: z.string().describe("Check-in date (YYYY-MM-DD)"),
|
|
28
|
+
checkOut: z.string().describe("Check-out date (YYYY-MM-DD)"),
|
|
29
|
+
guests: z.number().default(2).describe("Number of guests"),
|
|
30
|
+
rooms: z.number().default(1).describe("Number of rooms"),
|
|
31
|
+
});
|
|
32
|
+
// Comprehensive find_hotels schema with all filters
|
|
33
|
+
const FindHotelsSchema = z.object({
|
|
34
|
+
// Basic search params
|
|
35
|
+
destination: z.string().describe("City or location (e.g., 'San Juan, Puerto Rico')"),
|
|
36
|
+
checkIn: z.string().describe("Check-in date (YYYY-MM-DD)"),
|
|
37
|
+
checkOut: z.string().describe("Check-out date (YYYY-MM-DD)"),
|
|
38
|
+
guests: z.number().default(2).describe("Number of guests"),
|
|
39
|
+
rooms: z.number().default(1).describe("Number of rooms"),
|
|
40
|
+
// Rating & Price
|
|
41
|
+
minRating: z.number().optional().describe("Minimum rating (6=Pleasant, 7=Good, 8=Very Good, 9=Wonderful)"),
|
|
42
|
+
maxPrice: z.number().optional().describe("Maximum price per night in USD"),
|
|
43
|
+
// Property & Star Rating
|
|
44
|
+
propertyType: PropertyTypeEnum.optional().describe("Type of property (hotel, resort, apartment, villa, etc.)"),
|
|
45
|
+
starRating: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional().describe("Star rating (1-5)"),
|
|
46
|
+
// Beach & Location
|
|
47
|
+
beachfront: z.boolean().optional().describe("Beachfront property"),
|
|
48
|
+
beachAccess: z.boolean().optional().describe("Has beach access"),
|
|
49
|
+
oceanView: z.boolean().optional().describe("Room with ocean/sea view"),
|
|
50
|
+
maxDistanceFromCenter: DistanceEnum.optional().describe("Maximum distance from center"),
|
|
51
|
+
// Hotel Facilities
|
|
52
|
+
freeWifi: z.boolean().optional().describe("Free WiFi"),
|
|
53
|
+
pool: z.boolean().optional().describe("Swimming pool"),
|
|
54
|
+
spa: z.boolean().optional().describe("Spa/wellness center"),
|
|
55
|
+
fitness: z.boolean().optional().describe("Fitness center/gym"),
|
|
56
|
+
parking: z.boolean().optional().describe("Parking available"),
|
|
57
|
+
restaurant: z.boolean().optional().describe("On-site restaurant"),
|
|
58
|
+
bar: z.boolean().optional().describe("On-site bar"),
|
|
59
|
+
roomService: z.boolean().optional().describe("24-hour room service"),
|
|
60
|
+
airportShuttle: z.boolean().optional().describe("Airport shuttle"),
|
|
61
|
+
hotTub: z.boolean().optional().describe("Hot tub/Jacuzzi"),
|
|
62
|
+
sauna: z.boolean().optional().describe("Sauna"),
|
|
63
|
+
garden: z.boolean().optional().describe("Garden"),
|
|
64
|
+
terrace: z.boolean().optional().describe("Terrace"),
|
|
65
|
+
nonSmokingRooms: z.boolean().optional().describe("Non-smoking rooms"),
|
|
66
|
+
familyRooms: z.boolean().optional().describe("Family rooms"),
|
|
67
|
+
evCharging: z.boolean().optional().describe("Electric vehicle charging"),
|
|
68
|
+
casino: z.boolean().optional().describe("Casino"),
|
|
69
|
+
golf: z.boolean().optional().describe("Golf course nearby"),
|
|
70
|
+
tennis: z.boolean().optional().describe("Tennis court"),
|
|
71
|
+
businessCenter: z.boolean().optional().describe("Business center"),
|
|
72
|
+
// Room Facilities
|
|
73
|
+
airConditioning: z.boolean().optional().describe("Air conditioning"),
|
|
74
|
+
kitchen: z.boolean().optional().describe("Kitchen/kitchenette"),
|
|
75
|
+
balcony: z.boolean().optional().describe("Balcony"),
|
|
76
|
+
privatePool: z.boolean().optional().describe("Private pool"),
|
|
77
|
+
privateBathroom: z.boolean().optional().describe("Private bathroom"),
|
|
78
|
+
bath: z.boolean().optional().describe("Bathtub"),
|
|
79
|
+
tv: z.boolean().optional().describe("TV"),
|
|
80
|
+
minibar: z.boolean().optional().describe("Minibar"),
|
|
81
|
+
safe: z.boolean().optional().describe("In-room safe"),
|
|
82
|
+
washingMachine: z.boolean().optional().describe("Washing machine"),
|
|
83
|
+
soundproofing: z.boolean().optional().describe("Soundproofing"),
|
|
84
|
+
// Bed Type
|
|
85
|
+
bedType: BedTypeEnum.optional().describe("Preferred bed type"),
|
|
86
|
+
// Meal Plans
|
|
87
|
+
breakfast: z.boolean().optional().describe("Breakfast included"),
|
|
88
|
+
allInclusive: z.boolean().optional().describe("All-inclusive"),
|
|
89
|
+
selfCatering: z.boolean().optional().describe("Self-catering/kitchen amenities"),
|
|
90
|
+
// Stay Type
|
|
91
|
+
petFriendly: z.boolean().optional().describe("Pet friendly"),
|
|
92
|
+
adultsOnly: z.boolean().optional().describe("Adults only"),
|
|
93
|
+
lgbtqFriendly: z.boolean().optional().describe("LGBTQ+ friendly (Travel Proud)"),
|
|
94
|
+
// Booking Policies
|
|
95
|
+
freeCancellation: z.boolean().optional().describe("Free cancellation"),
|
|
96
|
+
noPrepayment: z.boolean().optional().describe("No prepayment needed"),
|
|
97
|
+
// Sustainability
|
|
98
|
+
sustainabilityCertified: z.boolean().optional().describe("Sustainability certification"),
|
|
99
|
+
// Activities
|
|
100
|
+
snorkeling: z.boolean().optional().describe("Snorkeling"),
|
|
101
|
+
diving: z.boolean().optional().describe("Diving"),
|
|
102
|
+
fishing: z.boolean().optional().describe("Fishing"),
|
|
103
|
+
hiking: z.boolean().optional().describe("Hiking"),
|
|
104
|
+
cycling: z.boolean().optional().describe("Cycling"),
|
|
105
|
+
skiing: z.boolean().optional().describe("Skiing"),
|
|
106
|
+
waterSports: z.boolean().optional().describe("Water sports"),
|
|
107
|
+
// Accessibility
|
|
108
|
+
wheelchairAccessible: z.boolean().optional().describe("Wheelchair accessible"),
|
|
109
|
+
groundFloor: z.boolean().optional().describe("Ground floor unit"),
|
|
110
|
+
elevatorAccess: z.boolean().optional().describe("Elevator access"),
|
|
111
|
+
walkInShower: z.boolean().optional().describe("Walk-in shower"),
|
|
112
|
+
rollInShower: z.boolean().optional().describe("Roll-in shower"),
|
|
113
|
+
grabRails: z.boolean().optional().describe("Grab rails"),
|
|
114
|
+
// Hotel Chain
|
|
115
|
+
hotelChain: HotelChainEnum.optional().describe("Specific hotel chain"),
|
|
116
|
+
});
|
|
117
|
+
const HotelDetailsSchema = z.object({
|
|
118
|
+
url: z.string().describe("Booking.com URL for the hotel"),
|
|
119
|
+
});
|
|
120
|
+
// Global browser instance (reuse for efficiency)
|
|
121
|
+
let browser = null;
|
|
122
|
+
async function getBrowser() {
|
|
123
|
+
if (!browser) {
|
|
124
|
+
browser = new HotelBrowser();
|
|
125
|
+
await browser.init(true);
|
|
126
|
+
}
|
|
127
|
+
return browser;
|
|
128
|
+
}
|
|
129
|
+
function formatHotelResult(hotel, index) {
|
|
130
|
+
const lines = [];
|
|
131
|
+
lines.push(`${index + 1}. ${hotel.name}`);
|
|
132
|
+
lines.push(` Price: ${hotel.priceDisplay}`);
|
|
133
|
+
if (hotel.rating) {
|
|
134
|
+
lines.push(` Rating: ${hotel.rating}/10 ${hotel.ratingText} (${hotel.reviewCount || "?"} reviews)`);
|
|
135
|
+
}
|
|
136
|
+
if (hotel.distanceToCenter) {
|
|
137
|
+
lines.push(` Location: ${hotel.distanceToCenter}`);
|
|
138
|
+
}
|
|
139
|
+
if (hotel.amenities.length > 0) {
|
|
140
|
+
lines.push(` Amenities: ${hotel.amenities.join(", ")}`);
|
|
141
|
+
}
|
|
142
|
+
if (hotel.matchScore !== undefined && hotel.matchScore > 0) {
|
|
143
|
+
lines.push(` Match Score: ${hotel.matchScore}`);
|
|
144
|
+
if (hotel.matchReasons && hotel.matchReasons.length > 0) {
|
|
145
|
+
lines.push(` Why it matches: ${hotel.matchReasons.join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (hotel.link) {
|
|
149
|
+
// Shorten link for readability
|
|
150
|
+
const shortLink = hotel.link.split("?")[0];
|
|
151
|
+
lines.push(` Book: ${shortLink}`);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
// Create MCP server
|
|
156
|
+
const server = new Server({
|
|
157
|
+
name: "hotel-booking-mcp",
|
|
158
|
+
version: "2.0.0",
|
|
159
|
+
}, {
|
|
160
|
+
capabilities: {
|
|
161
|
+
tools: {},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// Build comprehensive input schema for find_hotels
|
|
165
|
+
const findHotelsInputSchema = {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
// Basic search
|
|
169
|
+
destination: { type: "string", description: "City or location (e.g., 'San Juan, Puerto Rico')" },
|
|
170
|
+
checkIn: { type: "string", description: "Check-in date (YYYY-MM-DD)" },
|
|
171
|
+
checkOut: { type: "string", description: "Check-out date (YYYY-MM-DD)" },
|
|
172
|
+
guests: { type: "number", description: "Number of guests", default: 2 },
|
|
173
|
+
rooms: { type: "number", description: "Number of rooms", default: 1 },
|
|
174
|
+
// Rating & Price
|
|
175
|
+
minRating: { type: "number", description: "Minimum rating: 6=Pleasant, 7=Good, 8=Very Good, 9=Wonderful" },
|
|
176
|
+
maxPrice: { type: "number", description: "Maximum price per night in USD" },
|
|
177
|
+
// Property Type
|
|
178
|
+
propertyType: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "Property type",
|
|
181
|
+
enum: ["hotel", "apartment", "resort", "villa", "vacation_home", "hostel", "bnb", "guesthouse", "homestay", "motel", "inn", "lodge", "chalet", "campground", "glamping", "boat", "capsule", "ryokan", "riad", "country_house", "farm_stay"]
|
|
182
|
+
},
|
|
183
|
+
starRating: { type: "number", description: "Star rating (1-5)", enum: [1, 2, 3, 4, 5] },
|
|
184
|
+
// Beach & Location
|
|
185
|
+
beachfront: { type: "boolean", description: "Beachfront property" },
|
|
186
|
+
beachAccess: { type: "boolean", description: "Has beach access" },
|
|
187
|
+
oceanView: { type: "boolean", description: "Room with ocean/sea view" },
|
|
188
|
+
maxDistanceFromCenter: { type: "string", description: "Max distance from center", enum: ["half_mile", "1_mile", "2_miles"] },
|
|
189
|
+
// Hotel Facilities
|
|
190
|
+
freeWifi: { type: "boolean", description: "Free WiFi" },
|
|
191
|
+
pool: { type: "boolean", description: "Swimming pool" },
|
|
192
|
+
spa: { type: "boolean", description: "Spa/wellness" },
|
|
193
|
+
fitness: { type: "boolean", description: "Fitness center/gym" },
|
|
194
|
+
parking: { type: "boolean", description: "Parking" },
|
|
195
|
+
restaurant: { type: "boolean", description: "Restaurant" },
|
|
196
|
+
bar: { type: "boolean", description: "Bar" },
|
|
197
|
+
roomService: { type: "boolean", description: "24-hour room service" },
|
|
198
|
+
airportShuttle: { type: "boolean", description: "Airport shuttle" },
|
|
199
|
+
hotTub: { type: "boolean", description: "Hot tub/Jacuzzi" },
|
|
200
|
+
sauna: { type: "boolean", description: "Sauna" },
|
|
201
|
+
garden: { type: "boolean", description: "Garden" },
|
|
202
|
+
terrace: { type: "boolean", description: "Terrace" },
|
|
203
|
+
nonSmokingRooms: { type: "boolean", description: "Non-smoking rooms" },
|
|
204
|
+
familyRooms: { type: "boolean", description: "Family rooms" },
|
|
205
|
+
evCharging: { type: "boolean", description: "EV charging" },
|
|
206
|
+
casino: { type: "boolean", description: "Casino" },
|
|
207
|
+
golf: { type: "boolean", description: "Golf nearby" },
|
|
208
|
+
tennis: { type: "boolean", description: "Tennis" },
|
|
209
|
+
businessCenter: { type: "boolean", description: "Business center" },
|
|
210
|
+
bbqFacilities: { type: "boolean", description: "BBQ facilities" },
|
|
211
|
+
laundry: { type: "boolean", description: "Laundry service" },
|
|
212
|
+
concierge: { type: "boolean", description: "Concierge" },
|
|
213
|
+
// Room Facilities
|
|
214
|
+
airConditioning: { type: "boolean", description: "Air conditioning" },
|
|
215
|
+
kitchen: { type: "boolean", description: "Kitchen/kitchenette" },
|
|
216
|
+
balcony: { type: "boolean", description: "Balcony" },
|
|
217
|
+
privatePool: { type: "boolean", description: "Private pool" },
|
|
218
|
+
privateBathroom: { type: "boolean", description: "Private bathroom" },
|
|
219
|
+
bath: { type: "boolean", description: "Bathtub" },
|
|
220
|
+
tv: { type: "boolean", description: "TV" },
|
|
221
|
+
minibar: { type: "boolean", description: "Minibar" },
|
|
222
|
+
safe: { type: "boolean", description: "In-room safe" },
|
|
223
|
+
washingMachine: { type: "boolean", description: "Washing machine" },
|
|
224
|
+
soundproofing: { type: "boolean", description: "Soundproofing" },
|
|
225
|
+
// Bed Type
|
|
226
|
+
bedType: { type: "string", description: "Bed type", enum: ["king", "queen", "double", "twin", "single"] },
|
|
227
|
+
// Meal Plans
|
|
228
|
+
breakfast: { type: "boolean", description: "Breakfast included" },
|
|
229
|
+
allInclusive: { type: "boolean", description: "All-inclusive" },
|
|
230
|
+
selfCatering: { type: "boolean", description: "Self-catering" },
|
|
231
|
+
// Stay Type
|
|
232
|
+
petFriendly: { type: "boolean", description: "Pet friendly" },
|
|
233
|
+
adultsOnly: { type: "boolean", description: "Adults only" },
|
|
234
|
+
lgbtqFriendly: { type: "boolean", description: "LGBTQ+ friendly" },
|
|
235
|
+
// Booking Policies
|
|
236
|
+
freeCancellation: { type: "boolean", description: "Free cancellation" },
|
|
237
|
+
noPrepayment: { type: "boolean", description: "No prepayment" },
|
|
238
|
+
noBookingFee: { type: "boolean", description: "No booking fee" },
|
|
239
|
+
// Sustainability
|
|
240
|
+
sustainabilityCertified: { type: "boolean", description: "Sustainability certified" },
|
|
241
|
+
// Activities
|
|
242
|
+
snorkeling: { type: "boolean", description: "Snorkeling" },
|
|
243
|
+
diving: { type: "boolean", description: "Diving" },
|
|
244
|
+
fishing: { type: "boolean", description: "Fishing" },
|
|
245
|
+
hiking: { type: "boolean", description: "Hiking" },
|
|
246
|
+
cycling: { type: "boolean", description: "Cycling" },
|
|
247
|
+
skiing: { type: "boolean", description: "Skiing" },
|
|
248
|
+
waterSports: { type: "boolean", description: "Water sports" },
|
|
249
|
+
horseRiding: { type: "boolean", description: "Horse riding" },
|
|
250
|
+
// Accessibility
|
|
251
|
+
wheelchairAccessible: { type: "boolean", description: "Wheelchair accessible" },
|
|
252
|
+
groundFloor: { type: "boolean", description: "Ground floor" },
|
|
253
|
+
elevatorAccess: { type: "boolean", description: "Elevator access" },
|
|
254
|
+
walkInShower: { type: "boolean", description: "Walk-in shower" },
|
|
255
|
+
rollInShower: { type: "boolean", description: "Roll-in shower" },
|
|
256
|
+
showerChair: { type: "boolean", description: "Shower chair" },
|
|
257
|
+
grabRails: { type: "boolean", description: "Grab rails" },
|
|
258
|
+
raisedToilet: { type: "boolean", description: "Raised toilet" },
|
|
259
|
+
loweredSink: { type: "boolean", description: "Lowered sink" },
|
|
260
|
+
braille: { type: "boolean", description: "Braille signage" },
|
|
261
|
+
tactileSigns: { type: "boolean", description: "Tactile signs" },
|
|
262
|
+
auditoryGuidance: { type: "boolean", description: "Auditory guidance" },
|
|
263
|
+
// Hotel Chain
|
|
264
|
+
hotelChain: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Hotel chain",
|
|
267
|
+
enum: ["marriott", "hilton", "hyatt", "ihg", "wyndham", "best_western", "accor", "choice", "radisson", "ritz_carlton", "four_seasons", "fairmont", "sheraton", "westin", "w_hotels", "courtyard", "residence_inn", "hampton", "embassy_suites", "doubletree"]
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
required: ["destination", "checkIn", "checkOut"],
|
|
271
|
+
};
|
|
272
|
+
// List available tools
|
|
273
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
274
|
+
return {
|
|
275
|
+
tools: [
|
|
276
|
+
{
|
|
277
|
+
name: "find_hotels",
|
|
278
|
+
description: `Search for hotels with comprehensive filtering options. Supports 80+ filters including:
|
|
279
|
+
- Property types (hotel, resort, villa, apartment, hostel, B&B, glamping, ryokan, etc.)
|
|
280
|
+
- Star ratings (1-5 stars)
|
|
281
|
+
- Beach/location (beachfront, ocean view, distance from center)
|
|
282
|
+
- Amenities (pool, spa, gym, WiFi, parking, restaurant, bar, hot tub, casino, golf, tennis)
|
|
283
|
+
- Room features (A/C, kitchen, balcony, private pool, bathtub, minibar, safe)
|
|
284
|
+
- Bed types (king, queen, double, twin, single)
|
|
285
|
+
- Meal plans (breakfast, all-inclusive, self-catering)
|
|
286
|
+
- Policies (pet-friendly, adults-only, LGBTQ+ friendly, free cancellation)
|
|
287
|
+
- Activities (snorkeling, diving, fishing, hiking, skiing, water sports)
|
|
288
|
+
- Accessibility (wheelchair, elevator, walk-in shower, grab rails, braille)
|
|
289
|
+
- Hotel chains (Marriott, Hilton, Hyatt, IHG, Ritz-Carlton, Four Seasons, etc.)
|
|
290
|
+
Results are scored and ranked by how well they match the criteria.`,
|
|
291
|
+
inputSchema: findHotelsInputSchema,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "search_hotels",
|
|
295
|
+
description: "Basic hotel search without filters. Use find_hotels for filtered searches.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
destination: { type: "string", description: "City or location" },
|
|
300
|
+
checkIn: { type: "string", description: "Check-in date (YYYY-MM-DD)" },
|
|
301
|
+
checkOut: { type: "string", description: "Check-out date (YYYY-MM-DD)" },
|
|
302
|
+
guests: { type: "number", description: "Number of guests", default: 2 },
|
|
303
|
+
rooms: { type: "number", description: "Number of rooms", default: 1 },
|
|
304
|
+
},
|
|
305
|
+
required: ["destination", "checkIn", "checkOut"],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "get_hotel_details",
|
|
310
|
+
description: "Get detailed information about a specific hotel including full amenity list, description, and photos",
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
url: { type: "string", description: "Booking.com URL for the hotel" },
|
|
315
|
+
},
|
|
316
|
+
required: ["url"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
// Handle tool calls
|
|
323
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
324
|
+
const { name, arguments: args } = request.params;
|
|
325
|
+
try {
|
|
326
|
+
const b = await getBrowser();
|
|
327
|
+
switch (name) {
|
|
328
|
+
case "find_hotels": {
|
|
329
|
+
const parsed = FindHotelsSchema.parse(args);
|
|
330
|
+
const searchParams = {
|
|
331
|
+
destination: parsed.destination,
|
|
332
|
+
checkIn: parsed.checkIn,
|
|
333
|
+
checkOut: parsed.checkOut,
|
|
334
|
+
guests: parsed.guests,
|
|
335
|
+
rooms: parsed.rooms,
|
|
336
|
+
};
|
|
337
|
+
// Build filters object from all parsed parameters
|
|
338
|
+
const filters = {};
|
|
339
|
+
// Copy all filter properties
|
|
340
|
+
const filterKeys = Object.keys(parsed).filter(k => !['destination', 'checkIn', 'checkOut', 'guests', 'rooms'].includes(k));
|
|
341
|
+
for (const key of filterKeys) {
|
|
342
|
+
const value = parsed[key];
|
|
343
|
+
if (value !== undefined && value !== null) {
|
|
344
|
+
filters[key] = value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const results = await b.searchHotels(searchParams, filters);
|
|
348
|
+
// Format results nicely
|
|
349
|
+
const header = `Found ${results.length} hotels in ${parsed.destination} matching your criteria:\n`;
|
|
350
|
+
// Build active filters display
|
|
351
|
+
const activeFilters = [];
|
|
352
|
+
if (parsed.beachfront)
|
|
353
|
+
activeFilters.push("beachfront");
|
|
354
|
+
if (parsed.beachAccess)
|
|
355
|
+
activeFilters.push("beach access");
|
|
356
|
+
if (parsed.oceanView)
|
|
357
|
+
activeFilters.push("ocean view");
|
|
358
|
+
if (parsed.freeWifi)
|
|
359
|
+
activeFilters.push("wifi");
|
|
360
|
+
if (parsed.pool)
|
|
361
|
+
activeFilters.push("pool");
|
|
362
|
+
if (parsed.spa)
|
|
363
|
+
activeFilters.push("spa");
|
|
364
|
+
if (parsed.fitness)
|
|
365
|
+
activeFilters.push("gym");
|
|
366
|
+
if (parsed.breakfast)
|
|
367
|
+
activeFilters.push("breakfast");
|
|
368
|
+
if (parsed.parking)
|
|
369
|
+
activeFilters.push("parking");
|
|
370
|
+
if (parsed.hotTub)
|
|
371
|
+
activeFilters.push("hot tub");
|
|
372
|
+
if (parsed.kitchen)
|
|
373
|
+
activeFilters.push("kitchen");
|
|
374
|
+
if (parsed.balcony)
|
|
375
|
+
activeFilters.push("balcony");
|
|
376
|
+
if (parsed.petFriendly)
|
|
377
|
+
activeFilters.push("pet-friendly");
|
|
378
|
+
if (parsed.adultsOnly)
|
|
379
|
+
activeFilters.push("adults-only");
|
|
380
|
+
if (parsed.freeCancellation)
|
|
381
|
+
activeFilters.push("free cancellation");
|
|
382
|
+
if (parsed.wheelchairAccessible)
|
|
383
|
+
activeFilters.push("wheelchair accessible");
|
|
384
|
+
if (parsed.propertyType)
|
|
385
|
+
activeFilters.push(parsed.propertyType);
|
|
386
|
+
if (parsed.starRating)
|
|
387
|
+
activeFilters.push(`${parsed.starRating}-star`);
|
|
388
|
+
if (parsed.hotelChain)
|
|
389
|
+
activeFilters.push(parsed.hotelChain);
|
|
390
|
+
if (parsed.minRating)
|
|
391
|
+
activeFilters.push(`rating ≥${parsed.minRating}`);
|
|
392
|
+
if (parsed.maxPrice)
|
|
393
|
+
activeFilters.push(`≤$${parsed.maxPrice}/night`);
|
|
394
|
+
if (parsed.allInclusive)
|
|
395
|
+
activeFilters.push("all-inclusive");
|
|
396
|
+
if (parsed.snorkeling)
|
|
397
|
+
activeFilters.push("snorkeling");
|
|
398
|
+
if (parsed.diving)
|
|
399
|
+
activeFilters.push("diving");
|
|
400
|
+
if (parsed.skiing)
|
|
401
|
+
activeFilters.push("skiing");
|
|
402
|
+
const filtersLine = activeFilters.length > 0
|
|
403
|
+
? `Filters: ${activeFilters.join(", ")}\n\n`
|
|
404
|
+
: "\n";
|
|
405
|
+
const hotelList = results
|
|
406
|
+
.slice(0, 15) // Top 15 results
|
|
407
|
+
.map((h, i) => formatHotelResult(h, i))
|
|
408
|
+
.join("\n\n");
|
|
409
|
+
return {
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: header + filtersLine + hotelList,
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
case "search_hotels": {
|
|
419
|
+
const parsed = SearchHotelsSchema.parse(args);
|
|
420
|
+
const results = await b.searchHotels({
|
|
421
|
+
destination: parsed.destination,
|
|
422
|
+
checkIn: parsed.checkIn,
|
|
423
|
+
checkOut: parsed.checkOut,
|
|
424
|
+
guests: parsed.guests,
|
|
425
|
+
rooms: parsed.rooms,
|
|
426
|
+
});
|
|
427
|
+
const header = `Found ${results.length} hotels in ${parsed.destination}:\n\n`;
|
|
428
|
+
const hotelList = results
|
|
429
|
+
.slice(0, 15)
|
|
430
|
+
.map((h, i) => formatHotelResult(h, i))
|
|
431
|
+
.join("\n\n");
|
|
432
|
+
return {
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: "text",
|
|
436
|
+
text: header + hotelList,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
case "get_hotel_details": {
|
|
442
|
+
const parsed = HotelDetailsSchema.parse(args);
|
|
443
|
+
const details = await b.getHotelDetails(parsed.url);
|
|
444
|
+
return {
|
|
445
|
+
content: [
|
|
446
|
+
{
|
|
447
|
+
type: "text",
|
|
448
|
+
text: JSON.stringify(details, null, 2),
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
default:
|
|
454
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
459
|
+
return {
|
|
460
|
+
content: [
|
|
461
|
+
{
|
|
462
|
+
type: "text",
|
|
463
|
+
text: `Error: ${errorMessage}`,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
isError: true,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// Cleanup on exit
|
|
471
|
+
async function cleanup() {
|
|
472
|
+
if (browser) {
|
|
473
|
+
await browser.close();
|
|
474
|
+
browser = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
process.on("SIGINT", async () => {
|
|
478
|
+
await cleanup();
|
|
479
|
+
process.exit(0);
|
|
480
|
+
});
|
|
481
|
+
process.on("SIGTERM", async () => {
|
|
482
|
+
await cleanup();
|
|
483
|
+
process.exit(0);
|
|
484
|
+
});
|
|
485
|
+
// Start server
|
|
486
|
+
async function main() {
|
|
487
|
+
const transport = new StdioServerTransport();
|
|
488
|
+
await server.connect(transport);
|
|
489
|
+
console.error("HotelZero v1.0.0 running on stdio");
|
|
490
|
+
}
|
|
491
|
+
main().catch((error) => {
|
|
492
|
+
console.error("Fatal error:", error);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test-mcp.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Test: Find hotels in Puerto Rico via MCP-style call with comprehensive filters
|
|
2
|
+
import { HotelBrowser } from "./browser.js";
|
|
3
|
+
async function testFindHotels() {
|
|
4
|
+
const browser = new HotelBrowser();
|
|
5
|
+
console.log("Initializing browser...");
|
|
6
|
+
await browser.init(false); // visible browser for debugging
|
|
7
|
+
// MCP-style parameters
|
|
8
|
+
const searchParams = {
|
|
9
|
+
destination: "Puerto Rico",
|
|
10
|
+
checkIn: "2026-03-07",
|
|
11
|
+
checkOut: "2026-03-14",
|
|
12
|
+
guests: 2,
|
|
13
|
+
rooms: 1,
|
|
14
|
+
};
|
|
15
|
+
// Test with comprehensive filters
|
|
16
|
+
const filters = {
|
|
17
|
+
beachfront: true, // Beachfront property
|
|
18
|
+
freeWifi: true, // Free WiFi
|
|
19
|
+
fitness: true, // Gym/fitness center
|
|
20
|
+
pool: true, // Swimming pool
|
|
21
|
+
minRating: 8.0, // Very Good or better
|
|
22
|
+
propertyType: "resort", // Resorts only
|
|
23
|
+
freeCancellation: true, // Free cancellation
|
|
24
|
+
};
|
|
25
|
+
console.log("\n=== Finding RESORT hotels in Puerto Rico ===");
|
|
26
|
+
console.log("Dates: March 7-14, 2026");
|
|
27
|
+
console.log("Filters: beachfront, resort, wifi, gym, pool, rating >= 8.0, free cancellation\n");
|
|
28
|
+
const results = await browser.searchHotels(searchParams, filters);
|
|
29
|
+
console.log(`Found ${results.length} hotels:\n`);
|
|
30
|
+
results.slice(0, 15).forEach((hotel, i) => {
|
|
31
|
+
console.log(`${i + 1}. ${hotel.name}`);
|
|
32
|
+
console.log(` Price: ${hotel.priceDisplay}`);
|
|
33
|
+
if (hotel.rating) {
|
|
34
|
+
console.log(` Rating: ${hotel.rating}/10 ${hotel.ratingText} (${hotel.reviewCount || "?"} reviews)`);
|
|
35
|
+
}
|
|
36
|
+
if (hotel.distanceToCenter) {
|
|
37
|
+
console.log(` Location: ${hotel.distanceToCenter}`);
|
|
38
|
+
}
|
|
39
|
+
if (hotel.amenities.length > 0) {
|
|
40
|
+
console.log(` Amenities: ${hotel.amenities.join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
if (hotel.highlights.length > 0) {
|
|
43
|
+
console.log(` Highlights: ${hotel.highlights.join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
if (hotel.matchScore !== undefined && hotel.matchScore > 0) {
|
|
46
|
+
console.log(` Match Score: ${hotel.matchScore}`);
|
|
47
|
+
if (hotel.matchReasons && hotel.matchReasons.length > 0) {
|
|
48
|
+
console.log(` Why it matches: ${hotel.matchReasons.join(", ")}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (hotel.link) {
|
|
52
|
+
const shortLink = hotel.link.split("?")[0];
|
|
53
|
+
console.log(` Book: ${shortLink}`);
|
|
54
|
+
}
|
|
55
|
+
console.log();
|
|
56
|
+
});
|
|
57
|
+
await browser.takeScreenshot("puerto-rico-results.png");
|
|
58
|
+
console.log("Screenshot saved to puerto-rico-results.png");
|
|
59
|
+
await browser.close();
|
|
60
|
+
}
|
|
61
|
+
testFindHotels().catch(console.error);
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Test: Find hotels near beach with good wifi in San Juan
|
|
2
|
+
import { HotelBrowser } from "./browser.js";
|
|
3
|
+
async function test() {
|
|
4
|
+
const browser = new HotelBrowser();
|
|
5
|
+
console.log("Initializing browser...");
|
|
6
|
+
await browser.init(false); // visible browser
|
|
7
|
+
console.log("\n=== Finding beach hotels with WiFi in San Juan ===\n");
|
|
8
|
+
const results = await browser.searchHotels({
|
|
9
|
+
destination: "San Juan, Puerto Rico",
|
|
10
|
+
checkIn: "2026-03-01",
|
|
11
|
+
checkOut: "2026-03-05",
|
|
12
|
+
guests: 2,
|
|
13
|
+
rooms: 1,
|
|
14
|
+
}, {
|
|
15
|
+
beachfront: true,
|
|
16
|
+
freeWifi: true,
|
|
17
|
+
minRating: 8.0,
|
|
18
|
+
});
|
|
19
|
+
console.log(`Found ${results.length} hotels:\n`);
|
|
20
|
+
results.slice(0, 10).forEach((hotel, i) => {
|
|
21
|
+
console.log(`${i + 1}. ${hotel.name}`);
|
|
22
|
+
console.log(` Price: ${hotel.priceDisplay}`);
|
|
23
|
+
console.log(` Rating: ${hotel.rating}/10 (${hotel.reviewCount} reviews)`);
|
|
24
|
+
console.log(` Location: ${hotel.distanceToCenter}`);
|
|
25
|
+
console.log(` Amenities: ${hotel.amenities.join(", ") || "None detected"}`);
|
|
26
|
+
if (hotel.matchScore) {
|
|
27
|
+
console.log(` Match Score: ${hotel.matchScore}`);
|
|
28
|
+
console.log(` Why: ${hotel.matchReasons?.join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
console.log(` Link: ${hotel.link}`);
|
|
31
|
+
console.log();
|
|
32
|
+
});
|
|
33
|
+
await browser.takeScreenshot("beach-wifi-results.png");
|
|
34
|
+
console.log("Screenshot saved to beach-wifi-results.png");
|
|
35
|
+
await browser.close();
|
|
36
|
+
}
|
|
37
|
+
test().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hotelzero",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for searching hotels on Booking.com with 80+ filters",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"hotelzero": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"test": "tsx src/test.ts",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"hotel",
|
|
25
|
+
"booking",
|
|
26
|
+
"travel",
|
|
27
|
+
"playwright",
|
|
28
|
+
"ai",
|
|
29
|
+
"claude"
|
|
30
|
+
],
|
|
31
|
+
"author": "",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/insprd/hotelzero.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/insprd/hotelzero/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/insprd/hotelzero#readme",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
46
|
+
"playwright": "^1.58.2",
|
|
47
|
+
"zod": "^4.3.6"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^25.2.3",
|
|
51
|
+
"tsx": "^4.21.0",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
|
+
}
|
|
54
|
+
}
|