hotelzero 1.13.0 → 1.15.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 +247 -10
- package/dist/browser.d.ts +96 -0
- package/dist/browser.js +1471 -143
- package/dist/debug-checkin.d.ts +1 -0
- package/dist/debug-checkin.js +31 -0
- package/dist/debug-extraction.d.ts +1 -0
- package/dist/debug-extraction.js +49 -0
- package/dist/debug-search.d.ts +1 -0
- package/dist/debug-search.js +47 -0
- package/dist/explore-cache.d.ts +1 -0
- package/dist/explore-cache.js +78 -0
- package/dist/index.js +2 -2
- package/dist/intercept-test.d.ts +1 -0
- package/dist/intercept-test.js +161 -0
- package/dist/verify-api-extraction.d.ts +1 -0
- package/dist/verify-api-extraction.js +116 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
async function debugCheckin() {
|
|
3
|
+
const browser = await chromium.launch({ headless: true });
|
|
4
|
+
const context = await browser.newContext({
|
|
5
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
|
6
|
+
});
|
|
7
|
+
const page = await context.newPage();
|
|
8
|
+
await page.goto("https://www.booking.com/hotel/fr/lejardindecluny.html", {
|
|
9
|
+
waitUntil: 'networkidle',
|
|
10
|
+
timeout: 60000
|
|
11
|
+
});
|
|
12
|
+
await page.waitForTimeout(2000);
|
|
13
|
+
const result = await page.evaluate(() => {
|
|
14
|
+
const w = window;
|
|
15
|
+
const cache = w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
16
|
+
if (!cache)
|
|
17
|
+
return { error: 'No cache' };
|
|
18
|
+
const propertyKey = Object.keys(cache).find(k => k.startsWith('Property:{"id":'));
|
|
19
|
+
if (!propertyKey)
|
|
20
|
+
return { error: 'No property key' };
|
|
21
|
+
const property = cache[propertyKey];
|
|
22
|
+
return {
|
|
23
|
+
hasHouseRules: !!property?.houseRules,
|
|
24
|
+
houseRules: property?.houseRules,
|
|
25
|
+
propertyKeys: Object.keys(property).filter(k => k.includes('house') || k.includes('check'))
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
console.log(JSON.stringify(result, null, 2));
|
|
29
|
+
await browser.close();
|
|
30
|
+
}
|
|
31
|
+
debugCheckin().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Debug extraction on actual URL
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
async function debugExtraction() {
|
|
4
|
+
const browser = await chromium.launch({ headless: true });
|
|
5
|
+
const context = await browser.newContext({
|
|
6
|
+
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"
|
|
7
|
+
});
|
|
8
|
+
const page = await context.newPage();
|
|
9
|
+
// Use URL format WITH country code
|
|
10
|
+
const hotelUrl = "https://www.booking.com/hotel/fr/la-sanguine.html";
|
|
11
|
+
console.log("Navigating to:", hotelUrl);
|
|
12
|
+
await page.goto(hotelUrl, { waitUntil: 'networkidle', timeout: 60000 });
|
|
13
|
+
await page.waitForTimeout(2000);
|
|
14
|
+
console.log("Final URL:", page.url());
|
|
15
|
+
const result = await page.evaluate(() => {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
const w = window;
|
|
18
|
+
const cache = w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
19
|
+
if (!cache) {
|
|
20
|
+
return { error: 'No cache found', hasCapla: !!w.__caplaDataStore };
|
|
21
|
+
}
|
|
22
|
+
// Find Property key
|
|
23
|
+
const propertyKey = Object.keys(cache).find(k => k.startsWith('Property:{"id":'));
|
|
24
|
+
if (!propertyKey) {
|
|
25
|
+
return {
|
|
26
|
+
error: 'No Property key found',
|
|
27
|
+
allKeys: Object.keys(cache).slice(0, 30),
|
|
28
|
+
rootQueryKeys: cache['ROOT_QUERY'] ? Object.keys(cache['ROOT_QUERY']) : []
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const property = cache[propertyKey];
|
|
32
|
+
const idMatch = propertyKey.match(/Property:\{"id":(\d+)\}/);
|
|
33
|
+
const hotelId = idMatch ? idMatch[1] : null;
|
|
34
|
+
const basicDataKey = hotelId ? `BasicPropertyData:${hotelId}` : null;
|
|
35
|
+
const basicData = basicDataKey ? cache[basicDataKey] : null;
|
|
36
|
+
return {
|
|
37
|
+
propertyKey,
|
|
38
|
+
propertyName: property?.name,
|
|
39
|
+
basicDataName: basicData?.name,
|
|
40
|
+
hasReviews: !!property?.reviews,
|
|
41
|
+
reviewsCount: property?.reviews?.reviewsCount,
|
|
42
|
+
propertyKeys: Object.keys(property || {}).slice(0, 20)
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
console.log("\n=== EXTRACTION RESULT ===");
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
47
|
+
await browser.close();
|
|
48
|
+
}
|
|
49
|
+
debugExtraction().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Debug search results cache structure
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
async function debugSearch() {
|
|
4
|
+
const browser = await chromium.launch({ headless: true });
|
|
5
|
+
const context = await browser.newContext({
|
|
6
|
+
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"
|
|
7
|
+
});
|
|
8
|
+
const page = await context.newPage();
|
|
9
|
+
const searchUrl = "https://www.booking.com/searchresults.html?ss=Paris&checkin=2026-06-01&checkout=2026-06-03&group_adults=2&no_rooms=1";
|
|
10
|
+
console.log("Navigating to:", searchUrl);
|
|
11
|
+
await page.goto(searchUrl, { waitUntil: 'networkidle', timeout: 60000 });
|
|
12
|
+
await page.waitForTimeout(3000);
|
|
13
|
+
const result = await page.evaluate(() => {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
const w = window;
|
|
16
|
+
const cache = w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
17
|
+
if (!cache) {
|
|
18
|
+
return { error: 'No cache found' };
|
|
19
|
+
}
|
|
20
|
+
const rootQuery = cache['ROOT_QUERY'];
|
|
21
|
+
if (!rootQuery)
|
|
22
|
+
return { error: 'No ROOT_QUERY' };
|
|
23
|
+
const searchQueries = rootQuery.searchQueries;
|
|
24
|
+
if (!searchQueries)
|
|
25
|
+
return { error: 'No searchQueries' };
|
|
26
|
+
const searchKey = Object.keys(searchQueries).find(k => k.startsWith('search('));
|
|
27
|
+
if (!searchKey)
|
|
28
|
+
return { error: 'No search key' };
|
|
29
|
+
const searchOutput = searchQueries[searchKey];
|
|
30
|
+
const results = searchOutput?.results;
|
|
31
|
+
if (!results || !Array.isArray(results) || results.length === 0) {
|
|
32
|
+
return { error: 'No results' };
|
|
33
|
+
}
|
|
34
|
+
// Get first hotel's structure
|
|
35
|
+
const firstHotel = results[0];
|
|
36
|
+
return {
|
|
37
|
+
basicPropertyDataKeys: Object.keys(firstHotel.basicPropertyData || {}),
|
|
38
|
+
pageName: firstHotel.basicPropertyData?.pageName,
|
|
39
|
+
location: firstHotel.basicPropertyData?.location,
|
|
40
|
+
fullBasicData: JSON.stringify(firstHotel.basicPropertyData).substring(0, 2000)
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
console.log("\n=== SEARCH RESULT STRUCTURE ===");
|
|
44
|
+
console.log(JSON.stringify(result, null, 2));
|
|
45
|
+
await browser.close();
|
|
46
|
+
}
|
|
47
|
+
debugSearch().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Explore Apollo cache on hotel detail page
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
async function exploreHotelDetailCache() {
|
|
5
|
+
const browser = await chromium.launch({ headless: true });
|
|
6
|
+
const context = await browser.newContext({
|
|
7
|
+
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"
|
|
8
|
+
});
|
|
9
|
+
const page = await context.newPage();
|
|
10
|
+
// Use a known hotel URL
|
|
11
|
+
const hotelUrl = "https://www.booking.com/hotel/fr/la-sanguine.html?checkin=2026-06-01&checkout=2026-06-03&group_adults=2&no_rooms=1";
|
|
12
|
+
console.log("Navigating to:", hotelUrl);
|
|
13
|
+
await page.goto(hotelUrl, { waitUntil: 'networkidle', timeout: 60000 });
|
|
14
|
+
await page.waitForTimeout(3000);
|
|
15
|
+
console.log('\nExploring Apollo cache on hotel detail page...\n');
|
|
16
|
+
// Extract cache structure
|
|
17
|
+
const cacheAnalysis = await page.evaluate(() => {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const w = window;
|
|
20
|
+
const cache = w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
21
|
+
if (!cache)
|
|
22
|
+
return { error: 'No cache found' };
|
|
23
|
+
const rootQuery = cache['ROOT_QUERY'];
|
|
24
|
+
if (!rootQuery)
|
|
25
|
+
return { error: 'No ROOT_QUERY', cacheKeys: Object.keys(cache).slice(0, 50) };
|
|
26
|
+
return {
|
|
27
|
+
rootQueryKeys: Object.keys(rootQuery),
|
|
28
|
+
cacheKeys: Object.keys(cache).slice(0, 100),
|
|
29
|
+
// Look for property-related keys
|
|
30
|
+
propertyKeys: Object.keys(cache).filter(k => k.toLowerCase().includes('property') ||
|
|
31
|
+
k.toLowerCase().includes('hotel') ||
|
|
32
|
+
k.toLowerCase().includes('accommodation')),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
console.log('=== CACHE ANALYSIS ===');
|
|
36
|
+
console.log('ROOT_QUERY keys:', cacheAnalysis.rootQueryKeys);
|
|
37
|
+
console.log('\nProperty-related keys:', cacheAnalysis.propertyKeys);
|
|
38
|
+
console.log('\nAll cache keys (first 100):', cacheAnalysis.cacheKeys);
|
|
39
|
+
// Look for specific hotel data
|
|
40
|
+
const hotelData = await page.evaluate(() => {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const w = window;
|
|
43
|
+
const cache = w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
44
|
+
if (!cache)
|
|
45
|
+
return null;
|
|
46
|
+
const rootQuery = cache['ROOT_QUERY'];
|
|
47
|
+
if (!rootQuery)
|
|
48
|
+
return null;
|
|
49
|
+
// Find keys that might contain hotel details
|
|
50
|
+
const detailKeys = Object.keys(rootQuery).filter(k => k.includes('propertyPage') ||
|
|
51
|
+
k.includes('hotelPage') ||
|
|
52
|
+
k.includes('accommodation') ||
|
|
53
|
+
k.includes('basicPropertyData'));
|
|
54
|
+
const results = {};
|
|
55
|
+
for (const key of detailKeys) {
|
|
56
|
+
results[key] = rootQuery[key];
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
detailKeys,
|
|
60
|
+
sampleData: JSON.stringify(results).substring(0, 5000)
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
console.log('\n=== HOTEL DETAIL DATA ===');
|
|
64
|
+
console.log('Detail keys:', hotelData?.detailKeys);
|
|
65
|
+
console.log('\nSample data:', hotelData?.sampleData);
|
|
66
|
+
// Save full cache for analysis
|
|
67
|
+
const fullCache = await page.evaluate(() => {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
const w = window;
|
|
70
|
+
return w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
71
|
+
});
|
|
72
|
+
if (fullCache) {
|
|
73
|
+
fs.writeFileSync('/Users/matt/Developer/hotel-booking-mcp/hotel-detail-cache.json', JSON.stringify(fullCache, null, 2));
|
|
74
|
+
console.log('\nFull cache saved to: hotel-detail-cache.json');
|
|
75
|
+
}
|
|
76
|
+
await browser.close();
|
|
77
|
+
}
|
|
78
|
+
exploreHotelDetailCache().catch(console.error);
|
package/dist/index.js
CHANGED
|
@@ -550,7 +550,7 @@ function formatPriceCalendarResult(result) {
|
|
|
550
550
|
// Create MCP server
|
|
551
551
|
const server = new Server({
|
|
552
552
|
name: "hotelzero",
|
|
553
|
-
version: "1.
|
|
553
|
+
version: "1.14.0",
|
|
554
554
|
}, {
|
|
555
555
|
capabilities: {
|
|
556
556
|
tools: {},
|
|
@@ -1051,7 +1051,7 @@ process.on("SIGTERM", async () => {
|
|
|
1051
1051
|
async function main() {
|
|
1052
1052
|
const transport = new StdioServerTransport();
|
|
1053
1053
|
await server.connect(transport);
|
|
1054
|
-
logger.info({ version: "1.
|
|
1054
|
+
logger.info({ version: "1.14.0", transport: "stdio" }, "HotelZero server started");
|
|
1055
1055
|
}
|
|
1056
1056
|
main().catch((error) => {
|
|
1057
1057
|
logger.fatal({ err: error }, "Fatal error during server startup");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Extract hotel search results from Booking.com Apollo cache
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
async function extractSearchResults() {
|
|
5
|
+
const browser = await chromium.launch({ headless: true });
|
|
6
|
+
const context = await browser.newContext({
|
|
7
|
+
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"
|
|
8
|
+
});
|
|
9
|
+
const page = await context.newPage();
|
|
10
|
+
const searchUrl = "https://www.booking.com/searchresults.html?ss=Paris&checkin=2026-06-01&checkout=2026-06-03&group_adults=2&no_rooms=1";
|
|
11
|
+
console.log("Navigating to:", searchUrl);
|
|
12
|
+
await page.goto(searchUrl, { waitUntil: 'networkidle', timeout: 60000 });
|
|
13
|
+
await page.waitForTimeout(3000);
|
|
14
|
+
console.log('\nExtracting search results from Apollo cache...\n');
|
|
15
|
+
// Extract search results from Apollo cache
|
|
16
|
+
const searchResults = await page.evaluate(() => {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
const w = window;
|
|
19
|
+
const cache = w.__caplaDataStore?.apollo?.cache?.data?.data;
|
|
20
|
+
if (!cache)
|
|
21
|
+
return null;
|
|
22
|
+
const rootQuery = cache['ROOT_QUERY'];
|
|
23
|
+
if (!rootQuery)
|
|
24
|
+
return null;
|
|
25
|
+
// searchQueries is at ROOT_QUERY.searchQueries
|
|
26
|
+
const searchQueries = rootQuery.searchQueries;
|
|
27
|
+
if (!searchQueries)
|
|
28
|
+
return null;
|
|
29
|
+
// Find the search key (it's a complex key with the query parameters)
|
|
30
|
+
const searchKey = Object.keys(searchQueries).find(k => k.startsWith('search('));
|
|
31
|
+
if (!searchKey)
|
|
32
|
+
return { error: 'No search key found', keys: Object.keys(searchQueries) };
|
|
33
|
+
const searchOutput = searchQueries[searchKey];
|
|
34
|
+
if (!searchOutput)
|
|
35
|
+
return { error: 'No search output', searchKey };
|
|
36
|
+
// Get the results array
|
|
37
|
+
const results = searchOutput.results;
|
|
38
|
+
if (!results || !Array.isArray(results)) {
|
|
39
|
+
return { error: 'No results array', searchOutputKeys: Object.keys(searchOutput) };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
totalResults: searchOutput.pagination?.nbResultsTotal || results.length,
|
|
44
|
+
resultsInPage: results.length,
|
|
45
|
+
results
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
if (!searchResults) {
|
|
49
|
+
console.log('No cache found');
|
|
50
|
+
await browser.close();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if ('error' in searchResults) {
|
|
54
|
+
console.log('Error:', searchResults.error);
|
|
55
|
+
console.log('Details:', searchResults);
|
|
56
|
+
await browser.close();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(`Total hotels available: ${searchResults.totalResults}`);
|
|
60
|
+
console.log(`Hotels in this page: ${searchResults.resultsInPage}`);
|
|
61
|
+
// Save first result for analysis
|
|
62
|
+
const firstResult = searchResults.results[0];
|
|
63
|
+
if (firstResult) {
|
|
64
|
+
fs.writeFileSync('/Users/matt/Developer/hotel-booking-mcp/first-hotel-result.json', JSON.stringify(firstResult, null, 2));
|
|
65
|
+
console.log('\nFirst result saved to: first-hotel-result.json');
|
|
66
|
+
console.log('\nFirst result top-level keys:', Object.keys(firstResult));
|
|
67
|
+
}
|
|
68
|
+
// Map results to our format
|
|
69
|
+
console.log('\n=== SAMPLE MAPPED RESULTS ===');
|
|
70
|
+
for (let i = 0; i < Math.min(5, searchResults.results.length); i++) {
|
|
71
|
+
const hotel = searchResults.results[i];
|
|
72
|
+
if (!hotel)
|
|
73
|
+
continue;
|
|
74
|
+
const mapped = mapGraphQLToHotelResult(hotel);
|
|
75
|
+
console.log(`\n${i + 1}. ${mapped.name}`);
|
|
76
|
+
console.log(` Price: ${mapped.priceDisplay} (${mapped.price})`);
|
|
77
|
+
console.log(` Rating: ${mapped.rating} "${mapped.ratingText}" (${mapped.reviewCount} reviews)`);
|
|
78
|
+
console.log(` Location: ${mapped.location}`);
|
|
79
|
+
console.log(` Distance: ${mapped.distanceToCenter}`);
|
|
80
|
+
console.log(` Link: ${mapped.link}`);
|
|
81
|
+
console.log(` Image: ${mapped.thumbnailUrl?.substring(0, 80)}...`);
|
|
82
|
+
}
|
|
83
|
+
await browser.close();
|
|
84
|
+
}
|
|
85
|
+
function mapGraphQLToHotelResult(hotel) {
|
|
86
|
+
// Extract nested objects
|
|
87
|
+
const displayName = hotel.displayName;
|
|
88
|
+
const basicPropertyData = hotel.basicPropertyData;
|
|
89
|
+
const location = hotel.location;
|
|
90
|
+
const priceDisplayInfoIrene = hotel.priceDisplayInfoIrene;
|
|
91
|
+
const reviews = hotel.reviews;
|
|
92
|
+
// Name
|
|
93
|
+
const name = displayName?.text || basicPropertyData?.pageName || 'Unknown';
|
|
94
|
+
// Price - navigate the nested structure
|
|
95
|
+
let price = null;
|
|
96
|
+
let priceDisplay = 'Price not shown';
|
|
97
|
+
if (priceDisplayInfoIrene?.displayPrice) {
|
|
98
|
+
const displayPrice = priceDisplayInfoIrene.displayPrice;
|
|
99
|
+
// Try different price fields
|
|
100
|
+
const amountPerStay = displayPrice.amountPerStay;
|
|
101
|
+
if (amountPerStay) {
|
|
102
|
+
priceDisplay = amountPerStay.amountRounded || amountPerStay.amount || priceDisplay;
|
|
103
|
+
price = amountPerStay.amountUnformatted ?? null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Rating
|
|
107
|
+
let rating = null;
|
|
108
|
+
let ratingText = '';
|
|
109
|
+
let reviewCount = null;
|
|
110
|
+
if (reviews) {
|
|
111
|
+
rating = reviews.totalScore ?? null;
|
|
112
|
+
ratingText = reviews.totalScoreTextTag?.translation || '';
|
|
113
|
+
reviewCount = reviews.reviewsCount ?? null;
|
|
114
|
+
}
|
|
115
|
+
// Location
|
|
116
|
+
const displayLocation = location?.displayLocation || '';
|
|
117
|
+
const mainDistance = location?.mainDistance || '';
|
|
118
|
+
// Thumbnail URL - construct full URL
|
|
119
|
+
let thumbnailUrl = null;
|
|
120
|
+
if (basicPropertyData?.photos?.main) {
|
|
121
|
+
const mainPhoto = basicPropertyData.photos.main;
|
|
122
|
+
const relativeUrl = mainPhoto.highResJpegUrl?.relativeUrl ||
|
|
123
|
+
mainPhoto.highResUrl?.relativeUrl ||
|
|
124
|
+
mainPhoto.lowResJpegUrl?.relativeUrl;
|
|
125
|
+
if (relativeUrl) {
|
|
126
|
+
thumbnailUrl = `https://cf.bstatic.com${relativeUrl}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Link
|
|
130
|
+
let link = '';
|
|
131
|
+
if (basicPropertyData?.pageName) {
|
|
132
|
+
link = `https://www.booking.com/hotel/${basicPropertyData.pageName}.html`;
|
|
133
|
+
}
|
|
134
|
+
// Extract amenities from various places
|
|
135
|
+
const amenities = [];
|
|
136
|
+
// propertySustainability
|
|
137
|
+
if (hotel.propertySustainability?.isSustainable) {
|
|
138
|
+
amenities.push('Sustainable');
|
|
139
|
+
}
|
|
140
|
+
// Check policies for free cancellation
|
|
141
|
+
const highlights = [];
|
|
142
|
+
if (hotel.policies?.enableJap498702498702FreeCancellation498702498702) {
|
|
143
|
+
highlights.push('Free Cancellation');
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
name,
|
|
147
|
+
price,
|
|
148
|
+
priceDisplay,
|
|
149
|
+
rating,
|
|
150
|
+
ratingText,
|
|
151
|
+
reviewCount,
|
|
152
|
+
location: displayLocation,
|
|
153
|
+
distanceToCenter: mainDistance,
|
|
154
|
+
amenities,
|
|
155
|
+
highlights,
|
|
156
|
+
link,
|
|
157
|
+
thumbnailUrl,
|
|
158
|
+
availability: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
extractSearchResults().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify that API extraction is actually working for all MCP features
|
|
3
|
+
*/
|
|
4
|
+
import { HotelBrowser } from './browser.js';
|
|
5
|
+
async function verifyAPIExtraction() {
|
|
6
|
+
const browser = new HotelBrowser();
|
|
7
|
+
console.log('\n========================================');
|
|
8
|
+
console.log('VERIFYING API EXTRACTION FOR MCP FEATURES');
|
|
9
|
+
console.log('========================================\n');
|
|
10
|
+
try {
|
|
11
|
+
await browser.init(true);
|
|
12
|
+
// Test 1: searchHotels - should use API extraction
|
|
13
|
+
console.log('TEST 1: searchHotels');
|
|
14
|
+
console.log('-------------------');
|
|
15
|
+
const searchResults = await browser.searchHotels({
|
|
16
|
+
destination: 'Paris',
|
|
17
|
+
checkIn: '2026-06-01',
|
|
18
|
+
checkOut: '2026-06-03',
|
|
19
|
+
guests: 2,
|
|
20
|
+
rooms: 1,
|
|
21
|
+
limit: 3,
|
|
22
|
+
});
|
|
23
|
+
if (searchResults.length === 0) {
|
|
24
|
+
console.log('❌ FAIL: No search results returned');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const firstResult = searchResults[0];
|
|
28
|
+
console.log(` Results: ${searchResults.length}`);
|
|
29
|
+
console.log(` First hotel: ${firstResult.name}`);
|
|
30
|
+
console.log(` Has rating: ${firstResult.rating !== null}`);
|
|
31
|
+
console.log(` Has price: ${firstResult.price !== null}`);
|
|
32
|
+
console.log(` Link format: ${firstResult.link}`);
|
|
33
|
+
// Check if link has country code (required for API extraction on detail page)
|
|
34
|
+
const hasCountryCode = /\/hotel\/[a-z]{2}\//.test(firstResult.link);
|
|
35
|
+
console.log(` Link has country code: ${hasCountryCode ? '✅ YES' : '❌ NO'}`);
|
|
36
|
+
if (!hasCountryCode) {
|
|
37
|
+
console.log(' ⚠️ WARNING: Links without country code will break API extraction on detail pages!');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Test 2: getHotelDetailsForComparison - should use API extraction
|
|
41
|
+
console.log('\nTEST 2: getHotelDetailsForComparison');
|
|
42
|
+
console.log('------------------------------------');
|
|
43
|
+
if (searchResults.length > 0 && searchResults[0].link) {
|
|
44
|
+
const details = await browser.getHotelDetailsForComparison(searchResults[0].link);
|
|
45
|
+
console.log(` Name: ${details.name}`);
|
|
46
|
+
console.log(` Rating: ${details.rating}`);
|
|
47
|
+
console.log(` Reviews: ${details.reviewCount}`);
|
|
48
|
+
console.log(` Address: ${details.address || '(empty)'}`);
|
|
49
|
+
console.log(` Check-in: ${details.checkInTime || '(empty)'}`);
|
|
50
|
+
console.log(` Facilities: ${details.popularFacilities.length}`);
|
|
51
|
+
console.log(` Photos: ${details.photos.length}`);
|
|
52
|
+
// Verify this looks like real API data, not DOM scraping artifacts
|
|
53
|
+
const isLikelyAPIData = details.name &&
|
|
54
|
+
details.name.length > 3 &&
|
|
55
|
+
!details.name.includes('Verified reviews') &&
|
|
56
|
+
details.address &&
|
|
57
|
+
details.address.includes(',');
|
|
58
|
+
console.log(` Looks like API data: ${isLikelyAPIData ? '✅ YES' : '❌ NO (might be DOM fallback)'}`);
|
|
59
|
+
}
|
|
60
|
+
// Test 3: checkAvailability - currently uses DOM scraping
|
|
61
|
+
console.log('\nTEST 3: checkAvailability');
|
|
62
|
+
console.log('-------------------------');
|
|
63
|
+
if (searchResults.length > 0 && searchResults[0].link) {
|
|
64
|
+
const availability = await browser.checkAvailability({
|
|
65
|
+
hotelUrl: searchResults[0].link,
|
|
66
|
+
checkIn: '2026-06-01',
|
|
67
|
+
checkOut: '2026-06-03',
|
|
68
|
+
guests: 2,
|
|
69
|
+
rooms: 1,
|
|
70
|
+
});
|
|
71
|
+
console.log(` Hotel: ${availability.hotelName}`);
|
|
72
|
+
console.log(` Available: ${availability.isAvailable}`);
|
|
73
|
+
console.log(` Room options: ${availability.rooms.length}`);
|
|
74
|
+
console.log(` Message: ${availability.message || '(none)'}`);
|
|
75
|
+
// Check if hotel name looks like API data
|
|
76
|
+
const nameIsValid = availability.hotelName &&
|
|
77
|
+
availability.hotelName.length > 3 &&
|
|
78
|
+
!availability.hotelName.includes('Verified');
|
|
79
|
+
console.log(` Name looks valid: ${nameIsValid ? '✅ YES' : '❌ NO'}`);
|
|
80
|
+
console.log(` ⚠️ Note: checkAvailability does NOT use API extraction yet`);
|
|
81
|
+
}
|
|
82
|
+
// Test 4: getReviews - currently uses DOM scraping
|
|
83
|
+
console.log('\nTEST 4: getReviews');
|
|
84
|
+
console.log('------------------');
|
|
85
|
+
if (searchResults.length > 0 && searchResults[0].link) {
|
|
86
|
+
try {
|
|
87
|
+
const reviews = await browser.getReviews(searchResults[0].link);
|
|
88
|
+
console.log(` Hotel: ${reviews.hotelName}`);
|
|
89
|
+
console.log(` Overall rating: ${reviews.overallRating}`);
|
|
90
|
+
console.log(` Total reviews: ${reviews.totalReviews}`);
|
|
91
|
+
console.log(` Categories: ${Object.keys(reviews.ratingBreakdown || {}).length}`);
|
|
92
|
+
console.log(` Sample reviews: ${reviews.reviews.length}`);
|
|
93
|
+
console.log(` ⚠️ Note: getReviews does NOT use API extraction yet`);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.log(` ⚠️ getReviews failed: ${e.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
console.log('\n========================================');
|
|
100
|
+
console.log('SUMMARY');
|
|
101
|
+
console.log('========================================');
|
|
102
|
+
console.log('✅ searchHotels: Uses API extraction');
|
|
103
|
+
console.log('✅ getHotelDetailsForComparison: Uses API extraction');
|
|
104
|
+
console.log('⚠️ checkAvailability: DOM scraping only');
|
|
105
|
+
console.log('⚠️ getReviews: DOM scraping only');
|
|
106
|
+
console.log('⚠️ getPriceCalendar: DOM scraping only');
|
|
107
|
+
console.log('\n');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error('Error:', error);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await browser.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
verifyAPIExtraction().catch(console.error);
|