agentleFS
BloomideaBloomidea/drush-mcp/CLAUDE.md

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.

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

    Add the builder in packages/mcp-server/src/tools/<area>.ts.

    line 35
  2. Very specific:

    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 …

    line 36
  3. Very specific:

    If structured I/O is needed, add the matching mcp:<area>-<verb> Drush command in McpBridgeDrushCommands.php returning JSON.

    line 38
  4. Distinctive:

    Add a Vitest unit test for the builder.

    line 39
  5. Very specific:

    Update the tools table in README.md, and the profile table under Security if the tool is in one.

    line 40
  6. Very specific:

    If a tool contract changed: update skills/drupal/SKILL.md (table + frontmatter version:), README.md tool/bridge tables, …

    line 72
  7. Very specific:

    Commit (feat: for new tools, fix: for bug fixes, chore: for version-only bumps).

    line 73
  8. Very specific:

    Create the GitHub Release (project convention is one Release per tag, formatted like v0.2.1):

    line 76
  9. Very specific:

    After publishing, consumers (atrium and others) need their bloomidea/drush-mcp-bridge composer constraint bumped, then composer update …

    line 92
  10. Very specific:

    If composer update fails on a consumer with a composer-plugin-api constraint conflict (we hit this with …

    line 94
  11. Distinctive:

    Do not add helper abstractions for a single call site.

    line 116

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

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

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 package bloomidea/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:

  1. Tool stub (packages/mcp-server/src/tools/<area>.ts) — receives validated input, returns a DrushArgs object describing which Drush command to run and with what flags. No I/O.
  2. Transport (packages/mcp-server/src/transport/{local,ssh,docker}.ts) — executes the Drush command against the configured site. Uses execFile with argv; stdin is not currently piped through any transport.
  3. Drush bridge (packages/drush-mcp-bridge/src/Drush/Commands/McpBridgeDrushCommands.php) — for structured operations (entity CRUD, introspection), commands named mcp:* 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>Args returning DrushArgs.
  • Site config keys live under sites: in drush-mcp.yml; see drush-mcp.example.yml for the shape and supported transports.

When adding a tool

  1. Add the builder in packages/mcp-server/src/tools/<area>.ts. A value the agent supplies for a positional Drush argument goes through positional() (tools/args.ts); --name=value options need nothing.
  2. 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 read-only or content; anything that runs as the Drush superuser is full only.
  3. Register it with register() in packages/mcp-server/src/server.ts, with a Zod schema and a description (the description is what agents see — be precise).
  4. If structured I/O is needed, add the matching mcp:<area>-<verb> Drush command in McpBridgeDrushCommands.php returning JSON. It acts as --user (default anonymous) and checks entity and field access for that account; extend packages/drush-mcp-bridge/tests/functional/access.sh when it does something new.
  5. Add a Vitest unit test for the builder. Functional/Drupal-side tests live with the bridge.
  6. 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 update skills/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 with ARG_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 extending Transport.execute.
  • The --uri flag is appended automatically by transport/base.ts:19 when config.uri is 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 for shell-quote's quote(): 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-mcp

SemVer 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.x is patch-only (^0.3 means >=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:

  1. Bump packages/mcp-server/package.json to the new version.
  2. If a tool contract changed: update skills/drupal/SKILL.md (table + frontmatter version:), README.md tool/bridge tables, drush-mcp.example.yml if config schema grew.
  3. 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.
  4. Annotated tag: git tag -a v0.x.y -m "v0.x.y — short description".
  5. Push: git push origin main && git push origin v0.x.y.
  6. 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
    )"
  7. cd packages/mcp-server && npm publish — pushes to the npm registry.
  8. Refresh Packagist if composer update doesn'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/*.php

PHP 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 in drush.services.yml, not *.services.yml.
  • entity:delete is a stock Drush command; the project deliberately reuses it instead of shipping mcp:entity-delete (see buildEntityDeleteArgs in tools/entity.ts). Keep that pattern unless a reason to diverge appears.
  • siteParam in server.ts is a Zod helper for the per-tool site argument that selects which configured site to target — every new tool must accept it.
  • CLI flags and drush-mcp.yml do not merge per site: parseCliArgs returns null only when no site flag is given, and only then does mergeConfig use the file's sites:. A server started with --docker --host ... therefore never sees a site's file_upload from the YAML, only defaults:, which mergeConfig keeps in both branches. That is why file_upload is accepted under defaults as of 0.5.0: it is the one route an upload policy has into a flag-configured server, and SiteManager.getFileUploadConfig() folds it in the same way getTransport() already folds defaults.timeout and defaults.drush. Before 0.4.0 the file was never used at all (and --config was ignored).
  • The file commands have two accounts: --uid is the owner and --user the acting account. The bridge acts as --uid when --user is absent, and commonArgs in tools/file.ts sends --user only when the two differ, so a 0.6 server still works with a 0.5 bridge for any call that names its own uid. Keep both halves of that.
  • The bridge decides the upload scheme, not the TS side: commonArgs in tools/file.ts forwards --scheme only when the agent set it, and mcp:file-attach falls back to the field's uri_scheme, mcp:file-upload to system.file default_scheme. Do not reintroduce a public default in TypeScript; on private-files sites it silently exposes uploads.

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.