hotelzero 1.6.0 → 1.7.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
@@ -199,6 +199,27 @@ export interface ReviewsResult {
199
199
  reviews: Review[];
200
200
  url: string;
201
201
  }
202
+ export interface DatePrice {
203
+ date: string;
204
+ price: number | null;
205
+ priceDisplay: string;
206
+ available: boolean;
207
+ currency: string;
208
+ }
209
+ export interface PriceCalendarResult {
210
+ hotelName: string;
211
+ startDate: string;
212
+ endDate: string;
213
+ nights: number;
214
+ currency: string;
215
+ prices: DatePrice[];
216
+ lowestPrice: number | null;
217
+ lowestPriceDate: string | null;
218
+ highestPrice: number | null;
219
+ highestPriceDate: string | null;
220
+ averagePrice: number | null;
221
+ url: string;
222
+ }
202
223
  export declare class HotelBrowser {
203
224
  private browser;
204
225
  private page;
@@ -240,4 +261,8 @@ export declare class HotelBrowser {
240
261
  * Get reviews for a specific hotel
241
262
  */
242
263
  getReviews(hotelUrl: string, limit?: number, sortBy?: "recent" | "highest" | "lowest", filterBy?: "couples" | "families" | "solo" | "business" | "groups"): Promise<ReviewsResult>;
264
+ /**
265
+ * Get price calendar for a hotel - shows prices for multiple dates
266
+ */
267
+ getPriceCalendar(hotelUrl: string, startDate: string, nights?: number, guests?: number, rooms?: number, currency?: string): Promise<PriceCalendarResult>;
243
268
  }
package/dist/browser.js CHANGED
@@ -1747,4 +1747,184 @@ export class HotelBrowser {
1747
1747
  console.error(`Get reviews attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delayMs / 1000)}s...`);
1748
1748
  });
1749
1749
  }
1750
+ /**
1751
+ * Get price calendar for a hotel - shows prices for multiple dates
1752
+ */
1753
+ async getPriceCalendar(hotelUrl, startDate, nights = 14, guests = 2, rooms = 1, currency = "USD") {
1754
+ // Validate inputs
1755
+ const start = new Date(startDate);
1756
+ if (isNaN(start.getTime())) {
1757
+ throw new Error("Invalid start date format. Use YYYY-MM-DD.");
1758
+ }
1759
+ // Limit to reasonable range
1760
+ const actualNights = Math.min(Math.max(nights, 1), 30);
1761
+ // Generate date range
1762
+ const dates = [];
1763
+ for (let i = 0; i < actualNights; i++) {
1764
+ const checkIn = new Date(start);
1765
+ checkIn.setDate(checkIn.getDate() + i);
1766
+ const checkOut = new Date(checkIn);
1767
+ checkOut.setDate(checkOut.getDate() + 1);
1768
+ dates.push({
1769
+ checkIn: checkIn.toISOString().split("T")[0],
1770
+ checkOut: checkOut.toISOString().split("T")[0],
1771
+ });
1772
+ }
1773
+ // Clean the hotel URL
1774
+ const cleanUrl = hotelUrl.split("?")[0].split("#")[0];
1775
+ // Collect prices for each date
1776
+ const prices = [];
1777
+ let hotelName = "";
1778
+ for (const dateRange of dates) {
1779
+ await this.enforceRateLimit();
1780
+ if (!this.page)
1781
+ throw new Error("Browser not initialized");
1782
+ const urlWithDates = `${cleanUrl}?checkin=${dateRange.checkIn}&checkout=${dateRange.checkOut}&group_adults=${guests}&no_rooms=${rooms}&selected_currency=${currency}`;
1783
+ try {
1784
+ await this.page.goto(urlWithDates, {
1785
+ waitUntil: "domcontentloaded",
1786
+ timeout: 30000,
1787
+ });
1788
+ await this.page.waitForTimeout(2000);
1789
+ // Close any popups
1790
+ try {
1791
+ await this.page.keyboard.press("Escape");
1792
+ }
1793
+ catch { }
1794
+ // Extract price data
1795
+ const priceData = await this.page.evaluate(`
1796
+ (function() {
1797
+ var result = { hotelName: '', price: null, priceDisplay: '', available: true, currency: '' };
1798
+
1799
+ // Get hotel name (only need it once)
1800
+ var nameEl = document.querySelector('h2[class*="pp-header__title"], [data-testid="PropertyHeaderDesktop-wrapper"] h2, h2.d2fee87262');
1801
+ result.hotelName = nameEl?.textContent?.trim() || '';
1802
+
1803
+ // Check for no availability message
1804
+ var noAvail = document.querySelector('[class*="soldout"], [class*="no-availability"], [data-testid="no-rooms-available"]');
1805
+ if (noAvail) {
1806
+ result.available = false;
1807
+ return result;
1808
+ }
1809
+
1810
+ // Find the lowest price - look for price elements
1811
+ var priceElements = document.querySelectorAll('[data-testid="price-and-discounted-price"], .bui-price-display__value, .prco-valign-middle-helper');
1812
+ var prices = [];
1813
+
1814
+ priceElements.forEach(function(el) {
1815
+ var text = el.textContent?.trim() || '';
1816
+ // Extract price number and currency
1817
+ var match = text.match(/([\\$€£¥₹])\\s*([\\d,]+)/);
1818
+ if (match) {
1819
+ var currencySymbol = match[1];
1820
+ var priceNum = parseInt(match[2].replace(/,/g, ''));
1821
+ if (!isNaN(priceNum) && priceNum > 0) {
1822
+ prices.push({ price: priceNum, display: text.trim(), currency: currencySymbol });
1823
+ }
1824
+ }
1825
+ // Also try format like "241 $" or "241 USD"
1826
+ var match2 = text.match(/([\\d,]+)\\s*([\\$€£¥₹]|USD|EUR|GBP)/);
1827
+ if (match2) {
1828
+ var priceNum2 = parseInt(match2[1].replace(/,/g, ''));
1829
+ if (!isNaN(priceNum2) && priceNum2 > 0) {
1830
+ prices.push({ price: priceNum2, display: text.trim(), currency: match2[2] });
1831
+ }
1832
+ }
1833
+ });
1834
+
1835
+ // Get the lowest price
1836
+ if (prices.length > 0) {
1837
+ prices.sort(function(a, b) { return a.price - b.price; });
1838
+ result.price = prices[0].price;
1839
+ result.priceDisplay = prices[0].display;
1840
+ result.currency = prices[0].currency;
1841
+ } else {
1842
+ // Try alternative price selectors
1843
+ var altPrice = document.querySelector('[class*="bui-price-display"], [class*="price"]');
1844
+ if (altPrice) {
1845
+ var altText = altPrice.textContent?.trim() || '';
1846
+ var altMatch = altText.match(/([\\$€£¥₹])\\s*([\\d,]+)/);
1847
+ if (altMatch) {
1848
+ result.price = parseInt(altMatch[2].replace(/,/g, ''));
1849
+ result.priceDisplay = altMatch[0];
1850
+ result.currency = altMatch[1];
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ // If still no price found, might be unavailable
1856
+ if (result.price === null) {
1857
+ result.available = false;
1858
+ }
1859
+
1860
+ return result;
1861
+ })()
1862
+ `);
1863
+ // Store hotel name from first result
1864
+ if (!hotelName && priceData.hotelName) {
1865
+ hotelName = priceData.hotelName;
1866
+ }
1867
+ // Map currency symbol to code
1868
+ const currencyMap = {
1869
+ "$": "USD",
1870
+ "€": "EUR",
1871
+ "£": "GBP",
1872
+ "¥": "JPY",
1873
+ "₹": "INR",
1874
+ };
1875
+ const currencyCode = currencyMap[priceData.currency] || priceData.currency || currency;
1876
+ prices.push({
1877
+ date: dateRange.checkIn,
1878
+ price: priceData.price,
1879
+ priceDisplay: priceData.priceDisplay || (priceData.price ? `${priceData.currency}${priceData.price}` : "N/A"),
1880
+ available: priceData.available,
1881
+ currency: currencyCode,
1882
+ });
1883
+ }
1884
+ catch (error) {
1885
+ // If page load fails, mark as unavailable
1886
+ prices.push({
1887
+ date: dateRange.checkIn,
1888
+ price: null,
1889
+ priceDisplay: "Error",
1890
+ available: false,
1891
+ currency,
1892
+ });
1893
+ }
1894
+ }
1895
+ // Calculate statistics
1896
+ const availablePrices = prices.filter(p => p.price !== null && p.available);
1897
+ const priceValues = availablePrices.map(p => p.price);
1898
+ let lowestPrice = null;
1899
+ let lowestPriceDate = null;
1900
+ let highestPrice = null;
1901
+ let highestPriceDate = null;
1902
+ let averagePrice = null;
1903
+ if (priceValues.length > 0) {
1904
+ lowestPrice = Math.min(...priceValues);
1905
+ highestPrice = Math.max(...priceValues);
1906
+ averagePrice = Math.round(priceValues.reduce((a, b) => a + b, 0) / priceValues.length);
1907
+ const lowestPriceEntry = availablePrices.find(p => p.price === lowestPrice);
1908
+ const highestPriceEntry = availablePrices.find(p => p.price === highestPrice);
1909
+ lowestPriceDate = lowestPriceEntry?.date || null;
1910
+ highestPriceDate = highestPriceEntry?.date || null;
1911
+ }
1912
+ // Calculate end date
1913
+ const endDate = new Date(start);
1914
+ endDate.setDate(endDate.getDate() + actualNights - 1);
1915
+ return {
1916
+ hotelName,
1917
+ startDate,
1918
+ endDate: endDate.toISOString().split("T")[0],
1919
+ nights: actualNights,
1920
+ currency,
1921
+ prices,
1922
+ lowestPrice,
1923
+ lowestPriceDate,
1924
+ highestPrice,
1925
+ highestPriceDate,
1926
+ averagePrice,
1927
+ url: cleanUrl,
1928
+ };
1929
+ }
1750
1930
  }
package/dist/index.js CHANGED
@@ -139,6 +139,14 @@ const GetReviewsSchema = z.object({
139
139
  sortBy: z.enum(["recent", "highest", "lowest"]).optional().describe("Sort reviews by: recent (default), highest score, or lowest score"),
140
140
  filterBy: z.enum(["couples", "families", "solo", "business", "groups"]).optional().describe("Filter reviews by traveler type"),
141
141
  });
142
+ const GetPriceCalendarSchema = z.object({
143
+ hotelUrl: z.string().describe("Booking.com hotel URL to get prices for"),
144
+ startDate: z.string().describe("Start date for price calendar (YYYY-MM-DD)"),
145
+ nights: z.number().min(1).max(30).optional().describe("Number of nights to check (default: 14, max: 30)"),
146
+ guests: z.number().min(1).max(30).optional().describe("Number of guests (default: 2)"),
147
+ rooms: z.number().min(1).max(10).optional().describe("Number of rooms (default: 1)"),
148
+ currency: z.string().optional().describe("Currency code (default: USD)"),
149
+ });
142
150
  // Global browser instance (reuse for efficiency)
143
151
  let browser = null;
144
152
  async function getBrowser() {
@@ -438,10 +446,72 @@ function formatReviewsResult(result) {
438
446
  lines.push(`Hotel URL: ${result.url}`);
439
447
  return lines.join("\n");
440
448
  }
449
+ function formatPriceCalendarResult(result) {
450
+ const lines = [];
451
+ lines.push("=".repeat(60));
452
+ lines.push(`PRICE CALENDAR: ${result.hotelName}`);
453
+ lines.push("=".repeat(60));
454
+ lines.push("");
455
+ lines.push(`Date Range: ${result.startDate} to ${result.endDate} (${result.nights} nights)`);
456
+ lines.push(`Currency: ${result.currency}`);
457
+ lines.push("");
458
+ // Summary statistics
459
+ lines.push("--- SUMMARY ---");
460
+ if (result.lowestPrice !== null) {
461
+ lines.push(`Lowest Price: ${result.currency} ${result.lowestPrice} (${result.lowestPriceDate})`);
462
+ }
463
+ if (result.highestPrice !== null) {
464
+ lines.push(`Highest Price: ${result.currency} ${result.highestPrice} (${result.highestPriceDate})`);
465
+ }
466
+ if (result.averagePrice !== null) {
467
+ lines.push(`Average Price: ${result.currency} ${result.averagePrice}`);
468
+ }
469
+ const availableCount = result.prices.filter(p => p.available).length;
470
+ const unavailableCount = result.prices.length - availableCount;
471
+ lines.push(`Available: ${availableCount}/${result.prices.length} nights`);
472
+ if (unavailableCount > 0) {
473
+ lines.push(`Unavailable: ${unavailableCount} nights`);
474
+ }
475
+ lines.push("");
476
+ // Price calendar
477
+ lines.push("--- PRICES BY DATE ---");
478
+ // Group by week for better readability
479
+ let currentWeek = [];
480
+ let lastWeekNum = -1;
481
+ result.prices.forEach((datePrice) => {
482
+ const date = new Date(datePrice.date);
483
+ const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
484
+ const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
485
+ let priceStr;
486
+ if (!datePrice.available) {
487
+ priceStr = "N/A";
488
+ }
489
+ else if (datePrice.price !== null) {
490
+ // Highlight lowest price
491
+ if (datePrice.price === result.lowestPrice) {
492
+ priceStr = `${datePrice.priceDisplay} ★ LOWEST`;
493
+ }
494
+ else if (datePrice.price === result.highestPrice) {
495
+ priceStr = `${datePrice.priceDisplay} (highest)`;
496
+ }
497
+ else {
498
+ priceStr = datePrice.priceDisplay;
499
+ }
500
+ }
501
+ else {
502
+ priceStr = "N/A";
503
+ }
504
+ lines.push(`${dayName} ${monthDay}: ${priceStr}`);
505
+ });
506
+ lines.push("");
507
+ lines.push("=".repeat(60));
508
+ lines.push(`Hotel URL: ${result.url}`);
509
+ return lines.join("\n");
510
+ }
441
511
  // Create MCP server
442
512
  const server = new Server({
443
513
  name: "hotelzero",
444
- version: "1.6.0",
514
+ version: "1.7.0",
445
515
  }, {
446
516
  capabilities: {
447
517
  tools: {},
@@ -658,6 +728,22 @@ Results are scored and ranked by how well they match the criteria.`,
658
728
  required: ["hotelUrl"],
659
729
  },
660
730
  },
731
+ {
732
+ name: "get_price_calendar",
733
+ description: "Get prices for a hotel across multiple dates to find the cheapest time to stay. Shows nightly prices, identifies the lowest/highest price dates, and calculates average pricing. Useful for flexible travelers looking for the best deal.",
734
+ inputSchema: {
735
+ type: "object",
736
+ properties: {
737
+ hotelUrl: { type: "string", description: "Booking.com hotel URL to check prices for" },
738
+ startDate: { type: "string", description: "Start date for price calendar (YYYY-MM-DD)" },
739
+ nights: { type: "number", description: "Number of nights to check (default: 14, max: 30)", minimum: 1, maximum: 30 },
740
+ guests: { type: "number", description: "Number of guests (default: 2)", minimum: 1, maximum: 30 },
741
+ rooms: { type: "number", description: "Number of rooms (default: 1)", minimum: 1, maximum: 10 },
742
+ currency: { type: "string", description: "Currency code (default: USD)" },
743
+ },
744
+ required: ["hotelUrl", "startDate"],
745
+ },
746
+ },
661
747
  ],
662
748
  };
663
749
  });
@@ -845,6 +931,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
845
931
  ],
846
932
  };
847
933
  }
934
+ case "get_price_calendar": {
935
+ const parsed = GetPriceCalendarSchema.parse(args);
936
+ const result = await b.getPriceCalendar(parsed.hotelUrl, parsed.startDate, parsed.nights, parsed.guests, parsed.rooms, parsed.currency);
937
+ const formatted = formatPriceCalendarResult(result);
938
+ return {
939
+ content: [
940
+ {
941
+ type: "text",
942
+ text: formatted,
943
+ },
944
+ ],
945
+ };
946
+ }
848
947
  default:
849
948
  throw new Error(`Unknown tool: ${name}`);
850
949
  }
@@ -913,7 +1012,7 @@ process.on("SIGTERM", async () => {
913
1012
  async function main() {
914
1013
  const transport = new StdioServerTransport();
915
1014
  await server.connect(transport);
916
- console.error("HotelZero v1.6.0 running on stdio");
1015
+ console.error("HotelZero v1.7.0 running on stdio");
917
1016
  }
918
1017
  main().catch((error) => {
919
1018
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hotelzero",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",