MCP: Scopes and Tokens
How to restrict which MCP server tools each token can use via scopes.
HotelDataHub's MCP server (POST /mcp) exposes tools ("tools") grouped by domain
(documentation, logs, contacts, movements, customers, hotels, segments and loyalty). Each
token can be restricted to a subset of those tools via scopes, so, for example, one
person can have access to just documentation and logs, while another has access to just
contacts.
Table of contents¶
- How it works
- Scope catalog
- Which tool requires which scope
- Creating a token with scopes
- Internal applications (access to all customers)
- Behavior when using the token
- Internal users (JWT)
How it works¶
The identity behind a token (whether an API key or an OAuth2 token) is a Customer OAuth Application. That application carries a list of granted scopes. Authorization is applied at two points:
tools/list— the token only "sees" the tools it has scope for. The others don't appear, so the agent doesn't attempt to use them.tools/call— the scope is checked again before running a tool. If it's missing, the call returns an error:{"error": "Permiso denegado: se requiere el scope 'contacts:read'"}.
Scope catalog¶
Scopes follow the domain:action convention (read for reading, write for
creating/modifying):
| Scope | Allows |
|---|---|
docs:read |
Search the documentation |
logs:read |
Query HDH API logs, push requests and internal events (Elasticsearch) |
contacts:read |
Read contacts |
contacts:write |
Create/modify contacts |
movements:read |
Read movements (bookings) |
movements:write |
Create/modify movements |
customers:read |
Read customers (chains), hotels and connections |
hotels:read |
Read Fideltour hotels |
segments:read |
Read segments/audiences |
loyalty:read |
Read loyalty information |
Write scopes for domains that don't yet have a write tool (
customers:write,hotels:write,segments:write,loyalty:write) exist in the catalog for future use, but don't enable any tool today.
Which tool requires which scope¶
Each tool's scope is derived from its domain and whether it reads or writes. Examples:
| Tool | Scope |
|---|---|
search_documentation |
docs:read |
list_api_users, search_api_requests, get_api_request_body |
logs:read |
search_push_requests, get_push_request_body, list_loggable_integrations |
logs:read |
search_integration_events, search_internal_logs, search_logs |
logs:read |
search_contact_by_email, list_contacts, … |
contacts:read |
create_contact |
contacts:write |
list_movements, search_movement_by_localizer, … |
movements:read |
create_movement |
movements:write |
list_customers, get_customer_summary, list_connections, … |
customers:read |
list_hotels, get_hotel |
hotels:read |
list_segments, get_segment, get_segment_contacts |
segments:read |
list_operations, list_products, list_levels, … |
loyalty:read |
Creating a token with scopes¶
From the Django admin (Customer OAuth Applications):
- Create (or edit) an application, and assign it a
userand acustomer(chain) to restrict the data to that customer. If the application is for internal Fideltour processes and needs to query any chain, leavecustomerempty and check Internal application (see Internal applications). - In the Scopes section, check the relevant boxes. For a "documentation and logs"
token, check only
docs:readandlogs:read. - Save. The
client_id/client_secretcredentials are the token.
Internal applications (access to all customers)¶
A normal Customer OAuth Application is tied to a single chain: its tokens can only
query data for the assigned customer. For internal Fideltour processes that need to
query any chain, there's the Internal application flag (is_internal):
- It's set in the admin, on the same application screen, leaving the
customerfield empty (they're mutually exclusive: the admin won't let you save an internal app with a customer assigned). - The resulting token (whether via API key or OAuth2) can pass any
customer_codeto the MCP tools, andlist_customers/search_customerreturn all chains. - Scopes still apply the same way: an internal app with only
contacts:readcan't use tools from other domains.
An application without a customer and without is_internal is locked out: it can't
access any chain's data (the tools return Not authorized). This prevents an app
created by mistake without a customer from getting global access.
Behavior when using the token¶
With an API key (client_id:client_secret), listing tools:
curl -s -X POST https://<host>/mcp \
-H "X-API-Key: <client_id>:<client_secret>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
A token with ["docs:read","logs:read"] will only see search_documentation and the
logs tools (plus the generic tools that have no scope). If it tries to call
search_contact_by_email, it will get a permission error.
Internal users (JWT)¶
Users authenticated via JWT (internal staff) have no associated application and, therefore, are not limited by scopes: they continue to see and use all tools. Scopes only affect external customers via API key or OAuth2.