Skip to main content
Glama

Agentic MCP Itinerary — PoC

An MCP server that internally runs an LLM agent (Gemini Flash + LangGraph) and orchestrates multiple downstream MCP servers. The client (Claude Desktop, ChatGPT) sees a clean interface with persistent state between iterations.

Concept

Claude Desktop / ChatGPT
        │
        │  MCP (HTTP/SSE + OAuth 2.1)
        ▼
┌─────────────────────────────────────┐
│         travel-agent (este repo)    │
│  FastMCP server + LangGraph agent   │
│                                     │
│  ┌──────┐  ┌────────┐  ┌──────────┐│
│  │Vuelos│  │Hoteles │  │Actividad.││  ← MCP mocks STDIO
│  └──────┘  └────────┘  └──────────┘│
└─────────────────────────────────────┘

Why is this different? No company yet offers a "vertical agent packaged as an MCP server." This PoC demonstrates the pattern: the client only sees 4-5 clean tools, but behind them is an agent with memory, parallel fan-out, and persistent state.


Related MCP server: ts-travel-mcp-server

Stack

Component

Technology

Exposed MCP Server

FastMCP 3.1.1 (streamable-http)

Internal Agent

LangGraph (StateGraph + parallel fan-out)

LLM Model

Gemini Flash (gemini-2.0-flash)

Auth

OAuth 2.1 Authorization Code Flow + JWT HS256

Checkpointing

MemorySaver (in-memory, sufficient for PoC)

Downstream MCP

Official MCP SDK (mcp.client.stdio)

Mocks

3 FastMCP servers STDIO (flights, hotels, activities)

Deploy

Railway (RAILPACK + pyproject.toml)


Exposed Tools (Public API)

Tool

Parameters

Description

create_itinerary

requirements: str

Creates a complete draft (flights + hotel + activities in parallel)

refine_itinerary

itinerary_id: str, change_request: str

Refines an existing draft

get_itinerary

itinerary_id: str

Retrieves the current state

list_itineraries

Lists all active itineraries

confirm_itinerary

itinerary_id: str

Confirms and generates a confirmation_code


Deploy on Railway

URLs

Railway IDs

  • Project: e50da57f-ee0b-47a3-81a3-55556fe6de0d

  • Service: 09065312-ac84-4876-b9c9-dd5d6439f1d4

  • Environment: 09b3f0c9-e5ad-4f61-b351-275bbcffd5ad

Required Environment Variables

Variable

Description

GEMINI_API_KEY

Google Gemini API key

MCP_USERNAME

Username for OAuth login

MCP_PASSWORD

Password for OAuth login

MCP_JWT_SECRET

Secret to sign JWT (generated with secrets.token_urlsafe(32))

MCP_BASE_URL

Public server URL (to build redirect URIs)


Auth: OAuth 2.1 Authorization Code Flow

Full Flow

1. Claude Desktop detecta el MCP server
2. Descubre /.well-known/oauth-authorization-server
3. Redirige al usuario a /authorize
4. El servidor redirige a /oauth/authorize (form de login HTML)
5. Usuario introduce user/pass → POST /oauth/authorize
6. Servidor valida credenciales (MCP_USERNAME / MCP_PASSWORD)
7. Emite auth code → redirect a Claude Desktop
8. Claude Desktop intercambia code → JWT en /token
9. JWT usado como Bearer en todas las llamadas MCP

Implementation

  • server/auth.py: SimpleOAuthProvider (extends FastMCP's OAuthProvider)

  • JWT HS256, 1h validity

  • Auth codes: 5 min validity

  • PKCE (S256) supported

  • /health remains public without auth


Configure Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "travel-agent": {
      "type": "http",
      "url": "https://travel-agent-production-c1c4.up.railway.app/mcp"
    }
  }
}

No headers — Claude Desktop manages the OAuth flow automatically. The first time, it will open the browser for login.


Local Development

Requirements

pip install -e ".[dev]"

Start Server

PYTHONPATH=server MCP_USERNAME=alexguerra MCP_PASSWORD=tu_pass \
  MCP_JWT_SECRET=dev_secret python3 server/main.py

Smoke test

PYTHONPATH=server python3 tests/smoke_test.py

Verify syntax

PYTHONPATH=server python3 -m py_compile server/main.py server/auth.py server/agent.py

Project Structure

agentic-mcp-itinerary/
├── server/
│   ├── main.py          # FastMCP server (4 tools + OAuth + /health)
│   ├── auth.py          # SimpleOAuthProvider (OAuth 2.1 + JWT)
│   ├── agent.py         # LangGraph graph con fan-out paralelo
│   ├── state.py         # ItineraryState TypedDict + checkpointer
│   └── tools/
│       ├── flights.py   # Cliente MCP → mock vuelos
│       ├── hotels.py    # Cliente MCP → mock hoteles
│       └── activities.py # Cliente MCP → mock actividades
├── mocks/
│   ├── flights_mcp.py   # Mock server vuelos (FastMCP STDIO)
│   ├── hotels_mcp.py    # Mock server hoteles (FastMCP STDIO)
│   └── activities_mcp.py # Mock server actividades (FastMCP STDIO)
├── tests/
│   └── smoke_test.py    # Test end-to-end básico
├── docs/
│   └── OAUTH_PLAN.md    # Spec del OAuth (referencia de diseño)
├── pyproject.toml       # Deps para RAILPACK
├── railway.toml         # Builder=RAILPACK, startCommand
└── claude_desktop_config.json  # Config para Claude Desktop (sin Bearer manual)

Key Decision Log

Decision

Discarded Alternative

Reason

RAILPACK + pyproject.toml

nixpacks

nixpacks fails on pip inside immutable env

OAuth 2.1 Authorization Code

Static Bearer token

Claude Desktop manages native OAuth; more production-ready

JWT HS256 in-memory

Token DB

PoC — no persistent state between restarts

FastMCP 3.1.1 OAuthProvider

Manual auth with Starlette

FastMCP integrates the flow with the MCP transport

MemorySaver

SQLite/Redis

Sufficient for local PoC; easy to migrate to SqliteSaver

Gemini Flash

Claude Haiku

Codex had credential conflict with Anthropic


Next Steps (post-PoC)

  • [ ] Test in Claude Desktop — verify full OAuth flow

  • [ ] Real persistenceSqliteSaver or Postgres for state between restarts

  • [ ] Real downstream MCPs — replace mocks with real APIs (Amadeus, Booking, etc.)

  • [ ] Multi-user — User DB instead of env vars

  • [ ] Rate limiting — by JWT token

  • [ ] Telemetry — LangSmith or similar to trace the internal agent

Related MCP Connectors

Related MCP Servers