Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

TravelMesh-MCP

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.

Screenshots

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

Table of contents

Architecture

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.

Why TravelMesh

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.

Live-data policy

See ADR-013. In short: no fabricated flights, hotels, availability, activity prices, or exchange rates, ever, at runtime. Mocks/fixtures exist only under tests/.

Provider matrix

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.

Repository layout

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

Installation

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

Running locally without Docker

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)

Running with Docker

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).

Credentials

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.

API usage

# 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).

The frontend

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.

Testing

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.

1. Automated test suite

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.

2. Local dev run (see real external calls happen)

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 :8000

Open 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.

3. Full stack via Docker (closest to production)

cp .env.example .env # optionally fill in real credentials first
docker compose up --build

Wait 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

4. Live scenario against real credentials

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.

What each test layer actually covers

  • 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.

Troubleshooting

  • pytest fails on collection with an import error: make sure you activated the venv (source .venv/bin/activate) and ran make install first -- pyproject.toml's pythonpath setting expects to be run from the repo root.
  • A make dev-* MCP server exits immediately: check the terminal output -- it's almost always a missing REDIS_URL/DATABASE_URL or a port already in use (lsof -i :8001, adjust the --port flag or kill the conflicting process).
  • docker compose up fails with "port is already allocated": another project on your machine is using that port -- use the docker-compose.override.yml pattern above, or docker ps to find and stop the conflicting container.
  • Provider status shows NOT_CONFIGURED even after adding keys: the API/MCP server processes read env vars at startup -- restart them (or docker compose restart api flight-mcp hotel-mcp experience-mcp) after editing .env.
  • GET /api/v1/trips/{id}/itinerary returns 404: you have to call POST /api/v1/trips/{id}/generate at least once first -- creating a trip only stores the request, it doesn't run the pipeline yet.

Observability

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, ...).

Adding a provider

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.

ADRs

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.

Roadmap

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

Known limitations

  • 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/health reports 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_availability always reports availability_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.

About

Open multi-provider travel intelligence and itinerary orchestration platform built with MCP and LangGraph - live flight/hotel/FX/routing data, never fabricated.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /