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

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:

  1. 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.
  2. 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):

  1. Create (or edit) an application, and assign it a user and a customer (chain) to restrict the data to that customer. If the application is for internal Fideltour processes and needs to query any chain, leave customer empty and check Internal application (see Internal applications).
  2. In the Scopes section, check the relevant boxes. For a "documentation and logs" token, check only docs:read and logs:read.
  3. Save. The client_id / client_secret credentials 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 customer field 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_code to the MCP tools, and list_customers / search_customer return all chains.
  • Scopes still apply the same way: an internal app with only contacts:read can'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.