imprint-mcp 0.5.1 → 0.6.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.
Files changed (136) hide show
  1. package/examples/google-hotels/README.md +40 -15
  2. package/examples/google-hotels/get_hotel_details/backends.json +16 -0
  3. package/examples/google-hotels/get_hotel_details/index.ts +122 -0
  4. package/examples/google-hotels/get_hotel_details/parser.ts +275 -0
  5. package/examples/google-hotels/get_hotel_details/playbook.yaml +40 -0
  6. package/examples/google-hotels/get_hotel_details/request-transform.ts +67 -0
  7. package/examples/google-hotels/get_hotel_details/workflow.json +67 -0
  8. package/examples/google-hotels/get_hotel_photos/backends.json +16 -0
  9. package/examples/google-hotels/get_hotel_photos/index.ts +90 -0
  10. package/examples/google-hotels/get_hotel_photos/parser.ts +75 -0
  11. package/examples/google-hotels/get_hotel_photos/playbook.yaml +23 -0
  12. package/examples/google-hotels/get_hotel_photos/request-transform.ts +27 -0
  13. package/examples/google-hotels/get_hotel_photos/workflow.json +44 -0
  14. package/examples/google-hotels/get_hotel_reviews/backends.json +16 -0
  15. package/examples/google-hotels/get_hotel_reviews/index.ts +14 -17
  16. package/examples/google-hotels/get_hotel_reviews/parser.ts +177 -105
  17. package/examples/google-hotels/get_hotel_reviews/playbook.yaml +9 -54
  18. package/examples/google-hotels/get_hotel_reviews/workflow.json +10 -13
  19. package/examples/google-hotels/get_travel_area_boundaries/backends.json +16 -0
  20. package/examples/google-hotels/get_travel_area_boundaries/index.ts +104 -0
  21. package/examples/google-hotels/get_travel_area_boundaries/package.json +9 -0
  22. package/examples/google-hotels/get_travel_area_boundaries/parser.ts +127 -0
  23. package/examples/google-hotels/get_travel_area_boundaries/playbook.yaml +38 -0
  24. package/examples/google-hotels/get_travel_area_boundaries/request-transform.ts +52 -0
  25. package/examples/google-hotels/get_travel_area_boundaries/workflow.json +54 -0
  26. package/examples/google-hotels/list_hotel_booking_options/backends.json +16 -0
  27. package/examples/google-hotels/list_hotel_booking_options/index.ts +123 -0
  28. package/examples/google-hotels/list_hotel_booking_options/package.json +9 -0
  29. package/examples/google-hotels/list_hotel_booking_options/parser.ts +308 -0
  30. package/examples/google-hotels/list_hotel_booking_options/playbook.yaml +95 -0
  31. package/examples/google-hotels/list_hotel_booking_options/request-transform.ts +77 -0
  32. package/examples/google-hotels/list_hotel_booking_options/workflow.json +67 -0
  33. package/examples/google-hotels/search_hotel_web_links/backends.json +16 -0
  34. package/examples/google-hotels/search_hotel_web_links/index.ts +90 -0
  35. package/examples/google-hotels/search_hotel_web_links/package.json +9 -0
  36. package/examples/google-hotels/search_hotel_web_links/parser.ts +96 -0
  37. package/examples/google-hotels/search_hotel_web_links/playbook.yaml +53 -0
  38. package/examples/google-hotels/search_hotel_web_links/request-transform.ts +17 -0
  39. package/examples/google-hotels/search_hotel_web_links/workflow.json +43 -0
  40. package/examples/google-hotels/search_hotels/backends.json +16 -0
  41. package/examples/google-hotels/search_hotels/index.ts +32 -102
  42. package/examples/google-hotels/search_hotels/parser.ts +237 -216
  43. package/examples/google-hotels/search_hotels/playbook.yaml +177 -42
  44. package/examples/google-hotels/search_hotels/request-transform.ts +101 -170
  45. package/examples/google-hotels/search_hotels/workflow.json +24 -73
  46. package/examples/google-hotels/suggest_hotel_destinations/backends.json +16 -0
  47. package/examples/google-hotels/{autocomplete_hotel_location → suggest_hotel_destinations}/index.ts +30 -19
  48. package/examples/google-hotels/suggest_hotel_destinations/package.json +9 -0
  49. package/examples/google-hotels/suggest_hotel_destinations/parser.ts +138 -0
  50. package/examples/google-hotels/suggest_hotel_destinations/playbook.yaml +47 -0
  51. package/examples/google-hotels/suggest_hotel_destinations/request-transform.ts +33 -0
  52. package/examples/google-hotels/suggest_hotel_destinations/workflow.json +44 -0
  53. package/examples/southwest/README.md +31 -70
  54. package/examples/southwest/get_account_details/backends.json +22 -0
  55. package/examples/southwest/get_account_details/credentials.manifest.json +17 -0
  56. package/examples/southwest/get_account_details/index.ts +138 -0
  57. package/examples/{google-hotels/_shared → southwest/get_account_details}/package.json +2 -2
  58. package/examples/southwest/get_account_details/parser.ts +136 -0
  59. package/examples/southwest/get_account_details/playbook.yaml +19 -0
  60. package/examples/southwest/get_account_details/workflow.json +93 -0
  61. package/examples/southwest/get_flight_status/backends.json +21 -0
  62. package/examples/southwest/get_flight_status/index.ts +130 -0
  63. package/examples/southwest/get_flight_status/package.json +9 -0
  64. package/examples/southwest/get_flight_status/parser.ts +138 -0
  65. package/examples/southwest/get_flight_status/playbook.yaml +58 -0
  66. package/examples/southwest/get_flight_status/workflow.json +77 -0
  67. package/examples/southwest/get_low_fare_calendar/backends.json +21 -0
  68. package/examples/southwest/get_low_fare_calendar/index.ts +134 -0
  69. package/examples/southwest/get_low_fare_calendar/package.json +9 -0
  70. package/examples/southwest/get_low_fare_calendar/parser.ts +107 -0
  71. package/examples/southwest/get_low_fare_calendar/playbook.yaml +41 -0
  72. package/examples/southwest/get_low_fare_calendar/workflow.json +81 -0
  73. package/examples/southwest/list_upcoming_trips/backends.json +22 -0
  74. package/examples/southwest/list_upcoming_trips/credentials.manifest.json +17 -0
  75. package/examples/southwest/list_upcoming_trips/index.ts +123 -0
  76. package/examples/southwest/list_upcoming_trips/package.json +9 -0
  77. package/examples/southwest/list_upcoming_trips/parser.ts +160 -0
  78. package/examples/southwest/list_upcoming_trips/playbook.yaml +24 -0
  79. package/examples/southwest/list_upcoming_trips/workflow.json +78 -0
  80. package/examples/southwest/search_flights/backends.json +21 -0
  81. package/examples/southwest/search_flights/index.ts +150 -0
  82. package/examples/southwest/search_flights/package.json +9 -0
  83. package/examples/southwest/search_flights/parser.ts +214 -0
  84. package/examples/southwest/search_flights/playbook.yaml +52 -0
  85. package/examples/southwest/search_flights/workflow.json +88 -0
  86. package/package.json +1 -1
  87. package/prompts/audit-agent.md +1 -0
  88. package/prompts/build-planning.md +19 -0
  89. package/prompts/compile-agent.md +6 -2
  90. package/prompts/tool-planning.md +2 -0
  91. package/src/imprint/audit.ts +34 -13
  92. package/src/imprint/backend-cache.ts +114 -0
  93. package/src/imprint/backend-ladder.ts +74 -24
  94. package/src/imprint/build-plan.ts +273 -58
  95. package/src/imprint/cdp-browser-fetch.ts +75 -30
  96. package/src/imprint/codex-cli-compile.ts +13 -2
  97. package/src/imprint/compile-agent.ts +1 -0
  98. package/src/imprint/compile-tools.ts +548 -136
  99. package/src/imprint/compile.ts +28 -6
  100. package/src/imprint/concurrency.ts +26 -3
  101. package/src/imprint/cron.ts +0 -3
  102. package/src/imprint/llm-url.ts +69 -0
  103. package/src/imprint/llm.ts +366 -47
  104. package/src/imprint/mcp-compile-server.ts +1 -0
  105. package/src/imprint/mcp-server.ts +48 -13
  106. package/src/imprint/probe-backends.ts +21 -115
  107. package/src/imprint/request-capture.ts +2 -1
  108. package/src/imprint/session-diff.ts +14 -1
  109. package/src/imprint/stealth-fetch.ts +27 -1
  110. package/src/imprint/stealth-token-cache.ts +2 -2
  111. package/src/imprint/teach-plan.ts +2 -0
  112. package/src/imprint/teach-state.ts +14 -9
  113. package/src/imprint/teach.ts +39 -2
  114. package/src/imprint/telemetry.ts +3 -0
  115. package/src/imprint/tool-candidates.ts +6 -3
  116. package/src/imprint/tool-plan.ts +29 -3
  117. package/src/imprint/tracing.ts +18 -2
  118. package/examples/google-hotels/_shared/batchexecute.ts +0 -73
  119. package/examples/google-hotels/_shared/freq.ts +0 -158
  120. package/examples/google-hotels/autocomplete_hotel_location/parser.ts +0 -71
  121. package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +0 -36
  122. package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +0 -37
  123. package/examples/google-hotels/autocomplete_hotel_location/workflow.json +0 -36
  124. package/examples/google-hotels/get_hotel_booking_options/index.ts +0 -143
  125. package/examples/google-hotels/get_hotel_booking_options/parser.ts +0 -271
  126. package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +0 -154
  127. package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +0 -154
  128. package/examples/google-hotels/get_hotel_booking_options/workflow.json +0 -84
  129. package/examples/google-hotels/get_hotel_reviews/request-transform.ts +0 -42
  130. package/examples/southwest/search_southwest_flights/backends.json +0 -23
  131. package/examples/southwest/search_southwest_flights/cron.json +0 -19
  132. package/examples/southwest/search_southwest_flights/index.ts +0 -128
  133. package/examples/southwest/search_southwest_flights/playbook.yaml +0 -46
  134. package/examples/southwest/search_southwest_flights/workflow.json +0 -72
  135. /package/examples/google-hotels/{autocomplete_hotel_location → get_hotel_details}/package.json +0 -0
  136. /package/examples/google-hotels/{get_hotel_booking_options → get_hotel_photos}/package.json +0 -0
@@ -1,24 +1,47 @@
1
- # Google Hotels — `imprint-google-hotels`
1
+ # Google Hotels - `imprint-google-hotels`
2
2
 
3
- > **One-shot compiled, proof of concept.** Every file in this directory was generated by a single `imprint teach google-hotels` run against **one** recorded browser session — no hand-written request code, parsers, or selectors. It is committed here as a proof of concept of what the compiler produces, not as a maintained integration.
3
+ > Generated Google Hotels MCP example. Every tool directory here was produced
4
+ > from a recorded Google Hotels session and refreshed from the verified
5
+ > generated artifacts under `~/.imprint/google-hotels`.
4
6
 
5
- A 4-tool MCP server for Google Hotels, compiled from a recording of a normal hotel search. Headless-claude differential audit: **91.7%** — every tool `liveVerified=true`.
7
+ An 8-tool MCP server for Google Hotels search, selected-hotel details,
8
+ booking options, reviews, photos, area boundaries, and related web links.
9
+
10
+ Fresh live verification on July 9, 2026:
11
+
12
+ ```bash
13
+ bun test ~/.imprint/google-hotels/*/integration.test.ts --timeout 180000
14
+ ```
15
+
16
+ Result: `14 pass`, `0 fail`, `66 expect() calls` across `8` generated
17
+ workflow integration suites. All generated `backends.json` files prefer
18
+ `fetch`, with browser-backed rungs available as fallbacks.
6
19
 
7
20
  ## Tools
8
21
 
9
22
  | Tool | What it does | Notes |
10
23
  |---|---|---|
11
- | `autocomplete_hotel_location` | Resolve a location query to a place token | |
12
- | `search_hotels` | Search hotels (location, dates, guests, rating, price, amenities, class, sort, property type) | the star tool |
13
- | `get_hotel_reviews` | Reviews for a hotel | **consumes** a `hotel_id` produced by `search_hotels` |
14
- | `get_hotel_booking_options` | Booking/price options for a hotel | **consumes** a `location_context` produced by `search_hotels` |
15
-
16
- ## How it was compiled
17
-
18
- - **Protocol**: Google's `batchexecute` endpoint returns a nested-array payload; the compiler reverse-engineered the decoder into `_shared/batchexecute.ts` + per-tool `parser.ts`, and the request shape into per-tool `request-transform.ts`.
19
- - **Producer → consumer chaining**: `search_hotels` emits opaque ids that `get_hotel_reviews` / `get_hotel_booking_options` consume — the compiler detected the cross-tool token flow and wired it.
20
- - **Anti-bot**: calls run on the **cdp-replay** rung (inside a live trusted Chrome) with a **stealth-fetch** fallback.
21
- - **Artifacts per tool**: `workflow.json` (API replay), `playbook.yaml` (DOM fallback), `index.ts` (MCP tool), `parser.ts` + `request-transform.ts` (codecs).
24
+ | `suggest_hotel_destinations` | Return destination and lodging autocomplete suggestions for a query. | Replacement for the older autocomplete example. |
25
+ | `search_hotels` | Search lodging results by destination, dates, minimum price, hotel class, and property type. | Emits reusable `hotel_token` / `result_token` values. |
26
+ | `list_hotel_booking_options` | List booking providers and prices for a selected hotel stay. | Consumes a `hotel_token`, typically from `search_hotels`. |
27
+ | `get_hotel_reviews` | Fetch review data for a selected hotel. | Consumes a `hotel_token`, typically from `search_hotels`. |
28
+ | `get_hotel_photos` | Fetch photo thumbnail URLs for a selected hotel. | Consumes a `hotel_token`, typically from `search_hotels`. |
29
+ | `get_hotel_details` | Fetch detailed information for a selected hotel or vacation rental. | Consumes a `hotel_token`. |
30
+ | `get_travel_area_boundaries` | Fetch map boundary or area geometry data for a destination. | Uses place id and coordinates. |
31
+ | `search_hotel_web_links` | Search web links for a selected hotel name. | Useful for official site and related links. |
32
+
33
+ ## How It Was Compiled
34
+
35
+ - **Protocol**: Google's `batchexecute` endpoint returns nested-array payloads.
36
+ Each tool includes its generated request transform and parser.
37
+ - **Producer to consumer flow**: `search_hotels` emits selected-result tokens
38
+ that booking, reviews, photos, and detail tools can consume.
39
+ - **Backend reliability**: checked-in `backends.json` files record `fetch` as
40
+ the working preferred backend for every tool. Runtime can still fall back to
41
+ browser-backed rungs when needed.
42
+ - **Artifacts per tool**: `workflow.json` (API replay), `index.ts` (MCP tool),
43
+ `parser.ts`, optional `request-transform.ts`, `playbook.yaml` (DOM fallback),
44
+ `package.json`, and `backends.json`.
22
45
 
23
46
  ## Install
24
47
 
@@ -26,4 +49,6 @@ A 4-tool MCP server for Google Hotels, compiled from a recording of a normal hot
26
49
  imprint install google-hotels --source examples --platform claude-desktop
27
50
  ```
28
51
 
29
- *Recording-derived defaults (dates, the recorded location) age out — pass explicit values. See the repo [README](../../README.md) and [docs](../../docs/architecture.md).*
52
+ Recording-derived defaults such as dates and locations age out. Pass explicit
53
+ dates, destinations, and selected-result tokens when using the tools. See the
54
+ repo [README](../../README.md) and [docs](../../docs/architecture.md).
@@ -0,0 +1,16 @@
1
+ {
2
+ "probedAt": "2026-07-09T01:36:48.358Z",
3
+ "imprintVersion": "0.5.1",
4
+ "schemaVersion": 2,
5
+ "workflowHash": "2a7f5ddb6919ec3366885acf4ab4d3d7833b1e5c40888a8c6e34e7617cde0381",
6
+ "capabilityHash": "d0891f676244406c38af43944a1d24d32c068a9d1a75986d260a63bffd29e1b2",
7
+ "preferredOrder": [
8
+ "fetch"
9
+ ],
10
+ "results": {
11
+ "fetch": {
12
+ "outcome": "ok",
13
+ "durationMs": 685
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Tool: get_hotel_details
5
+ * Site: google-hotels
6
+ * Intent: Fetch detailed Google Hotels information for a selected hotel or vacation-rental result.
7
+ *
8
+ * To regenerate: imprint emit ~/.imprint/google-hotels/get_hotel_details/workflow.json --force
9
+ */
10
+
11
+ import { fileURLToPath } from 'node:url';
12
+ import { dirname, join } from 'node:path';
13
+ import {
14
+ executeWorkflow,
15
+ type CredentialStore,
16
+ } from 'imprint/runtime';
17
+ import type { ToolResult, Workflow } from 'imprint/types';
18
+
19
+ const WORKFLOW: Workflow = {
20
+ "toolName": "get_hotel_details",
21
+ "intent": {
22
+ "description": "Fetch detailed Google Hotels information for a selected hotel or vacation-rental result.",
23
+ "userSaid": "clicked one of hte offerings, saw booking options; changed property type to vacation rentals instead of hotels; changed location to tahoe city"
24
+ },
25
+ "parameters": [
26
+ {
27
+ "name": "hotel_token",
28
+ "type": "string",
29
+ "description": "Google Hotels selected result token for the hotel, usually emitted by a hotel search result.",
30
+ "verified": false,
31
+ "verifyNote": "annotated"
32
+ },
33
+ {
34
+ "name": "destination",
35
+ "type": "string",
36
+ "description": "Destination context used when opening the hotel result.",
37
+ "default": "tahoe city",
38
+ "verified": false,
39
+ "verifyNote": "annotated"
40
+ },
41
+ {
42
+ "name": "check_in_date",
43
+ "type": "string",
44
+ "description": "Check-in date in YYYY-MM-DD format.",
45
+ "default": "2026-08-09",
46
+ "verified": false,
47
+ "verifyNote": "annotated"
48
+ },
49
+ {
50
+ "name": "check_out_date",
51
+ "type": "string",
52
+ "description": "Check-out date in YYYY-MM-DD format.",
53
+ "default": "2026-08-16",
54
+ "verified": false,
55
+ "verifyNote": "annotated"
56
+ },
57
+ {
58
+ "name": "property_type",
59
+ "type": "string",
60
+ "description": "Property type context, either hotels or vacation_rentals.",
61
+ "default": "vacation_rentals",
62
+ "verified": true
63
+ }
64
+ ],
65
+ "requests": [
66
+ {
67
+ "method": "POST",
68
+ "url": "https://www.google.com/_/TravelFrontendUi/data/batchexecute?rpcids=AtySUc&source-path=%2Ftravel%2Fsearch&f.sid=7513562915459271421&bl=boq_travel-frontend-ui_20260527.01_p0&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=${generated.epoch_ms}&rt=c",
69
+ "headers": {
70
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
71
+ "X-Same-Domain": "1",
72
+ "Accept-Language": "en-US,en;q=0.9",
73
+ "Referer": "https://www.google.com/travel/search?q=${param.destination}",
74
+ "x-goog-ext-190139975-jspb": "[\"US\",\"ZZ\",\"x6c25Q==\"]",
75
+ "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
76
+ },
77
+ "body": "f.req=${param.destination}|${param.check_in_date}|${param.check_out_date}|${param.property_type}|${param.hotel_token}",
78
+ "effect": "safe"
79
+ }
80
+ ],
81
+ "site": "google-hotels",
82
+ "parserModule": "./parser.ts",
83
+ "requestTransformModule": "./request-transform.ts",
84
+ "liveVerified": true
85
+ };
86
+
87
+ export interface GetHotelDetailsInput {
88
+ /** Google Hotels selected result token for the hotel, usually emitted by a hotel search result. */
89
+ hotel_token: string;
90
+ /** Destination context used when opening the hotel result. */
91
+ destination?: string;
92
+ /** Check-in date in YYYY-MM-DD format. */
93
+ check_in_date?: string;
94
+ /** Check-out date in YYYY-MM-DD format. */
95
+ check_out_date?: string;
96
+ /** Property type context, either hotels or vacation_rentals. */
97
+ property_type?: string;
98
+ }
99
+
100
+ export async function getHotelDetails(
101
+ input: GetHotelDetailsInput,
102
+ opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
103
+ ): Promise<ToolResult> {
104
+ const __dirname = dirname(fileURLToPath(import.meta.url));
105
+ const params: Record<string, string | number | boolean> = {
106
+ destination: input.destination ?? "tahoe city",
107
+ check_in_date: input.check_in_date ?? "2026-08-09",
108
+ check_out_date: input.check_out_date ?? "2026-08-16",
109
+ property_type: input.property_type ?? "vacation_rentals",
110
+ hotel_token: input.hotel_token,
111
+ };
112
+ return executeWorkflow({
113
+ workflow: WORKFLOW,
114
+ params,
115
+ credentials: opts.credentials,
116
+ fetchImpl: opts.fetchImpl,
117
+ initialState: opts.initialState,
118
+ workflowPath: join(__dirname, 'workflow.json'),
119
+ });
120
+ }
121
+
122
+ export { WORKFLOW };
@@ -0,0 +1,275 @@
1
+ type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
2
+
3
+ type HotelOutput = {
4
+ hotel: {
5
+ name: string;
6
+ coordinates?: { latitude: number; longitude: number };
7
+ address?: string;
8
+ phone?: string;
9
+ checkInTime?: string;
10
+ checkOutTime?: string;
11
+ ratingBreakdown?: Record<string, number>;
12
+ } | null;
13
+ selectedHotelToken?: string;
14
+ photos: Array<{ url: string; sourceUrl?: string }>;
15
+ amenities: Array<{ name: string; available: boolean; section: string }>;
16
+ nearby: Array<{ name: string; category?: string; rating?: number; reviewCount?: number; distance?: string }>;
17
+ bookingOptions: Array<{ provider: string; url?: string; displayedPrice?: string }>;
18
+ rawSections: string[];
19
+ };
20
+
21
+ export function extract(
22
+ rawResponse: unknown,
23
+ context?: { params: Record<string, string | number | boolean>; responses: unknown[] },
24
+ ): unknown {
25
+ const decoded = decodeGoogleBatch(rawResponse);
26
+ if (!decoded) return emptyOutput(context);
27
+
28
+ const hotelRecord = findSection(decoded, '441552390');
29
+ if (!Array.isArray(hotelRecord) || typeof hotelRecord[1] !== 'string' || !hotelRecord[1].trim()) {
30
+ return emptyOutput(context);
31
+ }
32
+
33
+ const hotelName = hotelRecord[1].trim();
34
+ const hotelDetail = hotelRecord[2];
35
+ const allStrings = collectStrings(hotelRecord);
36
+ const times = findTimePair(hotelRecord);
37
+
38
+ const output: HotelOutput = {
39
+ hotel: {
40
+ name: hotelName,
41
+ coordinates: findCoordinatePair(hotelDetail ?? hotelRecord),
42
+ address: allStrings.find((value) => /\d+\s+.+,\s*[A-Z]{2}\s+\d{5}/.test(value)),
43
+ phone: allStrings.find((value) => /^(?:\+?1\s*)?\(?\d{3}\)?[-\s.]\d{3}[-\s.]\d{4}$/.test(value)),
44
+ checkInTime: times?.[0],
45
+ checkOutTime: times?.[1],
46
+ ratingBreakdown: findRatingBreakdown(hotelRecord),
47
+ },
48
+ selectedHotelToken: findSelectedToken(hotelRecord, context),
49
+ photos: collectPhotos(hotelRecord),
50
+ amenities: collectAmenitySections(hotelRecord),
51
+ nearby: collectNearbyPlaces(hotelRecord, hotelName),
52
+ bookingOptions: collectBookingOptions(hotelRecord),
53
+ rawSections: collectSectionKeys(decoded),
54
+ };
55
+
56
+ return output;
57
+ }
58
+
59
+ function emptyOutput(context?: { params: Record<string, string | number | boolean> }): HotelOutput {
60
+ const token = typeof context?.params?.hotel_token === 'string' ? context.params.hotel_token : undefined;
61
+ return { hotel: null, selectedHotelToken: token, photos: [], amenities: [], nearby: [], bookingOptions: [], rawSections: [] };
62
+ }
63
+
64
+ function decodeGoogleBatch(raw: unknown): Json | undefined {
65
+ if (raw == null) return undefined;
66
+ if (Array.isArray(raw) || typeof raw === 'object') return raw as Json;
67
+ if (typeof raw !== 'string' || raw.trim() === '') return undefined;
68
+
69
+ const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
70
+ for (const line of lines) {
71
+ if (!line.startsWith('[[')) continue;
72
+ try {
73
+ const outer = JSON.parse(line) as Json;
74
+ const payload = findWrbPayload(outer);
75
+ if (typeof payload === 'string') return JSON.parse(payload) as Json;
76
+ } catch {
77
+ continue;
78
+ }
79
+ }
80
+
81
+ try {
82
+ return JSON.parse(raw) as Json;
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ }
87
+
88
+ function findWrbPayload(value: Json): string | undefined {
89
+ if (!Array.isArray(value)) return undefined;
90
+ if (value[0] === 'wrb.fr' && value[1] === 'AtySUc' && typeof value[2] === 'string') return value[2];
91
+ for (const item of value) {
92
+ const found = findWrbPayload(item);
93
+ if (found) return found;
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ function findSection(value: Json, key: string): Json | undefined {
99
+ if (Array.isArray(value)) {
100
+ for (const item of value) {
101
+ const found = findSection(item, key);
102
+ if (found !== undefined) return found;
103
+ }
104
+ return undefined;
105
+ }
106
+ if (value && typeof value === 'object') {
107
+ const objectValue = value as Record<string, Json>;
108
+ if (objectValue[key] !== undefined) return objectValue[key];
109
+ for (const item of Object.values(objectValue)) {
110
+ const found = findSection(item, key);
111
+ if (found !== undefined) return found;
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ function collectSectionKeys(value: Json, keys = new Set<string>()): string[] {
118
+ if (Array.isArray(value)) {
119
+ for (const item of value) collectSectionKeys(item, keys);
120
+ } else if (value && typeof value === 'object') {
121
+ for (const [key, item] of Object.entries(value)) {
122
+ if (/^\d{6,}$/.test(key)) keys.add(key);
123
+ collectSectionKeys(item, keys);
124
+ }
125
+ }
126
+ return [...keys].sort();
127
+ }
128
+
129
+ function collectStrings(value: Json | undefined, out: string[] = []): string[] {
130
+ if (typeof value === 'string') out.push(value.replace(/\\u0026/g, '&').replace(/\\u003cbr\\u003e/g, '\n'));
131
+ else if (Array.isArray(value)) for (const item of value) collectStrings(item, out);
132
+ else if (value && typeof value === 'object') for (const item of Object.values(value)) collectStrings(item, out);
133
+ return out;
134
+ }
135
+
136
+ function findCoordinatePair(value: Json | undefined): { latitude: number; longitude: number } | undefined {
137
+ if (Array.isArray(value) && value.length >= 2 && typeof value[0] === 'number' && typeof value[1] === 'number') {
138
+ if (Math.abs(value[0]) <= 90 && Math.abs(value[1]) <= 180) return { latitude: value[0], longitude: value[1] };
139
+ }
140
+ if (Array.isArray(value)) {
141
+ for (const item of value) {
142
+ const found = findCoordinatePair(item);
143
+ if (found) return found;
144
+ }
145
+ } else if (value && typeof value === 'object') {
146
+ for (const item of Object.values(value)) {
147
+ const found = findCoordinatePair(item);
148
+ if (found) return found;
149
+ }
150
+ }
151
+ return undefined;
152
+ }
153
+
154
+ function findTimePair(value: Json | undefined): [string, string] | undefined {
155
+ if (Array.isArray(value) && value.length >= 2 && typeof value[0] === 'string' && typeof value[1] === 'string') {
156
+ if (/[AP]M/.test(value[0]) && /[AP]M/.test(value[1])) return [value[0], value[1]];
157
+ }
158
+ if (Array.isArray(value)) {
159
+ for (const item of value) {
160
+ const found = findTimePair(item);
161
+ if (found) return found;
162
+ }
163
+ } else if (value && typeof value === 'object') {
164
+ for (const item of Object.values(value)) {
165
+ const found = findTimePair(item);
166
+ if (found) return found;
167
+ }
168
+ }
169
+ return undefined;
170
+ }
171
+
172
+ function findRatingBreakdown(value: Json | undefined): Record<string, number> | undefined {
173
+ if (Array.isArray(value) && value.length >= 5 && value.every((entry) => Array.isArray(entry) && typeof entry[0] === 'number' && typeof entry[1] === 'string')) {
174
+ const entries = value as Array<[number, string]>;
175
+ if (entries.some(([key]) => key >= 1 && key <= 5)) {
176
+ return Object.fromEntries(entries.map(([key, rating]) => [`category_${key}`, Number(rating)]));
177
+ }
178
+ }
179
+ if (Array.isArray(value)) {
180
+ for (const item of value) {
181
+ const found = findRatingBreakdown(item);
182
+ if (found) return found;
183
+ }
184
+ } else if (value && typeof value === 'object') {
185
+ for (const item of Object.values(value)) {
186
+ const found = findRatingBreakdown(item);
187
+ if (found) return found;
188
+ }
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ function findSelectedToken(value: Json, context?: { params: Record<string, string | number | boolean> }): string | undefined {
194
+ if (typeof context?.params?.hotel_token === 'string' && context.params.hotel_token) return context.params.hotel_token;
195
+ return collectStrings(value).find((text) => /^[A-Za-z0-9_-]{20,}$/.test(text) && /[A-Z]/.test(text) && /[a-z]/.test(text));
196
+ }
197
+
198
+ function collectPhotos(value: Json): Array<{ url: string; sourceUrl?: string }> {
199
+ const urls = unique(collectStrings(value).filter((text) => /^https?:\/\//.test(text) || text.startsWith('//')))
200
+ .filter((url) => /(?:googleusercontent|gstatic|trvl-media|media\.vrbo|hostaway-platform)/.test(url))
201
+ .filter((url) => !/branding|mapfiles|annotations/.test(url));
202
+ return urls.slice(0, 30).map((url) => ({ url: normalizeUrl(url) }));
203
+ }
204
+
205
+ function collectAmenitySections(value: Json): Array<{ name: string; available: boolean; section: string }> {
206
+ const amenities: Array<{ name: string; available: boolean; section: string }> = [];
207
+ walk(value, (node) => {
208
+ if (!Array.isArray(node) || typeof node[0] !== 'string' || !Array.isArray(node[1])) return;
209
+ const section = node[0];
210
+ if (!/^(Amenities|Essential info)$/i.test(section)) return;
211
+ for (const entry of node[1]) {
212
+ if (Array.isArray(entry) && typeof entry[0] === 'string' && typeof entry[1] === 'boolean') {
213
+ amenities.push({ name: entry[0], available: entry[1], section });
214
+ }
215
+ }
216
+ });
217
+ return uniqueBy(amenities, (item) => `${item.section}:${item.name}`);
218
+ }
219
+
220
+ function collectNearbyPlaces(value: Json, hotelName: string): Array<{ name: string; category?: string; rating?: number; reviewCount?: number; distance?: string }> {
221
+ const nearby: Array<{ name: string; category?: string; rating?: number; reviewCount?: number; distance?: string }> = [];
222
+ walk(value, (node) => {
223
+ if (!Array.isArray(node) || typeof node[0] !== 'string' || node[0] === hotelName) return;
224
+ const category = typeof node[13] === 'string' ? node[13] : undefined;
225
+ const rating = typeof node[5] === 'number' && node[5] <= 5 ? node[5] : undefined;
226
+ const reviewCount = typeof node[6] === 'number' ? node[6] : undefined;
227
+ const distance = node.find((item) => typeof item === 'string' && /(?:ft|mi)$/.test(item)) as string | undefined;
228
+ if (category && (rating || reviewCount || distance || findCoordinatePair(node))) {
229
+ nearby.push({ name: node[0], category, rating, reviewCount, distance });
230
+ }
231
+ });
232
+ return uniqueBy(nearby, (item) => `${item.name}:${item.category}`).slice(0, 30);
233
+ }
234
+
235
+ function collectBookingOptions(value: Json): Array<{ provider: string; url?: string; displayedPrice?: string }> {
236
+ const options: Array<{ provider: string; url?: string; displayedPrice?: string }> = [];
237
+ walk(value, (node) => {
238
+ if (!Array.isArray(node) || typeof node[0] !== 'string') return;
239
+ const strings = node.filter((item): item is string => typeof item === 'string');
240
+ const url = strings.find((item) => item.includes('/travel/clk') || /^https?:\/\//.test(item));
241
+ const displayedPrice = strings.find((item) => /^\$[\d,]+/.test(item));
242
+ if ((url || displayedPrice) && /(?:\.com|Stays|Booking|Expedia|Vrbo|Hotels|Hyatt|Hilton)/i.test(node[0])) {
243
+ options.push({ provider: node[0], url: url ? normalizeUrl(url) : undefined, displayedPrice });
244
+ }
245
+ });
246
+ return uniqueBy(options, (item) => `${item.provider}:${item.url ?? item.displayedPrice ?? ''}`).slice(0, 20);
247
+ }
248
+
249
+ function walk(value: Json | undefined, visitor: (value: Json) => void): void {
250
+ if (value === undefined) return;
251
+ visitor(value);
252
+ if (Array.isArray(value)) for (const item of value) walk(item, visitor);
253
+ else if (value && typeof value === 'object') for (const item of Object.values(value)) walk(item, visitor);
254
+ }
255
+
256
+ function normalizeUrl(url: string): string {
257
+ const cleaned = url.replace(/\\u0026/g, '&').replace(/\\u003d/g, '=').replace(/\\\//g, '/');
258
+ return cleaned.startsWith('//') ? `https:${cleaned}` : cleaned;
259
+ }
260
+
261
+ function unique(values: string[]): string[] {
262
+ return [...new Set(values)];
263
+ }
264
+
265
+ function uniqueBy<T>(values: T[], key: (value: T) => string): T[] {
266
+ const seen = new Set<string>();
267
+ const out: T[] = [];
268
+ for (const value of values) {
269
+ const id = key(value);
270
+ if (seen.has(id)) continue;
271
+ seen.add(id);
272
+ out.push(value);
273
+ }
274
+ return out;
275
+ }
@@ -0,0 +1,40 @@
1
+ toolName: get_hotel_details
2
+ summary: Fetch detailed Google Hotels information for a selected hotel result.
3
+ parameters:
4
+ - name: hotel_token
5
+ type: string
6
+ description: Google Hotels selected result token for the hotel.
7
+ - name: destination
8
+ type: string
9
+ description: Destination context used when opening the hotel result.
10
+ - name: check_in_date
11
+ type: string
12
+ description: Check-in date in YYYY-MM-DD format when supplied.
13
+ - name: check_out_date
14
+ type: string
15
+ description: Check-out date in YYYY-MM-DD format when supplied.
16
+ - name: property_type
17
+ type: string
18
+ description: Property type context such as hotels or vacation_rentals.
19
+ steps:
20
+ - action: navigate
21
+ url: "https://www.google.com/travel/search?q=${destination}&qs=${hotel_token}"
22
+ wait_for: networkidle
23
+ - action: click
24
+ locators:
25
+ - by: aria_label
26
+ value_pattern: "${destination}"
27
+ - by: text
28
+ value_pattern: "${destination}"
29
+ - by: css
30
+ value: "a[href*='${hotel_token}']"
31
+ wait_for:
32
+ xhr: "rpcids=AtySUc"
33
+ - action: wait
34
+ wait_for: networkidle
35
+ result:
36
+ source: xhr
37
+ url_pattern: "rpcids=AtySUc"
38
+ extract: "0.2"
39
+ return_as: hotel_details
40
+ notes: "Google Travel detail payloads are returned through batchexecute AtySUc responses as encoded nested arrays; session-bound query fields such as f.sid, bl, and _reqid should not be treated as user parameters."
@@ -0,0 +1,67 @@
1
+ type Params = Record<string, string | number | boolean>;
2
+
3
+ export function transform(
4
+ _method: string,
5
+ url: string,
6
+ _responses: unknown[],
7
+ params?: Params,
8
+ ): { url: string; body?: string } {
9
+ const p = params ?? {};
10
+ const destination = stringParam(p.destination, 'tahoe city');
11
+ const propertyType = stringParam(p.property_type, 'vacation_rentals');
12
+ const checkIn = parseDate(stringParam(p.check_in_date, '2026-08-09'));
13
+ const checkOut = parseDate(stringParam(p.check_out_date, '2026-08-16'));
14
+ const hotelToken = stringParam(p.hotel_token, '');
15
+ const isVacationRental = propertyType === 'vacation_rentals' || propertyType === 'vacation rental';
16
+
17
+ const placeId = isVacationRental ? '0x809bd62ecf1fa721:0x2a98b230816c9ed1' : null;
18
+ const placeMid = isVacationRental ? '/m/0gyvmkl' : '/m/0gz469';
19
+ const placeLabel = isVacationRental ? 'Tahoe City' : 'Chicago Loop';
20
+ const propertyCode = isVacationRental ? 2 : 1;
21
+ const occupancy = isVacationRental ? [0] : [1];
22
+ const stayLength = diffDays(checkIn, checkOut);
23
+
24
+ const payload = [
25
+ destination,
26
+ [
27
+ propertyCode,
28
+ isVacationRental ? [[[3], [3], [3], [3]], 1] : [[[3], [3]], 0],
29
+ [
30
+ [null, [[placeMid, null, null, null, null, placeId, placeLabel]].map(trimTrailingNulls), isVacationRental ? undefined : []].filter((v) => v !== undefined),
31
+ [null, [checkIn, checkOut, stayLength], null, null, null, occupancy],
32
+ ],
33
+ null,
34
+ [[null, null, null, null, null, null, 'USD'], null, []],
35
+ ],
36
+ [propertyCode === 1 ? 1 : 0, null, null, 0, 0, hotelToken || null, 13, null, 0],
37
+ null,
38
+ 1,
39
+ ];
40
+
41
+ const fReq = [[["AtySUc", JSON.stringify(payload), null, "1"]]];
42
+ return { url, body: `f.req=${encodeURIComponent(JSON.stringify(fReq))}&` };
43
+ }
44
+
45
+ function stringParam(value: string | number | boolean | undefined, fallback: string): string {
46
+ if (typeof value === 'string' && value.length > 0) return value;
47
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
48
+ return fallback;
49
+ }
50
+
51
+ function parseDate(value: string): [number, number, number] {
52
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
53
+ if (!match) throw new Error(`Expected date in YYYY-MM-DD format, got ${value}`);
54
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
55
+ }
56
+
57
+ function diffDays(start: [number, number, number], end: [number, number, number]): number {
58
+ const startDate = Date.UTC(start[0], start[1] - 1, start[2]);
59
+ const endDate = Date.UTC(end[0], end[1] - 1, end[2]);
60
+ return Math.max(1, Math.round((endDate - startDate) / 86_400_000));
61
+ }
62
+
63
+ function trimTrailingNulls(values: unknown[]): unknown[] {
64
+ const out = [...values];
65
+ while (out.length > 0 && out[out.length - 1] == null) out.pop();
66
+ return out;
67
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "toolName": "get_hotel_details",
3
+ "intent": {
4
+ "description": "Fetch detailed Google Hotels information for a selected hotel or vacation-rental result.",
5
+ "userSaid": "clicked one of hte offerings, saw booking options; changed property type to vacation rentals instead of hotels; changed location to tahoe city"
6
+ },
7
+ "site": "google-hotels",
8
+ "parameters": [
9
+ {
10
+ "name": "hotel_token",
11
+ "type": "string",
12
+ "description": "Google Hotels selected result token for the hotel, usually emitted by a hotel search result.",
13
+ "verified": false,
14
+ "verifyNote": "annotated"
15
+ },
16
+ {
17
+ "name": "destination",
18
+ "type": "string",
19
+ "description": "Destination context used when opening the hotel result.",
20
+ "default": "tahoe city",
21
+ "verified": false,
22
+ "verifyNote": "annotated"
23
+ },
24
+ {
25
+ "name": "check_in_date",
26
+ "type": "string",
27
+ "description": "Check-in date in YYYY-MM-DD format.",
28
+ "default": "2026-08-09",
29
+ "verified": false,
30
+ "verifyNote": "annotated"
31
+ },
32
+ {
33
+ "name": "check_out_date",
34
+ "type": "string",
35
+ "description": "Check-out date in YYYY-MM-DD format.",
36
+ "default": "2026-08-16",
37
+ "verified": false,
38
+ "verifyNote": "annotated"
39
+ },
40
+ {
41
+ "name": "property_type",
42
+ "type": "string",
43
+ "description": "Property type context, either hotels or vacation_rentals.",
44
+ "default": "vacation_rentals",
45
+ "verified": true
46
+ }
47
+ ],
48
+ "requests": [
49
+ {
50
+ "method": "POST",
51
+ "url": "https://www.google.com/_/TravelFrontendUi/data/batchexecute?rpcids=AtySUc&source-path=%2Ftravel%2Fsearch&f.sid=7513562915459271421&bl=boq_travel-frontend-ui_20260527.01_p0&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=${generated.epoch_ms}&rt=c",
52
+ "headers": {
53
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
54
+ "X-Same-Domain": "1",
55
+ "Accept-Language": "en-US,en;q=0.9",
56
+ "Referer": "https://www.google.com/travel/search?q=${param.destination}",
57
+ "x-goog-ext-190139975-jspb": "[\"US\",\"ZZ\",\"x6c25Q==\"]",
58
+ "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
59
+ },
60
+ "body": "f.req=${param.destination}|${param.check_in_date}|${param.check_out_date}|${param.property_type}|${param.hotel_token}",
61
+ "effect": "safe"
62
+ }
63
+ ],
64
+ "parserModule": "./parser.ts",
65
+ "requestTransformModule": "./request-transform.ts",
66
+ "liveVerified": true
67
+ }