Travel Booking MCP Server
For a high level overview of how MCP servers work with Descope, see the MCP Server docs.
This example builds an MCP server for a consumer travel app, so your customers can search flights, check their trips, and book travel from Claude, ChatGPT, or any other MCP client. It covers what most B2C MCP servers need: consumer sign-in, scopes the customer approves, policies that decide who gets which tools, calls from your server to your own API, and an optional third-party integration.
If your customers are companies rather than individuals, see the Multi-Tenant MCP Server example instead.
Why Build Your Own Server
An MCP server is worth building when it fronts data and actions only your product has. Your customers can already connect Google Calendar to their MCP client directly, so a server that only wraps Google's API adds little. No generic connector can see a customer's bookings or book a seat on their behalf, though. That's the job of your server.
Once an agent can book and cancel real trips, three questions matter on every call: which customer signed in, which client is acting for them, and what each is allowed to do. Descope answers all three, so your server only has to check a scope and call your API.
How It Works
The server exposes five tools. Each tool requires one scope, and policies decide which customers and clients can receive each scope:
| Tool | Scope | Who gets it |
|---|---|---|
search_flights | mcp:flights.search | Every signed-in customer |
get_my_trips | mcp:trips.read | Every signed-in customer |
book_trip, cancel_trip | mcp:trips.write | Customers using a verified client |
set_price_alert | mcp:alerts.write | Premium members |
add_trip_to_calendar | mcp:calendar.write | Every signed-in customer who connects Google Calendar |
Behind the tools sit two backends. Your Bookings API is your own service, registered in Descope as a Resource, and the server reaches it with token exchange. Google Calendar is a third-party service, reached through a Connection that the customer links the first time they need it.
Step 1: Create the MCP Server and Its Scopes
The MCP server in Descope is where you define the scopes your tools check and the text customers see on the consent screen.
- Go to MCP Servers and create a server.
- Enable Dynamic Client Registration (DCR) or Client ID Metadata Documents (CIMD), so clients like Claude and ChatGPT can register themselves.
- Copy the Well-Known / discovery URL. Your server uses it to validate access tokens.
- Under MCP Server Scopes, add the five scopes from the table above, each with a consent description a customer would understand, such as "Book and cancel trips for you."
- On
mcp:calendar.write, set the Connection scopes to the Google Calendar scope your tool needs, for examplehttps://www.googleapis.com/auth/calendar.events. You create the Connection itself in Step 6.
Consider turning on Allow user to decline scope for mcp:trips.write and mcp:calendar.write. Customers who only want to browse can then decline booking and calendar access on the consent screen.
Step 2: Let Customers Sign In
Customers should sign in to your MCP server the same way they sign in to your app. Because the MCP server lives in the same Descope project, a returning customer lands on their existing user record, and your Bookings API sees the same user ID it already knows.
Set the sign-in methods in the server's User Consent Flow, which runs when a client sends the customer to authorize. Use the methods your app already offers, for example passkeys or social login with Google or Apple. After sign-in, the flow shows the consent screen with the scope descriptions from Step 1.
Next, decide which clients you trust. Newly registered clients are unverified by default. In the Client Registration Flow, or on the Clients page, mark the clients you trust to book travel, such as Claude and ChatGPT, as verified. Step 3 uses that status to keep booking away from clients you haven't reviewed.
Step 3: Decide Who Gets Which Tools
Policies set the outer limit on which scopes can land in a token, and the customer's consent picks within that limit. A scope no policy allows never appears on the consent screen. Three policies with User access as the grant type cover the tool table:
Rule name: Travel MCP - Everyone
Subjects: Any
Targets: Specified → Travel MCP Server
Scopes: mcp:flights.search, mcp:trips.read, mcp:calendar.write
Grant types: User accessRule name: Travel MCP - Booking from Verified Clients
Subjects: Custom conditions
client.status EQUAL "Verified"
Targets: Specified → Travel MCP Server
Scopes: mcp:trips.write
Grant types: User accessRule name: Travel MCP - Premium Price Alerts
Subjects: Custom conditions
user.roles CONTAINS "premium"
Targets: Specified → Travel MCP Server
Scopes: mcp:alerts.write
Grant types: User accessThe premium policy relies on a premium role. Create the role in your project, and have your billing system add or remove it with the Management SDK when a customer upgrades or cancels. Policies are evaluated when a token is issued, so the change applies to the next token the customer's client receives, with no change to your server. See Custom conditions for the other keys you can use.
Step 4: Connect the Server to Your Bookings API
The access token the client sends is issued for your MCP server, and its aud claim is the MCP server's URL. Your Bookings API should reject it. Instead, the server exchanges it for a new token whose audience is the Bookings API. The Bookings API then only trusts tokens minted for it, so a token issued for the MCP server can't be replayed against it, and it still sees the same customer.
To set this up, follow the setup in Calling External APIs from MCP Tools:
- Create a Resource for the Bookings API, with the scopes
bookings.readandbookings.write. - On the Clients page, create a client that represents the MCP server itself, and store its client ID and secret in the server's environment.
- Add a policy that lets that client exchange tokens for the Bookings API:
Rule name: Travel MCP Server - Bookings API
Subjects: Select clients → "Travel MCP Server"
Targets: Specified → Bookings API
Scopes: bookings.read, bookings.write
Grant types: Delegated access (token exchange)Token exchange never shows a consent screen, so this policy is what limits it. It's also why each tool checks its MCP scope before exchanging, and requests only the Bookings API scope it needs. The helper below sends the exchange request to the token endpoint:
import os
import httpx
from fastmcp.server.dependencies import get_access_token
TOKEN_URL = "__BaseURL__/oauth2/v1/apps/token"
BOOKINGS_API = "https://bookings.example.com"
async def bookings_api_token(scope: str) -> str:
"""Exchange the customer's MCP access token for a Bookings API token."""
async with httpx.AsyncClient() as http:
resp = await http.post(
TOKEN_URL,
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"client_id": os.environ["DESCOPE_CLIENT_ID"],
"client_secret": os.environ["DESCOPE_CLIENT_SECRET"],
"subject_token": get_access_token().token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"resource": BOOKINGS_API,
"scope": scope,
},
)
resp.raise_for_status()
return resp.json()["access_token"]On the Bookings API side, validate the token's iss, aud, exp, and scope as described in Using a Resource Token.
Step 5: Register Tools with Scopes
Each tool declares the scope it requires. The SDK rejects calls from tokens without that scope, so a customer on an unverified client can't book even if their client calls book_trip directly. The server never checks plans or client status itself, because the policies from Step 3 already decided what's in the token.
from fastmcp import FastMCP
from fastmcp.server.auth import require_scopes
mcp = FastMCP("travel")
@mcp.tool(auth=require_scopes("mcp:trips.read"))
async def get_my_trips() -> dict:
"""List the signed-in customer's upcoming trips."""
token = await bookings_api_token("bookings.read")
async with httpx.AsyncClient() as http:
resp = await http.get(
f"{BOOKINGS_API}/trips",
headers={"Authorization": f"Bearer {token}"},
)
resp.raise_for_status()
return resp.json()
@mcp.tool(auth=require_scopes("mcp:trips.write"))
async def book_trip(offer_id: str) -> dict:
"""Book a flight offer returned by search_flights."""
token = await bookings_api_token("bookings.write")
async with httpx.AsyncClient() as http:
resp = await http.post(
f"{BOOKINGS_API}/bookings",
headers={"Authorization": f"Bearer {token}"},
json={"offer_id": offer_id},
)
resp.raise_for_status()
return resp.json()The other tools follow the same pattern. search_flights and cancel_trip exchange for bookings.read and bookings.write, and set_price_alert requires mcp:alerts.write. For how scope checks differ from what appears in tools/list, see Scope Enforcement vs. tools/list.
Step 6: Add Trips to Google Calendar (Optional)
Adding a booked trip to the customer's calendar is where a third-party service fits in. It's a supporting feature, not the product, so the customer shouldn't have to connect Google at sign-in. With Adaptive Connect, the server asks for the connection the first time a customer calls add_trip_to_calendar:
- Create a Google Calendar Connection under Connections.
- Add a policy with Delegated access (token exchange) that lets the MCP server's client reach the Google Calendar Connection. Descope checks it whenever the server fetches the customer's Google token.
- In
add_trip_to_calendar, fetch the Connection token with the customer's access token. If the customer hasn't connected Google yet, return a connection URL to the client instead of failing. The customer approves access, and the tool succeeds on retry.
Because mcp:calendar.write is mapped to the Google Calendar scope in Step 1, Descope requests exactly that scope when the customer connects and returns a token with it when the server fetches. The Python MCP SDK wraps the fetch in get_connection_token().
Next Steps
- Read Calling External APIs from MCP Tools for more on token exchange from MCP servers, including exchanging for Connections.
- See Policies for every condition key and grant type you can use to shape what customers and clients receive.
- If you later serve business customers from the same server, see the Multi-Tenant MCP Server example.