Drupal + MCP = AI-driven CMS
create_node, upload_media, publish… from Claude or Cursor
CMS platforms are silos. To publish an article, you open the back office, fill in fields, upload images, set taxonomy, then publish. What if your AI assistant — Claude, ChatGPT, or Cursor — could do it for you, straight from a conversation?
That is exactly what the Model Context Protocol (MCP) enables: an open protocol that standardizes how AI models talk to external systems. By turning Drupal into an MCP server, you expose actions (create_node, upload_media, publish_node…) that any MCP client can invoke.
At VOID, we built a Drupal module that does just that. Here is how.
1MCP in 2 minutes
Model Context Protocol (MCP)
Created by Anthropic, MCP is an open protocol that defines how AI models interact with external systems in a standardized way. Think of it as a universal API for AI — instead of building a custom integration for every tool, one protocol connects them all.
Tools
Actions the AI can run. In our case: create_node, upload_media, publish_node…
Resources
Data the AI can read. Here: content types, taxonomies, Drupal field structure.
Prompts
Contextual instructions the server gives the AI to guide actions (e.g. "articles must include an image field").
Architecture: Drupal as an MCP server
┌─────────────────┐ ┌──────────────────────────────────┐
│ MCP client │ │ DRUPAL (MCP server) │
│ │ JSON │ │
│ • Claude │ ◄─────► │ ┌──────────────────────────┐ │
│ • ChatGPT │ REST │ │ drupal_mcp module │ │
│ • Cursor │ │ │ │ │
│ • Continue.dev │ │ │ Tools: │ │
│ │ │ │ • create_node │ │
└─────────────────┘ │ │ • upload_media │ │
│ │ • update_node │ │
│ │ • publish_node │ │
│ │ • list_content_types │ │
│ │ • get_node │ │
│ │ • list_taxonomy_terms │ │
│ │ • delete_node │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼───────────────┐ │
│ │ Drupal Core APIs │ │
│ │ Node, Media, Taxonomy, │ │
│ │ Field, User, File │ │
│ └──────────────────────────┘ │
└──────────────────────────────────┘2MCP tools exposed by the module
The drupal_mcp module exposes 8 tools covering essential CRUD operations. Each tool maps to an authenticated REST route.
create_node
Create content (article, page, landing page)
Params: type, title, body, fields, status
update_node
Update existing content
Params: nid, fields, revision_message
upload_media
Upload an image or file
Params: file (base64), filename, alt, media_type
list_content_types
List available content types
Params: —
get_node
Fetch a node with its fields
Params: nid, include_fields
list_taxonomy_terms
List terms in a vocabulary
Params: vocabulary_id
delete_node
Delete content
Params: nid, confirm
publish_node
Publish / unpublish content
Params: nid, status
Extensibility
The module is designed to be extended. Adding a new tool (e.g. create_menu_link, manage_blocks, clear_cache) means creating a new controller and declaring the route in drupal_mcp.routing.yml. The MCP protocol handles discovery automatically.
3Implementation: module structure
drupal_mcp/
├── drupal_mcp.info.yml # Module declaration
├── drupal_mcp.routing.yml # REST routes (/mcp/tools/*)
├── drupal_mcp.permissions.yml # Granular permissions
├── drupal_mcp.services.yml # Services (auth, rate limit)
├── src/
│ ├── Controller/
│ │ ├── McpDiscoveryController.php # GET /mcp — tool list
│ │ ├── CreateNodeController.php # POST /mcp/tools/create_node
│ │ ├── UpdateNodeController.php # PATCH /mcp/tools/update_node
│ │ ├── GetNodeController.php # GET /mcp/tools/get_node/{nid}
│ │ ├── DeleteNodeController.php # DELETE /mcp/tools/delete_node/{nid}
│ │ ├── PublishNodeController.php # POST /mcp/tools/publish_node
│ │ ├── UploadMediaController.php # POST /mcp/tools/upload_media
│ │ ├── ListContentTypesController.php # GET /mcp/resources/content_types
│ │ └── ListTaxonomyController.php # GET /mcp/resources/taxonomy/{vid}
│ ├── Authentication/
│ │ └── McpTokenAuth.php # Bearer token validation
│ └── EventSubscriber/
│ └── McpAuditSubscriber.php # Log all operations
└── config/
└── install/
└── drupal_mcp.settings.yml # Default settings# MCP discovery — lists all available tools
drupal_mcp.discovery:
path: '/mcp'
defaults:
_controller: '\Drupal\drupal_mcp\Controller\McpDiscoveryController::list'
requirements:
_permission: 'access mcp api'
methods: [GET]
# Tool: Create a node
drupal_mcp.create_node:
path: '/mcp/tools/create_node'
defaults:
_controller: '\Drupal\drupal_mcp\Controller\CreateNodeController::execute'
requirements:
_permission: 'mcp create content'
methods: [POST]
# Tool: Upload media
drupal_mcp.upload_media:
path: '/mcp/tools/upload_media'
defaults:
_controller: '\Drupal\drupal_mcp\Controller\UploadMediaController::execute'
requirements:
_permission: 'mcp upload media'
methods: [POST]
# Tool: Publish / unpublish
drupal_mcp.publish_node:
path: '/mcp/tools/publish_node'
defaults:
_controller: '\Drupal\drupal_mcp\Controller\PublishNodeController::execute'
requirements:
_permission: 'mcp publish content'
methods: [POST]<?php
namespace Drupal\drupal_mcp\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class CreateNodeController extends ControllerBase {
public function execute(Request $request): JsonResponse {
$data = json_decode($request->getContent(), TRUE);
// Validation des paramètres requis
if (empty($data['type']) || empty($data['title'])) {
return new JsonResponse([
'error' => 'Missing required fields: type, title',
], 400);
}
$node = \Drupal\node\Entity\Node::create([
'type' => $data['type'],
'title' => $data['title'],
'body' => [
'value' => $data['body'] ?? '',
'format' => $data['format'] ?? 'full_html',
],
'status' => $data['status'] ?? 0, // brouillon par défaut
]);
// Champs dynamiques (field_image, field_tags, etc.)
if (!empty($data['fields'])) {
foreach ($data['fields'] as $field_name => $value) {
if ($node->hasField($field_name)) {
$node->set($field_name, $value);
}
}
}
$node->save();
return new JsonResponse([
'success' => TRUE,
'nid' => (int) $node->id(),
'uuid' => $node->uuid(),
'url' => $node->toUrl()->toString(),
'status' => $node->isPublished() ? 'published' : 'draft',
], 201);
}
}<?php
namespace Drupal\drupal_mcp\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class UploadMediaController extends ControllerBase {
public function execute(Request $request): JsonResponse {
$data = json_decode($request->getContent(), TRUE);
if (empty($data['file']) || empty($data['filename'])) {
return new JsonResponse([
'error' => 'Missing required fields: file (base64), filename',
], 400);
}
// Décoder le fichier base64
$file_data = base64_decode($data['file']);
$directory = 'public://mcp-uploads/' . date('Y-m');
\Drupal::service('file_system')->prepareDirectory(
$directory,
\Drupal\Core\File\FileSystemInterface::CREATE_DIRECTORY
);
$file = \Drupal::service('file.repository')->writeData(
$file_data,
$directory . '/' . $data['filename'],
\Drupal\Core\File\FileExists::Rename
);
// Créer l'entité Media
$media = \Drupal\media\Entity\Media::create([
'bundle' => $data['media_type'] ?? 'image',
'name' => $data['alt'] ?? $data['filename'],
'field_media_image' => [
'target_id' => $file->id(),
'alt' => $data['alt'] ?? $data['filename'],
],
'status' => 1,
]);
$media->save();
return new JsonResponse([
'success' => TRUE,
'mid' => (int) $media->id(),
'fid' => (int) $file->id(),
'url' => \Drupal::service('file_url_generator')
->generateAbsoluteString($file->getFileUri()),
], 201);
}
}4Discovery endpoint: GET /mcp
The /mcp entry point returns the list of available tools in MCP format. This is what the client (Claude, Cursor…) calls first to know what it can do.
{
"name": "drupal-mcp-server",
"version": "1.0.0",
"description": "Drupal CMS — MCP Server",
"tools": [
{
"name": "create_node",
"description": "Create a new content node in Drupal",
"inputSchema": {
"type": "object",
"properties": {
"type": { "type": "string", "description": "Content type machine name" },
"title": { "type": "string", "description": "Node title" },
"body": { "type": "string", "description": "Body content (HTML)" },
"status": { "type": "integer", "description": "1=published, 0=draft" },
"fields": { "type": "object", "description": "Additional fields" }
},
"required": ["type", "title"]
}
},
{
"name": "upload_media",
"description": "Upload an image or file to Drupal media library",
"inputSchema": {
"type": "object",
"properties": {
"file": { "type": "string", "description": "Base64 encoded file" },
"filename": { "type": "string", "description": "File name with extension" },
"alt": { "type": "string", "description": "Alt text for images" },
"media_type": { "type": "string", "description": "Media bundle (image, document)" }
},
"required": ["file", "filename"]
}
}
],
"resources": [
{ "uri": "drupal://content_types", "name": "Content Types", "description": "Available content types" },
{ "uri": "drupal://taxonomy/{vid}", "name": "Taxonomy Terms", "description": "Terms for a vocabulary" }
]
}5Security: do not expose your CMS without guardrails
Golden rule
An MCP server exposes write capabilities on your CMS. Without rigorous hardening, it is an open door. Every layer below is essential.
🔑 Authentication
- • Bearer token in the
Authorizationheader - • Token bound to a Drupal user (with their roles and permissions)
- • Periodic token rotation
- • Immediate rejection of requests without a valid token
🛡️ Granular permissions
- •
access mcp api— access the MCP API - •
mcp create content— create content - •
mcp upload media— upload media - •
mcp publish content— publish (separate from create) - •
mcp delete content— delete (restricted)
⏱️ Rate limiting
- • Max 60 requests/minute per token
- • Max 10 uploads/minute (large files)
- • Max file size: configurable (default 10 MB)
- •
429 Too Many RequestswithRetry-After
📋 Audit log
- • Every MCP operation is logged (action, user, timestamp, payload)
- • Integration with Drupal’s native logging
- • Alerts on sensitive operations (delete, bulk create)
- • Export for compliance
access mcp api:
title: 'Access MCP API'
description: 'Access the MCP discovery endpoint and tool listing'
mcp create content:
title: 'MCP: Create content'
description: 'Create nodes via MCP tools'
restrict access: true
mcp upload media:
title: 'MCP: Upload media'
description: 'Upload files and create media entities via MCP'
restrict access: true
mcp publish content:
title: 'MCP: Publish content'
description: 'Publish or unpublish nodes via MCP'
restrict access: true
mcp delete content:
title: 'MCP: Delete content'
description: 'Delete nodes via MCP (dangerous)'
restrict access: true6Concrete use cases
AI-assisted content production
A writer uses Claude to draft an article, then publishes it straight to Drupal without leaving the conversation.
Prompt in Claude Desktop:
"Write an 800-word article on e-commerce trends in Morocco in 2026, then publish it to our Drupal site as a draft with the category 'Digital'."
Batch content migration
Migrate hundreds of articles from a CSV or legacy CMS via a conversational prompt, without writing a migration script.
Prompt in Cursor:
"Read articles.csv and for each row create an 'article' node on Drupal with title, body, and field_category."
Automated editorial workflow
The AI creates draft content, adds images, applies tags, and the editor only has to review and publish.
Typical flow:
AI create_node (draft) → AI upload_media → AI update_node (attach image) → Human review → Human publish_node
Developer + Cursor = 10× productivity
A developer codes in Cursor and can seed or test content on Drupal without opening the browser.
In Cursor:
"Create 5 test articles on Drupal with lorem titles and publish them to test the listing page."
7Demo: from conversation to published content
Create an article "The 5 UX Trends for 2026" on our Drupal site with a body structured in H2 and a strong intro. Type: article. Status: draft.
MCP call: create_node
{
"type": "article",
"title": "The 5 UX Trends for 2026",
"body": "<p>In 2026, user experience...</p><h2>1. Conversational AI</h2>...",
"status": 0,
"fields": {
"field_category": [{"target_id": 42}]
}
}✅ Node #1847 created as draft — view in Drupal
Add a hero image and publish the article.
MCP calls: upload_media → update_node → publish_node
✅ Image uploaded (mid: 523) — Node #1847 updated — Published
8Configure the MCP client
To connect Claude Desktop, Cursor, or any other MCP client to your Drupal site, declare the server in the client configuration.
{
"mcpServers": {
"drupal": {
"url": "https://your-site.com/mcp",
"transport": "rest",
"headers": {
"Authorization": "Bearer YOUR_MCP_TOKEN"
}
}
}
}Your Drupal, driven by AI
VOID builds and integrates MCP servers on Drupal to connect your CMS to AI assistants. Accelerated content production, automated editorial workflows, and AI-assisted development.