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 +25 -0
- package/dist/browser.js +180 -0
- package/dist/index.js +101 -2
- package/package.json +1 -1
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.
|
|
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.
|
|
1015
|
+
console.error("HotelZero v1.7.0 running on stdio");
|
|
917
1016
|
}
|
|
918
1017
|
main().catch((error) => {
|
|
919
1018
|
console.error("Fatal error:", error);
|