drush-mcp
CLAUDE.md5 starsChanged 8 months agoMITView on GitHub
Guidance for AI agents working in this repository. drush-mcp exposes Drupal sites to MCP-aware agents by running Drush. It is a monorepo with two packages that ship in lockstep: User-facing docs are in README.md and CONTRIBUTING.md. This file is for agents. The MCP server does not call Drupal directly. Each tool flows through three layers: Power tools (drupal_drush, drupal_php_eval, drupal_sql_query) skip the bridge and run raw Drush. Every tool is registered through register() in packages/mcp-server/src/server.ts, which applies the site's tool…
- Installs packages
- Commits and pushes
In this repositoryOne other file it gives its agents.
- drupalSkill
The file
We split this CLAUDE.md into the rules it gives an agent
11 rules. About 0 are common, 2 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 35
Add the builder in packages/mcp-server/src/tools/<area>.ts.
- Very specific: line 36
Add the name to TOOL_NAMES in src/policy.ts and decide its profile: only a bridge command that applies Drupal access checks belongs in …
- Very specific: line 38
If structured I/O is needed, add the matching mcp:<area>-<verb> Drush command in McpBridgeDrushCommands.php returning JSON.
- Distinctive: line 39
Add a Vitest unit test for the builder.
- Very specific: line 40
Update the tools table in README.md, and the profile table under Security if the tool is in one.
- Very specific: line 72
If a tool contract changed: update skills/drupal/SKILL.md (table + frontmatter version:), README.md tool/bridge tables, …
- Very specific: line 73
Commit (feat: for new tools, fix: for bug fixes, chore: for version-only bumps).
- Very specific: line 76
Create the GitHub Release (project convention is one Release per tag, formatted like v0.2.1):
- Very specific: line 92
After publishing, consumers (atrium and others) need their bloomidea/drush-mcp-bridge composer constraint bumped, then composer update …
- Very specific: line 94
If composer update fails on a consumer with a composer-plugin-api constraint conflict (we hit this with …
- Distinctive: line 116
Do not add helper abstractions for a single call site.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md 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
CLAUDE.md
Guidance for AI agents working in this repository.
What this project is
drush-mcp exposes Drupal sites to MCP-aware agents by running Drush. It is a monorepo with two packages that ship in lockstep:
packages/mcp-server/— TypeScript MCP server, npm package@bloomidea/drush-mcp.packages/drush-mcp-bridge/— PHP Drush commands, Composer packagebloomidea/drush-mcp-bridge.
User-facing docs are in README.md and CONTRIBUTING.md. This file is for agents.
Architecture
The MCP server does not call Drupal directly. Each tool flows through three layers:
- Tool stub (
packages/mcp-server/src/tools/<area>.ts) — receives validated input, returns aDrushArgsobject describing which Drush command to run and with what flags. No I/O. - Transport (
packages/mcp-server/src/transport/{local,ssh,docker}.ts) — executes the Drush command against the configured site. UsesexecFilewith argv; stdin is not currently piped through any transport. - Drush bridge (
packages/drush-mcp-bridge/src/Drush/Commands/McpBridgeDrushCommands.php) — for structured operations (entity CRUD, introspection), commands namedmcp:*accept JSON args and return JSON.
Power tools (drupal_drush, drupal_php_eval, drupal_sql_query) skip the bridge and run raw Drush.
Every tool is registered through register() in packages/mcp-server/src/server.ts, which applies the site's tool profile (src/policy.ts), resolves the acting account (run_as, pinned on restricted profiles) and appends the audit line (src/audit.ts). src/index.ts only parses the command line and connects stdio; tests drive createServer() through the SDK's in-memory transport (tests/server.test.ts).
Naming conventions
- MCP tool names:
drupal_<area>_<verb>(e.g.drupal_entity_create,drupal_user_block). - Drush bridge commands:
mcp:<area>-<verb>(e.g.mcp:entity-create,mcp:entity-list). - TypeScript builders:
build<Area><Verb>ArgsreturningDrushArgs. - Site config keys live under
sites:indrush-mcp.yml; seedrush-mcp.example.ymlfor the shape and supported transports.
When adding a tool
- Add the builder in
packages/mcp-server/src/tools/<area>.ts. A value the agent supplies for a positional Drush argument goes throughpositional()(tools/args.ts);--name=valueoptions need nothing. - Add the name to
TOOL_NAMESinsrc/policy.tsand decide its profile: only a bridge command that applies Drupal access checks belongs inread-onlyorcontent; anything that runs as the Drush superuser isfullonly. - Register it with
register()inpackages/mcp-server/src/server.ts, with a Zod schema and a description (the description is what agents see — be precise). - If structured I/O is needed, add the matching
mcp:<area>-<verb>Drush command inMcpBridgeDrushCommands.phpreturning JSON. It acts as--user(default anonymous) and checks entity and field access for that account; extendpackages/drush-mcp-bridge/tests/functional/access.shwhen it does something new. - Add a Vitest unit test for the builder. Functional/Drupal-side tests live with the bridge.
- Update the tools table in
README.md, and the profile table under Security if the tool is in one. If the change is user-visible, also updateskills/drupal/SKILL.md.
Transport limits to remember
- Args are passed via shell argv. SSH and docker transports nest the command inside
ssh user@host '... docker exec ... drush ...', so payloads compete withARG_MAX(typically a few hundred KB after nesting). - Anything above ~200 KB needs a different mechanism than
--data=<base64>. There is no stdin path on the transport today; adding one means extendingTransport.execute. - The
--uriflag is appended automatically bytransport/base.ts:19whenconfig.uriis set; do not pass it from tool stubs. - SSH and docker quote every argument with
shellQuote(transport/shell.ts), a POSIX single-quote wrapper. Do not swap it back forshell-quote'squote(): for values holding both a single quote and whitespace it picks a double-quoted form and escapes!as\!, which a non-interactive bash does not unescape, so!==reached PHP as\!==and exclamation marks landed inside stored content with a stray backslash.
Versioning
The npm package and the Composer package release together via a single git tag. The bridge's composer.json has no version field — its version comes from the tag. Only packages/mcp-server/package.json carries an explicit version number.
The skill at skills/drupal/SKILL.md is the third leg — update it whenever a tool's contract changes, and bump its own frontmatter version:.
Publishing a new release
Always check what's already taken before picking a version — doc-only patch releases happen and collisions are easy:
npm view @bloomidea/drush-mcp versions --json
gh release list --repo Bloomidea/drush-mcpSemVer policy (we're in 0.x):
- Patch (
0.x.y → 0.x.y+1): bug fixes, doc updates, no behaviour change. - Minor (
0.x.y → 0.(x+1).0): new tools, new config blocks, additive features. - Composer's
^0.xis patch-only (^0.3means>=0.3.0 <0.4.0, NOT<1.0.0). Consumers must bump their constraint on every minor —"^0.1 || ^0.3"is the standard cross-minor pattern, not"^0.1".
Release steps in order:
- Bump
packages/mcp-server/package.jsonto the new version. - If a tool contract changed: update
skills/drupal/SKILL.md(table + frontmatterversion:),README.mdtool/bridge tables,drush-mcp.example.ymlif config schema grew. - Commit (
feat:for new tools,fix:for bug fixes,chore:for version-only bumps). One commit per logical change is preferred over a single mega-commit. - Annotated tag:
git tag -a v0.x.y -m "v0.x.y — short description". - Push:
git push origin main && git push origin v0.x.y. - Create the GitHub Release (project convention is one Release per tag, formatted like
v0.2.1):gh release create v0.x.y --title "v0.x.y" --notes "$(cat <<'EOF' # drush-mcp v0.x.y ## Features / Fixes / Skill / ... - bullet points ## Install \`\`\`bash npm install -g @bloomidea/drush-mcp@0.x.y \`\`\` EOF )" cd packages/mcp-server && npm publish— pushes to the npm registry.- Refresh Packagist if
composer updatedoesn't see the new version: log in to https://packagist.org/packages/bloomidea/drush-mcp-bridge and click "Update". The GitHub→Packagist webhook can lag; manual refresh resolves within ~30s.
After publishing, consumers (atrium and others) need their bloomidea/drush-mcp-bridge composer constraint bumped, then composer update bloomidea/drush-mcp-bridge --with-dependencies, then a deploy. Tell agents to restart their Claude Code session so the MCP server respawns with the new npm binary.
If composer update fails on a consumer with a composer-plugin-api constraint conflict (we hit this with simplesamlphp/composer-module-installer ~2.9.0), composer self-update to the latest is usually the fix — the plugin API version is decoupled from the composer binary version.
Build, test, lint
# TypeScript
cd packages/mcp-server
npm install
npm run build # tsc
npm test # vitest run
# PHP (syntax check only — no autoloader at repo root)
php -l packages/drush-mcp-bridge/src/Drush/Commands/*.phpPHP unit/functional tests run inside a real Drupal site that has the bridge installed; there is no in-repo PHPUnit harness.
Style
- TypeScript: strict mode, ES2022, NodeNext modules. Prefer explicit types on exports.
- PHP:
declare(strict_types=1), PSR-12, Drush 12/13 attributes (#[CLI\Command],#[CLI\Option]). - JSON contracts between TS and PHP are the source of truth; if you change a field name, change it in both places and in the skill.
- Do not add helper abstractions for a single call site. The current builders are deliberately flat.
Things that are easy to get wrong
- The bridge is a Drush package (
type: drupal-drush), not a Drupal module. Service registration goes indrush.services.yml, not*.services.yml. entity:deleteis a stock Drush command; the project deliberately reuses it instead of shippingmcp:entity-delete(seebuildEntityDeleteArgsintools/entity.ts). Keep that pattern unless a reason to diverge appears.siteParaminserver.tsis a Zod helper for the per-toolsiteargument that selects which configured site to target — every new tool must accept it.- CLI flags and
drush-mcp.ymldo not merge per site:parseCliArgsreturnsnullonly when no site flag is given, and only then doesmergeConfiguse the file'ssites:. A server started with--docker --host ...therefore never sees a site'sfile_uploadfrom the YAML, onlydefaults:, whichmergeConfigkeeps in both branches. That is whyfile_uploadis accepted underdefaultsas of 0.5.0: it is the one route an upload policy has into a flag-configured server, andSiteManager.getFileUploadConfig()folds it in the same waygetTransport()already foldsdefaults.timeoutanddefaults.drush. Before 0.4.0 the file was never used at all (and--configwas ignored). - The file commands have two accounts:
--uidis the owner and--userthe acting account. The bridge acts as--uidwhen--useris absent, andcommonArgsintools/file.tssends--useronly when the two differ, so a 0.6 server still works with a 0.5 bridge for any call that names its ownuid. Keep both halves of that. - The bridge decides the upload scheme, not the TS side:
commonArgsintools/file.tsforwards--schemeonly when the agent set it, andmcp:file-attachfalls back to the field'suri_scheme,mcp:file-uploadtosystem.file default_scheme. Do not reintroduce apublicdefault in TypeScript; on private-files sites it silently exposes uploads.
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.

