hotelzero 1.6.0 → 1.8.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 +32 -0
- package/dist/browser.d.ts +40 -1
- package/dist/browser.js +208 -3
- package/dist/index.js +137 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,30 @@ Or run directly with npx:
|
|
|
25
25
|
npx hotelzero
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Proxy Support
|
|
29
|
+
|
|
30
|
+
For heavy usage or to avoid IP blocks, you can configure a proxy server via the `HOTELZERO_PROXY` environment variable:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# HTTP proxy
|
|
34
|
+
HOTELZERO_PROXY=http://proxy.example.com:8080 npx hotelzero
|
|
35
|
+
|
|
36
|
+
# HTTP proxy with authentication
|
|
37
|
+
HOTELZERO_PROXY=http://user:pass@proxy.example.com:8080 npx hotelzero
|
|
38
|
+
|
|
39
|
+
# SOCKS5 proxy
|
|
40
|
+
HOTELZERO_PROXY=socks5://proxy.example.com:1080 npx hotelzero
|
|
41
|
+
|
|
42
|
+
# SOCKS5 proxy with authentication
|
|
43
|
+
HOTELZERO_PROXY=socks5://user:pass@proxy.example.com:1080 npx hotelzero
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When a proxy is configured, you'll see confirmation in the startup logs:
|
|
47
|
+
```
|
|
48
|
+
Proxy enabled: http://proxy.example.com:8080
|
|
49
|
+
HotelZero v1.8.0 running on stdio
|
|
50
|
+
```
|
|
51
|
+
|
|
28
52
|
## Quick Start
|
|
29
53
|
|
|
30
54
|
### Run as MCP Server
|
|
@@ -516,6 +540,14 @@ Run `npx playwright install chromium` to install the browser.
|
|
|
516
540
|
|
|
517
541
|
- Wait a few minutes before retrying
|
|
518
542
|
- The server uses anti-detection measures, but excessive requests may trigger blocks
|
|
543
|
+
- Consider using a proxy server (see [Proxy Support](#proxy-support))
|
|
544
|
+
|
|
545
|
+
### Proxy not working
|
|
546
|
+
|
|
547
|
+
- Verify the proxy server is running and accessible
|
|
548
|
+
- Check credentials if using authentication
|
|
549
|
+
- Ensure the proxy supports HTTPS connections
|
|
550
|
+
- Try a different proxy or test without proxy first
|
|
519
551
|
|
|
520
552
|
---
|
|
521
553
|
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export interface ProxyConfig {
|
|
2
|
+
server: string;
|
|
3
|
+
username?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
}
|
|
1
6
|
export declare class HotelSearchError extends Error {
|
|
2
7
|
code: string;
|
|
3
8
|
retryable: boolean;
|
|
@@ -199,12 +204,42 @@ export interface ReviewsResult {
|
|
|
199
204
|
reviews: Review[];
|
|
200
205
|
url: string;
|
|
201
206
|
}
|
|
207
|
+
export interface DatePrice {
|
|
208
|
+
date: string;
|
|
209
|
+
price: number | null;
|
|
210
|
+
priceDisplay: string;
|
|
211
|
+
available: boolean;
|
|
212
|
+
currency: string;
|
|
213
|
+
}
|
|
214
|
+
export interface PriceCalendarResult {
|
|
215
|
+
hotelName: string;
|
|
216
|
+
startDate: string;
|
|
217
|
+
endDate: string;
|
|
218
|
+
nights: number;
|
|
219
|
+
currency: string;
|
|
220
|
+
prices: DatePrice[];
|
|
221
|
+
lowestPrice: number | null;
|
|
222
|
+
lowestPriceDate: string | null;
|
|
223
|
+
highestPrice: number | null;
|
|
224
|
+
highestPriceDate: string | null;
|
|
225
|
+
averagePrice: number | null;
|
|
226
|
+
url: string;
|
|
227
|
+
}
|
|
202
228
|
export declare class HotelBrowser {
|
|
203
229
|
private browser;
|
|
204
230
|
private page;
|
|
205
231
|
private lastRequestTime;
|
|
206
232
|
private minRequestIntervalMs;
|
|
207
|
-
|
|
233
|
+
private proxyConfig;
|
|
234
|
+
init(headless?: boolean, proxy?: ProxyConfig): Promise<void>;
|
|
235
|
+
/**
|
|
236
|
+
* Check if a proxy is configured
|
|
237
|
+
*/
|
|
238
|
+
hasProxy(): boolean;
|
|
239
|
+
/**
|
|
240
|
+
* Get the current proxy server (without credentials)
|
|
241
|
+
*/
|
|
242
|
+
getProxyServer(): string | null;
|
|
208
243
|
close(): Promise<void>;
|
|
209
244
|
private buildBookingUrl;
|
|
210
245
|
private enforceRateLimit;
|
|
@@ -240,4 +275,8 @@ export declare class HotelBrowser {
|
|
|
240
275
|
* Get reviews for a specific hotel
|
|
241
276
|
*/
|
|
242
277
|
getReviews(hotelUrl: string, limit?: number, sortBy?: "recent" | "highest" | "lowest", filterBy?: "couples" | "families" | "solo" | "business" | "groups"): Promise<ReviewsResult>;
|
|
278
|
+
/**
|
|
279
|
+
* Get price calendar for a hotel - shows prices for multiple dates
|
|
280
|
+
*/
|
|
281
|
+
getPriceCalendar(hotelUrl: string, startDate: string, nights?: number, guests?: number, rooms?: number, currency?: string): Promise<PriceCalendarResult>;
|
|
243
282
|
}
|
package/dist/browser.js
CHANGED
|
@@ -232,17 +232,42 @@ export class HotelBrowser {
|
|
|
232
232
|
page = null;
|
|
233
233
|
lastRequestTime = 0;
|
|
234
234
|
minRequestIntervalMs = 2000; // Minimum 2 seconds between requests
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
proxyConfig = null;
|
|
236
|
+
async init(headless = true, proxy) {
|
|
237
|
+
// Store proxy config for reference
|
|
238
|
+
this.proxyConfig = proxy || null;
|
|
239
|
+
// Build launch options
|
|
240
|
+
const launchOptions = {
|
|
237
241
|
headless,
|
|
238
242
|
args: ["--disable-blink-features=AutomationControlled"],
|
|
239
|
-
}
|
|
243
|
+
};
|
|
244
|
+
// Add proxy to launch options if provided
|
|
245
|
+
if (proxy) {
|
|
246
|
+
launchOptions.proxy = {
|
|
247
|
+
server: proxy.server,
|
|
248
|
+
username: proxy.username,
|
|
249
|
+
password: proxy.password,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
this.browser = await chromium.launch(launchOptions);
|
|
240
253
|
const context = await this.browser.newContext({
|
|
241
254
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
242
255
|
viewport: { width: 1280, height: 900 },
|
|
243
256
|
});
|
|
244
257
|
this.page = await context.newPage();
|
|
245
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if a proxy is configured
|
|
261
|
+
*/
|
|
262
|
+
hasProxy() {
|
|
263
|
+
return this.proxyConfig !== null;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get the current proxy server (without credentials)
|
|
267
|
+
*/
|
|
268
|
+
getProxyServer() {
|
|
269
|
+
return this.proxyConfig?.server || null;
|
|
270
|
+
}
|
|
246
271
|
async close() {
|
|
247
272
|
if (this.browser) {
|
|
248
273
|
await this.browser.close();
|
|
@@ -1747,4 +1772,184 @@ export class HotelBrowser {
|
|
|
1747
1772
|
console.error(`Get reviews attempt ${attempt} failed: ${error.message}. Retrying in ${Math.round(delayMs / 1000)}s...`);
|
|
1748
1773
|
});
|
|
1749
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Get price calendar for a hotel - shows prices for multiple dates
|
|
1777
|
+
*/
|
|
1778
|
+
async getPriceCalendar(hotelUrl, startDate, nights = 14, guests = 2, rooms = 1, currency = "USD") {
|
|
1779
|
+
// Validate inputs
|
|
1780
|
+
const start = new Date(startDate);
|
|
1781
|
+
if (isNaN(start.getTime())) {
|
|
1782
|
+
throw new Error("Invalid start date format. Use YYYY-MM-DD.");
|
|
1783
|
+
}
|
|
1784
|
+
// Limit to reasonable range
|
|
1785
|
+
const actualNights = Math.min(Math.max(nights, 1), 30);
|
|
1786
|
+
// Generate date range
|
|
1787
|
+
const dates = [];
|
|
1788
|
+
for (let i = 0; i < actualNights; i++) {
|
|
1789
|
+
const checkIn = new Date(start);
|
|
1790
|
+
checkIn.setDate(checkIn.getDate() + i);
|
|
1791
|
+
const checkOut = new Date(checkIn);
|
|
1792
|
+
checkOut.setDate(checkOut.getDate() + 1);
|
|
1793
|
+
dates.push({
|
|
1794
|
+
checkIn: checkIn.toISOString().split("T")[0],
|
|
1795
|
+
checkOut: checkOut.toISOString().split("T")[0],
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
// Clean the hotel URL
|
|
1799
|
+
const cleanUrl = hotelUrl.split("?")[0].split("#")[0];
|
|
1800
|
+
// Collect prices for each date
|
|
1801
|
+
const prices = [];
|
|
1802
|
+
let hotelName = "";
|
|
1803
|
+
for (const dateRange of dates) {
|
|
1804
|
+
await this.enforceRateLimit();
|
|
1805
|
+
if (!this.page)
|
|
1806
|
+
throw new Error("Browser not initialized");
|
|
1807
|
+
const urlWithDates = `${cleanUrl}?checkin=${dateRange.checkIn}&checkout=${dateRange.checkOut}&group_adults=${guests}&no_rooms=${rooms}&selected_currency=${currency}`;
|
|
1808
|
+
try {
|
|
1809
|
+
await this.page.goto(urlWithDates, {
|
|
1810
|
+
waitUntil: "domcontentloaded",
|
|
1811
|
+
timeout: 30000,
|
|
1812
|
+
});
|
|
1813
|
+
await this.page.waitForTimeout(2000);
|
|
1814
|
+
// Close any popups
|
|
1815
|
+
try {
|
|
1816
|
+
await this.page.keyboard.press("Escape");
|
|
1817
|
+
}
|
|
1818
|
+
catch { }
|
|
1819
|
+
// Extract price data
|
|
1820
|
+
const priceData = await this.page.evaluate(`
|
|
1821
|
+
(function() {
|
|
1822
|
+
var result = { hotelName: '', price: null, priceDisplay: '', available: true, currency: '' };
|
|
1823
|
+
|
|
1824
|
+
// Get hotel name (only need it once)
|
|
1825
|
+
var nameEl = document.querySelector('h2[class*="pp-header__title"], [data-testid="PropertyHeaderDesktop-wrapper"] h2, h2.d2fee87262');
|
|
1826
|
+
result.hotelName = nameEl?.textContent?.trim() || '';
|
|
1827
|
+
|
|
1828
|
+
// Check for no availability message
|
|
1829
|
+
var noAvail = document.querySelector('[class*="soldout"], [class*="no-availability"], [data-testid="no-rooms-available"]');
|
|
1830
|
+
if (noAvail) {
|
|
1831
|
+
result.available = false;
|
|
1832
|
+
return result;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Find the lowest price - look for price elements
|
|
1836
|
+
var priceElements = document.querySelectorAll('[data-testid="price-and-discounted-price"], .bui-price-display__value, .prco-valign-middle-helper');
|
|
1837
|
+
var prices = [];
|
|
1838
|
+
|
|
1839
|
+
priceElements.forEach(function(el) {
|
|
1840
|
+
var text = el.textContent?.trim() || '';
|
|
1841
|
+
// Extract price number and currency
|
|
1842
|
+
var match = text.match(/([\\$€£¥₹])\\s*([\\d,]+)/);
|
|
1843
|
+
if (match) {
|
|
1844
|
+
var currencySymbol = match[1];
|
|
1845
|
+
var priceNum = parseInt(match[2].replace(/,/g, ''));
|
|
1846
|
+
if (!isNaN(priceNum) && priceNum > 0) {
|
|
1847
|
+
prices.push({ price: priceNum, display: text.trim(), currency: currencySymbol });
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// Also try format like "241 $" or "241 USD"
|
|
1851
|
+
var match2 = text.match(/([\\d,]+)\\s*([\\$€£¥₹]|USD|EUR|GBP)/);
|
|
1852
|
+
if (match2) {
|
|
1853
|
+
var priceNum2 = parseInt(match2[1].replace(/,/g, ''));
|
|
1854
|
+
if (!isNaN(priceNum2) && priceNum2 > 0) {
|
|
1855
|
+
prices.push({ price: priceNum2, display: text.trim(), currency: match2[2] });
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// Get the lowest price
|
|
1861
|
+
if (prices.length > 0) {
|
|
1862
|
+
prices.sort(function(a, b) { return a.price - b.price; });
|
|
1863
|
+
result.price = prices[0].price;
|
|
1864
|
+
result.priceDisplay = prices[0].display;
|
|
1865
|
+
result.currency = prices[0].currency;
|
|
1866
|
+
} else {
|
|
1867
|
+
// Try alternative price selectors
|
|
1868
|
+
var altPrice = document.querySelector('[class*="bui-price-display"], [class*="price"]');
|
|
1869
|
+
if (altPrice) {
|
|
1870
|
+
var altText = altPrice.textContent?.trim() || '';
|
|
1871
|
+
var altMatch = altText.match(/([\\$€£¥₹])\\s*([\\d,]+)/);
|
|
1872
|
+
if (altMatch) {
|
|
1873
|
+
result.price = parseInt(altMatch[2].replace(/,/g, ''));
|
|
1874
|
+
result.priceDisplay = altMatch[0];
|
|
1875
|
+
result.currency = altMatch[1];
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// If still no price found, might be unavailable
|
|
1881
|
+
if (result.price === null) {
|
|
1882
|
+
result.available = false;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
return result;
|
|
1886
|
+
})()
|
|
1887
|
+
`);
|
|
1888
|
+
// Store hotel name from first result
|
|
1889
|
+
if (!hotelName && priceData.hotelName) {
|
|
1890
|
+
hotelName = priceData.hotelName;
|
|
1891
|
+
}
|
|
1892
|
+
// Map currency symbol to code
|
|
1893
|
+
const currencyMap = {
|
|
1894
|
+
"$": "USD",
|
|
1895
|
+
"€": "EUR",
|
|
1896
|
+
"£": "GBP",
|
|
1897
|
+
"¥": "JPY",
|
|
1898
|
+
"₹": "INR",
|
|
1899
|
+
};
|
|
1900
|
+
const currencyCode = currencyMap[priceData.currency] || priceData.currency || currency;
|
|
1901
|
+
prices.push({
|
|
1902
|
+
date: dateRange.checkIn,
|
|
1903
|
+
price: priceData.price,
|
|
1904
|
+
priceDisplay: priceData.priceDisplay || (priceData.price ? `${priceData.currency}${priceData.price}` : "N/A"),
|
|
1905
|
+
available: priceData.available,
|
|
1906
|
+
currency: currencyCode,
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
catch (error) {
|
|
1910
|
+
// If page load fails, mark as unavailable
|
|
1911
|
+
prices.push({
|
|
1912
|
+
date: dateRange.checkIn,
|
|
1913
|
+
price: null,
|
|
1914
|
+
priceDisplay: "Error",
|
|
1915
|
+
available: false,
|
|
1916
|
+
currency,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
// Calculate statistics
|
|
1921
|
+
const availablePrices = prices.filter(p => p.price !== null && p.available);
|
|
1922
|
+
const priceValues = availablePrices.map(p => p.price);
|
|
1923
|
+
let lowestPrice = null;
|
|
1924
|
+
let lowestPriceDate = null;
|
|
1925
|
+
let highestPrice = null;
|
|
1926
|
+
let highestPriceDate = null;
|
|
1927
|
+
let averagePrice = null;
|
|
1928
|
+
if (priceValues.length > 0) {
|
|
1929
|
+
lowestPrice = Math.min(...priceValues);
|
|
1930
|
+
highestPrice = Math.max(...priceValues);
|
|
1931
|
+
averagePrice = Math.round(priceValues.reduce((a, b) => a + b, 0) / priceValues.length);
|
|
1932
|
+
const lowestPriceEntry = availablePrices.find(p => p.price === lowestPrice);
|
|
1933
|
+
const highestPriceEntry = availablePrices.find(p => p.price === highestPrice);
|
|
1934
|
+
lowestPriceDate = lowestPriceEntry?.date || null;
|
|
1935
|
+
highestPriceDate = highestPriceEntry?.date || null;
|
|
1936
|
+
}
|
|
1937
|
+
// Calculate end date
|
|
1938
|
+
const endDate = new Date(start);
|
|
1939
|
+
endDate.setDate(endDate.getDate() + actualNights - 1);
|
|
1940
|
+
return {
|
|
1941
|
+
hotelName,
|
|
1942
|
+
startDate,
|
|
1943
|
+
endDate: endDate.toISOString().split("T")[0],
|
|
1944
|
+
nights: actualNights,
|
|
1945
|
+
currency,
|
|
1946
|
+
prices,
|
|
1947
|
+
lowestPrice,
|
|
1948
|
+
lowestPriceDate,
|
|
1949
|
+
highestPrice,
|
|
1950
|
+
highestPriceDate,
|
|
1951
|
+
averagePrice,
|
|
1952
|
+
url: cleanUrl,
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1750
1955
|
}
|
package/dist/index.js
CHANGED
|
@@ -139,12 +139,55 @@ 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;
|
|
152
|
+
/**
|
|
153
|
+
* Parse proxy configuration from environment variable
|
|
154
|
+
* Supports formats:
|
|
155
|
+
* - http://proxy.example.com:8080
|
|
156
|
+
* - http://user:pass@proxy.example.com:8080
|
|
157
|
+
* - socks5://proxy.example.com:1080
|
|
158
|
+
* - socks5://user:pass@proxy.example.com:1080
|
|
159
|
+
*/
|
|
160
|
+
function parseProxyFromEnv() {
|
|
161
|
+
const proxyUrl = process.env.HOTELZERO_PROXY;
|
|
162
|
+
if (!proxyUrl)
|
|
163
|
+
return undefined;
|
|
164
|
+
try {
|
|
165
|
+
const url = new URL(proxyUrl);
|
|
166
|
+
const config = {
|
|
167
|
+
server: `${url.protocol}//${url.host}`,
|
|
168
|
+
};
|
|
169
|
+
if (url.username) {
|
|
170
|
+
config.username = decodeURIComponent(url.username);
|
|
171
|
+
}
|
|
172
|
+
if (url.password) {
|
|
173
|
+
config.password = decodeURIComponent(url.password);
|
|
174
|
+
}
|
|
175
|
+
return config;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
console.error(`Invalid HOTELZERO_PROXY format: ${proxyUrl}`);
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
144
182
|
async function getBrowser() {
|
|
145
183
|
if (!browser) {
|
|
146
184
|
browser = new HotelBrowser();
|
|
147
|
-
|
|
185
|
+
const proxyConfig = parseProxyFromEnv();
|
|
186
|
+
await browser.init(true, proxyConfig);
|
|
187
|
+
// Log proxy status (without credentials)
|
|
188
|
+
if (browser.hasProxy()) {
|
|
189
|
+
console.error(`Proxy enabled: ${browser.getProxyServer()}`);
|
|
190
|
+
}
|
|
148
191
|
}
|
|
149
192
|
return browser;
|
|
150
193
|
}
|
|
@@ -438,10 +481,72 @@ function formatReviewsResult(result) {
|
|
|
438
481
|
lines.push(`Hotel URL: ${result.url}`);
|
|
439
482
|
return lines.join("\n");
|
|
440
483
|
}
|
|
484
|
+
function formatPriceCalendarResult(result) {
|
|
485
|
+
const lines = [];
|
|
486
|
+
lines.push("=".repeat(60));
|
|
487
|
+
lines.push(`PRICE CALENDAR: ${result.hotelName}`);
|
|
488
|
+
lines.push("=".repeat(60));
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push(`Date Range: ${result.startDate} to ${result.endDate} (${result.nights} nights)`);
|
|
491
|
+
lines.push(`Currency: ${result.currency}`);
|
|
492
|
+
lines.push("");
|
|
493
|
+
// Summary statistics
|
|
494
|
+
lines.push("--- SUMMARY ---");
|
|
495
|
+
if (result.lowestPrice !== null) {
|
|
496
|
+
lines.push(`Lowest Price: ${result.currency} ${result.lowestPrice} (${result.lowestPriceDate})`);
|
|
497
|
+
}
|
|
498
|
+
if (result.highestPrice !== null) {
|
|
499
|
+
lines.push(`Highest Price: ${result.currency} ${result.highestPrice} (${result.highestPriceDate})`);
|
|
500
|
+
}
|
|
501
|
+
if (result.averagePrice !== null) {
|
|
502
|
+
lines.push(`Average Price: ${result.currency} ${result.averagePrice}`);
|
|
503
|
+
}
|
|
504
|
+
const availableCount = result.prices.filter(p => p.available).length;
|
|
505
|
+
const unavailableCount = result.prices.length - availableCount;
|
|
506
|
+
lines.push(`Available: ${availableCount}/${result.prices.length} nights`);
|
|
507
|
+
if (unavailableCount > 0) {
|
|
508
|
+
lines.push(`Unavailable: ${unavailableCount} nights`);
|
|
509
|
+
}
|
|
510
|
+
lines.push("");
|
|
511
|
+
// Price calendar
|
|
512
|
+
lines.push("--- PRICES BY DATE ---");
|
|
513
|
+
// Group by week for better readability
|
|
514
|
+
let currentWeek = [];
|
|
515
|
+
let lastWeekNum = -1;
|
|
516
|
+
result.prices.forEach((datePrice) => {
|
|
517
|
+
const date = new Date(datePrice.date);
|
|
518
|
+
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
|
|
519
|
+
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
520
|
+
let priceStr;
|
|
521
|
+
if (!datePrice.available) {
|
|
522
|
+
priceStr = "N/A";
|
|
523
|
+
}
|
|
524
|
+
else if (datePrice.price !== null) {
|
|
525
|
+
// Highlight lowest price
|
|
526
|
+
if (datePrice.price === result.lowestPrice) {
|
|
527
|
+
priceStr = `${datePrice.priceDisplay} ★ LOWEST`;
|
|
528
|
+
}
|
|
529
|
+
else if (datePrice.price === result.highestPrice) {
|
|
530
|
+
priceStr = `${datePrice.priceDisplay} (highest)`;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
priceStr = datePrice.priceDisplay;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
priceStr = "N/A";
|
|
538
|
+
}
|
|
539
|
+
lines.push(`${dayName} ${monthDay}: ${priceStr}`);
|
|
540
|
+
});
|
|
541
|
+
lines.push("");
|
|
542
|
+
lines.push("=".repeat(60));
|
|
543
|
+
lines.push(`Hotel URL: ${result.url}`);
|
|
544
|
+
return lines.join("\n");
|
|
545
|
+
}
|
|
441
546
|
// Create MCP server
|
|
442
547
|
const server = new Server({
|
|
443
548
|
name: "hotelzero",
|
|
444
|
-
version: "1.
|
|
549
|
+
version: "1.8.0",
|
|
445
550
|
}, {
|
|
446
551
|
capabilities: {
|
|
447
552
|
tools: {},
|
|
@@ -658,6 +763,22 @@ Results are scored and ranked by how well they match the criteria.`,
|
|
|
658
763
|
required: ["hotelUrl"],
|
|
659
764
|
},
|
|
660
765
|
},
|
|
766
|
+
{
|
|
767
|
+
name: "get_price_calendar",
|
|
768
|
+
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.",
|
|
769
|
+
inputSchema: {
|
|
770
|
+
type: "object",
|
|
771
|
+
properties: {
|
|
772
|
+
hotelUrl: { type: "string", description: "Booking.com hotel URL to check prices for" },
|
|
773
|
+
startDate: { type: "string", description: "Start date for price calendar (YYYY-MM-DD)" },
|
|
774
|
+
nights: { type: "number", description: "Number of nights to check (default: 14, max: 30)", minimum: 1, maximum: 30 },
|
|
775
|
+
guests: { type: "number", description: "Number of guests (default: 2)", minimum: 1, maximum: 30 },
|
|
776
|
+
rooms: { type: "number", description: "Number of rooms (default: 1)", minimum: 1, maximum: 10 },
|
|
777
|
+
currency: { type: "string", description: "Currency code (default: USD)" },
|
|
778
|
+
},
|
|
779
|
+
required: ["hotelUrl", "startDate"],
|
|
780
|
+
},
|
|
781
|
+
},
|
|
661
782
|
],
|
|
662
783
|
};
|
|
663
784
|
});
|
|
@@ -845,6 +966,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
845
966
|
],
|
|
846
967
|
};
|
|
847
968
|
}
|
|
969
|
+
case "get_price_calendar": {
|
|
970
|
+
const parsed = GetPriceCalendarSchema.parse(args);
|
|
971
|
+
const result = await b.getPriceCalendar(parsed.hotelUrl, parsed.startDate, parsed.nights, parsed.guests, parsed.rooms, parsed.currency);
|
|
972
|
+
const formatted = formatPriceCalendarResult(result);
|
|
973
|
+
return {
|
|
974
|
+
content: [
|
|
975
|
+
{
|
|
976
|
+
type: "text",
|
|
977
|
+
text: formatted,
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
};
|
|
981
|
+
}
|
|
848
982
|
default:
|
|
849
983
|
throw new Error(`Unknown tool: ${name}`);
|
|
850
984
|
}
|
|
@@ -913,7 +1047,7 @@ process.on("SIGTERM", async () => {
|
|
|
913
1047
|
async function main() {
|
|
914
1048
|
const transport = new StdioServerTransport();
|
|
915
1049
|
await server.connect(transport);
|
|
916
|
-
console.error("HotelZero v1.
|
|
1050
|
+
console.error("HotelZero v1.8.0 running on stdio");
|
|
917
1051
|
}
|
|
918
1052
|
main().catch((error) => {
|
|
919
1053
|
console.error("Fatal error:", error);
|