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/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 {};
@@ -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
+ }