hotelzero 1.1.0 → 1.3.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/browser.d.ts CHANGED
@@ -1,3 +1,19 @@
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;
@@ -6,6 +22,8 @@ export interface HotelSearchParams {
6
22
  rooms: number;
7
23
  currency?: string;
8
24
  sortBy?: "popularity" | "price_lowest" | "price_highest" | "rating" | "distance";
25
+ limit?: number;
26
+ offset?: number;
9
27
  }
10
28
  export interface HotelFilters {
11
29
  minRating?: number;
@@ -110,9 +128,14 @@ export interface HotelResult {
110
128
  export declare class HotelBrowser {
111
129
  private browser;
112
130
  private page;
131
+ private lastRequestTime;
132
+ private minRequestIntervalMs;
113
133
  init(headless?: boolean): Promise<void>;
114
134
  close(): Promise<void>;
115
135
  private buildBookingUrl;
136
+ private enforceRateLimit;
137
+ private checkForBlocking;
138
+ private checkForNoResults;
116
139
  searchHotels(params: HotelSearchParams, filters?: HotelFilters): Promise<HotelResult[]>;
117
140
  private dismissPopups;
118
141
  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,7 +251,7 @@ export class HotelBrowser {
191
251
  }
192
252
  }
193
253
  buildBookingUrl(params, filters) {
194
- const { destination, checkIn, checkOut, guests, rooms, currency, sortBy } = params;
254
+ const { destination, checkIn, checkOut, guests, rooms, currency, sortBy, offset } = 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);
@@ -199,6 +259,10 @@ export class HotelBrowser {
199
259
  url.searchParams.set("group_adults", guests.toString());
200
260
  url.searchParams.set("no_rooms", rooms.toString());
201
261
  url.searchParams.set("selected_currency", currency || "USD");
262
+ // Pagination offset
263
+ if (offset && offset > 0) {
264
+ url.searchParams.set("offset", offset.toString());
265
+ }
202
266
  // Sort order
203
267
  if (sortBy) {
204
268
  const sortMap = {
@@ -413,23 +477,126 @@ export class HotelBrowser {
413
477
  }
414
478
  return url.toString();
415
479
  }
416
- async searchHotels(params, filters) {
480
+ // Rate limiting: ensure minimum time between requests
481
+ async enforceRateLimit() {
482
+ const now = Date.now();
483
+ const timeSinceLastRequest = now - this.lastRequestTime;
484
+ if (timeSinceLastRequest < this.minRequestIntervalMs) {
485
+ const waitTime = this.minRequestIntervalMs - timeSinceLastRequest;
486
+ await sleep(waitTime);
487
+ }
488
+ this.lastRequestTime = Date.now();
489
+ }
490
+ // Check for CAPTCHA or blocking pages
491
+ async checkForBlocking() {
417
492
  if (!this.page)
418
- throw new Error("Browser not initialized");
419
- const url = this.buildBookingUrl(params, filters);
420
- await this.page.goto(url, { waitUntil: "networkidle" });
421
- await this.page.waitForTimeout(2000);
422
- // Close any popups/modals
423
- await this.dismissPopups();
424
- // Scroll to load more results
425
- await this.scrollToLoadMore();
426
- // Extract detailed hotel info
427
- const hotels = await this.extractHotelDetails();
428
- // Apply client-side filtering and scoring if we have preferences
429
- if (filters) {
430
- return this.scoreAndFilterHotels(hotels, filters);
493
+ return;
494
+ const pageContent = await this.page.content();
495
+ const pageUrl = this.page.url();
496
+ // Check for CAPTCHA
497
+ const captchaIndicators = [
498
+ "captcha",
499
+ "recaptcha",
500
+ "hcaptcha",
501
+ "challenge-running",
502
+ "challenge-form",
503
+ "px-captcha",
504
+ ];
505
+ const hasCaptcha = captchaIndicators.some(indicator => pageContent.toLowerCase().includes(indicator));
506
+ if (hasCaptcha) {
507
+ throw new HotelSearchError("CAPTCHA detected. Please wait a few minutes before retrying.", ErrorCodes.CAPTCHA_DETECTED, false // Not retryable automatically
508
+ );
509
+ }
510
+ // Check for rate limiting / blocking
511
+ const blockIndicators = [
512
+ "access denied",
513
+ "too many requests",
514
+ "rate limit",
515
+ "blocked",
516
+ "forbidden",
517
+ "error 403",
518
+ "error 429",
519
+ ];
520
+ const isBlocked = blockIndicators.some(indicator => pageContent.toLowerCase().includes(indicator));
521
+ if (isBlocked || pageUrl.includes("blocked") || pageUrl.includes("error")) {
522
+ throw new HotelSearchError("Request blocked by Booking.com. Please wait a few minutes before retrying.", ErrorCodes.BLOCKED, true // Retryable with backoff
523
+ );
431
524
  }
432
- return hotels;
525
+ }
526
+ // Check if destination was found
527
+ async checkForNoResults() {
528
+ if (!this.page)
529
+ return;
530
+ const pageContent = await this.page.content();
531
+ // Check for "no results" or "destination not found" messages
532
+ const noResultsIndicators = [
533
+ "no properties found",
534
+ "no results",
535
+ "0 properties",
536
+ "we couldn't find",
537
+ "try different dates",
538
+ ];
539
+ const hasNoResults = noResultsIndicators.some(indicator => pageContent.toLowerCase().includes(indicator));
540
+ if (hasNoResults) {
541
+ // Check if it's a destination issue or just no matching filters
542
+ const destinationIssue = pageContent.toLowerCase().includes("destination") &&
543
+ (pageContent.toLowerCase().includes("not found") ||
544
+ pageContent.toLowerCase().includes("couldn't find"));
545
+ if (destinationIssue) {
546
+ throw new HotelSearchError("Destination not found. Please check the spelling and try again.", ErrorCodes.DESTINATION_NOT_FOUND, false);
547
+ }
548
+ // No results but destination is valid - this is not an error, just empty results
549
+ }
550
+ }
551
+ async searchHotels(params, filters) {
552
+ if (!this.page) {
553
+ throw new HotelSearchError("Browser not initialized. Call init() first.", ErrorCodes.BROWSER_NOT_INITIALIZED, false);
554
+ }
555
+ const url = this.buildBookingUrl(params, filters);
556
+ // Use retry with exponential backoff for the main search operation
557
+ return await retryWithBackoff(async () => {
558
+ // Enforce rate limiting
559
+ await this.enforceRateLimit();
560
+ try {
561
+ await this.page.goto(url, {
562
+ waitUntil: "networkidle",
563
+ timeout: 30000
564
+ });
565
+ }
566
+ catch (error) {
567
+ const err = error;
568
+ if (err.message.includes("timeout") || err.message.includes("Timeout")) {
569
+ throw new HotelSearchError("Page load timed out. The server may be slow or unavailable.", ErrorCodes.TIMEOUT, true);
570
+ }
571
+ if (err.message.includes("net::") || err.message.includes("Network")) {
572
+ throw new HotelSearchError("Network error occurred. Please check your connection.", ErrorCodes.NETWORK_ERROR, true);
573
+ }
574
+ throw new HotelSearchError(`Navigation failed: ${err.message}`, ErrorCodes.NAVIGATION_FAILED, true);
575
+ }
576
+ await this.page.waitForTimeout(2000);
577
+ // Check for blocking/CAPTCHA before proceeding
578
+ await this.checkForBlocking();
579
+ // Check for no results / destination issues
580
+ await this.checkForNoResults();
581
+ // Close any popups/modals
582
+ await this.dismissPopups();
583
+ // Scroll to load more results (pass limit to control how many to load)
584
+ const targetResults = params.limit || 25;
585
+ await this.scrollToLoadMore(targetResults);
586
+ // Extract detailed hotel info
587
+ let hotels = await this.extractHotelDetails();
588
+ // Apply limit to cap results
589
+ if (params.limit && params.limit > 0) {
590
+ hotels = hotels.slice(0, params.limit);
591
+ }
592
+ // Apply client-side filtering and scoring if we have preferences
593
+ if (filters) {
594
+ return this.scoreAndFilterHotels(hotels, filters);
595
+ }
596
+ return hotels;
597
+ }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
598
+ console.error(`Search attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delayMs / 1000)}s...`);
599
+ });
433
600
  }
434
601
  async dismissPopups() {
435
602
  if (!this.page)
@@ -455,15 +622,31 @@ export class HotelBrowser {
455
622
  }
456
623
  }
457
624
  }
458
- async scrollToLoadMore() {
625
+ async scrollToLoadMore(targetResults = 25) {
459
626
  if (!this.page)
460
627
  return;
461
- // Scroll down a few times to load more results
462
- for (let i = 0; i < 3; i++) {
628
+ // Calculate how many scroll iterations needed
629
+ // Each scroll typically loads ~15-25 more results
630
+ // We start with ~25, so to get to targetResults we need (targetResults - 25) / 20 more scrolls
631
+ const scrollsNeeded = Math.max(1, Math.ceil((targetResults - 25) / 20));
632
+ const maxScrolls = Math.min(scrollsNeeded, 5); // Cap at 5 scrolls to avoid excessive loading
633
+ // Scroll down to load more results
634
+ for (let i = 0; i < maxScrolls; i++) {
463
635
  await this.page.evaluate(() => {
464
636
  window.scrollBy(0, window.innerHeight);
465
637
  });
466
638
  await this.page.waitForTimeout(1000);
639
+ // Check if "Load more" button exists and click it
640
+ try {
641
+ const loadMoreBtn = await this.page.$('button[data-testid="load-more-results"]');
642
+ if (loadMoreBtn) {
643
+ await loadMoreBtn.click();
644
+ await this.page.waitForTimeout(1500);
645
+ }
646
+ }
647
+ catch {
648
+ // Ignore if button not found
649
+ }
467
650
  }
468
651
  // Scroll back to top
469
652
  await this.page.evaluate(() => {
@@ -727,32 +910,53 @@ export class HotelBrowser {
727
910
  .sort((a, b) => (b.matchScore || 0) - (a.matchScore || 0));
728
911
  }
729
912
  async getHotelDetails(hotelUrl) {
730
- if (!this.page)
731
- throw new Error("Browser not initialized");
732
- await this.page.goto(hotelUrl, { waitUntil: "networkidle" });
733
- await this.page.waitForTimeout(2000);
734
- await this.dismissPopups();
735
- // Extract detailed information from hotel page
736
- return await this.page.evaluate(() => {
737
- const details = {};
738
- // Hotel name
739
- const nameEl = document.querySelector('h2[class*="pp-header"]');
740
- details.name = nameEl?.textContent?.trim();
741
- // Description
742
- const descEl = document.querySelector('[data-testid="property-description"]');
743
- details.description = descEl?.textContent?.trim();
744
- // All facilities
745
- const facilityEls = document.querySelectorAll('[data-testid="property-section-facilities"] li');
746
- details.facilities = Array.from(facilityEls).map((el) => el.textContent?.trim());
747
- // Photos
748
- const photoEls = document.querySelectorAll('[data-testid="gallery-image"] img');
749
- details.photos = Array.from(photoEls)
750
- .slice(0, 5)
751
- .map((el) => el.src);
752
- // Popular facilities highlighted
753
- const popularFacilities = document.querySelectorAll('[data-testid="property-most-popular-facilities"] span');
754
- details.popularFacilities = Array.from(popularFacilities).map((el) => el.textContent?.trim());
755
- return details;
913
+ if (!this.page) {
914
+ throw new HotelSearchError("Browser not initialized. Call init() first.", ErrorCodes.BROWSER_NOT_INITIALIZED, false);
915
+ }
916
+ return await retryWithBackoff(async () => {
917
+ // Enforce rate limiting
918
+ await this.enforceRateLimit();
919
+ try {
920
+ await this.page.goto(hotelUrl, {
921
+ waitUntil: "networkidle",
922
+ timeout: 30000
923
+ });
924
+ }
925
+ catch (error) {
926
+ const err = error;
927
+ if (err.message.includes("timeout") || err.message.includes("Timeout")) {
928
+ throw new HotelSearchError("Page load timed out. The server may be slow or unavailable.", ErrorCodes.TIMEOUT, true);
929
+ }
930
+ throw new HotelSearchError(`Navigation failed: ${err.message}`, ErrorCodes.NAVIGATION_FAILED, true);
931
+ }
932
+ await this.page.waitForTimeout(2000);
933
+ // Check for blocking/CAPTCHA
934
+ await this.checkForBlocking();
935
+ await this.dismissPopups();
936
+ // Extract detailed information from hotel page
937
+ return await this.page.evaluate(() => {
938
+ const details = {};
939
+ // Hotel name
940
+ const nameEl = document.querySelector('h2[class*="pp-header"]');
941
+ details.name = nameEl?.textContent?.trim();
942
+ // Description
943
+ const descEl = document.querySelector('[data-testid="property-description"]');
944
+ details.description = descEl?.textContent?.trim();
945
+ // All facilities
946
+ const facilityEls = document.querySelectorAll('[data-testid="property-section-facilities"] li');
947
+ details.facilities = Array.from(facilityEls).map((el) => el.textContent?.trim());
948
+ // Photos
949
+ const photoEls = document.querySelectorAll('[data-testid="gallery-image"] img');
950
+ details.photos = Array.from(photoEls)
951
+ .slice(0, 5)
952
+ .map((el) => el.src);
953
+ // Popular facilities highlighted
954
+ const popularFacilities = document.querySelectorAll('[data-testid="property-most-popular-facilities"] span');
955
+ details.popularFacilities = Array.from(popularFacilities).map((el) => el.textContent?.trim());
956
+ return details;
957
+ });
958
+ }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
959
+ console.error(`Get details attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delayMs / 1000)}s...`);
756
960
  });
757
961
  }
758
962
  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",
@@ -40,6 +40,9 @@ const FindHotelsSchema = z.object({
40
40
  // Currency & Sorting
41
41
  currency: z.string().optional().describe("Currency code (USD, EUR, GBP, JPY, etc.)"),
42
42
  sortBy: z.enum(["popularity", "price_lowest", "price_highest", "rating", "distance"]).optional().describe("Sort results by"),
43
+ // Pagination
44
+ limit: z.number().min(1).max(100).optional().describe("Maximum number of results to return (default: 25, max: 100)"),
45
+ offset: z.number().min(0).optional().describe("Number of results to skip for pagination (default: 0)"),
43
46
  // Rating & Price
44
47
  minRating: z.number().optional().describe("Minimum rating (6=Pleasant, 7=Good, 8=Very Good, 9=Wonderful)"),
45
48
  minPrice: z.number().optional().describe("Minimum price per night"),
@@ -165,7 +168,7 @@ function formatHotelResult(hotel, index) {
165
168
  // Create MCP server
166
169
  const server = new Server({
167
170
  name: "hotelzero",
168
- version: "1.1.0",
171
+ version: "1.3.0",
169
172
  }, {
170
173
  capabilities: {
171
174
  tools: {},
@@ -192,6 +195,9 @@ const findHotelsInputSchema = {
192
195
  description: "Sort results by",
193
196
  enum: ["popularity", "price_lowest", "price_highest", "rating", "distance"]
194
197
  },
198
+ // Pagination
199
+ limit: { type: "number", description: "Maximum results to return (default: 25, max: 100)", default: 25 },
200
+ offset: { type: "number", description: "Number of results to skip for pagination", default: 0 },
195
201
  // Property Type
196
202
  propertyType: {
197
203
  type: "string",
@@ -353,11 +359,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
353
359
  rooms: parsed.rooms,
354
360
  currency: parsed.currency,
355
361
  sortBy: parsed.sortBy,
362
+ limit: parsed.limit,
363
+ offset: parsed.offset,
356
364
  };
357
365
  // Build filters object from all parsed parameters
358
366
  const filters = {};
359
- // Copy all filter properties
360
- const filterKeys = Object.keys(parsed).filter(k => !['destination', 'checkIn', 'checkOut', 'guests', 'rooms', 'currency', 'sortBy'].includes(k));
367
+ // Copy all filter properties (exclude search params)
368
+ const filterKeys = Object.keys(parsed).filter(k => !['destination', 'checkIn', 'checkOut', 'guests', 'rooms', 'currency', 'sortBy', 'limit', 'offset'].includes(k));
361
369
  for (const key of filterKeys) {
362
370
  const value = parsed[key];
363
371
  if (value !== undefined && value !== null) {
@@ -428,15 +436,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
428
436
  const filtersLine = activeFilters.length > 0
429
437
  ? `Filters: ${activeFilters.join(", ")}\n\n`
430
438
  : "\n";
439
+ // Pagination info
440
+ const offset = parsed.offset || 0;
441
+ const displayLimit = parsed.limit || 25;
442
+ const paginationLine = offset > 0
443
+ ? `Showing results ${offset + 1}-${offset + results.length} of available hotels\n\n`
444
+ : "";
431
445
  const hotelList = results
432
- .slice(0, 15) // Top 15 results
433
- .map((h, i) => formatHotelResult(h, i))
446
+ .map((h, i) => formatHotelResult(h, i + offset))
434
447
  .join("\n\n");
435
448
  return {
436
449
  content: [
437
450
  {
438
451
  type: "text",
439
- text: header + filtersLine + hotelList,
452
+ text: header + filtersLine + paginationLine + hotelList,
440
453
  },
441
454
  ],
442
455
  };
@@ -481,6 +494,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
481
494
  }
482
495
  }
483
496
  catch (error) {
497
+ // Handle custom HotelSearchError with structured error info
498
+ if (error instanceof HotelSearchError) {
499
+ let helpText = "";
500
+ switch (error.code) {
501
+ case ErrorCodes.CAPTCHA_DETECTED:
502
+ helpText = "\n\nTip: Wait 5-10 minutes before trying again.";
503
+ break;
504
+ case ErrorCodes.RATE_LIMITED:
505
+ case ErrorCodes.BLOCKED:
506
+ helpText = "\n\nTip: The server is rate-limiting requests. Wait a few minutes before trying again.";
507
+ break;
508
+ case ErrorCodes.DESTINATION_NOT_FOUND:
509
+ helpText = "\n\nTip: Check the destination spelling. Try a more specific location like 'Paris, France' instead of just 'Paris'.";
510
+ break;
511
+ case ErrorCodes.TIMEOUT:
512
+ helpText = "\n\nTip: The request timed out. This may be due to slow network or server issues. Try again.";
513
+ break;
514
+ case ErrorCodes.NETWORK_ERROR:
515
+ helpText = "\n\nTip: Check your internet connection and try again.";
516
+ break;
517
+ }
518
+ return {
519
+ content: [
520
+ {
521
+ type: "text",
522
+ text: `Error [${error.code}]: ${error.message}${helpText}`,
523
+ },
524
+ ],
525
+ isError: true,
526
+ };
527
+ }
528
+ // Handle generic errors
484
529
  const errorMessage = error instanceof Error ? error.message : String(error);
485
530
  return {
486
531
  content: [
@@ -512,7 +557,7 @@ process.on("SIGTERM", async () => {
512
557
  async function main() {
513
558
  const transport = new StdioServerTransport();
514
559
  await server.connect(transport);
515
- console.error("HotelZero v1.1.0 running on stdio");
560
+ console.error("HotelZero v1.3.0 running on stdio");
516
561
  }
517
562
  main().catch((error) => {
518
563
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hotelzero",
3
- "version": "1.1.0",
3
+ "version": "1.3.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",