drupal
Skill5 starsChanged 8 months agoMITView on GitHub
Drupal site management: create, read, update, delete, and query entities, upload and attach files to fields, introspect field schemas, manage configuration, rebuild caches, view logs, and run arbitrary Drush commands. Use when the user asks to "create a node", "add a comment", "list tasks", "upload a file", "attach a PRD to a task", "attach an image to a node", "check Drupal status", "clear caches", "read config", "query the database", "introspect fields", "drupal", "drush", or any content management task involving a Drupal site.
2 other projects use a version of this.
Adopted by
Most recently taken up by
- gingerminds/claude-skillstheir own version, first seen Sep 23, 2026
- conjure-3301/skillstheir own version, first seen Aug 17, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- gingerminds/claude-skills★ 2same name
- conjure-3301/skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repositoryOne other file it gives its agents.
- CLAUDE.mdCLAUDE.md
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. About 0 are common, 3 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 26
Introspect before creating: Always call drupal_introspect with entity_type and bundle before creating or updating entities.
- Very specific: line 30
Multi-site: When multiple Drupal sites are configured, every tool accepts a site parameter.
- Very specific: line 31
Drush site aliases: If the project has a drush/sites/self.site.yml file, its entries (host, user, root, uri) map directly to drush-mcp.yml …
- Very specific: line 114
Use the machine value, not the label.
- Very specific: line 185
Use drupal_file_attach to upload bytes and attach to a file or image field in one call.
- Very specific: line 206
When unsure, call drupal_introspect on the bundle and look for fields with "type":"file" or "type":"image".
- Distinctive: line 222
Create the comment first (see "Add a comment to a node" workflow above), capture the returned comment id.
- Very specific: line 261
Default mode is append (adds to the existing list, respecting cardinality).
- Very specific: line 350
When deleting: remove group_relationship entities first, then the content, then the group
- Very specific: line 377
Use format="json" to get structured output from built-in Drush commands.
- Distinctive: line 528
Always use the third mode (type + bundle) before creating entities to discover required fields and valid values.
- Distinctive: line 555
All errors are returned as structured JSON:
- Very specific: line 565
When you get a validation_error, check the violations array for which fields failed and why.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
drush-mcp
MCP server for Drupal via Drush. Connects to any Drupal 10+/11+ site over local shell, SSH, or Docker.
Rules of Engagement
- Introspect before creating: Always call
drupal_introspectwithentity_typeandbundlebefore creating or updating entities. This returns required fields, field types, allowed values for taxonomy references, and list field options. Never guess field values. - Use the
userparameter: Bridge tools (drupal_entity_create,drupal_entity_read,drupal_entity_update,drupal_entity_list,drupal_introspect, anduidon the file tools) act as one Drupal account and apply its access checks: entity create, view and update access, field view access on what comes back, and edit access on every field whose value changes. Passuserto act as a given account. Without it the site'srun_asapplies, and without that, anonymous; before 0.6.0 the fallback was uid 1, so a call that used to work withoutusermay now answeraccess_denied. Pass the account whose permissions the work should have, not uid 1. - Read refusals as answers, not glitches:
access_deniedmeans Drupal refused the acting account (the message carries Drupal's reason).user_not_allowedmeans the site runs on theread-onlyorcontenttools profile and pins the account to itsrun_as;tool_disabledmeans the site switched that tool off;param_not_allowedmeans a restricted site does not let the agent choose a file'sschemeordestination(drop them, the field decides);bridge_too_oldmeans the site's bridge must be upgraded before a restricted profile can run. Report these to the user; do not retry as another account or route around them withdrupal_php_eval. - Confirm before destructive operations:
drupal_entity_delete,drupal_config_set,drupal_php_eval, anddrupal_sql_querymodify data. Always confirm with the user before executing. - Multi-site: When multiple Drupal sites are configured, every tool accepts a
siteparameter. With a single site, it resolves automatically. - Drush site aliases: If the project has a
drush/sites/self.site.ymlfile, its entries (host,user,root,uri) map directly todrush-mcp.ymlsite config. Useful for discovering connection details when setting up new sites.
Tools
A site may expose fewer tools than listed here. Its tools profile decides: read-only keeps entity read and list, introspect, field info and status; content adds entity create and update and file attach; full (the default) has all of them. A tool missing from the tool list is switched off on purpose.
Entity Operations (bridge commands)
These use the drush-bridge package (bloomidea/drush-mcp-bridge) installed on the Drupal site.
| Tool | Description |
drupal_entity_create | Create entity from JSON fields |
drupal_entity_read | Load entity by type + numeric ID |
drupal_entity_update | Update specific fields on existing entity |
drupal_entity_list | Query entities with filters, pagination, sorting |
drupal_entity_delete | Delete entity by type + numeric ID |
drupal_introspect | Discover entity types, bundles, fields, allowed values |
drupal_file_upload | Upload bytes and create a managed file entity (returns fid, uri, url) |
drupal_file_attach | Upload bytes and attach to a file/image field on an existing entity in one round trip; supports mode=append/replace, plus alt/title for image fields |
System Operations (built-in Drush)
| Tool | Description |
drupal_cache_rebuild | Clear all caches |
drupal_watchdog | View recent log entries (supports count, severity filters) |
drupal_status | Site info: Drupal version, DB (never its username or password), PHP, paths |
drupal_config_get | Read a configuration value (e.g., system.site) |
drupal_config_set | Write a configuration value |
drupal_field_info | Field definitions for an entity type |
User Operations (built-in Drush)
| Tool | Description |
drupal_user_create | Create user account |
drupal_user_block | Block a user account |
Power Tools (built-in Drush)
| Tool | Description |
drupal_drush | Run a Drush command by name (e.g., pm:security, queue:list). command is the bare name only; options and arguments go in arguments. Accepts optional format param (json, table, yaml) |
drupal_php_eval | Execute arbitrary PHP on the Drupal site |
drupal_sql_query | Run SQL query against the database |
Field Value Formats
Entity references (taxonomy terms, users, nodes)
{"field_name": {"target_id": 42}}Multiple values (if cardinality > 1):
{"field_name": [{"target_id": 42}, {"target_id": 43}]}Text fields (body, text_with_summary, text_long)
{"body": {"value": "The content here.", "format": "basic_html"}}Common text formats: basic_html, full_html, plain_text, restricted_html. When unsure, use basic_html.
Simple fields (string, integer, boolean, email)
{"title": "My Title", "status": 1, "field_email": "user@example.com"}Date fields
{"field_date": "2026-03-15"}
{"field_date": {"value": "2026-03-15T09:00:00", "end_value": "2026-03-15T17:00:00"}}List fields (select lists)
Use the machine value, not the label. Call drupal_introspect to see allowed_values:
{"field_color": "red"}Common Workflows
Create an entity (node, comment, etc.)
- Introspect to discover required fields and allowed values:
drupal_introspect(entity_type="node", bundle="article") - Create with all required fields:
drupal_entity_create(entity_type="node", bundle="article", fields={"title": "My Article", "body": {"value": "Content", "format": "basic_html"}, "uid": 1})
Add a comment to a node
- Introspect the comment bundle:
The bundle name is typicallydrupal_introspect(entity_type="comment", bundle="comment_node_article")comment_node_<content_type>. - Create the comment with the correct
entity_id,entity_type,field_name, anduser:
Thedrupal_entity_create( entity_type="comment", bundle="comment_node_article", user=4, fields={ "entity_id": {"target_id": 123}, "entity_type": "node", "field_name": "comment_node_article", "uid": 4, "subject": "My comment", "comment_body": {"value": "Comment text.", "format": "basic_html"} } )userparameter is critical for comments since access checks verify the acting user can reference the target node and use the text format.
Query entities with filters
drupal_entity_list(
entity_type="node",
bundle="ol_todo",
filters={"status": 1},
limit=10,
sort="changed:DESC"
)Read a specific entity
drupal_entity_read(entity_type="node", id=12345)Update specific fields
drupal_entity_update(entity_type="node", id=12345, fields={"title": "New Title", "field_status": {"target_id": 2}})Attach a file to a node, comment, or other entity
Use drupal_file_attach to upload bytes and attach to a file or image field in one call. The tool registers file.usage against the host entity, so cascade-deletes work, and rolls back on save failure to avoid orphan files.
Choose the right input mode — content_path vs content_base64:
content_path(recommended for local agents) — pass an absolute path on the machine running the MCP server. The MCP reads the file from disk and pipes it to Drush. Use this whenever you have the file on the agent's local filesystem. Required for files over ~10–15 KB if the agent talks to the MCP through an LLM provider, because dense base64 inside tool arguments can trigger provider-side safety filters and the call gets rejected.content_pathkeeps the LLM message stream clean (just a short path string). Since 0.5.1 the path must resolve, symlinks followed, inside the site'scontent_roots(by default only the system temp directory and/tmp), and the file on disk must itself have an allowed extension. OnPATH_OUTSIDE_ROOTS, copy the file into/tmpand upload that copy; do not ask for the root to be widened just to finish the task.content_base64— base64-encoded file bytes. Use when the file is not on the agent's local filesystem (cross-host MCP deployments, content generated in-memory, etc.). Slower for large files because the bytes traverse the LLM message stream.
Exactly one of the two must be provided. Sending both → VALIDATION_ERROR. Sending neither → VALIDATION_ERROR.
Decide between _upload and _attach:
drupal_file_attach— you know the target (entity + field) and want it bound in one round trip. Field-level extension and size limits are validated before bytes are written.drupal_file_upload— you only need a managed file entity (e.g. to reference from a custom workflow). Returnsfidfor later use. The agent is then responsible for either attaching it (which registers usage) or deleting it.
Required field discovery:
Most file/image fields share the same shape. Common Atrium-style fields:
node:ol_todo→field_shared_files(multi, typefile)comment:comment_node_ol_todo→field_shared_files(multi, typefile)node:article→field_image(single, typeimage)
When unsure, call drupal_introspect on the bundle and look for fields with "type":"file" or "type":"image".
Attach a PRD/document to an existing task (node) — canonical local agent flow:
drupal_file_attach(
filename="PRD-feature.md",
content_path="/Users/me/Dev/myproject/PRD-feature.md",
entity_type="node",
entity_id=403055,
field_name="field_shared_files",
uid=4
)Attach to a comment (the more common Atrium pattern):
- Create the comment first (see "Add a comment to a node" workflow above), capture the returned comment id.
- Attach the file to the comment:
drupal_file_attach(
filename="screenshot.png",
content_path="/tmp/screenshot.png",
entity_type="comment",
entity_id=190282,
field_name="field_shared_files",
uid=4
)Image fields with alt/title:
drupal_file_attach(
filename="hero.jpg",
content_path="/Users/me/Pictures/hero.jpg",
entity_type="node", entity_id=12345,
field_name="field_image",
alt="Sunset over the harbour", title="Hero image",
uid=4
)Cross-host fallback (when the file is not on the agent's local filesystem):
drupal_file_attach(
filename="generated.md",
content_base64="<base64 of bytes generated in-memory>",
entity_type="node",
entity_id=403055,
field_name="field_shared_files",
uid=4
)Replace existing files instead of appending:
Default mode is append (adds to the existing list, respecting cardinality). Pass mode="replace" to overwrite. On a cardinality=1 field, append into a populated field returns CARDINALITY_EXCEEDED — use replace instead.
Defaults that matter:
schemefollows the site.drupal_file_attachuses the target field'suri_scheme(what the Drupal upload widget would do) anddrupal_file_uploaduses the site'ssystem.filedefault_scheme. Only passschemewhen you need to override that, e.g.privatefor a sensitive file on a site whose default ispublic. Before 0.4.0 both tools defaulted topublicregardless of the field; if you see uploads landing inpublic://on a private-files site, the bridge is older than 0.4.0.destinationdefaults tomcp-uploads/<UTC YYYY-MM>/so AI uploads stay separate from editorial ones.uiddefaults to the site'sdefault_uid(anonymous unless configured). For Atrium and similar group-aware sites, pass an explicituidbecause anonymous users typically can't post comments or save entities with restricted text formats.
Errors to expect:
| Code | Cause |
VALIDATION_ERROR | Both content_base64 and content_path provided, or neither |
PATH_NOT_ABSOLUTE | content_path is not an absolute path, or contains . or .. segments |
PATH_NOT_FOUND | content_path does not exist or is not readable by the MCP server process |
PATH_NOT_FILE | content_path points to a directory or non-regular file |
PATH_OUTSIDE_ROOTS | content_path resolves outside the site's content_roots; copy the file into /tmp first |
CONFIG_ERROR | A content_roots entry in drush-mcp.yml is relative; report it, it is not the agent's to fix |
EXTENSION_FORBIDDEN | Extension of filename, or of the file content_path resolves to, not in the configured allowlist or the field's file_extensions setting |
SIZE_EXCEEDED | Larger than max_size, the field's max_filesize, or PHP upload_max_filesize/post_max_size |
INVALID_SCHEME / INVALID_DESTINATION | Bad scheme or path traversal attempt on the destination (inside Drupal) |
ENTITY_NOT_FOUND / FIELD_INVALID | Target entity doesn't exist or field isn't a file/image type |
ACCESS_DENIED | The acting account (uid, else the site's run_as, else anonymous) may not update the target entity or edit the field |
INVALID_DESTINATION | Also: the destination starts with styles/, css/, js/ or php/, which Drupal generates itself |
CARDINALITY_EXCEEDED | mode=append against a full single-cardinality field — use mode=replace |
URL in response: url is null for private/temporary schemes, and also null when the site has no uri configured in drush-mcp.yml (because drush running in CLI without --uri can't build a real public URL). The uri field (e.g. public://mcp-uploads/2026-05/foo.md) is always populated and is what you should pass back to other Drupal calls.
Working with Groups (Group module v3)
Groups use group and group_relationship entities. Content is linked to groups through relationship entities, not directly. When in doubt about which group to use, ask the user for the group name rather than guessing.
List groups:
drupal_entity_list(entity_type="group", bundle="client", limit=10)Create a group:
drupal_entity_create(entity_type="group", bundle="bloom", user=4, fields={"label": "My Project", "uid": 4})Note: groups use label (not title) for the name field.
Add a member to a group:
drupal_entity_create(
entity_type="group_relationship",
bundle="bloom-group_membership",
user=4,
fields={
"gid": {"target_id": GROUP_ID},
"entity_id": {"target_id": USER_ID},
"plugin_id": "group_membership",
"group_type": {"target_id": "bloom"},
"uid": 4
}
)Add content (e.g. a task) to a group:
drupal_entity_create(
entity_type="group_relationship",
bundle="bloom-group_node-ol_todo",
user=4,
fields={
"gid": {"target_id": GROUP_ID},
"entity_id": {"target_id": NODE_ID},
"plugin_id": "group_node:ol_todo",
"group_type": {"target_id": "bloom"},
"uid": 4
}
)Discover available relationship types for a group type:
drupal_introspect(entity_type="group_relationship")This lists all relationship bundles (e.g., bloom-group_membership, bloom-group_node-ol_todo, client-group_node-ol_todo). Use this to find the correct bundle before adding content to a group.
Key details:
- Relationship bundle format:
{group_type}-{plugin_id_with_hyphens}(e.g.,bloom-group_node-ol_todo) - The
plugin_idfield value uses colons:group_node:ol_todo,group_membership - To find the right bundle: the bundle name replaces colons with hyphens and prepends the group type. So for group type
bloomand content plugingroup_node:ol_todo, the bundle isbloom-group_node-ol_todo - To list content in a group, query
group_relationshipfiltered bygid:drupal_entity_list(entity_type="group_relationship", bundle="bloom-group_node-ol_todo", filters={"gid": GROUP_ID}) - To list group members: filter by the
*-group_membershipbundle - When deleting: remove
group_relationshipentities first, then the content, then the group
Check site health
drupal_status()
drupal_watchdog(count=10, severity="error")
drupal_cache_rebuild()Read and write configuration
drupal_config_get(name="system.site")
drupal_config_get(name="system.site", key="name")
drupal_config_set(name="system.site", key="name", value="My Site")Run arbitrary Drush commands
drupal_drush(command="pm:security")
drupal_drush(command="queue:list")
drupal_drush(command="state:get", arguments=["system.cron_last"])
drupal_drush(command="core:status", format="json")Use format="json" to get structured output from built-in Drush commands. Common formats: json, table, yaml.
Execute PHP or SQL
drupal_php_eval(code="echo \\Drupal::VERSION;")
drupal_sql_query(query="SELECT COUNT(*) FROM node_field_data WHERE status = 1")Drush Commands Reference
Commands available via drupal_drush(command="...", arguments=[...], format="json"). This covers commands beyond the dedicated MCP tools.
Cron & Maintenance
| Command | Description |
core:cron | Execute cron hooks |
core:requirements | Check system requirements (PHP, DB, filesystem) |
maint:get | Get maintenance mode status |
maint:set 1 / maint:set 0 | Enable/disable maintenance mode |
Modules & Themes
| Command | Description |
pm:list | List installed modules and their status |
pm:install MODULE | Install and enable a module |
pm:uninstall MODULE | Uninstall a module |
pm:security | Check for security updates |
theme:install THEME | Install a theme |
theme:uninstall THEME | Uninstall a theme |
Config Management
| Command | Description |
config:status | Show config sync status (differences between active and staged) |
config:export | Export active config to sync directory |
config:import | Import staged config into active storage |
config:delete NAME | Delete a config object |
State API
| Command | Description |
state:get KEY | Get a state value (e.g., system.cron_last) |
state:set KEY VALUE | Set a state value |
state:delete KEY | Delete a state value |
State is for environment-specific values (last cron run, maintenance mode). Config is for site configuration that syncs between environments.
Database
| Command | Description |
sql:dump | Export full database as SQL |
sql:sanitize | Sanitize DB for development (removes emails, resets passwords) |
updatedb | Run pending database updates |
updatedb:status | Show pending update hooks |
Users (beyond create/block)
| Command | Description |
user:information NAME | Show user details by username |
user:login | Generate a one-time login link (uid 1 by default) |
user:login --uid=UID | Generate login link for specific user |
user:password NAME PASSWORD | Set a user's password |
user:unblock NAME | Unblock a user account |
Note: user:login generates URLs based on the site's uri setting. If links show http://default, the site needs a uri configured in drush-mcp.yml.
Roles & Permissions
| Command | Description |
role:list | List all roles |
role:create MACHINE_NAME --name="Label" | Create a role |
role:delete MACHINE_NAME | Delete a role |
role:perm:add ROLE PERMISSION | Grant a permission to a role |
role:perm:remove ROLE PERMISSION | Revoke a permission from a role |
Queues
| Command | Description |
queue:list | List all queues with item counts |
queue:run QUEUE_NAME | Process items in a queue |
queue:delete QUEUE_NAME | Delete all items from a queue |
Migrations
| Command | Description |
migrate:status | Show status of all migrations |
migrate:import MIGRATION_ID | Run a migration |
migrate:rollback MIGRATION_ID | Rollback a migration |
migrate:reset-status MIGRATION_ID | Reset a stuck migration |
migrate:messages MIGRATION_ID | Show migration error messages |
migrate:fields-source MIGRATION_ID | List source fields for a migration |
Views
| Command | Description |
views:list | List all views |
views:execute VIEW_ID | Execute a view and return results |
views:enable VIEW_ID | Enable a view |
views:disable VIEW_ID | Disable a view |
Locale & Translations
| Command | Description |
locale:check | Check for available translation updates |
locale:update | Download and import translation updates |
locale:import LANGCODE FILE | Import a .po translation file |
locale:export LANGCODE | Export translations to .po format |
Deploy
| Command | Description |
deploy | Run deployment sequence (updatedb, config:import, cache:rebuild) |
deploy:hook-status | Show status of deploy hooks |
deploy:mark-complete | Mark deploy hooks as executed |
Image
| Command | Description |
image:flush STYLE | Delete derived images for a style (use --all for all styles) |
image:derive STYLE SOURCE | Generate a derivative image |
Routing
| Command | Description |
core:route | List all routes (use --name=FILTER or --path=FILTER) |
Introspect Modes
drupal_introspect operates in three modes depending on which parameters you provide:
| Parameters | Returns |
| (none) | All content entity types with their bundles |
entity_type only | All bundles for that entity type |
entity_type + bundle | Full field definitions including type, required, cardinality, and allowed values for entity_reference and list fields |
Always use the third mode (type + bundle) before creating entities to discover required fields and valid values.
Entity Types Cheat Sheet
Common Drupal entity types:
| Entity Type | Typical Bundles | Notes |
node | article, page, custom types | Main content type. Bundle key: type |
comment | comment_node_* | One bundle per node type's comment field |
taxonomy_term | vocabulary machine names | Vocabulary is the bundle |
user | user | Single bundle, no bundle key needed |
paragraph | custom paragraph types | Used in nested content structures |
media | image, document, video | Media library items |
group | custom group types | Name field is label, not title. Requires Group module |
group_relationship | {group_type}-{plugin} | Joins groups to content/members. Bundle format: bloom-group_node-ol_todo |
Safety Rules
Write operations (
drupal_entity_create,drupal_entity_update,drupal_entity_delete,drupal_config_set) modify data. Always confirm with the user before executing.
Power tools (
drupal_drush,drupal_php_eval,drupal_sql_query) provide unrestricted access to the Drupal runtime and database. These can break a site. Use with caution and always confirm.
drupal_php_evalexecutes arbitrary PHP. Double-check the code before running. Syntax errors or fatal errors can produce unhelpful output.
Error Handling
All errors are returned as structured JSON:
{"error": "drush_error", "message": "Entity \"node\" with ID \"99999\" not found."}{"error": "validation_error", "message": "Entity validation failed.", "violations": [{"field": "title", "message": "This value should not be null."}]}When you get a validation_error, check the violations array for which fields failed and why. Often it means a required field is missing or an entity reference target doesn't exist.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

