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.
Files changed (3) hide show
  1. package/README.md +343 -0
  2. package/dist/index.js +370 -0
  3. 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
+ [![npm](https://img.shields.io/npm/v/boostedtravel-mcp)](https://www.npmjs.com/package/boostedtravel-mcp)
12
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../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
+ }