hotelzero 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- | `maxPrice` | number | Maximum price per night in USD (client-side filter) |
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
@@ -1,12 +1,31 @@
1
+ export declare class HotelSearchError extends Error {
2
+ code: string;
3
+ retryable: boolean;
4
+ constructor(message: string, code: string, retryable?: boolean);
5
+ }
6
+ export declare const ErrorCodes: {
7
+ readonly BROWSER_NOT_INITIALIZED: "BROWSER_NOT_INITIALIZED";
8
+ readonly NAVIGATION_FAILED: "NAVIGATION_FAILED";
9
+ readonly RATE_LIMITED: "RATE_LIMITED";
10
+ readonly CAPTCHA_DETECTED: "CAPTCHA_DETECTED";
11
+ readonly NO_RESULTS: "NO_RESULTS";
12
+ readonly DESTINATION_NOT_FOUND: "DESTINATION_NOT_FOUND";
13
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
14
+ readonly TIMEOUT: "TIMEOUT";
15
+ readonly BLOCKED: "BLOCKED";
16
+ };
1
17
  export interface HotelSearchParams {
2
18
  destination: string;
3
19
  checkIn: string;
4
20
  checkOut: string;
5
21
  guests: number;
6
22
  rooms: number;
23
+ currency?: string;
24
+ sortBy?: "popularity" | "price_lowest" | "price_highest" | "rating" | "distance";
7
25
  }
8
26
  export interface HotelFilters {
9
27
  minRating?: number;
28
+ minPrice?: number;
10
29
  maxPrice?: number;
11
30
  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
31
  starRating?: 1 | 2 | 3 | 4 | 5;
@@ -99,15 +118,22 @@ export interface HotelResult {
99
118
  amenities: string[];
100
119
  highlights: string[];
101
120
  link: string;
121
+ thumbnailUrl: string | null;
122
+ availability: string | null;
102
123
  matchScore?: number;
103
124
  matchReasons?: string[];
104
125
  }
105
126
  export declare class HotelBrowser {
106
127
  private browser;
107
128
  private page;
129
+ private lastRequestTime;
130
+ private minRequestIntervalMs;
108
131
  init(headless?: boolean): Promise<void>;
109
132
  close(): Promise<void>;
110
133
  private buildBookingUrl;
134
+ private enforceRateLimit;
135
+ private checkForBlocking;
136
+ private checkForNoResults;
111
137
  searchHotels(params: HotelSearchParams, filters?: HotelFilters): Promise<HotelResult[]>;
112
138
  private dismissPopups;
113
139
  private scrollToLoadMore;
package/dist/browser.js CHANGED
@@ -1,4 +1,62 @@
1
1
  import { chromium } from "playwright";
2
+ // Custom error types for better error handling
3
+ export class HotelSearchError extends Error {
4
+ code;
5
+ retryable;
6
+ constructor(message, code, retryable = false) {
7
+ super(message);
8
+ this.code = code;
9
+ this.retryable = retryable;
10
+ this.name = "HotelSearchError";
11
+ }
12
+ }
13
+ export const ErrorCodes = {
14
+ BROWSER_NOT_INITIALIZED: "BROWSER_NOT_INITIALIZED",
15
+ NAVIGATION_FAILED: "NAVIGATION_FAILED",
16
+ RATE_LIMITED: "RATE_LIMITED",
17
+ CAPTCHA_DETECTED: "CAPTCHA_DETECTED",
18
+ NO_RESULTS: "NO_RESULTS",
19
+ DESTINATION_NOT_FOUND: "DESTINATION_NOT_FOUND",
20
+ NETWORK_ERROR: "NETWORK_ERROR",
21
+ TIMEOUT: "TIMEOUT",
22
+ BLOCKED: "BLOCKED",
23
+ };
24
+ const DEFAULT_RETRY_CONFIG = {
25
+ maxRetries: 3,
26
+ baseDelayMs: 1000,
27
+ maxDelayMs: 10000,
28
+ };
29
+ // Sleep helper
30
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
31
+ // Retry with exponential backoff
32
+ async function retryWithBackoff(fn, config = DEFAULT_RETRY_CONFIG, onRetry) {
33
+ let lastError = null;
34
+ for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
35
+ try {
36
+ return await fn();
37
+ }
38
+ catch (error) {
39
+ lastError = error;
40
+ // Don't retry non-retryable errors
41
+ if (error instanceof HotelSearchError && !error.retryable) {
42
+ throw error;
43
+ }
44
+ // Don't retry on last attempt
45
+ if (attempt === config.maxRetries) {
46
+ break;
47
+ }
48
+ // Calculate delay with exponential backoff + jitter
49
+ const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt - 1);
50
+ const jitter = Math.random() * 1000;
51
+ const delay = Math.min(exponentialDelay + jitter, config.maxDelayMs);
52
+ if (onRetry) {
53
+ onRetry(attempt, lastError, delay);
54
+ }
55
+ await sleep(delay);
56
+ }
57
+ }
58
+ throw lastError;
59
+ }
2
60
  // Booking.com filter code mappings
3
61
  const FILTER_CODES = {
4
62
  // Property Types
@@ -172,6 +230,8 @@ const FILTER_CODES = {
172
230
  export class HotelBrowser {
173
231
  browser = null;
174
232
  page = null;
233
+ lastRequestTime = 0;
234
+ minRequestIntervalMs = 2000; // Minimum 2 seconds between requests
175
235
  async init(headless = true) {
176
236
  this.browser = await chromium.launch({
177
237
  headless,
@@ -191,14 +251,28 @@ export class HotelBrowser {
191
251
  }
192
252
  }
193
253
  buildBookingUrl(params, filters) {
194
- const { destination, checkIn, checkOut, guests, rooms } = params;
254
+ const { destination, checkIn, checkOut, guests, rooms, currency, sortBy } = params;
195
255
  const url = new URL("https://www.booking.com/searchresults.html");
196
256
  url.searchParams.set("ss", destination);
197
257
  url.searchParams.set("checkin", checkIn);
198
258
  url.searchParams.set("checkout", checkOut);
199
259
  url.searchParams.set("group_adults", guests.toString());
200
260
  url.searchParams.set("no_rooms", rooms.toString());
201
- url.searchParams.set("selected_currency", "USD");
261
+ url.searchParams.set("selected_currency", currency || "USD");
262
+ // Sort order
263
+ if (sortBy) {
264
+ const sortMap = {
265
+ popularity: "popularity",
266
+ price_lowest: "price",
267
+ price_highest: "price",
268
+ rating: "review_score_and_price",
269
+ distance: "distance_from_search",
270
+ };
271
+ url.searchParams.set("order", sortMap[sortBy] || "popularity");
272
+ if (sortBy === "price_highest") {
273
+ url.searchParams.set("sort_order", "desc");
274
+ }
275
+ }
202
276
  if (!filters)
203
277
  return url.toString();
204
278
  const nfltParts = [];
@@ -399,23 +473,121 @@ export class HotelBrowser {
399
473
  }
400
474
  return url.toString();
401
475
  }
402
- async searchHotels(params, filters) {
476
+ // Rate limiting: ensure minimum time between requests
477
+ async enforceRateLimit() {
478
+ const now = Date.now();
479
+ const timeSinceLastRequest = now - this.lastRequestTime;
480
+ if (timeSinceLastRequest < this.minRequestIntervalMs) {
481
+ const waitTime = this.minRequestIntervalMs - timeSinceLastRequest;
482
+ await sleep(waitTime);
483
+ }
484
+ this.lastRequestTime = Date.now();
485
+ }
486
+ // Check for CAPTCHA or blocking pages
487
+ async checkForBlocking() {
403
488
  if (!this.page)
404
- throw new Error("Browser not initialized");
405
- const url = this.buildBookingUrl(params, filters);
406
- await this.page.goto(url, { waitUntil: "networkidle" });
407
- await this.page.waitForTimeout(2000);
408
- // Close any popups/modals
409
- await this.dismissPopups();
410
- // Scroll to load more results
411
- await this.scrollToLoadMore();
412
- // Extract detailed hotel info
413
- const hotels = await this.extractHotelDetails();
414
- // Apply client-side filtering and scoring if we have preferences
415
- if (filters) {
416
- return this.scoreAndFilterHotels(hotels, filters);
489
+ return;
490
+ const pageContent = await this.page.content();
491
+ const pageUrl = this.page.url();
492
+ // Check for CAPTCHA
493
+ const captchaIndicators = [
494
+ "captcha",
495
+ "recaptcha",
496
+ "hcaptcha",
497
+ "challenge-running",
498
+ "challenge-form",
499
+ "px-captcha",
500
+ ];
501
+ const hasCaptcha = captchaIndicators.some(indicator => pageContent.toLowerCase().includes(indicator));
502
+ if (hasCaptcha) {
503
+ throw new HotelSearchError("CAPTCHA detected. Please wait a few minutes before retrying.", ErrorCodes.CAPTCHA_DETECTED, false // Not retryable automatically
504
+ );
505
+ }
506
+ // Check for rate limiting / blocking
507
+ const blockIndicators = [
508
+ "access denied",
509
+ "too many requests",
510
+ "rate limit",
511
+ "blocked",
512
+ "forbidden",
513
+ "error 403",
514
+ "error 429",
515
+ ];
516
+ const isBlocked = blockIndicators.some(indicator => pageContent.toLowerCase().includes(indicator));
517
+ if (isBlocked || pageUrl.includes("blocked") || pageUrl.includes("error")) {
518
+ throw new HotelSearchError("Request blocked by Booking.com. Please wait a few minutes before retrying.", ErrorCodes.BLOCKED, true // Retryable with backoff
519
+ );
417
520
  }
418
- return hotels;
521
+ }
522
+ // Check if destination was found
523
+ async checkForNoResults() {
524
+ if (!this.page)
525
+ return;
526
+ const pageContent = await this.page.content();
527
+ // Check for "no results" or "destination not found" messages
528
+ const noResultsIndicators = [
529
+ "no properties found",
530
+ "no results",
531
+ "0 properties",
532
+ "we couldn't find",
533
+ "try different dates",
534
+ ];
535
+ const hasNoResults = noResultsIndicators.some(indicator => pageContent.toLowerCase().includes(indicator));
536
+ if (hasNoResults) {
537
+ // Check if it's a destination issue or just no matching filters
538
+ const destinationIssue = pageContent.toLowerCase().includes("destination") &&
539
+ (pageContent.toLowerCase().includes("not found") ||
540
+ pageContent.toLowerCase().includes("couldn't find"));
541
+ if (destinationIssue) {
542
+ throw new HotelSearchError("Destination not found. Please check the spelling and try again.", ErrorCodes.DESTINATION_NOT_FOUND, false);
543
+ }
544
+ // No results but destination is valid - this is not an error, just empty results
545
+ }
546
+ }
547
+ async searchHotels(params, filters) {
548
+ if (!this.page) {
549
+ throw new HotelSearchError("Browser not initialized. Call init() first.", ErrorCodes.BROWSER_NOT_INITIALIZED, false);
550
+ }
551
+ const url = this.buildBookingUrl(params, filters);
552
+ // Use retry with exponential backoff for the main search operation
553
+ return await retryWithBackoff(async () => {
554
+ // Enforce rate limiting
555
+ await this.enforceRateLimit();
556
+ try {
557
+ await this.page.goto(url, {
558
+ waitUntil: "networkidle",
559
+ timeout: 30000
560
+ });
561
+ }
562
+ catch (error) {
563
+ const err = error;
564
+ if (err.message.includes("timeout") || err.message.includes("Timeout")) {
565
+ throw new HotelSearchError("Page load timed out. The server may be slow or unavailable.", ErrorCodes.TIMEOUT, true);
566
+ }
567
+ if (err.message.includes("net::") || err.message.includes("Network")) {
568
+ throw new HotelSearchError("Network error occurred. Please check your connection.", ErrorCodes.NETWORK_ERROR, true);
569
+ }
570
+ throw new HotelSearchError(`Navigation failed: ${err.message}`, ErrorCodes.NAVIGATION_FAILED, true);
571
+ }
572
+ await this.page.waitForTimeout(2000);
573
+ // Check for blocking/CAPTCHA before proceeding
574
+ await this.checkForBlocking();
575
+ // Check for no results / destination issues
576
+ await this.checkForNoResults();
577
+ // Close any popups/modals
578
+ await this.dismissPopups();
579
+ // Scroll to load more results
580
+ await this.scrollToLoadMore();
581
+ // Extract detailed hotel info
582
+ const hotels = await this.extractHotelDetails();
583
+ // Apply client-side filtering and scoring if we have preferences
584
+ if (filters) {
585
+ return this.scoreAndFilterHotels(hotels, filters);
586
+ }
587
+ return hotels;
588
+ }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
589
+ console.error(`Search attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delayMs / 1000)}s...`);
590
+ });
419
591
  }
420
592
  async dismissPopups() {
421
593
  if (!this.page)
@@ -557,6 +729,24 @@ export class HotelBrowser {
557
729
  highlights.push("Free Cancellation");
558
730
  if (cardText.includes("no prepayment"))
559
731
  highlights.push("No Prepayment");
732
+ // Extract thumbnail image URL
733
+ const imgEl = card.querySelector('img[data-testid="image"]');
734
+ const thumbnailUrl = imgEl?.src || null;
735
+ // Extract availability status (e.g., "Only 2 rooms left", "Last booked 5 minutes ago")
736
+ let availability = null;
737
+ const availabilityPatterns = [
738
+ /only\s*\d+\s*(rooms?|left)/i,
739
+ /last\s*(booked|reserved)\s*\d+\s*(minutes?|hours?)\s*ago/i,
740
+ /in\s*high\s*demand/i,
741
+ /selling\s*fast/i,
742
+ ];
743
+ for (const pattern of availabilityPatterns) {
744
+ const match = cardText.match(pattern);
745
+ if (match) {
746
+ availability = match[0];
747
+ break;
748
+ }
749
+ }
560
750
  results.push({
561
751
  name,
562
752
  price,
@@ -569,6 +759,8 @@ export class HotelBrowser {
569
759
  amenities,
570
760
  highlights,
571
761
  link,
762
+ thumbnailUrl,
763
+ availability,
572
764
  });
573
765
  });
574
766
  return results;
@@ -682,6 +874,9 @@ export class HotelBrowser {
682
874
  if (filters.minRating && hotel.rating && hotel.rating < filters.minRating) {
683
875
  return false;
684
876
  }
877
+ if (filters.minPrice && hotel.price && hotel.price < filters.minPrice) {
878
+ return false;
879
+ }
685
880
  if (filters.maxPrice && hotel.price && hotel.price > filters.maxPrice) {
686
881
  return false;
687
882
  }
@@ -690,32 +885,53 @@ export class HotelBrowser {
690
885
  .sort((a, b) => (b.matchScore || 0) - (a.matchScore || 0));
691
886
  }
692
887
  async getHotelDetails(hotelUrl) {
693
- if (!this.page)
694
- throw new Error("Browser not initialized");
695
- await this.page.goto(hotelUrl, { waitUntil: "networkidle" });
696
- await this.page.waitForTimeout(2000);
697
- await this.dismissPopups();
698
- // Extract detailed information from hotel page
699
- return await this.page.evaluate(() => {
700
- const details = {};
701
- // Hotel name
702
- const nameEl = document.querySelector('h2[class*="pp-header"]');
703
- details.name = nameEl?.textContent?.trim();
704
- // Description
705
- const descEl = document.querySelector('[data-testid="property-description"]');
706
- details.description = descEl?.textContent?.trim();
707
- // All facilities
708
- const facilityEls = document.querySelectorAll('[data-testid="property-section-facilities"] li');
709
- details.facilities = Array.from(facilityEls).map((el) => el.textContent?.trim());
710
- // Photos
711
- const photoEls = document.querySelectorAll('[data-testid="gallery-image"] img');
712
- details.photos = Array.from(photoEls)
713
- .slice(0, 5)
714
- .map((el) => el.src);
715
- // Popular facilities highlighted
716
- const popularFacilities = document.querySelectorAll('[data-testid="property-most-popular-facilities"] span');
717
- details.popularFacilities = Array.from(popularFacilities).map((el) => el.textContent?.trim());
718
- return details;
888
+ if (!this.page) {
889
+ throw new HotelSearchError("Browser not initialized. Call init() first.", ErrorCodes.BROWSER_NOT_INITIALIZED, false);
890
+ }
891
+ return await retryWithBackoff(async () => {
892
+ // Enforce rate limiting
893
+ await this.enforceRateLimit();
894
+ try {
895
+ await this.page.goto(hotelUrl, {
896
+ waitUntil: "networkidle",
897
+ timeout: 30000
898
+ });
899
+ }
900
+ catch (error) {
901
+ const err = error;
902
+ if (err.message.includes("timeout") || err.message.includes("Timeout")) {
903
+ throw new HotelSearchError("Page load timed out. The server may be slow or unavailable.", ErrorCodes.TIMEOUT, true);
904
+ }
905
+ throw new HotelSearchError(`Navigation failed: ${err.message}`, ErrorCodes.NAVIGATION_FAILED, true);
906
+ }
907
+ await this.page.waitForTimeout(2000);
908
+ // Check for blocking/CAPTCHA
909
+ await this.checkForBlocking();
910
+ await this.dismissPopups();
911
+ // Extract detailed information from hotel page
912
+ return await this.page.evaluate(() => {
913
+ const details = {};
914
+ // Hotel name
915
+ const nameEl = document.querySelector('h2[class*="pp-header"]');
916
+ details.name = nameEl?.textContent?.trim();
917
+ // Description
918
+ const descEl = document.querySelector('[data-testid="property-description"]');
919
+ details.description = descEl?.textContent?.trim();
920
+ // All facilities
921
+ const facilityEls = document.querySelectorAll('[data-testid="property-section-facilities"] li');
922
+ details.facilities = Array.from(facilityEls).map((el) => el.textContent?.trim());
923
+ // Photos
924
+ const photoEls = document.querySelectorAll('[data-testid="gallery-image"] img');
925
+ details.photos = Array.from(photoEls)
926
+ .slice(0, 5)
927
+ .map((el) => el.src);
928
+ // Popular facilities highlighted
929
+ const popularFacilities = document.querySelectorAll('[data-testid="property-most-popular-facilities"] span');
930
+ details.popularFacilities = Array.from(popularFacilities).map((el) => el.textContent?.trim());
931
+ return details;
932
+ });
933
+ }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
934
+ console.error(`Get details attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delayMs / 1000)}s...`);
719
935
  });
720
936
  }
721
937
  async takeScreenshot(path) {
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
- import { HotelBrowser } from "./browser.js";
6
+ import { HotelBrowser, HotelSearchError, ErrorCodes } from "./browser.js";
7
7
  // Property type enum
8
8
  const PropertyTypeEnum = z.enum([
9
9
  "hotel", "apartment", "resort", "villa", "vacation_home", "hostel", "bnb",
@@ -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
- maxPrice: z.number().optional().describe("Maximum price per night in USD"),
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: "hotel-booking-mcp",
158
- version: "2.0.0",
167
+ name: "hotelzero",
168
+ version: "1.2.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
- maxPrice: { type: "number", description: "Maximum price per night in USD" },
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";
@@ -455,6 +481,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
455
481
  }
456
482
  }
457
483
  catch (error) {
484
+ // Handle custom HotelSearchError with structured error info
485
+ if (error instanceof HotelSearchError) {
486
+ let helpText = "";
487
+ switch (error.code) {
488
+ case ErrorCodes.CAPTCHA_DETECTED:
489
+ helpText = "\n\nTip: Wait 5-10 minutes before trying again.";
490
+ break;
491
+ case ErrorCodes.RATE_LIMITED:
492
+ case ErrorCodes.BLOCKED:
493
+ helpText = "\n\nTip: The server is rate-limiting requests. Wait a few minutes before trying again.";
494
+ break;
495
+ case ErrorCodes.DESTINATION_NOT_FOUND:
496
+ helpText = "\n\nTip: Check the destination spelling. Try a more specific location like 'Paris, France' instead of just 'Paris'.";
497
+ break;
498
+ case ErrorCodes.TIMEOUT:
499
+ helpText = "\n\nTip: The request timed out. This may be due to slow network or server issues. Try again.";
500
+ break;
501
+ case ErrorCodes.NETWORK_ERROR:
502
+ helpText = "\n\nTip: Check your internet connection and try again.";
503
+ break;
504
+ }
505
+ return {
506
+ content: [
507
+ {
508
+ type: "text",
509
+ text: `Error [${error.code}]: ${error.message}${helpText}`,
510
+ },
511
+ ],
512
+ isError: true,
513
+ };
514
+ }
515
+ // Handle generic errors
458
516
  const errorMessage = error instanceof Error ? error.message : String(error);
459
517
  return {
460
518
  content: [
@@ -486,7 +544,7 @@ process.on("SIGTERM", async () => {
486
544
  async function main() {
487
545
  const transport = new StdioServerTransport();
488
546
  await server.connect(transport);
489
- console.error("HotelZero v1.0.0 running on stdio");
547
+ console.error("HotelZero v1.2.0 running on stdio");
490
548
  }
491
549
  main().catch((error) => {
492
550
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hotelzero",
3
- "version": "1.0.0",
3
+ "version": "1.2.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": "./dist/index.js"
9
+ "hotelzero": "dist/index.js"
10
10
  },
11
11
  "files": [
12
12
  "dist"