Drupal MCP Server

Drupal MCP Server

Drive your CMS with AI: Claude, ChatGPT, Cursor

CMS & Artificial IntelligenceMarch 27, 202620 min read
🐉MCP🤖

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.

8
MCP tools exposed
REST
Standard transport
0
Contrib modules required
10+
Compatible clients

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

modules/custom/drupal_mcp/
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
drupal_mcp.routing.yml
# 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]
CreateNodeController.php
<?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);
  }

}
UploadMediaController.php
<?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.

GET /mcp — JSON response
{
  "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 Authorization header
  • • 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 Requests with Retry-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
drupal_mcp.permissions.yml
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: true

6Concrete 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

U

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.

AI

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

U

Add a hero image and publish the article.

AI

MCP calls: upload_mediaupdate_nodepublish_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.

claude_desktop_config.json / .cursor/mcp.json
{
  "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.

Frequently asked questions

What is MCP (Model Context Protocol)?
MCP is an open protocol created by Anthropic that standardizes communication between AI models (Claude, ChatGPT, Cursor) and external systems. It defines Tools (actions), Resources (data), and Prompts that the AI can invoke in a structured way.
Why turn Drupal into an MCP server?
A Drupal MCP server lets AIs create content, upload media, manage taxonomies, and drive editorial workflows directly from a conversation. It automates repetitive tasks and speeds up content production while keeping Drupal as the source of truth.
Is it secure to expose Drupal via MCP?
Yes, provided you implement token (Bearer) authentication, granular permissions per Drupal role, rate limiting, and an audit log of all operations. The MCP module relies on Drupal’s native permission system.
Which MCP clients are compatible?
Claude Desktop, ChatGPT (via plugins/actions), Cursor IDE, Continue.dev, and any client implementing the MCP protocol. The Drupal server exposes standard REST endpoints that any MCP client can consume.
🌱Eco-designed site