MCP Drupal
# MCP Drupal
Servidor MCP que automatiza la subida de artículos de blog a un sitio Drupal a partir de un Google Sheet + documentos de Google Docs con el contenido ya escrito.
No es una app aparte ni tiene interfaz propia: se usa hablando en lenguaje normal dentro de [Claude Code](https://claude.com/claude-code), como si le pidieras cualquier otra cosa.
## Arquitectura
```
Google Sheet (una fila = un artículo)
→ columna D enlaza a un Google Doc con el contenido
→ MCP (Node/TS) lee el Sheet y el Doc vía Google Drive/Sheets API
→ parsea título, meta SEO, intro y body
→ crea el artículo en Drupal vía JSON:API, como borrador
→ pinta la fila del Sheet en verde
```
Todo corre como un proceso externo (este MCP) que habla con Drupal por su **JSON:API del core** y con Google por su API — no requiere instalar nada en el hosting de Drupal ni acceso a Composer/SSH.
## Cómo funciona
Tenéis un Google Sheet con una fila por artículo. Cada fila enlaza a un documento de Google Docs con el texto ya escrito (título, meta descripción, intro y cuerpo). Cuando le pides al asistente que procese una fila:
1. Entra a esa fila y a ese documento — solo a los que le compartáis, nada más.
2. Separa el contenido: título, título SEO, meta descripción, intro y cuerpo, siguiendo un formato fijo dentro del documento (ver [Formato del documento](#formato-esperado-del-documento-de-contenido)).
3. Lo sube a Drupal como **borrador**. El bot nunca tiene permiso para publicar — eso siempre lo hace una persona a mano, después de revisar.
4. Marca la fila del Sheet en verde claro.
5. Devuelve el enlace directo a la edición del artículo en Drupal para revisión.
El bot no "entiende" el contenido — sigue una plantilla determinista. Si un documento se aparta del formato habitual, revisa el resultado en Drupal antes de publicar.
## Qué necesitas antes de empezar
- [Claude Code](https://claude.com/claude-code) instalado — el MCP se usa hablando con él, no tiene interfaz propia.
- [Node.js](https://nodejs.org) instalado, para compilar y ejecutar el proyecto.
- Tus propias credenciales de Google (`google-service-account.json`) y las credenciales de un usuario restringido en Drupal (ver [Configuración en Drupal](#configuración-necesaria-en-drupal) y [Credenciales de Google](#credenciales-de-google)).
## Instalación paso a paso
```bash
git clone <url-de-este-repo> mcp-drupal
cd mcp-drupal
npm install
```
Crear `.env` (copiar `.env.example` y rellenar):
```bash
cp .env.example .env
```
```
DRUPAL_BASE_URL=https://www.tudominio.es
DRUPAL_USER=mcp_bot
DRUPAL_PASSWORD=<contraseña del usuario mcp_bot>
```
- `DRUPAL_BASE_URL` no debería cambiar salvo que el sitio cambie de dominio.
- `DRUPAL_USER` es un usuario restringido creado a propósito para el bot: solo puede crear/editar artículos, no puede borrar, publicar ni administrar nada más del sitio.
- `DRUPAL_PASSWORD` te la debe pasar quien tenga acceso al panel de administración de Drupal.
Este archivo **nunca** se sube a git (está en `.gitignore`) ni se comparte fuera del equipo.
Coloca el archivo de credenciales de Google (ver [Credenciales de Google](#credenciales-de-google)) en la raíz del proyecto, con el nombre exacto:
```
google-service-account.json
```
Compilar:
```bash
npm run build
```
Registrar el servidor en Claude Code:
```bash
claude mcp add --scope user drupal-mcp -- node /ruta/completa/a/mcp-drupal/dist/index.js
```
Sustituye `/ruta/completa/a/` por la ruta real donde tengas la carpeta. Tiene que ser la ruta completa, no relativa, porque Claude Code puede arrancar desde cualquier carpeta. `--scope user` hace que el MCP quede disponible en cualquier proyecto que abras con Claude Code en tu ordenador.
Reinicia Claude Code para que la conexión quede activa.
**Comprobar que funciona:** pídele a Claude, en lenguaje normal, "lista las filas del Sheet de [nombre]". Si te devuelve las filas, todo está conectado. Si da error, revisa que `.env` y `google-service-account.json` estén en la raíz del proyecto (no en `src/` ni `dist/`), y que la carpeta de Drive esté compartida con el email de la cuenta de servicio con permiso Editor.
## Uso diario
1. Rellena una fila del Sheet: keyword, titular, tipo, enlace al documento con el contenido, slug sugerido, cliente.
2. Pídele al bot que la suba, en tus propias palabras: *"sube el artículo de la fila 5 del Sheet de agosto"* o *"sube todas las filas del sheet"*.
3. El bot localiza la fila, extrae el contenido del documento enlazado, y crea el artículo en Drupal como borrador (nunca publicado).
4. La fila del Sheet se pinta de verde claro automáticamente.
5. El bot devuelve el enlace directo de edición del artículo en Drupal.
6. **Paso manual, siempre:** quien revise añade la imagen (la genérica del proyecto, o la definitiva) en la pestaña Media y marca "Publicado".
## Herramientas disponibles
| Herramienta | Qué hace |
|---|---|
| `list_resource_types` | Lista los content types y recursos disponibles en Drupal |
| `list_nodes` | Lista nodos de un content type, con paginación y filtro por título |
| `get_node` | Trae un nodo concreto por UUID |
| `create_node` | Crea un nodo (título, body, intro, meta tags, relaciones opcionales) |
| `update_node` | Edita un nodo existente |
| `list_sheet_rows` | Lee las filas del Google Sheet (keyword, titular, tipo, documento, slug, cliente) |
| `get_article_content_from_doc` | Descarga un Google Doc y extrae título / meta título / metadescripción / intro / body en HTML, con tablas convertidas a listas |
| `mark_sheet_row_done` | Pinta una fila del Sheet en verde claro |
No hace falta llamarlas por su nombre — pídele al bot lo que quieres hacer en lenguaje normal y él elige la herramienta adecuada.
## Formato esperado del documento de contenido
El parseo (`src/googleClient.ts`, función `parseArticleHtml`) asume esta estructura en el Google Doc:
1. **Primera línea** (estilo Título 1) → título del artículo.
2. Línea `Title=...` → título SEO (meta tag), sustituye el valor por defecto.
3. Línea `Metadescription=...` → metadescripción SEO.
4. Los párrafos siguientes, **hasta el primer subtítulo** (h1/h2/h3) → van al campo `field_intro`.
5. Desde el primer subtítulo en adelante → van al `body`. Las tablas se convierten automáticamente en listas (`<ul>`), porque no se visualizan bien en Drupal.
Si algún documento no sigue exactamente este patrón, revisa el resultado en Drupal antes de publicar — el parser es determinista pero no "entiende" el contenido, solo sigue la estructura.
## Campos de Drupal usados (content type `article`)
| Campo Drupal | Contenido |
|---|---|
| `title` | Título del artículo |
| `field_intro` | Intro (HTML, formato `basic_html`) |
| `body` | Cuerpo del artículo (HTML, formato `basic_html`) |
| `field_article_meta_tags` | Texto con JSON `{"title": "...", "description": "..."}` |
| `field_article_media` | (Manual) referencia a un paragraph `media_slider` con la imagen |
La URL amigable se genera sola vía Pathauto a partir del título.
## Configuración necesaria en Drupal
1. **Módulos core activados** (Extender → sin Composer, ya vienen con Drupal): `JSON:API`, `HTTP Basic Authentication`.
2. **JSON:API en modo escritura**: `/admin/config/services/jsonapi` → "Accept all JSON:API create, read, update, and delete operations" (por defecto viene en solo lectura).
3. **Rol y usuario restringidos**: rol con permiso únicamente de crear/editar el content type `article` (nada de administración, nada de borrado). Usuario (ej. `mcp_bot`) con ese rol, autenticado por Basic Auth.
4. **Borrador por defecto**: `Estructura → Tipos de contenido → Article → Opciones de publicación` → casilla "Publicado" desmarcada como valor por defecto. Así todo artículo nuevo nace sin publicar, sin que el bot necesite ningún permiso especial. *Este cambio afecta a cómo nace cualquier artículo nuevo para cualquier editor humano también — pedid aprobación antes de aplicarlo.*
### Por qué el bot nunca publica
Drupal protege el campo `status` (publicado/no publicado) con el permiso `administer nodes`, que da control total sobre todo el contenido del sitio, no solo `article`. Para mantener el riesgo acotado a "solo articles, sin borrado", el bot no tiene ese permiso:
- **Nunca puede publicar ni despublicar** nada — solo crear borradores.
- Publicar sigue siendo un clic manual de alguien con permiso de admin, después de revisar.
Se evaluó **Content Moderation** (`Workflows` + `Content Moderation`, ambos core) como alternativa que evitaría este permiso tan amplio, pero asignar el flujo Editorial al content type Article cambiaría la interfaz de publicación para todo el equipo editorial, no solo para el bot — así que se descartó por ahora.
### La imagen es manual
El campo de imagen (`field_article_media`) no es un campo de imagen simple: es una referencia a un bloque `paragraph` tipo `media_slider`, que a su vez referencia un archivo directamente (el pie de foto va en el atributo `alt`). Crear ese bloque vía API requiere un permiso adicional (crear `paragraph`) que no se concede al bot, porque abre más superficie de la deseada. El código para hacerlo existe (`uploadFile` / `createMediaSliderParagraph` en `src/drupalClient.ts`) pero no está expuesto como herramienta activa.
**Por eso: quien revise el borrador debe añadir la imagen manualmente antes de publicar.**
## Credenciales de Google
Cada persona/cliente que use este MCP con su propia carpeta de Drive necesita su propio proyecto y credenciales — no se comparten entre personas.
### Crear un proyecto y una cuenta de servicio nueva
1. Ir a [console.cloud.google.com](https://console.cloud.google.com), crear un proyecto nuevo.
2. **APIs y servicios → Biblioteca**: habilitar `Google Drive API` y `Google Sheets API`.
3. **APIs y servicios → Credenciales → Crear credenciales → Cuenta de servicio**. Nombre libre (ej. `content-bot`). No hace falta asignar roles ni "Principales con acceso".
4. Entrar en la cuenta de servicio creada → pestaña **Claves → Agregar clave → Crear clave nueva → JSON**. Se descarga un archivo `.json` — es una credencial, trátala como una contraseña: no la subas a ningún sitio público ni la compartas por chat/email sin cifrar.
5. Copiar el email de la cuenta de servicio (tipo `nombre@proyecto.iam.gserviceaccount.com`).
6. En Google Drive, compartir la carpeta que contiene el Sheet y los documentos con ese email, con permiso **Editor** (el bot necesita poder pintar la fila del Sheet, no solo leerla).
7. Renombrar el `.json` descargado exactamente a `google-service-account.json` y colocarlo en la raíz del proyecto.
### Cambiar solo de carpeta de Drive (misma cuenta de servicio)
No hace falta tocar `.env` ni el `.json`. Solo comparte la nueva carpeta con el mismo email de la cuenta de servicio, y dile a Claude qué Sheet usar (por ID o URL) al pedirle que procese una fila.
### Cambiar de persona que opera el bot
La persona nueva se crea su propia cuenta de servicio (pasos de arriba), comparte su carpeta con el email de esa cuenta nueva, y sustituye el `google-service-account.json` antiguo por el nuevo. Con eso, la persona anterior deja de tener acceso y la nueva ya puede usarlo — sin tocar el `.env` ni recompilar nada. Del lado de Drupal no hace falta ningún cambio: el usuario del bot es del sitio, no de la persona que lo opera.
### Revocar el acceso a una cuenta de servicio anterior
Con las credenciales nuevas ya funcionando (confirma listando filas del Sheet):
- Simplemente deja de usar la cuenta de servicio anterior — como el `.json` que tienes ahora es el tuyo, esa cuenta ya no interviene en nada.
- Si además quieres que esa clave deje de ser válida por completo: entra en Drive → la carpeta compartida → quita el acceso al email antiguo. Eso corta el acceso aunque la clave siga existiendo en su Google Cloud.
## Seguridad
- El usuario de Drupal usado por el bot tiene permisos mínimos: crear/editar solo `article`, sin borrado, sin publicar, sin acceso de administración.
- Las credenciales (`.env`, `google-service-account.json`) nunca se suben a git (están en `.gitignore`) ni se comparten fuera del equipo.
- Cada persona/cliente que use este MCP debe generar sus propias credenciales de Google — no reutilizar las de otra persona.
TDQS
Scored across 8 tools
Each tool targets a clearly distinct resource and action: Drupal nodes, Drupal resource types, Google Sheet rows, and Google Doc extraction are all unambiguous. No two tools could reasonably be confused for the same operation.
All tool names follow a consistent snake_case verb-first pattern such as list_nodes, create_node, update_node, list_sheet_rows, and mark_sheet_row_done. The naming is predictable and easy to remember.
With 8 tools, the server is well scoped for its purpose: it covers Drupal node operations, content type discovery, and the Google Sheets/Docs content-import pipeline. Each tool earns its place.
The core ingestion workflow is well covered: read from Sheet and Doc, create/update Drupal nodes, list types, and mark rows done. The main gap is the lack of a delete_node operation, which would make full content lifecycle management more complete.