letsfg-mcp 1.0.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 +343 -0
- package/dist/index.js +370 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# boostedtravel-mcp
|
|
2
|
+
|
|
3
|
+
The largest open flight-search MCP server. 73 ready-to-run airline connectors fire in parallel on your machine — Ryanair, EasyJet, Wizz Air, Southwest, AirAsia, and 68 more — plus enterprise GDS/NDC providers (Amadeus, Duffel, Sabre) through the BoostedTravel backend. One tool call scans the entire world for flights, including airlines your agent didn't know existed.
|
|
4
|
+
|
|
5
|
+
**Your agent doesn't need to build a flight integration. It doesn't need to scrape. Just add this MCP server and it can search and book flights in seconds — what would take 30+ minutes of integration work becomes a single tool call.**
|
|
6
|
+
|
|
7
|
+
Works with OpenClaw, Perplexity Computer, Claude Desktop, Cursor, Windsurf, and any MCP-compatible client.
|
|
8
|
+
|
|
9
|
+
> 🎥 **[Watch the demo](https://github.com/Boosted-Chat/LetsFG#demo-boostedtravel-vs-default-agent-search)** — side-by-side comparison of default agent search vs BoostedTravel CLI.
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/boostedtravel-mcp)
|
|
12
|
+
[](../../LICENSE)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What This Is
|
|
17
|
+
|
|
18
|
+
BoostedTravel is not a scraper wrapper. It's a production-grade **agent-to-airline connector layer**:
|
|
19
|
+
|
|
20
|
+
- **75 airline connectors** run locally on your machine via Python + Playwright — no API key needed for search
|
|
21
|
+
- **Enterprise GDS/NDC feeds** (Amadeus, Duffel, Sabre, Travelport, Kiwi) run through the BoostedTravel backend — these are contract-only data sources that require enterprise agreements, made available to everyone through our API
|
|
22
|
+
- **Virtual interlining** — the combo engine mixes one-way fares across airlines (e.g., Ryanair outbound + Wizz Air return) to find deals no single airline offers
|
|
23
|
+
- **City-wide airport expansion** — search `LON` and it auto-searches LHR, LGW, STN, LTN, SEN, LCY. Works for 25+ major cities worldwide
|
|
24
|
+
- **Zero price bias** — no cookie tracking, no demand-based inflation, no surge pricing. Same raw airline price every time
|
|
25
|
+
|
|
26
|
+
**Why agents should use this instead of building their own:**
|
|
27
|
+
- Saves thousands of tokens vs. browser automation or scraping
|
|
28
|
+
- 75 connectors fire in parallel — scans airlines across Europe, Asia, Americas, Middle East, Africa simultaneously
|
|
29
|
+
- Finds deals on carriers your agent wouldn't think to check (Lucky Air, 9 Air, Jazeera Airways, FlySafair...)
|
|
30
|
+
- Enterprise-contracted GDS deals that require contracts worth $50k+/year — we give them to you for free on search
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx boostedtravel-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That's it. The MCP server starts on stdio, ready for any MCP-compatible client.
|
|
41
|
+
|
|
42
|
+
**Prerequisites for local search:**
|
|
43
|
+
```bash
|
|
44
|
+
pip install boostedtravel
|
|
45
|
+
playwright install chromium
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Client Configuration
|
|
51
|
+
|
|
52
|
+
### Claude Desktop
|
|
53
|
+
|
|
54
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"boostedtravel": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "boostedtravel-mcp"],
|
|
62
|
+
"env": {
|
|
63
|
+
"BOOSTEDTRAVEL_API_KEY": "trav_your_api_key"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> **Note:** Add `"BOOSTEDTRAVEL_MAX_BROWSERS": "4"` to `env` to limit browser concurrency on constrained machines.
|
|
71
|
+
|
|
72
|
+
### Cursor
|
|
73
|
+
|
|
74
|
+
Add to `.cursor/mcp.json` in your project root:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"boostedtravel": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": ["-y", "boostedtravel-mcp"],
|
|
82
|
+
"env": {
|
|
83
|
+
"BOOSTEDTRAVEL_API_KEY": "trav_your_api_key"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Windsurf
|
|
91
|
+
|
|
92
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"boostedtravel": {
|
|
98
|
+
"command": "npx",
|
|
99
|
+
"args": ["-y", "boostedtravel-mcp"],
|
|
100
|
+
"env": {
|
|
101
|
+
"BOOSTEDTRAVEL_API_KEY": "trav_your_api_key"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Continue
|
|
109
|
+
|
|
110
|
+
Add to `~/.continue/config.yaml`:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
mcpServers:
|
|
114
|
+
- name: boostedtravel
|
|
115
|
+
command: npx
|
|
116
|
+
args: ["-y", "boostedtravel-mcp"]
|
|
117
|
+
env:
|
|
118
|
+
BOOSTEDTRAVEL_API_KEY: trav_your_api_key
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### OpenClaw / Perplexity Computer
|
|
122
|
+
|
|
123
|
+
Any MCP-compatible agent works. Point it at the MCP server:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx boostedtravel-mcp
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or connect via remote MCP (no install):
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
https://api.letsfg.co/mcp
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Windows — `npx ENOENT` Fix
|
|
136
|
+
|
|
137
|
+
If you get `spawn npx ENOENT` on Windows, use the full path to `npx`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"mcpServers": {
|
|
142
|
+
"boostedtravel": {
|
|
143
|
+
"command": "C:\\Program Files\\nodejs\\npx.cmd",
|
|
144
|
+
"args": ["-y", "boostedtravel-mcp"],
|
|
145
|
+
"env": {
|
|
146
|
+
"BOOSTEDTRAVEL_API_KEY": "trav_your_api_key"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Or use `node` directly:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"mcpServers": {
|
|
158
|
+
"boostedtravel": {
|
|
159
|
+
"command": "node",
|
|
160
|
+
"args": ["C:\\Users\\YOU\\AppData\\Roaming\\npm\\node_modules\\boostedtravel-mcp\\dist\\index.js"],
|
|
161
|
+
"env": {
|
|
162
|
+
"BOOSTEDTRAVEL_API_KEY": "trav_your_api_key"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Pin a Specific Version
|
|
170
|
+
|
|
171
|
+
To avoid unexpected updates:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"command": "npx",
|
|
176
|
+
"args": ["-y", "boostedtravel-mcp@0.2.4"]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Available Tools
|
|
183
|
+
|
|
184
|
+
| Tool | Description | Cost | Side Effects |
|
|
185
|
+
|------|-------------|------|--------------|
|
|
186
|
+
| `search_flights` | Search 400+ airlines worldwide | FREE | None (read-only) |
|
|
187
|
+
| `resolve_location` | City name → IATA code | FREE | None (read-only) |
|
|
188
|
+
| `unlock_flight_offer` | Confirm live price, reserve 30 min | $1 | Charges $1 |
|
|
189
|
+
| `book_flight` | Create real airline reservation (PNR) | FREE | Creates booking |
|
|
190
|
+
| `setup_payment` | Attach payment card (once) | FREE | Updates payment |
|
|
191
|
+
| `get_agent_profile` | Usage stats & payment status | FREE | None (read-only) |
|
|
192
|
+
| `system_info` | System resources & concurrency tier | FREE | None (read-only) |
|
|
193
|
+
|
|
194
|
+
### Booking Flow
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
search_flights → unlock_flight_offer → book_flight
|
|
198
|
+
(free) ($1 quote) (free, creates PNR)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
1. `search_flights("LON", "BCN", "2026-06-15")` — returns offers with prices from 75 airlines
|
|
202
|
+
2. `unlock_flight_offer("off_xxx")` — confirms live price with airline, reserves for 30 min, costs $1
|
|
203
|
+
3. `book_flight("off_xxx", passengers, email)` — creates real booking, airline sends e-ticket
|
|
204
|
+
|
|
205
|
+
The `search_flights` tool accepts an optional `max_browsers` parameter (1–32) to limit concurrent browser instances. Omit it to auto-detect based on system RAM.
|
|
206
|
+
|
|
207
|
+
The `system_info` tool returns your system profile (RAM, CPU, tier, recommended max browsers) — useful for agents to decide concurrency before searching.
|
|
208
|
+
|
|
209
|
+
The agent has native tools — no API docs needed, no URL building, no token-burning browser automation.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Get an API Key
|
|
214
|
+
|
|
215
|
+
**Search is free and works without a key.** An API key is needed for unlock, book, and enterprise GDS sources.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
curl -X POST https://api.letsfg.co/api/v1/agents/register \
|
|
219
|
+
-H "Content-Type: application/json" \
|
|
220
|
+
-d '{"agent_name": "my-agent", "email": "agent@example.com"}'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Or via CLI:
|
|
224
|
+
```bash
|
|
225
|
+
pip install boostedtravel
|
|
226
|
+
boostedtravel register --name my-agent --email you@example.com
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Architecture & Data Flow
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
235
|
+
│ MCP Client (Claude Desktop / Cursor / Windsurf / etc.) │
|
|
236
|
+
│ ↕ stdio (JSON-RPC, local only) │
|
|
237
|
+
├──────────────────────────────────────────────────────────────┤
|
|
238
|
+
│ boostedtravel-mcp (this package, runs on YOUR machine) │
|
|
239
|
+
│ │ │
|
|
240
|
+
│ ├─→ Python subprocess (local connectors) │
|
|
241
|
+
│ │ 75 airline connectors via Playwright + httpx │
|
|
242
|
+
│ │ Data goes: your machine → airline website → back │
|
|
243
|
+
│ │ │
|
|
244
|
+
│ └─→ HTTPS to api.letsfg.co (backend) │
|
|
245
|
+
│ unlock, book, payment, enterprise GDS search │
|
|
246
|
+
└──────────────────────────────────────────────────────────────┘
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### What data goes where
|
|
250
|
+
|
|
251
|
+
| Operation | Where data flows | What is sent |
|
|
252
|
+
|-----------|-----------------|--------------|
|
|
253
|
+
| `search_flights` (local) | Your machine → airline websites | Route, date, passenger count |
|
|
254
|
+
| `search_flights` (GDS) | Your machine → api.letsfg.co → GDS providers | Route, date, passenger count, API key |
|
|
255
|
+
| `resolve_location` | Your machine → api.letsfg.co | City/airport name |
|
|
256
|
+
| `unlock_flight_offer` | Your machine → api.letsfg.co → airline | Offer ID, payment token |
|
|
257
|
+
| `book_flight` | Your machine → api.letsfg.co → airline | Passenger name, DOB, email, phone |
|
|
258
|
+
| `setup_payment` | Your machine → api.letsfg.co → Stripe | Payment token (card handled by Stripe) |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Security & Privacy
|
|
263
|
+
|
|
264
|
+
- **TLS everywhere** — all backend communication uses HTTPS. Local connectors connect to airline websites over HTTPS.
|
|
265
|
+
- **No card storage** — payment cards are tokenized by Stripe. BoostedTravel never sees or stores raw card numbers.
|
|
266
|
+
- **API key scoping** — `BOOSTEDTRAVEL_API_KEY` grants access only to your agent's account. Keys are prefixed `trav_` for easy identification and revocation.
|
|
267
|
+
- **PII handling** — passenger names, emails, and DOBs are sent to the airline for booking (required by airlines). BoostedTravel does not store passenger PII after forwarding to the airline.
|
|
268
|
+
- **No tracking** — no cookies, no session-based pricing, no fingerprinting. Every search returns the same raw airline price.
|
|
269
|
+
- **Local search is fully local** — when searching without an API key, zero data leaves your machine except direct HTTPS requests to airline websites. The MCP server and Python connectors run entirely on your hardware.
|
|
270
|
+
- **Open source** — all connector code is MIT-licensed and auditable at [github.com/Boosted-Chat/LetsFG](https://github.com/Boosted-Chat/LetsFG).
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Sandbox / Test Mode
|
|
275
|
+
|
|
276
|
+
Use Stripe's test token for payment setup without real charges:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
setup_payment with token: "tok_visa"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
This attaches a test Visa card. Unlock calls will show `$1.00` but use Stripe test mode — no real money is charged. Useful for agent development and testing the full search → unlock → book flow.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## FAQ
|
|
287
|
+
|
|
288
|
+
### `spawn npx ENOENT` on Windows
|
|
289
|
+
|
|
290
|
+
Windows can't find `npx` in PATH. Use the full path:
|
|
291
|
+
```json
|
|
292
|
+
"command": "C:\\Program Files\\nodejs\\npx.cmd"
|
|
293
|
+
```
|
|
294
|
+
Or install globally and use `node` directly (see Windows config above).
|
|
295
|
+
|
|
296
|
+
### Search returns 0 results
|
|
297
|
+
|
|
298
|
+
- Check IATA codes are correct — use `resolve_location` first
|
|
299
|
+
- Try a date 2+ weeks in the future (airlines don't sell last-minute on all routes)
|
|
300
|
+
- Ensure `pip install boostedtravel && playwright install chromium` completed successfully
|
|
301
|
+
- Check Python is available: the MCP server spawns a Python subprocess for local search
|
|
302
|
+
|
|
303
|
+
### How do I search without an API key?
|
|
304
|
+
|
|
305
|
+
Just omit `BOOSTEDTRAVEL_API_KEY` from your config. Local search (75 airline connectors) works without any key. You'll only miss the enterprise GDS/NDC sources (Amadeus, Duffel, etc.).
|
|
306
|
+
|
|
307
|
+
### Can I use this for commercial projects?
|
|
308
|
+
|
|
309
|
+
Yes. MIT license. The local connectors and SDK are fully open source. The backend API (unlock/book/GDS) is a hosted service with usage-based pricing ($1 per unlock).
|
|
310
|
+
|
|
311
|
+
### How do I pin a version?
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
"args": ["-y", "boostedtravel-mcp@0.2.4"]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### MCP server hangs on start
|
|
318
|
+
|
|
319
|
+
Ensure Node.js 18+ is installed. The server communicates via stdio (stdin/stdout JSON-RPC) — it doesn't open a port or print a "ready" message. MCP clients handle the lifecycle automatically.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Supported Airlines (75 connectors)
|
|
324
|
+
|
|
325
|
+
| Region | Airlines |
|
|
326
|
+
|--------|----------|
|
|
327
|
+
| **Europe** | Ryanair, Wizz Air, EasyJet, Norwegian, Vueling, Eurowings, Transavia, Pegasus, Turkish Airlines, Condor, SunExpress, Volotea, Smartwings, Jet2, LOT Polish Airlines |
|
|
328
|
+
| **Middle East & Africa** | Emirates, Etihad, Qatar Airways, flydubai, Air Arabia, flynas, Salam Air, Air Peace, FlySafair |
|
|
329
|
+
| **Asia-Pacific** | AirAsia, IndiGo, SpiceJet, Akasa Air, Air India Express, VietJet, Cebu Pacific, Scoot, Jetstar, Peach, Spring Airlines, Lucky Air, 9 Air, Nok Air, Batik Air, Jeju Air, T'way Air, ZIPAIR, Singapore Airlines, Cathay Pacific, Malaysian Airlines, Thai Airways, Korean Air, ANA, US-Bangla, Biman Bangladesh |
|
|
330
|
+
| **Americas** | American Airlines, Delta, United, Southwest, JetBlue, Alaska Airlines, Hawaiian Airlines, Sun Country, Frontier, Volaris, VivaAerobus, Allegiant, Avelo, Breeze, Flair, GOL, Azul, JetSmart, Flybondi, Porter, WestJet, LATAM, Copa, Avianca |
|
|
331
|
+
| **Aggregator** | Kiwi.com (virtual interlining + LCC fallback) |
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Also Available As
|
|
336
|
+
|
|
337
|
+
- **Python SDK + CLI**: `pip install boostedtravel` — [PyPI](https://pypi.org/project/boostedtravel/)
|
|
338
|
+
- **JavaScript/TypeScript SDK + CLI**: `npm install boostedtravel` — [npm](https://www.npmjs.com/package/boostedtravel)
|
|
339
|
+
- **Agent docs**: [AGENTS.md](../../AGENTS.md) — complete reference for AI agents
|
|
340
|
+
|
|
341
|
+
## License
|
|
342
|
+
|
|
343
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var readline = __toESM(require("readline"));
|
|
28
|
+
var import_child_process = require("child_process");
|
|
29
|
+
var BASE_URL = (process.env.LETSFG_BASE_URL || process.env.BOOSTEDTRAVEL_BASE_URL || "https://api.letsfg.co").replace(/\/$/, "");
|
|
30
|
+
var API_KEY = process.env.LETSFG_API_KEY || process.env.BOOSTEDTRAVEL_API_KEY || "";
|
|
31
|
+
var PYTHON = process.env.LETSFG_PYTHON || process.env.BOOSTEDTRAVEL_PYTHON || "python3";
|
|
32
|
+
var VERSION = "1.0.0";
|
|
33
|
+
function searchLocal(params) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const input = JSON.stringify(params);
|
|
36
|
+
const pythonCmd = process.platform === "win32" ? "python" : PYTHON;
|
|
37
|
+
const child = (0, import_child_process.spawn)(pythonCmd, ["-m", "letsfg.local"], {
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39
|
+
timeout: 18e4
|
|
40
|
+
});
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
child.stdout.on("data", (d) => {
|
|
44
|
+
stdout += d.toString();
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on("data", (d) => {
|
|
47
|
+
stderr += d.toString();
|
|
48
|
+
});
|
|
49
|
+
child.on("close", (code) => {
|
|
50
|
+
if (stderr) process.stderr.write(`[letsfg] ${stderr}
|
|
51
|
+
`);
|
|
52
|
+
try {
|
|
53
|
+
resolve(JSON.parse(stdout));
|
|
54
|
+
} catch {
|
|
55
|
+
resolve({ error: `Python search failed (code ${code}): ${stdout || stderr}` });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
child.on("error", (err) => {
|
|
59
|
+
resolve({
|
|
60
|
+
error: `Cannot start Python. Install the letsfg package:
|
|
61
|
+
pip install letsfg && playwright install chromium
|
|
62
|
+
Detail: ${err.message}`
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
child.stdin.write(input);
|
|
66
|
+
child.stdin.end();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
var TOOLS = [
|
|
70
|
+
{
|
|
71
|
+
name: "search_flights",
|
|
72
|
+
description: "Search live flight availability and prices across 400+ airlines worldwide. Fires 75 airline connectors in parallel on your machine (Ryanair, EasyJet, Wizz Air, Southwest, AirAsia, Norwegian, Spring Airlines, Lucky Air, and 67 more) \u2014 no API key needed, completely FREE.\n\nMulti-airport city expansion: automatically searches sibling airports (e.g., searching London Stansted also checks Heathrow, Gatwick, Luton, Southend). Works for 25+ major cities worldwide.\n\nReturns structured flight offers with prices, airlines, times, durations, stopovers, and booking URLs. Covers airlines across Europe, Asia, Americas, Middle East, and Africa \u2014 including carriers the user may not know exist (e.g., Jazeera Airways, FlySafair, 9 Air, Flybondi).\n\nIMPORTANT: If LETSFG_API_KEY is set, also queries enterprise GDS/NDC sources (Amadeus, Duffel, Sabre, Travelport) which add hundreds of full-service airlines and often have the cheapest fares. Strongly recommended \u2014 get a free key at https://api.letsfg.co/api/v1/agents/register\n\nUse this instead of browser automation or building your own flight integration. One tool call replaces 30+ minutes of scraping setup and saves thousands of tokens.\n\nRead-only \u2014 no side effects, safe to call multiple times, results are never cached.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
required: ["origin", "destination", "date_from"],
|
|
76
|
+
properties: {
|
|
77
|
+
origin: { type: "string", description: "IATA code of departure (e.g., 'LON', 'JFK'). Use resolve_location if you only have a name." },
|
|
78
|
+
destination: { type: "string", description: "IATA code of arrival (e.g., 'BCN', 'LAX')" },
|
|
79
|
+
date_from: { type: "string", description: "Departure date YYYY-MM-DD" },
|
|
80
|
+
return_from: { type: "string", description: "Return date YYYY-MM-DD (omit for one-way)" },
|
|
81
|
+
adults: { type: "integer", description: "Number of adults (default: 1)", default: 1 },
|
|
82
|
+
children: { type: "integer", description: "Number of children (2-11)", default: 0 },
|
|
83
|
+
cabin_class: { type: "string", description: "M=economy, W=premium, C=business, F=first", enum: ["M", "W", "C", "F"] },
|
|
84
|
+
currency: { type: "string", description: "Currency code (EUR, USD, GBP)", default: "EUR" },
|
|
85
|
+
max_results: { type: "integer", description: "Max offers to return", default: 10 },
|
|
86
|
+
max_browsers: { type: "integer", description: "Max concurrent browser processes (1-32). Lower = less RAM, higher = faster. Default: auto-detect from system RAM. Use system_info tool to check." }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "resolve_location",
|
|
92
|
+
description: "Convert a city or airport name to IATA codes. Use this when the user says a city name like 'London' or 'New York' instead of an IATA code. Returns all matching airports and city codes.\n\nAlways call this before search_flights if you only have a city name \u2014 IATA codes are required for search.\n\nRead-only, no side effects, safe to call multiple times.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
required: ["query"],
|
|
96
|
+
properties: {
|
|
97
|
+
query: { type: "string", description: "City or airport name (e.g., 'London', 'Berlin')" }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "unlock_flight_offer",
|
|
103
|
+
description: 'Unlock a flight offer for booking \u2014 $1 proof-of-intent fee.\n\nThis is the "quote" step: confirms the latest price with the airline and reserves the offer for 30 minutes. ALWAYS call this before book_flight so the user can see the confirmed price.\n\nIf the confirmed price differs from the search price, inform the user before proceeding.\n\nRequires payment method (call setup_payment first).\n\nSAFETY: Charges $1. Not idempotent \u2014 calling twice on the same offer will charge twice.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
required: ["offer_id"],
|
|
107
|
+
properties: {
|
|
108
|
+
offer_id: { type: "string", description: "Offer ID from search results (off_xxx)" }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "book_flight",
|
|
114
|
+
description: "Book an unlocked flight \u2014 creates real airline reservation with PNR. FREE after unlock.\n\nFLOW: search_flights \u2192 unlock_flight_offer (quote) \u2192 book_flight\nRequirements: 1) Offer must be unlocked first 2) passenger_ids from search 3) Full passenger details\n\nSAFETY: Always provide idempotency_key to prevent double-bookings if this call is retried. Use any unique string (e.g., UUID). If the same key is sent twice, returns the original booking.\n\nERROR HANDLING: Errors include error_code and error_category fields.\n transient (SUPPLIER_TIMEOUT, RATE_LIMITED) \u2192 safe to retry after short delay\n validation (INVALID_IATA, INVALID_DATE) \u2192 fix input, then retry\n business (OFFER_EXPIRED, PAYMENT_DECLINED) \u2192 requires human decision",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
required: ["offer_id", "passengers", "contact_email"],
|
|
118
|
+
properties: {
|
|
119
|
+
offer_id: { type: "string", description: "Unlocked offer ID (off_xxx)" },
|
|
120
|
+
passengers: {
|
|
121
|
+
type: "array",
|
|
122
|
+
description: "Passengers with 'id' from search passenger_ids",
|
|
123
|
+
items: {
|
|
124
|
+
type: "object",
|
|
125
|
+
required: ["id", "given_name", "family_name", "born_on", "email"],
|
|
126
|
+
properties: {
|
|
127
|
+
id: { type: "string", description: "Passenger ID from search (pas_xxx)" },
|
|
128
|
+
given_name: { type: "string", description: "First name (passport)" },
|
|
129
|
+
family_name: { type: "string", description: "Last name (passport)" },
|
|
130
|
+
born_on: { type: "string", description: "DOB YYYY-MM-DD" },
|
|
131
|
+
gender: { type: "string", description: "m or f", default: "m" },
|
|
132
|
+
title: { type: "string", description: "mr, ms, mrs, miss", default: "mr" },
|
|
133
|
+
email: { type: "string", description: "Email" },
|
|
134
|
+
phone_number: { type: "string", description: "Phone with country code" }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
contact_email: { type: "string", description: "Booking contact email" },
|
|
139
|
+
idempotency_key: { type: "string", description: "Unique key to prevent double-bookings on retry (e.g., UUID). Strongly recommended." }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "setup_payment",
|
|
145
|
+
description: "Set up payment method. Required before unlock/book. For testing use token 'tok_visa'. Only needed once.\n\nIdempotent \u2014 safe to call multiple times (updates the payment method).",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
token: { type: "string", description: "Payment token (e.g., 'tok_visa' for testing)" },
|
|
150
|
+
payment_method_id: { type: "string", description: "Payment method ID (pm_xxx)" }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "get_agent_profile",
|
|
156
|
+
description: "Get agent profile, payment status, and usage stats (searches, unlocks, bookings, fees).\n\nRead-only. Safe to call multiple times.",
|
|
157
|
+
inputSchema: { type: "object", properties: {} }
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "start_checkout",
|
|
161
|
+
description: "Automate airline checkout up to the payment page \u2014 NEVER submits payment.\n\nFLOW: search_flights \u2192 unlock_flight_offer ($1) \u2192 start_checkout\n\nUses Playwright to drive the airline website: selects flights, fills passenger details, skips extras/seats, and stops at the payment form. Returns a screenshot and booking URL so the user can complete manually in their browser.\n\nSupported airlines: Ryanair, Wizz Air, EasyJet. Other airlines return booking URL only.\n\nSAFETY: Uses fake test data by default. Never enters payment info. The checkout_token from unlock_flight_offer is required \u2014 prevents unauthorized usage.\n\nRuns locally via Python subprocess (pip install letsfg && playwright install chromium).",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
required: ["offer_id", "checkout_token"],
|
|
165
|
+
properties: {
|
|
166
|
+
offer_id: { type: "string", description: "Offer ID from search results (off_xxx)" },
|
|
167
|
+
checkout_token: { type: "string", description: "Token from unlock_flight_offer response" },
|
|
168
|
+
passengers: {
|
|
169
|
+
type: "array",
|
|
170
|
+
description: "Passenger details. If omitted, uses safe test data (John Doe, test@example.com)",
|
|
171
|
+
items: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
given_name: { type: "string" },
|
|
175
|
+
family_name: { type: "string" },
|
|
176
|
+
born_on: { type: "string", description: "DOB YYYY-MM-DD" },
|
|
177
|
+
gender: { type: "string", description: "m or f" },
|
|
178
|
+
title: { type: "string", description: "mr, ms, mrs" },
|
|
179
|
+
email: { type: "string" },
|
|
180
|
+
phone_number: { type: "string" }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "system_info",
|
|
189
|
+
description: "Get system resource info (RAM, CPU cores) and recommended concurrency settings.\n\nUse this to determine optimal max_browsers value for search_flights. Returns RAM total/available, CPU cores, recommended max browsers, and performance tier.\n\nTiers: minimal (<2GB, max 2), low (2-4GB, max 3), moderate (4-8GB, max 5), standard (8-16GB, max 8), high (16-32GB, max 12), maximum (32+GB, max 16).\n\nRead-only, no side effects, instant response.",
|
|
190
|
+
inputSchema: { type: "object", properties: {} }
|
|
191
|
+
}
|
|
192
|
+
];
|
|
193
|
+
async function apiRequest(method, path, body) {
|
|
194
|
+
const headers = {
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
"User-Agent": "letsfg-mcp/1.0.0"
|
|
197
|
+
};
|
|
198
|
+
if (API_KEY) headers["X-API-Key"] = API_KEY;
|
|
199
|
+
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
200
|
+
method,
|
|
201
|
+
headers,
|
|
202
|
+
body: body ? JSON.stringify(body) : void 0
|
|
203
|
+
});
|
|
204
|
+
const data = await resp.json();
|
|
205
|
+
if (resp.status >= 400) {
|
|
206
|
+
return { error: true, status_code: resp.status, detail: data.detail || JSON.stringify(data) };
|
|
207
|
+
}
|
|
208
|
+
return data;
|
|
209
|
+
}
|
|
210
|
+
async function callTool(name, args) {
|
|
211
|
+
switch (name) {
|
|
212
|
+
case "search_flights": {
|
|
213
|
+
const params = {
|
|
214
|
+
origin: args.origin,
|
|
215
|
+
destination: args.destination,
|
|
216
|
+
date_from: args.date_from,
|
|
217
|
+
adults: args.adults ?? 1,
|
|
218
|
+
children: args.children ?? 0,
|
|
219
|
+
currency: args.currency ?? "EUR",
|
|
220
|
+
limit: args.max_results ?? 10
|
|
221
|
+
};
|
|
222
|
+
if (args.return_from) params.return_from = args.return_from;
|
|
223
|
+
if (args.cabin_class) params.cabin_class = args.cabin_class;
|
|
224
|
+
if (args.max_browsers) params.max_browsers = args.max_browsers;
|
|
225
|
+
const result = await searchLocal(params);
|
|
226
|
+
if (result.error) return JSON.stringify(result, null, 2);
|
|
227
|
+
const offers = result.offers || [];
|
|
228
|
+
const sourceTiers = result.source_tiers;
|
|
229
|
+
const hasBackend = sourceTiers ? Object.keys(sourceTiers).some((t) => t === "paid") : false;
|
|
230
|
+
const summary = {
|
|
231
|
+
total_offers: offers.length,
|
|
232
|
+
source: hasBackend ? "local_connectors (75 airlines) + backend (Amadeus, Duffel, Sabre)" : "local_connectors (75 airlines) \u2014 set LETSFG_API_KEY to also query Amadeus/Duffel/Sabre for more results",
|
|
233
|
+
offers: offers.map((o) => ({
|
|
234
|
+
offer_id: o.id,
|
|
235
|
+
price: `${o.price} ${o.currency}`,
|
|
236
|
+
airlines: o.airlines,
|
|
237
|
+
source: o.source,
|
|
238
|
+
booking_url: o.booking_url,
|
|
239
|
+
outbound: (() => {
|
|
240
|
+
const ob = o.outbound;
|
|
241
|
+
const segs = ob?.segments || [];
|
|
242
|
+
return segs.length ? {
|
|
243
|
+
from: segs[0].origin,
|
|
244
|
+
to: segs[segs.length - 1].destination,
|
|
245
|
+
departure: segs[0].departure,
|
|
246
|
+
flight: segs[0].flight_no,
|
|
247
|
+
airline: segs[0].airline_name || segs[0].airline,
|
|
248
|
+
stops: ob?.stopovers
|
|
249
|
+
} : null;
|
|
250
|
+
})()
|
|
251
|
+
}))
|
|
252
|
+
};
|
|
253
|
+
return JSON.stringify(summary, null, 2);
|
|
254
|
+
}
|
|
255
|
+
case "resolve_location": {
|
|
256
|
+
const result = await apiRequest("GET", `/api/v1/flights/locations/${encodeURIComponent(args.query)}`);
|
|
257
|
+
return JSON.stringify(result, null, 2);
|
|
258
|
+
}
|
|
259
|
+
case "unlock_flight_offer": {
|
|
260
|
+
const result = await apiRequest("POST", "/api/v1/bookings/unlock", { offer_id: args.offer_id });
|
|
261
|
+
return JSON.stringify(result, null, 2);
|
|
262
|
+
}
|
|
263
|
+
case "book_flight": {
|
|
264
|
+
const body = {
|
|
265
|
+
offer_id: args.offer_id,
|
|
266
|
+
booking_type: "flight",
|
|
267
|
+
passengers: args.passengers,
|
|
268
|
+
contact_email: args.contact_email
|
|
269
|
+
};
|
|
270
|
+
if (args.idempotency_key) body.idempotency_key = args.idempotency_key;
|
|
271
|
+
const result = await apiRequest("POST", "/api/v1/bookings/book", body);
|
|
272
|
+
return JSON.stringify(result, null, 2);
|
|
273
|
+
}
|
|
274
|
+
case "setup_payment": {
|
|
275
|
+
const body = {};
|
|
276
|
+
if (args.token) body.token = args.token;
|
|
277
|
+
if (args.payment_method_id) body.payment_method_id = args.payment_method_id;
|
|
278
|
+
const result = await apiRequest("POST", "/api/v1/agents/setup-payment", body);
|
|
279
|
+
return JSON.stringify(result, null, 2);
|
|
280
|
+
}
|
|
281
|
+
case "get_agent_profile": {
|
|
282
|
+
const result = await apiRequest("GET", "/api/v1/agents/me");
|
|
283
|
+
return JSON.stringify(result, null, 2);
|
|
284
|
+
}
|
|
285
|
+
case "system_info": {
|
|
286
|
+
const result = await searchLocal({ __system_info: true });
|
|
287
|
+
return JSON.stringify(result, null, 2);
|
|
288
|
+
}
|
|
289
|
+
case "start_checkout": {
|
|
290
|
+
const result = await searchLocal({
|
|
291
|
+
__checkout: true,
|
|
292
|
+
offer_id: args.offer_id,
|
|
293
|
+
passengers: args.passengers || null,
|
|
294
|
+
checkout_token: args.checkout_token,
|
|
295
|
+
api_key: API_KEY,
|
|
296
|
+
base_url: BASE_URL
|
|
297
|
+
});
|
|
298
|
+
if (result.error) return JSON.stringify(result, null, 2);
|
|
299
|
+
const summary = {
|
|
300
|
+
status: result.status,
|
|
301
|
+
step: result.step,
|
|
302
|
+
airline: result.airline,
|
|
303
|
+
message: result.message,
|
|
304
|
+
total_price: result.total_price ? `${result.total_price} ${result.currency}` : void 0,
|
|
305
|
+
booking_url: result.booking_url,
|
|
306
|
+
can_complete_manually: result.can_complete_manually,
|
|
307
|
+
elapsed_seconds: result.elapsed_seconds
|
|
308
|
+
};
|
|
309
|
+
if (result.screenshot_b64) {
|
|
310
|
+
summary.screenshot = "(base64 screenshot attached \u2014 render with image tool if available)";
|
|
311
|
+
}
|
|
312
|
+
return JSON.stringify(summary, null, 2);
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function send(msg) {
|
|
319
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
320
|
+
}
|
|
321
|
+
var rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
322
|
+
rl.on("line", async (line) => {
|
|
323
|
+
let msg;
|
|
324
|
+
try {
|
|
325
|
+
msg = JSON.parse(line);
|
|
326
|
+
} catch {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const method = msg.method;
|
|
330
|
+
const id = msg.id;
|
|
331
|
+
switch (method) {
|
|
332
|
+
case "initialize":
|
|
333
|
+
send({
|
|
334
|
+
jsonrpc: "2.0",
|
|
335
|
+
id,
|
|
336
|
+
result: {
|
|
337
|
+
protocolVersion: "2024-11-05",
|
|
338
|
+
capabilities: { tools: {} },
|
|
339
|
+
serverInfo: { name: "letsfg", version: VERSION }
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
case "notifications/initialized":
|
|
344
|
+
break;
|
|
345
|
+
case "tools/list":
|
|
346
|
+
send({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
|
|
347
|
+
break;
|
|
348
|
+
case "tools/call": {
|
|
349
|
+
const params = msg.params;
|
|
350
|
+
const toolName = params.name;
|
|
351
|
+
const toolArgs = params.arguments || {};
|
|
352
|
+
try {
|
|
353
|
+
const text = await callTool(toolName, toolArgs);
|
|
354
|
+
send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text }] } });
|
|
355
|
+
} catch (e) {
|
|
356
|
+
send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: `Error: ${e}` }], isError: true } });
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "ping":
|
|
361
|
+
send({ jsonrpc: "2.0", id, result: {} });
|
|
362
|
+
break;
|
|
363
|
+
default:
|
|
364
|
+
if (id) {
|
|
365
|
+
send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
process.stderr.write(`LetsFG MCP v${VERSION} | local connectors: 75 airlines | api: ${API_KEY ? "key set" : "search-only (no key)"}
|
|
370
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "letsfg-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"mcpName": "io.github.Efistoffeles/letsfg",
|
|
5
|
+
"description": "LetsFG MCP Server — 75 airline connectors run locally + enterprise GDS/NDC APIs. Flight search & booking for Claude, Cursor, Windsurf, and any MCP-compatible AI agent.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"letsfg-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs --clean",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"claude",
|
|
23
|
+
"cursor",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
"flights",
|
|
26
|
+
"travel",
|
|
27
|
+
"booking"
|
|
28
|
+
],
|
|
29
|
+
"author": "LetsFG",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/LetsFG/LetsFG.git"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.5.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.3.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|