{
  "markdown": "# Rihla\n\n> **Rihla** (رحلة), *\"the journey.\"* Named for Ibn Battuta's 14th-century travelogue —\n> the record of history's farthest-traveling explorer, who left Tangier for Mecca and\n> kept going for 29 years and 75,000 miles.\n\nFlexible multi-leg, multi-airport flight search that finds the cheapest route across an\n**entire itinerary** — available as a CLI and as an MCP server for AI agents.\n\nConsumer flight sites price one leg at a time. Rihla treats the whole trip as a single\noptimization problem:\n\n> *\"Leave Montevideo — or Buenos Aires, it's a ferry ride away — around September 15\n> for Europe. Stay 20–30 days, then Japan for 15, then home. What's the cheapest\n> combination?\"*\n\nThat query has three legs, flexible airports on both ends of each, a flexible departure\nwindow, and stay-duration constraints linking the legs. Rihla prices each leg's\ndate×airport grid once, then finds the cheapest valid combinations in pure local\ncompute — so even a 3–4 leg flexible query costs only a handful of API calls.\n\n```\n1. 1,297 USD   (44 days door-to-door)\n     MVD -> AMS  2026-09-08  $    383  [mock, cached]  AA AA127\n     FRA -> NRT  2026-10-07  $    481  [mock, cached]  KL KL411\n     HND -> EZE  2026-10-22  $    433  [mock, cached]  AZ AZ933\n\n2. 1,308 USD   (44 days door-to-door)\n     MVD -> AMS  2026-09-14  $    446  [mock, cached]  AA AA127\n     ...\n```\n\nNote the airport substitution at work: it enters Europe through Amsterdam, leaves from\nFrankfurt, and flies home into Buenos Aires — each leg independently picks the cheapest\nairport pair from its sets.\n\n## Quick start\n\nRequires Python ≥ 3.10.\n\n```bash\npip install rihla            # CLI\npip install \"rihla[mcp]\"     # CLI + MCP server\n```\n\nRuns offline out of the box (no keys needed — a deterministic mock data source):\n\n```bash\nRIHLA_PROFILE=mock rihla                # the canonical demo trip above\nrihla examples/queries/canonical.json   # same trip, from a query file\nrihla -i                                # build a query interactively\n```\n\nFor live prices, copy `.env.example` to `.env` and add at least a\n[Travelpayouts](https://www.travelpayouts.com) token (free). See\n[Data sources](#data-sources--profiles).\n\n## Writing a query\n\nA query is a small JSON file: your origin airports, the ordered stops, a departure\nwindow for the first leg, and how long to stay at each stop. Downstream date windows\nare derived — you never hand-compute \"if I leave Sep 15 and stay 20–30 days, when do I\nfly to Tokyo?\"\n\n```json\n{\n  \"origins\": [\"MVD\", \"EZE\", \"AEP\"],\n  \"stops\": [\"EUROPE\", [\"NRT\", \"HND\"], \"MVD_AREA\"],\n  \"earliest\": \"2026-09-08\",\n  \"latest\": \"2026-09-22\",\n  \"stays\": [[20, 30], [15, 15]],\n  \"date_step\": 3,\n  \"top\": 5\n}\n```\n\n- **`origins`** — IATA codes tried as one origin set; the cheapest wins per date.\n- **`stops`** — each stop is a region name or a list of IATA codes. Built-in regions:\n  `EUROPE` (MAD, BCN, LIS, CDG, FCO, AMS, FRA), `TOKYO` (NRT, HND), `MVD_AREA`\n  (MVD, EZE, AEP). Make the last stop your origin set to fly home.\n- **`earliest` / `latest`** — the departure window for the *first* leg only.\n- **`stays`** — `[min, max]` nights at each intermediate stop\n  (`len(stays) == len(stops) - 1`).\n- **`date_step`** — sample every N days across date windows (coarser = fewer API calls).\n- **`top`** — how many ranked combinations to return.\n- **`currency`** — optional ISO 4217 code (default `USD`).\n\n`rihla -i` walks you through these questions and prints the resulting JSON to save for\nreuse. Add `--links` to any run to show booking URLs.\n\n## Reading results\n\nRihla is honest about data quality rather than pretending everything is bookable:\n\n- Each flight is tagged with its source, and `cached` when the price is **indicative**\n  (Travelpayouts data is aggregated search history, not a live fare) versus a real,\n  bookable fare (SerpApi / Google Flights).\n- If some legs can't be priced (thin routes are real — cached sources have gaps), you\n  get a **partial** result over the legs that were found, never a silently wrong total.\n- For unpriced legs, Rihla shows the nearest cached fares *outside* your departure\n  window — a hint to shift or widen dates.\n\nRanking is price-only in v0.1. Open-jaw within a region is allowed by default (enter\nEurope at one city, leave from another); the cost of repositioning inside the region is\nnot modeled.\n\n## Data sources & profiles\n\n| Source | Role | Cost | Notes |\n|---|---|---|---|\n| **Travelpayouts / Aviasales** | Primary | Free | Cached, redistribution-licensed. A month of prices per call, so the call budget stays tiny. Prices indicative; coverage follows route popularity. |\n| **SerpApi** (Google Flights) | Fill | 250 free searches/mo (BYO key) | Real bookable fares; fills routes Travelpayouts misses. Only spent on uncovered routes. |\n| **Mock** | Offline | — | Deterministic fake prices for development and demos. |\n\nConfigure via `.env` (see `.env.example`) or environment variables:\n`TRAVELPAYOUTS_TOKEN`, `SERPAPI_KEY`, and `RIHLA_PROFILE`:\n\n- `local` (default) — every source whose key is set, including SerpApi.\n- `hosted` — redistribution-licensed sources only (SerpApi disabled: it scrapes Google,\n  so **do not serve it from a public hosted instance**).\n- `mock` — force the offline fetcher, no network.\n\nWith no keys set, Rihla falls back to the mock source and says so.\n\n## MCP server\n\nRihla ships an MCP server (stdio) so agents like Claude can run trip searches:\n\n```bash\npip install \"rihla[mcp]\"\nclaude mcp add rihla -- rihla-mcp        # Claude Code\n```\n\nor in any MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"rihla\": {\n      \"command\": \"rihla-mcp\",\n      \"env\": {\n        \"TRAVELPAYOUTS_TOKEN\": \"your-token\",\n        \"SERPAPI_KEY\": \"your-key\"\n      }\n    }\n  }\n}\n```\n\n(The server also loads a `.env` from its working directory, so `env` is optional if you\nrun it from a checkout.)\n\nIt exposes two read-only tools with an enforced etiquette:\n\n1. **`resolve_airports`** — the agent proposes IATA codes for the traveler's named\n   places; Rihla validates and enriches them (nearby alternatives included). Cheap and\n   quota-free.\n2. **`search_trip`** — the priced, quota-limited search. Tool descriptions instruct the\n   agent to get the traveler's explicit confirmation of the airports *before* spending\n   quota here.\n\nmcp-name: io.github.leojg/rihla\n\n## How it works\n\nLeg prices are independent — the MVD→Europe fare doesn't depend on the Tokyo dates. So\nRihla fetches each leg's date×airport grid **once**, then enumerates valid date\ncombinations (respecting the stay constraints) entirely in memory. The combinatorial\nexplosion lives in local compute, not in API calls.\n\n```\ncore.py            data model + the pure optimizer (no I/O)\nfetchers/          PriceFetcher protocol: Mock / Travelpayouts / SerpApi + merge\nplaces.py          airport sets / regions\napi.py             search_trip: serializable query in, result dict out\ncli.py             thin CLI over search_trip\nmcp_server.py      thin MCP wrapper over the same seam\n```\n\nAdding a data source (Duffel, Kiwi, …) is one more class implementing a one-method\nprotocol: `quote(origin, dest, day) -> Quote`.\n\n**Scope (v0.1):** flight search only — no lodging, no booking or payments, single adult,\none cabin. Search returns booking links, never handles the transaction.\n\n## Development\n\n```bash\ngit clone https://github.com/leojg/rihla && cd rihla\npip install -e \".[dev,mcp]\"\npytest              # offline, no keys needed\nruff check .\n```\n\n## License\n\nLicensed under the Apache License 2.0 — see [LICENSE](LICENSE).\n",
  "bytes": 7544,
  "sha": "d660505e8b6013829ab53945805a967760d210dabf89785c0803fe18c974bf4a",
  "repo_slug": "leojg/rihla",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_leojg_rihla_3229ab26/readme"
}