drupal-mcp
MCP server for Drupal sites via the core JSON:API. List, search, create, update, and delete nodes / taxonomy terms / users on any Drupal 10/11 site with the jsonapi module enabled.
Works two ways:
- Claude Code plugin — install via the
lucaspretti-plugins marketplace and Claude prompts you for the env vars.
- Standalone MCP —
node drupal-mcp.js with env vars set. Plug into Claude Desktop, Cline, or any other MCP-compatible client.
Why JSON:API and not the older Drupal MCP module?
The contrib drupal/mcp module currently has ~250 installs and is in flux ("merging with the MCP Server module"). JSON:API is in Drupal core, stable, and standard. This server is a thin wrapper around endpoints your site already exposes — no new module to maintain on the Drupal side.
Drupal-side setup (one-time)
Required modules
drush en jsonapi serialization basic_auth -y
jsonapi — exposes /jsonapi/* endpoints. Core module.
serialization — JSON:API dependency. Core module.
basic_auth — required for Authorization: Basic header authentication. Core module, not enabled by default. Without it, only anonymous reads work; every write returns 401.
JSON:API writes
By default JSON:API is read-only. If you need create / update / delete, flip the switch:
drush config:set jsonapi.settings read_only false -y
Or in config/sync/jsonapi.settings.yml (CMI-managed sites):
read_only: false
Keep read_only: true for read-only deployments — the plugin still works for drupal_list_* / drupal_get_node / drupal_query_jsonapi.
Bot role + user
Two patterns, pick one based on how much you trust the bot:
A) Admin role (simplest, recommended for full-access bots). Setting is_admin: true bypasses every permission check, same as the default administrator role. The single setting + a strong password gates all access.
langcode: en
status: true
dependencies: {}
id: mcp_bot
label: 'MCP bot'
weight: 10
is_admin: true
permissions: {}
B) Scoped role (when you want explicit limits). List exactly the perms the bot may use. Note that administer nodes alone does not grant create / edit / delete on bundles — those need either bundle-specific perms ('create article content', 'delete any page content', etc.) or 'bypass node access'.
is_admin: false
permissions:
- 'access content'
- 'access user profiles'
- 'bypass node access' # or per-bundle perms
- 'administer taxonomy'
- 'view own unpublished content'
Then create the user:
drush user:create mcp_bot --password='<strong-pw>'
drush user:role:add mcp_bot mcp_bot
Store the password in your secrets manager.
Install (Claude Code plugin)
/plugin marketplace add lucaspretti/claude-plugins
/plugin install drupal-mcp@lucaspretti-plugins
Set these env vars (via shell, .env, or your secrets manager):
DRUPAL_BASE_URL=https://your-site.example.com
DRUPAL_USER=mcp_bot
DRUPAL_PASSWORD=••••••••
Install (standalone)
git clone https://github.com/lucaspretti/drupal-mcp.git
cd drupal-mcp
npm install
cp .env.example .env # fill in
node drupal-mcp.js
In your MCP client config (Claude Desktop claude_desktop_config.json, Cline, etc.):
{
"mcpServers": {
"drupal": {
"command": "node",
"args": ["/absolute/path/to/drupal-mcp/drupal-mcp.js"],
"env": {
"DRUPAL_BASE_URL": "https://your-site.example.com",
"DRUPAL_USER": "mcp_bot",
"DRUPAL_PASSWORD": "••••••••"
}
}
}
}
Tools
| Tool | What it does |
|---|
drupal_list_nodes | List nodes of a bundle, with filter / sort / paginate |
drupal_get_node | Fetch one node by UUID |
drupal_create_node | Create a node (POST) |
drupal_update_node | Patch attributes / relationships |
drupal_delete_node | Delete by UUID (irreversible) |
drupal_list_taxonomy_terms | List terms in a vocabulary |
drupal_list_users | List users |
drupal_query_jsonapi | Arbitrary GET against /jsonapi/* (escape hatch) |
Filter shorthand: { field_category: '<uuid>' } → filter[field_category]=<uuid>. Use { field: { value, operator } } for non-equality operators.
CLI flags
Each env var has an equivalent flag, useful when running outside an .env-aware shell:
node drupal-mcp.js \
--base-url=https://your-site.example.com \
--user=mcp_bot \
--password=•••• \
--jsonapi-prefix=/jsonapi \
--timeout=30000
node drupal-mcp.js --help for the full list.
Troubleshooting
Error: fetch failed (cause: getaddrinfo ENOTFOUND <host>${var_name})
Claude Code's ${VAR} interpolation in .mcp.json substitutes from process.env, not from the settings.json env block alone. When a referenced var is unset upstream, the literal string ${var_name} is passed to the spawned process and concatenated into the URL.