Open Multi-Provider Travel Intelligence and Itinerary Orchestration Platform
Flights • Hotels • Experiences • FX • Routing MCP-Native • Supplier-Neutral • Live-Data-First
CI Python FastAPI LangGraph MCP React TypeScript PostgreSQL Redis Docker Pydantic Vite Pytest Ruff License: MIT
TravelMesh does not fabricate runtime inventory. Supplier data retains
provider identity, original currency, price freshness, offer identity, and
pricing conditions throughout the planning pipeline. If a provider is
unreachable, unconfigured, or returns nothing, the platform says so
(PROVIDER_UNAVAILABLE / NOT_CONFIGURED / NO_RESULTS) instead of
inventing a plausible-looking flight, hotel, or exchange rate.
This is not a demo where a few agents call APIs and an LLM writes an itinerary. It's a travel-data platform: canonical models, multi-provider aggregation, hotel identity resolution, offer normalization, deterministic ranking and budget optimization, route-aware scheduling, and itinerary validation -- with MCP exposing capabilities and LangGraph owning the workflow, not the other way around.
The Trip Search form and Provider Status page are the app exactly as it runs. The trip-result screens (Summary through Provider Executions) are shown populated with sample data (clearly labeled below) so the UI's actual capability is visible without live provider credentials -- without keys configured, these screens are real but thinner (see Live-data policy).
Trip search (live) Trip search form
Provider status Provider status page
Hotel comparison (sample data) -- grouped by resolved property, with Cheapest / Most Flexible badges (ADR-006) Hotel comparison table
Budget breakdown (sample data) -- real offer prices vs. explicitly labeled per-diem estimates (ADR-015) Budget breakdown
Day-by-day itinerary (sample data) -- geographically clustered, with real routed travel-time estimates between stops (ADR-011) Day-by-day itinerary
Provider executions (sample data) -- per-provider status/latency for one trip generation (ADR-004) Provider execution status
- Screenshots
- Architecture
- Why TravelMesh
- Live-data policy
- Provider matrix
- Repository layout
- Installation
- Running with Docker
- Credentials
- API usage
- The frontend
- Testing
- Observability
- Adding a provider
- ADRs
- Roadmap
- Known limitations
External Travel Provider
|
Provider Adapter (mcp_servers/*/providers/*.py)
|
Provider-Specific Mapper (same file -- maps raw response -> canonical model)
|
Canonical Travel Model (backend/app/domain/*.py)
|
Provider Aggregation (provider_sdk/aggregation.py, concurrent, per-provider status)
|
Identity Resolution (app/services/identity, app/services/deduplication)
|
Offer Normalization (app/services/pricing/offer_equivalence.py)
|
Currency Normalization (app/services/pricing/currency_normalizer.py -> Currency MCP)
|
Constraint Evaluation (app/services/constraints)
|
Ranking (app/services/ranking)
|
Budget Optimization (app/services/budget)
|
Routing (app/services/routing -> Routing MCP)
|
Itinerary Scheduling (app/services/scheduling)
|
Itinerary Validation (app/services/validation, bounded repair loop)
|
Explainable Travel Plan (app/llm/explanations.py -- deterministic, no LLM required)
Every stage above except "Explainable Travel Plan" is plain, deterministic
Python with zero LLM dependency (see ADR-016).
LangGraph (backend/app/orchestration/graph.py) wires these stages
together with real parallelism (flights/hotels/places/activities search
run concurrently) and a bounded itinerary-repair loop -- see
docs/architecture/langgraph-workflow.md.
User
|
React / TypeScript (frontend/)
|
FastAPI (backend/app/api)
|
LangGraph Supervisor (backend/app/orchestration)
|
+-----------------+-----------------+
| | |
Flight MCP Hotel MCP Experience MCP + Currency MCP, Routing MCP
| | |
Amadeus, ... Amadeus, ... OpenTripMap, Amadeus
|
PostgreSQL + Redis
Five independent MCP servers (mcp_servers/{flights,hotels,experiences, currency,routing}/server.py), each exposing a small, explicit tool
allowlist over streamable-http via the official mcp Python SDK. See
docs/architecture/mcp-boundaries.md.
Most "AI travel agent" repos wire an LLM to a couple of APIs and let it free-associate an itinerary. That produces something that looks like a plan but has no real guarantee of being bookable, priced correctly, or even internally consistent (duplicate stops, closed attractions, an itinerary that doesn't add up to the stated budget). TravelMesh instead treats travel planning as what it actually is: a data-aggregation and constrained-optimization problem, where an LLM (optional, see below) adds value only for language interpretation and prose, never for the numbers.
See ADR-013. In short: no
fabricated flights, hotels, availability, activity prices, or exchange
rates, ever, at runtime. Mocks/fixtures exist only under tests/.
| Capability | Provider | Status |
|---|---|---|
| Flights | Amadeus | Implemented (self-service Flight Offers Search v2 + Pricing) |
| Flights | Duffel / Travelport / Sabre | Planned -- extension points only |
| Hotels | Amadeus | Implemented (Hotel Search v3 + static hotel-list resolution) |
| Hotels | Expedia Rapid / Booking.com / Hotelbeds | Planned -- extension points only |
| Places (POI) | OpenTripMap | Implemented |
| Activities | Amadeus Tours and Activities | Implemented (no availability API) |
| Activities | Viator / GetYourGuide | Planned -- extension points only |
| Currency | open.er-api.com | Implemented, default (broad ISO coverage incl. AED) |
| Currency | Frankfurter (ECB) | Implemented, registered as an alternate provider |
| Routing | OSRM | Implemented (public demo server by default; self-host for production) |
| Routing | Haversine | Implemented -- explicit ESTIMATED fallback only |
"Implemented" here means: real adapter code against the provider's actual
documented API shape, with contract tests (tests/contract/) verifying the
response mapping. Amadeus adapters require AMADEUS_CLIENT_ID/
AMADEUS_CLIENT_SECRET (free self-service sandbox credentials from
developers.amadeus.com) to return real
results -- without them, GET /api/v1/providers correctly reports
NOT_CONFIGURED and trip generation proceeds with whatever real data is
available (see the live-data policy above). OpenTripMap requires
OPENTRIPMAP_API_KEY similarly.
backend/app/ FastAPI app, domain models, services, orchestration, persistence
mcp_servers/ 5 MCP servers: flights, hotels, experiences, currency, routing
mcp_clients/ Typed clients LangGraph nodes use instead of raw MCP transport
provider_sdk/ Shared provider interfaces, registry, HTTP resilience, Amadeus auth
frontend/ React + Vite + TypeScript UI
tests/{unit,contract,integration,e2e}/
docs/{architecture,providers,adr}/
docker-compose.yml, Dockerfile
Requires Python 3.11+ and Node 18+ (frontend build tooling).
git clone <this-repo> cd TravelMesh-MCP cp .env.example .env # fill in real credentials for live results (optional) make install # creates .venv, installs backend + dev deps make frontend-install make test # 70 tests, no network/credentials required make lint
You need Postgres and Redis reachable (or point DATABASE_URL at SQLite
for a quick local run -- see tests/integration/conftest.py for the
pattern). Then, in separate terminals:
make dev-currency-mcp # :8004 make dev-routing-mcp # :8005 make dev-flight-mcp # :8001 make dev-hotel-mcp # :8002 make dev-experience-mcp # :8003 make dev-api # :8000 make frontend-dev # :3000 (proxies /api to :8000 in dev)
cp .env.example .env docker compose up --build
| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| API | http://localhost:8000 |
| Swagger | http://localhost:8000/docs |
| Flight MCP | http://localhost:8001/mcp |
| Hotel MCP | http://localhost:8002/mcp |
| Experience MCP | http://localhost:8003/mcp |
| Currency MCP | http://localhost:8004/mcp |
| Routing MCP | http://localhost:8005/mcp |
| Metrics | http://localhost:8000/metrics |
Postgres and Redis run as internal-only services (not published to the
host) -- add ports: ["5432:5432"] / ["6379:6379"] back locally if you
want a direct psql/redis-cli connection. The api container runs
alembic upgrade head against Postgres automatically on startup.
Verified during development: docker compose config (valid), a full
docker compose up --build bringing up all 9 services successfully, and a
real trip created/generated/fetched through the live containerized API
backed by real Postgres (see docs/adr/ for what that run showed with no
provider credentials configured: an honest NOT_CONFIGURED status per
capability and a budget built only from real per-diem estimates).
None are required to run the platform -- LLM_PROVIDER=none and no
provider API keys is a fully valid configuration; you'll see
NOT_CONFIGURED in GET /api/v1/providers and thinner (but honest)
itineraries. For real flight/hotel/activity results:
AMADEUS_CLIENT_ID=... AMADEUS_CLIENT_SECRET=... AMADEUS_ENVIRONMENT=test # Amadeus self-service sandbox OPENTRIPMAP_API_KEY=...
Amadeus self-service credentials are free at developers.amadeus.com. OpenTripMap keys are free at opentripmap.io/product. Currency (open.er-api.com) and routing (public OSRM demo) need no key.
# create a trip (idempotent by request_id) curl -X POST localhost:8000/api/v1/trips -H 'content-type: application/json' -d '{ "request": { "origin": {"city": "Bangalore", "airport_code": "BLR"}, "destination": {"city": "Dubai", "airport_code": "DXB", "coordinates": {"latitude": 25.2048, "longitude": 55.2708}}, "start_date": "2026-09-10", "end_date": "2026-09-15", "travellers": {"adults": 2, "children": 0, "infants": 0}, "budget": {"amount": "150000", "currency": "INR"}, "preferred_currency": "INR", "flight_preferences": {"cabin_class": "economy", "direct_preferred": true}, "hotel_preferences": {"minimum_stars": 4, "breakfast_preferred": true, "free_cancellation_preferred": true}, "interests": ["culture", "food", "shopping", "sightseeing"], "pace": "balanced" } }' # generate it (runs the full LangGraph pipeline) curl -X POST localhost:8000/api/v1/trips/{trip_id}/generate # fetch results curl localhost:8000/api/v1/trips/{trip_id} curl localhost:8000/api/v1/trips/{trip_id}/itinerary curl localhost:8000/api/v1/trips/{trip_id}/budget # provider status (never exposes credentials) curl localhost:8000/api/v1/providers
Or just run ./scripts/sample_trip.sh against a running API. Full endpoint
list, request/response schemas: GET /docs (Swagger UI).
React + Vite + TypeScript, screens: Trip Search, Generation Progress,
Travel Summary, Flight Results, Hotel Comparison (grouped by resolved
property, showing Cheapest/Most Flexible badges), Experiences, Budget,
Day-by-Day Itinerary, Provider Status. npm run build produces a static
bundle served by nginx in Docker, reverse-proxying /api/* to the api
service.
There are four ways to test this project, roughly in order of how much you want to see actually happen: the automated suite (seconds, no setup), a local dev run (see real HTTP traffic against live FX/routing APIs), the full Docker stack (closest to production), and a live scenario against real Amadeus credentials. All four are things this project was actually run through during development -- these aren't aspirational instructions.
No network, no credentials, no services to start.
cd TravelMesh-MCP source .venv/bin/activate # after `make install` pytest # or: make test
Expected:
...................................................................... [100%]
70 passed, 1 skipped in ~3s
The one skipped test is the live e2e scenario (see step 4) -- it's marked
@pytest.mark.live and skipifs itself when AMADEUS_CLIENT_ID/
AMADEUS_CLIENT_SECRET aren't set, so a bare pytest run never needs
-m to stay offline. Useful variants:
pytest tests/unit -q # fast, pure-Python logic only pytest tests/contract -q # adapter <-> mocked-provider-response mapping pytest tests/integration -q # full FastAPI app + LangGraph graph, stubbed providers ruff check . # lint (should print "All checks passed!") mypy backend/app provider_sdk mcp_servers mcp_clients --ignore-missing-imports # type checking (a handful of known strictness-only # false positives remain; see comments where they occur)
If you only run one thing, run pytest -- it exercises the property
identity false-positive guard, the budget optimizer, the itinerary
scheduler/validator/repair loop, and a full trip create→generate→fetch
lifecycle through real FastAPI routes and a real (SQLite) database, all
without touching the network.
This is the fastest way to watch the platform make genuine HTTP calls to
open.er-api.com and OSRM. Needs Python's venv (make install) already
set up; Postgres/Redis aren't required for this path if you point
DATABASE_URL at SQLite (see the tests/integration/conftest.py pattern,
or just export DATABASE_URL=sqlite+aiosqlite:////tmp/travelmesh.db).
Open 6 terminals (or run each with & and take note of the PIDs):
source .venv/bin/activate make dev-currency-mcp # :8004 -- watch it hit open.er-api.com make dev-routing-mcp # :8005 -- watch it hit OSRM make dev-flight-mcp # :8001 make dev-hotel-mcp # :8002 make dev-experience-mcp # :8003 make dev-api # :8000
Sanity check each MCP server came up:
curl -s -o /dev/null -w "flight-mcp: %{http_code}\n" -X POST http://localhost:8001/mcp curl -s -o /dev/null -w "currency-mcp:%{http_code}\n" -X POST http://localhost:8004/mcp # (400 is expected -- that's a bare POST with no MCP session; it means the server is up)
Then run the flagship scenario:
./scripts/sample_trip.sh
You should see three JSON blobs printed: the created trip (status: PENDING), the generated result (status: COMPLETED or
PARTIALLY_COMPLETED, with provider_executions showing real
NOT_CONFIGURED for Amadeus/OpenTripMap if you haven't added keys yet, and
a real budget total computed from actual per-diem math), and the provider
matrix. Add the frontend to click through the UI instead of reading JSON:
make frontend-dev # :3000, proxies /api to :8000Open http://localhost:3000, fill in the trip form (it's pre-filled with the BLR→DXB scenario), and click through the Summary / Flight / Hotel Comparison / Budget / Day-by-Day Itinerary / Provider Status tabs.
cp .env.example .env # optionally fill in real credentials first
docker compose up --buildWait for Uvicorn running on http://0.0.0.0:8000 in the logs, then:
curl http://localhost:8000/api/v1/health # {"status":"ok"} curl http://localhost:8000/api/v1/providers # per-provider CONFIGURED/NOT_CONFIGURED API_URL=http://localhost:8000 ./scripts/sample_trip.sh
Open http://localhost:3000 for the UI, http://localhost:8000/docs for
interactive Swagger (try requests directly from the browser), and
http://localhost:8000/metrics for the raw Prometheus output. Tear down
with docker compose down (add -v to also drop the Postgres volume and
start clean next time).
If a port is already taken on your machine, override it per-service without editing the committed file, e.g.:
cat > docker-compose.override.yml <<'EOF' services: api: ports: ["18000:8000"] EOF docker compose up --build # compose automatically picks up the override file
Get free sandbox credentials from
developers.amadeus.com (Flight Offers
Search, Hotel Search, and Tours and Activities APIs) and
opentripmap.io/product, put them in
.env, then either:
# against services started via step 2 or 3: AMADEUS_CLIENT_ID=... AMADEUS_CLIENT_SECRET=... pytest -m live # or just re-run the sample script once your services have the real keys: ./scripts/sample_trip.sh
With real credentials, provider_executions in the response will show
SUCCESS with real result_count values, selected_flight/
selected_hotel will be populated with real Amadeus offers (real prices,
real price_metadata.status: LIVE), and the itinerary will contain real
OpenTripMap-sourced activities clustered and scheduled around them.
- Unit (
tests/unit/): Money arithmetic, FX conversion/batching, hotel-name normalization, property identity matching including an explicit false-positive guard, offer equivalence, flight/hotel ranking, constraints, budget calculation/optimization, Haversine distance, geographic clustering, itinerary scheduling, opening-hour validation, duplicate-activity detection, price freshness, LLM-free explanations. - Contract (
tests/contract/): Amadeus flight/hotel/activity mapping, OpenTripMap mapping, Frankfurter mapping, OSRM mapping -- each exercises the real adapter code against a mocked HTTP response shaped like the provider's actual API, so a provider schema drift fails here first. - Integration (
tests/integration/): the full FastAPI app (real routes, real LangGraph graph, real SQLite-backed persistence) against stub provider clients, verifying the create -> generate -> fetch lifecycle and idempotency end to end. - E2E (
tests/e2e/,-m live): the flagship BLR->DXB scenario against real running services and real credentials; skipped otherwise.
pytestfails on collection with an import error: make sure you activated the venv (source .venv/bin/activate) and ranmake installfirst --pyproject.toml'spythonpathsetting expects to be run from the repo root.- A
make dev-*MCP server exits immediately: check the terminal output -- it's almost always a missingREDIS_URL/DATABASE_URLor a port already in use (lsof -i :8001, adjust the--portflag or kill the conflicting process). docker compose upfails with "port is already allocated": another project on your machine is using that port -- use thedocker-compose.override.ymlpattern above, ordocker psto find and stop the conflicting container.- Provider status shows
NOT_CONFIGUREDeven after adding keys: the API/MCP server processes read env vars at startup -- restart them (ordocker compose restart api flight-mcp hotel-mcp experience-mcp) after editing.env. GET /api/v1/trips/{id}/itineraryreturns 404: you have to callPOST /api/v1/trips/{id}/generateat least once first -- creating a trip only stores the request, it doesn't run the pipeline yet.
Structured JSON logging (structlog, secrets auto-redacted), OpenTelemetry
tracing (spans on trip.generate, flights.search, hotel.identity,
currency.normalize, rank, budget.optimize, itinerary.schedule,
itinerary.validate, ...; OTLP export when OTEL_EXPORTER_OTLP_ENDPOINT
is set, otherwise spans are created but not exported anywhere by default --
set TRACE_CONSOLE_EXPORT=true to dump them to stdout for local
debugging), and Prometheus metrics at /metrics (trip_generation_total,
provider_requests_total, provider_errors_total,
hotel_identity_matches_total, itinerary_validation_failures_total,
partial_trip_total, ...).
See docs/providers/adding-a-provider.md.
In short: implement the relevant Protocol from provider_sdk/interfaces.py,
register it, add it to the relevant *_PROVIDERS env var, add a contract
test. No changes needed in ranking, budget, scheduling, or the frontend.
16 architecture decision records in docs/adr/ covering the MCP boundary, provider adapter architecture, the canonical domain, provider mesh concurrency, hotel identity resolution, offer equivalence, currency normalization, LangGraph orchestration, deterministic ranking, constraint/budget optimization, route-aware scheduling, itinerary validation, the live-data-only policy, price freshness, provenance, and the LLM-optional design.
v0.1 Live provider foundation [this release]
v0.2 Complete itinerary generation [this release]
v0.3 Multi-provider aggregation (framework) [this release]
v0.4 Hotel identity + deduplication [this release]
v0.5 Constraint and budget optimization [this release]
v0.6 Advanced itinerary scheduler [this release]
v0.7 Provider SDK [this release]
v0.8 Observability + evaluation + trip replay tool next
v0.9 A second real provider per capability (Duffel, Expedia, Viator) next
v1.0 Booking-ready architecture (reprice -> confirm flow) future
- Only one real provider per capability is wired up today (Amadeus for flights/hotels/activities, OpenTripMap for places) -- the multi-provider architecture (aggregation, identity resolution, offer equivalence) is fully built and tested, but a second live competing provider per capability hasn't been added yet. See the provider matrix above.
- Geographic clustering is greedy proximity grouping, not a routing solver -- documented as a deliberate simplification in ADR-011.
- No booking flow exists (by design for v1 -- see
ADR-014);
reprice()is implemented for flights (via Amadeus Flight Offers Price) and hotels but isn't exercised by an end-to-end confirm-and-book UI. GET /api/v1/providers/healthreports configuration status only in v1 -- per-provider latency/error-rate metrics exist as real Prometheus series on each MCP server's own/metrics, but aren't yet aggregated into one cross-process health view (would need a shared Redis-backed store); not faked with placeholder numbers in the meantime.- Amadeus's Tours and Activities API has no real-time availability
endpoint, so
get_activity_availabilityalways reportsavailability_supported: false-- honest, not a bug. - Trip replay (
docs/architecture/replay.md) captures sanitized per-stage state today; a developer tool to actually replay a past generation without hitting live providers is still on the roadmap.