agentleFS
BloomideaBloomidea/drush-mcp/skills/drupal/SKILL.md

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

  • Other versions2 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 2

    1. gingerminds/claude-skills★ 2same name
    2. 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.

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
  1. Very specific:

    Introspect before creating: Always call drupal_introspect with entity_type and bundle before creating or updating entities.

    line 26
  2. Very specific:

    Multi-site: When multiple Drupal sites are configured, every tool accepts a site parameter.

    line 30
  3. Very specific:

    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 …

    line 31
  4. Very specific:

    Use the machine value, not the label.

    line 114
  5. Very specific:

    Use drupal_file_attach to upload bytes and attach to a file or image field in one call.

    line 185
  6. Very specific:

    When unsure, call drupal_introspect on the bundle and look for fields with "type":"file" or "type":"image".

    line 206
  7. Distinctive:

    Create the comment first (see "Add a comment to a node" workflow above), capture the returned comment id.

    line 222
  8. Very specific:

    Default mode is append (adds to the existing list, respecting cardinality).

    line 261
  9. Very specific:

    When deleting: remove group_relationship entities first, then the content, then the group

    line 350
  10. Very specific:

    Use format="json" to get structured output from built-in Drush commands.

    line 377
  11. Distinctive:

    Always use the third mode (type + bundle) before creating entities to discover required fields and valid values.

    line 528
  12. Distinctive:

    All errors are returned as structured JSON:

    line 555
  13. Very specific:

    When you get a validation_error, check the violations array for which fields failed and why.

    line 565

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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_introspect with entity_type and bundle before 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 user parameter: Bridge tools (drupal_entity_create, drupal_entity_read, drupal_entity_update, drupal_entity_list, drupal_introspect, and uid on 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. Pass user to act as a given account. Without it the site's run_as applies, and without that, anonymous; before 0.6.0 the fallback was uid 1, so a call that used to work without user may now answer access_denied. Pass the account whose permissions the work should have, not uid 1.
  • Read refusals as answers, not glitches: access_denied means Drupal refused the acting account (the message carries Drupal's reason). user_not_allowed means the site runs on the read-only or content tools profile and pins the account to its run_as; tool_disabled means the site switched that tool off; param_not_allowed means a restricted site does not let the agent choose a file's scheme or destination (drop them, the field decides); bridge_too_old means 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 with drupal_php_eval.
  • Confirm before destructive operations: drupal_entity_delete, drupal_config_set, drupal_php_eval, and drupal_sql_query modify data. Always confirm with the user before executing.
  • Multi-site: When multiple Drupal sites are configured, every tool accepts a site parameter. With a single site, it resolves automatically.
  • 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 site 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.

ToolDescription
drupal_entity_createCreate entity from JSON fields
drupal_entity_readLoad entity by type + numeric ID
drupal_entity_updateUpdate specific fields on existing entity
drupal_entity_listQuery entities with filters, pagination, sorting
drupal_entity_deleteDelete entity by type + numeric ID
drupal_introspectDiscover entity types, bundles, fields, allowed values
drupal_file_uploadUpload bytes and create a managed file entity (returns fid, uri, url)
drupal_file_attachUpload 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)

ToolDescription
drupal_cache_rebuildClear all caches
drupal_watchdogView recent log entries (supports count, severity filters)
drupal_statusSite info: Drupal version, DB (never its username or password), PHP, paths
drupal_config_getRead a configuration value (e.g., system.site)
drupal_config_setWrite a configuration value
drupal_field_infoField definitions for an entity type

User Operations (built-in Drush)

ToolDescription
drupal_user_createCreate user account
drupal_user_blockBlock a user account

Power Tools (built-in Drush)

ToolDescription
drupal_drushRun 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_evalExecute arbitrary PHP on the Drupal site
drupal_sql_queryRun 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.)

  1. Introspect to discover required fields and allowed values:
    drupal_introspect(entity_type="node", bundle="article")
  2. 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

  1. Introspect the comment bundle:
    drupal_introspect(entity_type="comment", bundle="comment_node_article")
    The bundle name is typically comment_node_<content_type>.
  2. Create the comment with the correct entity_id, entity_type, field_name, and user:
    drupal_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"}
      }
    )
    The user parameter 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_path keeps the LLM message stream clean (just a short path string). Since 0.5.1 the path must resolve, symlinks followed, inside the site's content_roots (by default only the system temp directory and /tmp), and the file on disk must itself have an allowed extension. On PATH_OUTSIDE_ROOTS, copy the file into /tmp and 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). Returns fid for 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, type file)
  • comment:comment_node_ol_todo → field_shared_files (multi, type file)
  • node:article → field_image (single, type image)

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):

  1. Create the comment first (see "Add a comment to a node" workflow above), capture the returned comment id.
  2. 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:

  • scheme follows the site. drupal_file_attach uses the target field's uri_scheme (what the Drupal upload widget would do) and drupal_file_upload uses the site's system.file default_scheme. Only pass scheme when you need to override that, e.g. private for a sensitive file on a site whose default is public. Before 0.4.0 both tools defaulted to public regardless of the field; if you see uploads landing in public:// on a private-files site, the bridge is older than 0.4.0.
  • destination defaults to mcp-uploads/<UTC YYYY-MM>/ so AI uploads stay separate from editorial ones.
  • uid defaults to the site's default_uid (anonymous unless configured). For Atrium and similar group-aware sites, pass an explicit uid because anonymous users typically can't post comments or save entities with restricted text formats.

Errors to expect:

CodeCause
VALIDATION_ERRORBoth content_base64 and content_path provided, or neither
PATH_NOT_ABSOLUTEcontent_path is not an absolute path, or contains . or .. segments
PATH_NOT_FOUNDcontent_path does not exist or is not readable by the MCP server process
PATH_NOT_FILEcontent_path points to a directory or non-regular file
PATH_OUTSIDE_ROOTScontent_path resolves outside the site's content_roots; copy the file into /tmp first
CONFIG_ERRORA content_roots entry in drush-mcp.yml is relative; report it, it is not the agent's to fix
EXTENSION_FORBIDDENExtension of filename, or of the file content_path resolves to, not in the configured allowlist or the field's file_extensions setting
SIZE_EXCEEDEDLarger than max_size, the field's max_filesize, or PHP upload_max_filesize/post_max_size
INVALID_SCHEME / INVALID_DESTINATIONBad scheme or path traversal attempt on the destination (inside Drupal)
ENTITY_NOT_FOUND / FIELD_INVALIDTarget entity doesn't exist or field isn't a file/image type
ACCESS_DENIEDThe acting account (uid, else the site's run_as, else anonymous) may not update the target entity or edit the field
INVALID_DESTINATIONAlso: the destination starts with styles/, css/, js/ or php/, which Drupal generates itself
CARDINALITY_EXCEEDEDmode=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_id field 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 bloom and content plugin group_node:ol_todo, the bundle is bloom-group_node-ol_todo
  • To list content in a group, query group_relationship filtered by gid:
    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_membership bundle
  • When deleting: remove group_relationship entities 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

CommandDescription
core:cronExecute cron hooks
core:requirementsCheck system requirements (PHP, DB, filesystem)
maint:getGet maintenance mode status
maint:set 1 / maint:set 0Enable/disable maintenance mode

Modules & Themes

CommandDescription
pm:listList installed modules and their status
pm:install MODULEInstall and enable a module
pm:uninstall MODULEUninstall a module
pm:securityCheck for security updates
theme:install THEMEInstall a theme
theme:uninstall THEMEUninstall a theme

Config Management

CommandDescription
config:statusShow config sync status (differences between active and staged)
config:exportExport active config to sync directory
config:importImport staged config into active storage
config:delete NAMEDelete a config object

State API

CommandDescription
state:get KEYGet a state value (e.g., system.cron_last)
state:set KEY VALUESet a state value
state:delete KEYDelete a state value

State is for environment-specific values (last cron run, maintenance mode). Config is for site configuration that syncs between environments.

Database

CommandDescription
sql:dumpExport full database as SQL
sql:sanitizeSanitize DB for development (removes emails, resets passwords)
updatedbRun pending database updates
updatedb:statusShow pending update hooks

Users (beyond create/block)

CommandDescription
user:information NAMEShow user details by username
user:loginGenerate a one-time login link (uid 1 by default)
user:login --uid=UIDGenerate login link for specific user
user:password NAME PASSWORDSet a user's password
user:unblock NAMEUnblock 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

CommandDescription
role:listList all roles
role:create MACHINE_NAME --name="Label"Create a role
role:delete MACHINE_NAMEDelete a role
role:perm:add ROLE PERMISSIONGrant a permission to a role
role:perm:remove ROLE PERMISSIONRevoke a permission from a role

Queues

CommandDescription
queue:listList all queues with item counts
queue:run QUEUE_NAMEProcess items in a queue
queue:delete QUEUE_NAMEDelete all items from a queue

Migrations

CommandDescription
migrate:statusShow status of all migrations
migrate:import MIGRATION_IDRun a migration
migrate:rollback MIGRATION_IDRollback a migration
migrate:reset-status MIGRATION_IDReset a stuck migration
migrate:messages MIGRATION_IDShow migration error messages
migrate:fields-source MIGRATION_IDList source fields for a migration

Views

CommandDescription
views:listList all views
views:execute VIEW_IDExecute a view and return results
views:enable VIEW_IDEnable a view
views:disable VIEW_IDDisable a view

Locale & Translations

CommandDescription
locale:checkCheck for available translation updates
locale:updateDownload and import translation updates
locale:import LANGCODE FILEImport a .po translation file
locale:export LANGCODEExport translations to .po format

Deploy

CommandDescription
deployRun deployment sequence (updatedb, config:import, cache:rebuild)
deploy:hook-statusShow status of deploy hooks
deploy:mark-completeMark deploy hooks as executed

Image

CommandDescription
image:flush STYLEDelete derived images for a style (use --all for all styles)
image:derive STYLE SOURCEGenerate a derivative image

Routing

CommandDescription
core:routeList all routes (use --name=FILTER or --path=FILTER)

Introspect Modes

drupal_introspect operates in three modes depending on which parameters you provide:

ParametersReturns
(none)All content entity types with their bundles
entity_type onlyAll bundles for that entity type
entity_type + bundleFull 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 TypeTypical BundlesNotes
nodearticle, page, custom typesMain content type. Bundle key: type
commentcomment_node_*One bundle per node type's comment field
taxonomy_termvocabulary machine namesVocabulary is the bundle
useruserSingle bundle, no bundle key needed
paragraphcustom paragraph typesUsed in nested content structures
mediaimage, document, videoMedia library items
groupcustom group typesName 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_eval executes 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.