Drupal is a registered trademark of Dries Buytaert
Release: Leaflet 10.4.11 Minor update available for module leaflet (10.4.11). Release: Session Inspector 1.0.8 Minor update available for module session_inspector (1.0.8). Release: Migrate QA 2.0.4 Minor update available for module migrate_qa (2.0.4). Release: CKEditor Description List 3.0.0 Major update available for module ckeditor_descriptionlist (3.0.0). Release: FlowDrop 2.4.0 Minor update available for module flowdrop (2.4.0). Release: JWT Token Refresh 1.0.4 Minor update available for module jwt_token_refresh (1.0.4). Release: ConReg 1.0.0-beta1 First beta version released for module conreg (1.0.0-beta1). Release: AI Image Studio 1.0.0-beta8 New beta version released for module ai_image_studio (1.0.0-beta8). Usage Milestone: Role Theme Switcher Module role_theme_switcher crossed 1,000 active installs. Module Revived: Decoupled Router 2.0.7 Module decoupled_router updated after 11 months of inactivity (2.0.7).

This module allows Drupal sites to act as OAuth 2.0 servers. It enables your Drupal site to authenticate users from other applications using the OAuth 2.0 protocol.

Built by a human using an AI assistant: ๐Ÿค– โž• ๐Ÿง 

A lot of the code in this module has been created using an AI assistant using Strikethroo & Kenkeep for improved Drupal code generation.

Overview

Adds OAuth2 scope checks to MCP Server tool calls. Set a tool's authentication mode to Required, pick the scopes an access token has to carry, and calls arriving without them are refused before the tool runs.

The module is opt-in and self-contained. Core MCP Server has no OAuth dependency; enable this and the /_mcp route starts accepting Bearer tokens alongside cookies, uninstall it and you are back to cookie auth plus Drupal permissions.

Built on Simple OAuth and Simple OAuth 2.1. Scope policy is stored on MCP Server Tool Bridge tool configurations, so that module is what makes enforcement useful.

Key capabilities

Per-tool scope policy

Each tool carries its own authentication mode and required scope list. A read-only lookup tool and a node-deleting tool can sit on the same server under different rules.

Policy travels with config

Settings are stored as third-party settings on the tool configuration entity, so they export to YAML and deploy with everything else. No separate OAuth config to keep in sync.

Correct 401 and 403

A missing or invalid token gets 401 with a WWW-Authenticate challenge. A valid token missing a scope gets 403. Clients can tell "log in" apart from "you cannot do this".

Scope enforcement, from config to refused call

The policy:

one details element added to the tool form. Authentication mode plus the scopes a token must carry.

The enforcement:

the call never reaches the tool. Refusal happens on the SDK request event, before execution.

Configuring a tool

  1. Go to Configuration ยป Web services ยป MCP Server ยป Tools and edit a tool.
  2. Open the OAuth2 Authorization details element.
  3. Set Authentication mode to Required. The scope selector appears.
  4. Pick the Required scopes. All of them must be present on the token; the check is an AND, not an OR.
  5. Save. The export for that tool now carries a third_party_settings.mcp_server_oauth block.
The scope options are aggregated from scopes already selected on other tool configurations, so on a fresh site the list starts empty. Create the scopes you need in your OAuth2 Scope configuration first, assign them to one tool, and they become available to the rest.

What happens on a call

The module subscribes to the PHP SDK's RequestEvent and inspects every CallToolRequest before the SDK dispatches it:

  1. Find the tool configuration entity whose ID matches the requested tool name. No match means no policy, and the call proceeds.
  2. Read authentication_mode. Anything other than required and the call proceeds.
  3. Read the Bearer token from the request, validate it through Simple OAuth's resource server, load the matching oauth2_token, confirm it is not revoked, and collect its scope names.
  4. Compare against the required list. If every required scope is present, the call proceeds.
  5. Otherwise throw McpAuthorizationDeniedException, which the controller turns into an HTTP error.

Token extraction is deliberately defensive. A malformed header, a failed signature check, a revoked token, or a storage error all resolve to "no scopes" rather than an exception, which then falls through to the refusal path below.

How a refusal is reported

Situation Response No Bearer token, or the token failed validation 401 authentication_required, with WWW-Authenticate: Bearer realm="mcp_server" Valid token, but missing at least one required scope 403 insufficient_scope

The distinction is drawn from whether any scopes were recoverable at all. An empty scope set means the caller is effectively unauthenticated, so the response invites credentials. A non-empty set that falls short means the caller is known but not permitted, so retrying with the same token is pointless and the response says so.

A separate subscriber catches JSON-RPC error code -32001 on the way out and attaches the same 401 status and challenge header, so authentication failures raised elsewhere in the stack surface consistently.

What is and is not gated

Only tools backed by a tool configuration entity can be gated. Tool plugins registered directly in code by a module are never matched, so they run without a scope check no matter what is configured elsewhere. This is deliberate, but it means "enable the OAuth module" is not by itself a statement about the whole server.

Reaching the endpoint at all still requires MCP Server's own access mcp server permission, and the tool's own access check still runs. Scope enforcement is a layer on top of both, not a replacement for either.

Discovery metadata

MCP clients discover how to authenticate before they call anything. Two pieces are handled for you:

  • The /.well-known/oauth-protected-resource document (RFC 9728) gets every scope configured across enabled tools merged into its scopes_supported list, deduplicated against whatever Simple OAuth already advertises.
  • That response is tagged with mcp_server:discovery, so changing a tool's scopes invalidates the published metadata instead of leaving a stale document cached.

Dynamic client registration is a declared dependency, since MCP clients are expected to register themselves rather than be provisioned by hand.

Turning it off

Uninstalling leaves the third_party_settings.mcp_server_oauth blocks in your tool YAML. Nothing reads them while the module is out, so they sit dormant, and re-enabling restores every policy without reconfiguration.

The /_mcp route drops back to cookie-only authentication on the next cache rebuild.

Other authentication schemes

The gating mechanism is not OAuth-specific. For API keys, mTLS, or a policy of your own, subscribe to the SDK's RequestEvent in a custom module and throw McpAuthorizationDeniedException to refuse a call. This module is a worked example of that contract rather than a privileged one.

Architecture

  • Authorization subscriber: the enforcement point on RequestEvent, matching tool names to configuration entities and applying policy.
  • Scope validator: validates the Bearer token through Simple OAuth's resource server and resolves it to a list of scope names, returning an empty list on any failure.
  • Scope discovery service: aggregates, deduplicates, and sorts scopes across enabled tool configurations. Feeds both the form's options and the published metadata.
  • Route subscriber: appends oauth2 to the MCP route's _auth option, keeping core's route free of a Simple OAuth dependency.
  • Metadata and error subscribers: enrich the protected-resource document, tag it for invalidation, and normalise authentication errors to 401 with a challenge header.
  • Form alter and entity builder: add the OAuth details element to the tool form and persist the values as third-party settings.

Related projects

Resources

Credits

Maintained by the MCP Server contributors.

Activity

Tracked releases
2
Tracked since
Jul 2026
Latest release
3 weeks ago
Releases (12 mo)
2 ▲ from 0
Maintenance
Active

Releases

Version Type Release date
1.0.0-alpha1 Pre-release Jul 29, 2026
1.x-dev Dev Jul 2, 2026