imprint-mcp 0.4.7 → 0.4.9
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 +4 -4
- package/examples/google-flights/README.md +0 -2
- package/examples/google-flights/_shared/flights_request.ts +4 -10
- package/examples/google-flights/get_flight_booking_details/index.ts +2 -5
- package/examples/google-flights/get_flight_booking_details/parser.ts +0 -8
- package/examples/google-flights/get_flight_booking_details/workflow.json +2 -5
- package/examples/google-flights/get_flight_calendar_prices/index.ts +2 -5
- package/examples/google-flights/get_flight_calendar_prices/parser.ts +11 -15
- package/examples/google-flights/get_flight_calendar_prices/workflow.json +2 -5
- package/examples/google-flights/lookup_airport/index.ts +0 -3
- package/examples/google-flights/lookup_airport/parser.ts +1 -8
- package/examples/google-flights/lookup_airport/workflow.json +0 -3
- package/examples/google-flights/search_flights/index.ts +7 -62
- package/examples/google-flights/search_flights/request-transform.ts +4 -47
- package/examples/google-flights/search_flights/workflow.json +7 -62
- package/package.json +1 -1
- package/prompts/build-planning.md +1 -1
- package/prompts/compile-agent.md +3 -5
- package/prompts/prereq-builder.md +1 -2
- package/src/imprint/backend-ladder.ts +47 -436
- package/src/imprint/cdp-browser-fetch.ts +6 -176
- package/src/imprint/cdp-jar-cache.ts +10 -105
- package/src/imprint/compile-tools.ts +2 -2
- package/src/imprint/mcp-server.ts +65 -152
- package/src/imprint/probe-backends.ts +10 -41
- package/src/imprint/runtime.ts +12 -24
- package/src/imprint/stealth-fetch.ts +0 -71
- package/src/imprint/stealth-token-cache.ts +1 -38
- package/src/imprint/types.ts +0 -45
package/README.md
CHANGED
|
@@ -169,8 +169,8 @@ When an API call gets blocked, Imprint doesn't jump to DOM replay. It escalates
|
|
|
169
169
|
│ + API
|
|
170
170
|
▼
|
|
171
171
|
cdp-replay ~2-35s API calls run inside a live, trusted Chrome —
|
|
172
|
-
│
|
|
173
|
-
│
|
|
172
|
+
│ a protected POST refreshes its anti-bot token
|
|
173
|
+
│ between calls (multi-step state-changing flows)
|
|
174
174
|
▼
|
|
175
175
|
stealth-fetch ~1-12s Defeats Akamai, Cloudflare, DataDome
|
|
176
176
|
│
|
|
@@ -178,9 +178,9 @@ When an API call gets blocked, Imprint doesn't jump to DOM replay. It escalates
|
|
|
178
178
|
playbook ~9s Full DOM replay — universal fallback
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
The full order is `fetch → fetch-bootstrap → cdp-replay → stealth-fetch → playbook`; `auto` mode walks it and stops at the first backend that works.
|
|
181
|
+
The full order is `fetch → fetch-bootstrap → cdp-replay → stealth-fetch → playbook`; `auto` mode walks it and stops at the first backend that works.
|
|
182
182
|
|
|
183
|
-
For bot-protected sites, `imprint probe-backends <site> --tool <toolName>` writes a `backends.json` preference cache so cron and MCP start from the known-good backend instead of rediscovering blocked rungs. Use `imprint probe-backends <site> --all` to refresh every tool in a multi-tool site; `imprint mcp status` reports stale or invalid backend caches before they quietly fall back to the default ladder.
|
|
183
|
+
For bot-protected sites, `imprint probe-backends <site> --tool <toolName>` writes a `backends.json` preference cache so cron and MCP start from the known-good backend instead of rediscovering blocked rungs. Use `imprint probe-backends <site> --all` to refresh every tool in a multi-tool site; `imprint mcp status` reports stale or invalid backend caches before they quietly fall back to the default ladder. CDP replay records both cold and warm timings when it succeeds: a timeout-safe cold start may rank by its fast warm runtime, but a cold start above the preferred threshold stays behind cold-safe backends in durable cache order.
|
|
184
184
|
|
|
185
185
|
Every recording compiles to *both* `workflow.json` and `playbook.yaml`, so the ladder always has a DOM fallback.
|
|
186
186
|
|
|
@@ -17,8 +17,6 @@ A 4-tool MCP server for Google Flights, compiled from a recording of a normal fl
|
|
|
17
17
|
|
|
18
18
|
- **Protocol**: Google's `/_/FlightsFrontendUi` **`batchexecute`** endpoint returns a nested-array (protobuf-ish) payload. The compiler reverse-engineered the encoding into `_shared/batchexecute.ts` (shared decoder) + per-tool `parser.ts`, and the `f.req` request shape into `_shared/flights_request.ts` + per-tool `request-transform.ts`.
|
|
19
19
|
- **Anti-bot**: the per-page `f.sid` / `bl` tokens are bootstrapped at runtime (`${state.f_sid}` placeholders), and calls run on the **cdp-replay** rung (requests issued inside a live, trusted Chrome) with a **stealth-fetch** fallback.
|
|
20
|
-
- **MCP pacing**: `search_flights` declares `execution.minCallSpacingMs: 2000` because Google Flights can return fast empty result sets when warm CDP searches are fired back-to-back with no breathing room.
|
|
21
|
-
- **Bounded fallback**: these tools declare `execution.skipPlaybookFallback` so MCP calls fail fast after the API/browser-backed rungs are exhausted instead of spending the rest of the agent timeout in an unstructured DOM replay.
|
|
22
20
|
- **Artifacts per tool**: `workflow.json` (API replay), `playbook.yaml` (DOM fallback), `index.ts` (MCP tool), `parser.ts` + `request-transform.ts` (codecs).
|
|
23
21
|
|
|
24
22
|
## Install
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
// with the date range living in the outer wrapper; Shopping/Booking use the full
|
|
9
9
|
// 15-slot leg with DATE at [6]. Verified by decoding seq 97 vs seq 111.
|
|
10
10
|
|
|
11
|
-
// Fresh searches emit wrapper `...,0,0,0,1]` and
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// In-page-refined searches use `...,0,1,0,1]` — a UI freshness flag, not a user
|
|
15
|
-
// param; we always emit the fresh form for shopping.
|
|
11
|
+
// Fresh searches emit wrapper `...,0,0,0,1]` and leg[14]=3 (proven seq 111/140).
|
|
12
|
+
// In-page-refined searches use `...,0,1,0,1]` with return-leg[14]=1 (seq 194/425) —
|
|
13
|
+
// a UI freshness flag, not a user param; we always emit the fresh form for shopping.
|
|
16
14
|
// Booking outbound legs use [14]=3, return legs [14]=1 (seq 764/811).
|
|
17
15
|
|
|
18
16
|
function buildLeg(leg: any): any[] {
|
|
@@ -76,11 +74,7 @@ export function transform(
|
|
|
76
74
|
let payload: any;
|
|
77
75
|
|
|
78
76
|
if (rpc === 'GetShoppingResults') {
|
|
79
|
-
|
|
80
|
-
typeof p.searchContextToken === 'string' && p.searchContextToken
|
|
81
|
-
? [null, null, null, p.searchContextToken]
|
|
82
|
-
: [];
|
|
83
|
-
payload = [searchContext, sp, 0, 0, 0, 1];
|
|
77
|
+
payload = [[], sp, 0, 0, 0, 1];
|
|
84
78
|
} else if (rpc === 'GetCalendarPicker') {
|
|
85
79
|
const legs = sp[13];
|
|
86
80
|
if (Array.isArray(legs)) sp[13] = legs.map((l: any) => (Array.isArray(l) ? l.slice(0, 4) : l));
|
|
@@ -94,7 +94,7 @@ const WORKFLOW: Workflow = {
|
|
|
94
94
|
"captures": [
|
|
95
95
|
{
|
|
96
96
|
"name": "f_sid",
|
|
97
|
-
"required":
|
|
97
|
+
"required": false,
|
|
98
98
|
"capability": "browser_bootstrap",
|
|
99
99
|
"source": "html_regex",
|
|
100
100
|
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
@@ -102,7 +102,7 @@ const WORKFLOW: Workflow = {
|
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"name": "bl",
|
|
105
|
-
"required":
|
|
105
|
+
"required": false,
|
|
106
106
|
"capability": "browser_bootstrap",
|
|
107
107
|
"source": "html_regex",
|
|
108
108
|
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
@@ -112,9 +112,6 @@ const WORKFLOW: Workflow = {
|
|
|
112
112
|
},
|
|
113
113
|
"parserModule": "./parser.ts",
|
|
114
114
|
"requestTransformModule": "./request-transform.ts",
|
|
115
|
-
"execution": {
|
|
116
|
-
"skipPlaybookFallback": true
|
|
117
|
-
},
|
|
118
115
|
"liveVerified": true
|
|
119
116
|
};
|
|
120
117
|
|
|
@@ -142,9 +142,6 @@ export function extract(
|
|
|
142
142
|
let frames: Array<{ rpcid: string | null; payload: any }> = [];
|
|
143
143
|
if (typeof rawResponse === 'string') {
|
|
144
144
|
frames = decodeBatchExecute(rawResponse);
|
|
145
|
-
if (frames.length === 0) {
|
|
146
|
-
throw new Error('Google Flights GetBookingResults response did not contain a batchexecute payload');
|
|
147
|
-
}
|
|
148
145
|
} else if (rawResponse != null) {
|
|
149
146
|
frames = [{ rpcid: null, payload: rawResponse }];
|
|
150
147
|
}
|
|
@@ -173,11 +170,6 @@ export function extract(
|
|
|
173
170
|
|
|
174
171
|
const segments = [...segMap.values()];
|
|
175
172
|
const fareOptions = [...fareMap.values()];
|
|
176
|
-
if (segments.length === 0 && fareOptions.length === 0) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
'Google Flights GetBookingResults payload did not contain recognizable booking details',
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
173
|
const prices = fareOptions.map((f) => f.priceUSD);
|
|
182
174
|
|
|
183
175
|
return {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"name": "f_sid",
|
|
66
66
|
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
67
67
|
"group": 1,
|
|
68
|
-
"required":
|
|
68
|
+
"required": false,
|
|
69
69
|
"capability": "browser_bootstrap"
|
|
70
70
|
},
|
|
71
71
|
{
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"name": "bl",
|
|
74
74
|
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
75
75
|
"group": 1,
|
|
76
|
-
"required":
|
|
76
|
+
"required": false,
|
|
77
77
|
"capability": "browser_bootstrap"
|
|
78
78
|
}
|
|
79
79
|
]
|
|
@@ -94,8 +94,5 @@
|
|
|
94
94
|
],
|
|
95
95
|
"requestTransformModule": "./request-transform.ts",
|
|
96
96
|
"parserModule": "./parser.ts",
|
|
97
|
-
"execution": {
|
|
98
|
-
"skipPlaybookFallback": true
|
|
99
|
-
},
|
|
100
97
|
"liveVerified": true
|
|
101
98
|
}
|
|
@@ -74,7 +74,7 @@ const WORKFLOW: Workflow = {
|
|
|
74
74
|
"captures": [
|
|
75
75
|
{
|
|
76
76
|
"name": "f_sid",
|
|
77
|
-
"required":
|
|
77
|
+
"required": false,
|
|
78
78
|
"capability": "browser_bootstrap",
|
|
79
79
|
"source": "html_regex",
|
|
80
80
|
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
@@ -82,7 +82,7 @@ const WORKFLOW: Workflow = {
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"name": "bl",
|
|
85
|
-
"required":
|
|
85
|
+
"required": false,
|
|
86
86
|
"capability": "browser_bootstrap",
|
|
87
87
|
"source": "html_regex",
|
|
88
88
|
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
@@ -92,9 +92,6 @@ const WORKFLOW: Workflow = {
|
|
|
92
92
|
},
|
|
93
93
|
"parserModule": "./parser.ts",
|
|
94
94
|
"requestTransformModule": "./request-transform.ts",
|
|
95
|
-
"execution": {
|
|
96
|
-
"skipPlaybookFallback": true
|
|
97
|
-
},
|
|
98
95
|
"liveVerified": true
|
|
99
96
|
};
|
|
100
97
|
|
|
@@ -17,9 +17,8 @@ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
17
17
|
// [0] is an ISO date string. We scan every nested array so we are robust to the
|
|
18
18
|
// list living at payload[1] (the recorded shape) or being flattened.
|
|
19
19
|
function collectEntries(payload: unknown): CalendarEntry[] {
|
|
20
|
-
const entries
|
|
21
|
-
|
|
22
|
-
if (!Array.isArray(payload)) return entries;
|
|
20
|
+
const entries = new Map<string, CalendarEntry>();
|
|
21
|
+
if (!Array.isArray(payload)) return [];
|
|
23
22
|
|
|
24
23
|
const consider = (item: unknown) => {
|
|
25
24
|
if (!Array.isArray(item)) return;
|
|
@@ -33,9 +32,10 @@ function collectEntries(payload: unknown): CalendarEntry[] {
|
|
|
33
32
|
price = (priceContainer[0] as unknown[])[1];
|
|
34
33
|
}
|
|
35
34
|
if (typeof price !== 'number') return; // no fare found for that date -> omit
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const existing = entries.get(dep);
|
|
36
|
+
if (!existing || price < existing.lowestPriceUSD) {
|
|
37
|
+
entries.set(dep, { departureDate: dep, returnDate: ret, lowestPriceUSD: price });
|
|
38
|
+
}
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
for (const top of payload) {
|
|
@@ -45,7 +45,7 @@ function collectEntries(payload: unknown): CalendarEntry[] {
|
|
|
45
45
|
for (const inner of top) consider(inner);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
return entries;
|
|
48
|
+
return [...entries.values()];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function extract(
|
|
@@ -54,9 +54,6 @@ export function extract(
|
|
|
54
54
|
): unknown {
|
|
55
55
|
const raw = typeof rawResponse === 'string' ? rawResponse : JSON.stringify(rawResponse ?? '');
|
|
56
56
|
const frames = decodeBatchExecute(raw);
|
|
57
|
-
if (frames.length === 0) {
|
|
58
|
-
throw new Error('Google Flights GetCalendarPicker response did not contain a batchexecute payload');
|
|
59
|
-
}
|
|
60
57
|
|
|
61
58
|
let payload: unknown = null;
|
|
62
59
|
for (const f of frames) {
|
|
@@ -66,14 +63,13 @@ export function extract(
|
|
|
66
63
|
break;
|
|
67
64
|
}
|
|
68
65
|
}
|
|
66
|
+
// If no frame produced entries, still attempt the first frame's payload so an
|
|
67
|
+
// empty (zero-result) response yields an empty calendar rather than throwing.
|
|
68
|
+
if (payload == null && frames.length > 0) payload = frames[0]?.payload ?? null;
|
|
69
|
+
|
|
69
70
|
const entries = collectEntries(payload).sort((a, b) =>
|
|
70
71
|
a.departureDate < b.departureDate ? -1 : a.departureDate > b.departureDate ? 1 : 0,
|
|
71
72
|
);
|
|
72
|
-
if (entries.length === 0) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
'Google Flights GetCalendarPicker payload did not contain recognizable calendar prices',
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
73
|
|
|
78
74
|
const prices: Record<string, number> = {};
|
|
79
75
|
for (const e of entries) prices[e.departureDate] = e.lowestPriceUSD;
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"name": "f_sid",
|
|
45
45
|
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
46
46
|
"group": 1,
|
|
47
|
-
"required":
|
|
47
|
+
"required": false,
|
|
48
48
|
"capability": "browser_bootstrap"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"name": "bl",
|
|
53
53
|
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
54
54
|
"group": 1,
|
|
55
|
-
"required":
|
|
55
|
+
"required": false,
|
|
56
56
|
"capability": "browser_bootstrap"
|
|
57
57
|
}
|
|
58
58
|
]
|
|
@@ -74,8 +74,5 @@
|
|
|
74
74
|
],
|
|
75
75
|
"requestTransformModule": "./request-transform.ts",
|
|
76
76
|
"parserModule": "./parser.ts",
|
|
77
|
-
"execution": {
|
|
78
|
-
"skipPlaybookFallback": true
|
|
79
|
-
},
|
|
80
77
|
"liveVerified": true
|
|
81
78
|
}
|
|
@@ -45,15 +45,8 @@ export function extract(
|
|
|
45
45
|
): unknown {
|
|
46
46
|
const raw = typeof rawResponse === 'string' ? rawResponse : JSON.stringify(rawResponse);
|
|
47
47
|
const payload = extractRpcPayload(raw, 'tDoGIe');
|
|
48
|
-
if (payload == null) {
|
|
49
|
-
throw new Error('Google Flights tDoGIe response did not contain a batchexecute payload');
|
|
50
|
-
}
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
throw new Error('Google Flights tDoGIe payload did not contain a recognizable match list');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const matchesRaw = payload[1];
|
|
49
|
+
const matchesRaw = Array.isArray(payload) && Array.isArray(payload[1]) ? payload[1] : [];
|
|
57
50
|
const matches = matchesRaw
|
|
58
51
|
.map(parseItem)
|
|
59
52
|
.filter((m): m is AirportMatch => m !== null);
|
|
@@ -120,13 +120,11 @@ const WORKFLOW: Workflow = {
|
|
|
120
120
|
"requests": [
|
|
121
121
|
{
|
|
122
122
|
"method": "POST",
|
|
123
|
-
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid
|
|
123
|
+
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=1708023&rt=c",
|
|
124
124
|
"headers": {
|
|
125
125
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
126
126
|
"X-Same-Domain": "1",
|
|
127
|
-
"Referer": "https://www.google.com/travel/flights
|
|
128
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
129
|
-
"X-Goog-BatchExecute-Bgr": "${state.bgr}",
|
|
127
|
+
"Referer": "https://www.google.com/travel/flights",
|
|
130
128
|
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
131
129
|
},
|
|
132
130
|
"body": "f.req=${param.origin}|${param.destination}|${param.departure_date}|${param.return_date}|${param.trip_type}|${param.max_stops}|${param.airlines}|${param.max_price}|${param.outbound_times}|${param.return_times}|${param.max_duration}|${param.carry_on_bags}&",
|
|
@@ -135,7 +133,7 @@ const WORKFLOW: Workflow = {
|
|
|
135
133
|
],
|
|
136
134
|
"site": "google-flights",
|
|
137
135
|
"bootstrap": {
|
|
138
|
-
"url": "https://www.google.com/travel/flights
|
|
136
|
+
"url": "https://www.google.com/travel/flights",
|
|
139
137
|
"waitUntil": "domcontentloaded",
|
|
140
138
|
"timeoutMs": 30000,
|
|
141
139
|
"captures": [
|
|
@@ -143,75 +141,22 @@ const WORKFLOW: Workflow = {
|
|
|
143
141
|
"name": "f_sid",
|
|
144
142
|
"required": true,
|
|
145
143
|
"capability": "browser_bootstrap",
|
|
146
|
-
"source": "
|
|
147
|
-
"pattern": "
|
|
148
|
-
"method": "POST",
|
|
149
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
150
|
-
"mode": "last",
|
|
144
|
+
"source": "html_regex",
|
|
145
|
+
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
151
146
|
"group": 1
|
|
152
147
|
},
|
|
153
148
|
{
|
|
154
149
|
"name": "bl",
|
|
155
150
|
"required": true,
|
|
156
151
|
"capability": "browser_bootstrap",
|
|
157
|
-
"source": "
|
|
158
|
-
"pattern": "
|
|
159
|
-
"method": "POST",
|
|
160
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
161
|
-
"mode": "last",
|
|
162
|
-
"group": 1
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
"name": "bgr",
|
|
166
|
-
"required": true,
|
|
167
|
-
"capability": "browser_bootstrap",
|
|
168
|
-
"source": "request_header",
|
|
169
|
-
"header": "X-Goog-BatchExecute-Bgr",
|
|
170
|
-
"method": "POST",
|
|
171
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
172
|
-
"mode": "last"
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
"name": "reqid",
|
|
176
|
-
"required": true,
|
|
177
|
-
"capability": "browser_bootstrap",
|
|
178
|
-
"source": "request_url_regex",
|
|
179
|
-
"pattern": "[?&]_reqid=([^&]+)",
|
|
180
|
-
"method": "POST",
|
|
181
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
182
|
-
"mode": "last",
|
|
183
|
-
"group": 1
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
"name": "search_context_token",
|
|
187
|
-
"required": false,
|
|
188
|
-
"capability": "browser_bootstrap",
|
|
189
|
-
"source": "request_body_regex",
|
|
190
|
-
"pattern": "%5B%5Bnull%2Cnull%2Cnull%2C%5C%22(.+?)%5C%22%5D",
|
|
191
|
-
"method": "POST",
|
|
192
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
193
|
-
"mode": "last",
|
|
194
|
-
"group": 1
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
"name": "observed_search_body",
|
|
198
|
-
"required": false,
|
|
199
|
-
"capability": "browser_bootstrap",
|
|
200
|
-
"source": "request_body_regex",
|
|
201
|
-
"pattern": "^(f\\.req=.*)$",
|
|
202
|
-
"method": "POST",
|
|
203
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
204
|
-
"mode": "last",
|
|
152
|
+
"source": "html_regex",
|
|
153
|
+
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
205
154
|
"group": 1
|
|
206
155
|
}
|
|
207
156
|
]
|
|
208
157
|
},
|
|
209
158
|
"parserModule": "./parser.ts",
|
|
210
159
|
"requestTransformModule": "./request-transform.ts",
|
|
211
|
-
"execution": {
|
|
212
|
-
"minCallSpacingMs": 2000,
|
|
213
|
-
"skipPlaybookFallback": true
|
|
214
|
-
},
|
|
215
160
|
"liveVerified": true
|
|
216
161
|
};
|
|
217
162
|
|
|
@@ -13,7 +13,7 @@ const ALLIANCES = new Set(['ONEWORLD', 'SKYTEAM', 'STAR_ALLIANCE']);
|
|
|
13
13
|
function mapTripType(v: unknown): number {
|
|
14
14
|
if (v == null || v === '') return 1;
|
|
15
15
|
if (typeof v === 'number') return v;
|
|
16
|
-
const s = String(v).toLowerCase();
|
|
16
|
+
const s = String(v).trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
17
17
|
if (s === 'one_way' || s === 'oneway' || s === '2') return 2;
|
|
18
18
|
if (s === 'multi_city' || s === 'multicity' || s === '3') return 3;
|
|
19
19
|
return 1; // round_trip
|
|
@@ -62,57 +62,16 @@ function num(v: unknown): number | undefined {
|
|
|
62
62
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function buildBootstrapQuery(params: Params): string {
|
|
66
|
-
const origin = params.origin != null ? String(params.origin) : '';
|
|
67
|
-
const destination = params.destination != null ? String(params.destination) : '';
|
|
68
|
-
const departureDate = params.departure_date != null ? String(params.departure_date) : '';
|
|
69
|
-
const tripType = String(params.trip_type ?? 'round_trip').toLowerCase();
|
|
70
|
-
if (tripType === 'one_way' || tripType === 'oneway' || tripType === '2') {
|
|
71
|
-
return `One way flights from ${origin} to ${destination} on ${departureDate}`;
|
|
72
|
-
}
|
|
73
|
-
if (params.return_date) {
|
|
74
|
-
return `Round trip flights from ${origin} to ${destination} departing ${departureDate} returning ${params.return_date}`;
|
|
75
|
-
}
|
|
76
|
-
return `Flights from ${origin} to ${destination} on ${departureDate}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function prepareParams(params?: Params): Params {
|
|
80
|
-
const p: Params = params ?? {};
|
|
81
|
-
return {
|
|
82
|
-
...p,
|
|
83
|
-
bootstrap_query: buildBootstrapQuery(p),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function hasNonDefaultFilters(params: Params): boolean {
|
|
88
|
-
if (params.max_stops != null && params.max_stops !== '' && Number(params.max_stops) !== 3) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
return Boolean(
|
|
92
|
-
params.airlines ||
|
|
93
|
-
num(params.max_price) ||
|
|
94
|
-
params.outbound_times ||
|
|
95
|
-
params.return_times ||
|
|
96
|
-
num(params.max_duration) ||
|
|
97
|
-
num(params.carry_on_bags),
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
65
|
export function transform(
|
|
102
66
|
method: string,
|
|
103
67
|
url: string,
|
|
104
68
|
responses: Record<string, any>,
|
|
105
69
|
params?: Params,
|
|
106
|
-
state?: Record<string, unknown>,
|
|
107
70
|
): { url: string; body: string } {
|
|
108
71
|
const p: Params = params ?? {};
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return { url, body: observedSearchBody };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const tripType = mapTripType(p.trip_type);
|
|
72
|
+
const requestedTripType = mapTripType(p.trip_type);
|
|
73
|
+
const hasReturnDate = p.return_date != null && String(p.return_date).trim() !== '';
|
|
74
|
+
const tripType = requestedTripType === 1 && !hasReturnDate ? 2 : requestedTripType;
|
|
116
75
|
const stops = p.max_stops != null && p.max_stops !== '' ? mapStops(p.max_stops) : 0;
|
|
117
76
|
const { alliances, carriers } = parseAirlines(p.airlines);
|
|
118
77
|
const maxDur = num(p.max_duration);
|
|
@@ -156,8 +115,6 @@ export function transform(
|
|
|
156
115
|
// CONFIG[10] wire form is [1, <carry-on count>]; shared builder emits
|
|
157
116
|
// [carryOn, checked], so map count -> checked slot, constant 1 -> first.
|
|
158
117
|
bags: carryOn != null ? { carryOn: 1, checked: carryOn } : undefined,
|
|
159
|
-
searchContextToken:
|
|
160
|
-
typeof state?.search_context_token === 'string' ? state.search_context_token : undefined,
|
|
161
118
|
};
|
|
162
119
|
|
|
163
120
|
return sharedTransform(method, url, responses, mapped);
|
|
@@ -101,87 +101,36 @@
|
|
|
101
101
|
}
|
|
102
102
|
],
|
|
103
103
|
"bootstrap": {
|
|
104
|
-
"url": "https://www.google.com/travel/flights
|
|
104
|
+
"url": "https://www.google.com/travel/flights",
|
|
105
105
|
"waitUntil": "domcontentloaded",
|
|
106
106
|
"timeoutMs": 30000,
|
|
107
107
|
"captures": [
|
|
108
108
|
{
|
|
109
|
-
"source": "
|
|
109
|
+
"source": "html_regex",
|
|
110
110
|
"name": "f_sid",
|
|
111
|
-
"pattern": "
|
|
111
|
+
"pattern": "\"FdrFJe\":\"([^\"]+)\"",
|
|
112
112
|
"group": 1,
|
|
113
|
-
"method": "POST",
|
|
114
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
115
|
-
"mode": "last",
|
|
116
113
|
"required": true,
|
|
117
114
|
"capability": "browser_bootstrap"
|
|
118
115
|
},
|
|
119
116
|
{
|
|
120
|
-
"source": "
|
|
117
|
+
"source": "html_regex",
|
|
121
118
|
"name": "bl",
|
|
122
|
-
"pattern": "
|
|
119
|
+
"pattern": "\"cfb2h\":\"([^\"]+)\"",
|
|
123
120
|
"group": 1,
|
|
124
|
-
"method": "POST",
|
|
125
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
126
|
-
"mode": "last",
|
|
127
121
|
"required": true,
|
|
128
122
|
"capability": "browser_bootstrap"
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
"source": "request_header",
|
|
132
|
-
"name": "bgr",
|
|
133
|
-
"header": "X-Goog-BatchExecute-Bgr",
|
|
134
|
-
"method": "POST",
|
|
135
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
136
|
-
"mode": "last",
|
|
137
|
-
"required": true,
|
|
138
|
-
"capability": "browser_bootstrap"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
"source": "request_url_regex",
|
|
142
|
-
"name": "reqid",
|
|
143
|
-
"pattern": "[?&]_reqid=([^&]+)",
|
|
144
|
-
"group": 1,
|
|
145
|
-
"method": "POST",
|
|
146
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
147
|
-
"mode": "last",
|
|
148
|
-
"required": true,
|
|
149
|
-
"capability": "browser_bootstrap"
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
"source": "request_body_regex",
|
|
153
|
-
"name": "search_context_token",
|
|
154
|
-
"pattern": "%5B%5Bnull%2Cnull%2Cnull%2C%5C%22(.+?)%5C%22%5D",
|
|
155
|
-
"group": 1,
|
|
156
|
-
"method": "POST",
|
|
157
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
158
|
-
"mode": "last",
|
|
159
|
-
"required": false,
|
|
160
|
-
"capability": "browser_bootstrap"
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
"source": "request_body_regex",
|
|
164
|
-
"name": "observed_search_body",
|
|
165
|
-
"pattern": "^(f\\.req=.*)$",
|
|
166
|
-
"group": 1,
|
|
167
|
-
"method": "POST",
|
|
168
|
-
"urlPattern": "FlightsFrontendService/GetShoppingResults",
|
|
169
|
-
"mode": "last",
|
|
170
|
-
"required": false,
|
|
171
|
-
"capability": "browser_bootstrap"
|
|
172
123
|
}
|
|
173
124
|
]
|
|
174
125
|
},
|
|
175
126
|
"requests": [
|
|
176
127
|
{
|
|
177
128
|
"method": "POST",
|
|
178
|
-
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid
|
|
129
|
+
"url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=1708023&rt=c",
|
|
179
130
|
"headers": {
|
|
180
131
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
181
132
|
"X-Same-Domain": "1",
|
|
182
|
-
"Referer": "https://www.google.com/travel/flights
|
|
183
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
184
|
-
"X-Goog-BatchExecute-Bgr": "${state.bgr}",
|
|
133
|
+
"Referer": "https://www.google.com/travel/flights",
|
|
185
134
|
"x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
|
|
186
135
|
},
|
|
187
136
|
"body": "f.req=${param.origin}|${param.destination}|${param.departure_date}|${param.return_date}|${param.trip_type}|${param.max_stops}|${param.airlines}|${param.max_price}|${param.outbound_times}|${param.return_times}|${param.max_duration}|${param.carry_on_bags}&",
|
|
@@ -190,9 +139,5 @@
|
|
|
190
139
|
],
|
|
191
140
|
"requestTransformModule": "./request-transform.ts",
|
|
192
141
|
"parserModule": "./parser.ts",
|
|
193
|
-
"execution": {
|
|
194
|
-
"minCallSpacingMs": 2000,
|
|
195
|
-
"skipPlaybookFallback": true
|
|
196
|
-
},
|
|
197
142
|
"liveVerified": true
|
|
198
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprint-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Teach an AI agent how to use any website. Once. Records a real browser session + narration; generates a deterministic MCP tool plus a DOM-replay playbook fallback.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,7 +59,7 @@ You receive:
|
|
|
59
59
|
|
|
60
60
|
1. **Emit exactly one `perTool` entry per `selectedTools` entry**, using the same `toolName`. Do not invent or drop tools.
|
|
61
61
|
2. **Only hoist a shared module when ≥2 selected tools genuinely share it.** Single-use logic stays inside that tool's own parser.ts / request-transform.ts — do NOT create a `_shared/` module for it.
|
|
62
|
-
3. **`request-transform`** — URL signing
|
|
62
|
+
3. **`request-transform`** — URL signing or body construction shared across tools. Wire-up: the consuming tool sets `requestTransformModule: "../_shared/<name>.ts"`. Ground it in `ephemeralValues` (browser_minted, high-entropy query param) and `sourceSeqs`. The exported `transform(method, url, responses, params?)` returns the signed URL (or `{ url, body? }`).
|
|
63
63
|
4. **`parser-helper`** — a decoder/normalizer ≥2 tools' parsers call (e.g. a shared JSPB walker, a shared field mapper). The consuming tool's parser.ts does `import { ... } from '../_shared/<name>.ts'`. Ground it in a captured response body (`sourceSeqs`).
|
|
64
64
|
5. **`types`** — shared TypeScript interfaces used by ≥2 parsers. Type-only; no runtime behavior.
|
|
65
65
|
6. **Auth is NEVER a shared module.** Login is request data, and the runtime cannot run a shared sub-workflow. Put the exact recipe in each tool's `authRecipe` (login seqs, credential names, captures with `${state.X}` wiring) and set `required: false` with empty arrays when a tool needs no login. Every authed tool replicates the same recipe inline.
|