drupal-mcp-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. drupal_mcp_server-0.1.0/.env.example +38 -0
  2. drupal_mcp_server-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
  3. drupal_mcp_server-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  4. drupal_mcp_server-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +32 -0
  5. drupal_mcp_server-0.1.0/.github/workflows/ci.yml +75 -0
  6. drupal_mcp_server-0.1.0/.github/workflows/docs.yml +40 -0
  7. drupal_mcp_server-0.1.0/.github/workflows/release.yml +38 -0
  8. drupal_mcp_server-0.1.0/.gitignore +78 -0
  9. drupal_mcp_server-0.1.0/CHANGELOG.md +306 -0
  10. drupal_mcp_server-0.1.0/CODE_OF_CONDUCT.md +61 -0
  11. drupal_mcp_server-0.1.0/CONTRIBUTING.md +202 -0
  12. drupal_mcp_server-0.1.0/LICENSE +21 -0
  13. drupal_mcp_server-0.1.0/MAINTAINERS.md +40 -0
  14. drupal_mcp_server-0.1.0/PKG-INFO +503 -0
  15. drupal_mcp_server-0.1.0/README.md +461 -0
  16. drupal_mcp_server-0.1.0/SECURITY.md +41 -0
  17. drupal_mcp_server-0.1.0/docs/adr/0001-use-fastmcp.md +39 -0
  18. drupal_mcp_server-0.1.0/docs/adr/0002-pydantic-settings.md +38 -0
  19. drupal_mcp_server-0.1.0/docs/adr/0003-subprocess-drush.md +38 -0
  20. drupal_mcp_server-0.1.0/docs/adr/0004-http-transport.md +37 -0
  21. drupal_mcp_server-0.1.0/docs/architecture.md +213 -0
  22. drupal_mcp_server-0.1.0/docs/configuration.md +138 -0
  23. drupal_mcp_server-0.1.0/docs/docstring_standard.md +86 -0
  24. drupal_mcp_server-0.1.0/docs/index.md +39 -0
  25. drupal_mcp_server-0.1.0/docs/installation.md +42 -0
  26. drupal_mcp_server-0.1.0/docs/migration.md +139 -0
  27. drupal_mcp_server-0.1.0/docs/tools/content.md +34 -0
  28. drupal_mcp_server-0.1.0/docs/tools/devops.md +60 -0
  29. drupal_mcp_server-0.1.0/docs/tools/index.md +88 -0
  30. drupal_mcp_server-0.1.0/docs/tools/users.md +34 -0
  31. drupal_mcp_server-0.1.0/examples/.env.example +38 -0
  32. drupal_mcp_server-0.1.0/examples/README.md +132 -0
  33. drupal_mcp_server-0.1.0/examples/basic_usage.py +69 -0
  34. drupal_mcp_server-0.1.0/examples/content_workflow.py +97 -0
  35. drupal_mcp_server-0.1.0/examples/custom_tool.py +129 -0
  36. drupal_mcp_server-0.1.0/examples/migration_workflow.py +106 -0
  37. drupal_mcp_server-0.1.0/mkdocs.yml +60 -0
  38. drupal_mcp_server-0.1.0/pyproject.toml +80 -0
  39. drupal_mcp_server-0.1.0/scripts/lint.sh +14 -0
  40. drupal_mcp_server-0.1.0/scripts/run.sh +15 -0
  41. drupal_mcp_server-0.1.0/scripts/setup.sh +40 -0
  42. drupal_mcp_server-0.1.0/scripts/test.sh +10 -0
  43. drupal_mcp_server-0.1.0/src/drupal_mcp_server/__init__.py +3 -0
  44. drupal_mcp_server-0.1.0/src/drupal_mcp_server/auth/__init__.py +94 -0
  45. drupal_mcp_server-0.1.0/src/drupal_mcp_server/auth/basic.py +10 -0
  46. drupal_mcp_server-0.1.0/src/drupal_mcp_server/auth/mcp_oauth.py +256 -0
  47. drupal_mcp_server-0.1.0/src/drupal_mcp_server/auth/session.py +108 -0
  48. drupal_mcp_server-0.1.0/src/drupal_mcp_server/auth/token.py +9 -0
  49. drupal_mcp_server-0.1.0/src/drupal_mcp_server/clients/__init__.py +7 -0
  50. drupal_mcp_server-0.1.0/src/drupal_mcp_server/clients/base.py +163 -0
  51. drupal_mcp_server-0.1.0/src/drupal_mcp_server/clients/drush.py +150 -0
  52. drupal_mcp_server-0.1.0/src/drupal_mcp_server/clients/graphql.py +182 -0
  53. drupal_mcp_server-0.1.0/src/drupal_mcp_server/clients/jsonapi.py +248 -0
  54. drupal_mcp_server-0.1.0/src/drupal_mcp_server/config.py +84 -0
  55. drupal_mcp_server-0.1.0/src/drupal_mcp_server/errors.py +45 -0
  56. drupal_mcp_server-0.1.0/src/drupal_mcp_server/exceptions.py +77 -0
  57. drupal_mcp_server-0.1.0/src/drupal_mcp_server/logging_config.py +37 -0
  58. drupal_mcp_server-0.1.0/src/drupal_mcp_server/schemas.py +55 -0
  59. drupal_mcp_server-0.1.0/src/drupal_mcp_server/server.py +233 -0
  60. drupal_mcp_server-0.1.0/src/drupal_mcp_server/services/__init__.py +6 -0
  61. drupal_mcp_server-0.1.0/src/drupal_mcp_server/services/ai_service.py +89 -0
  62. drupal_mcp_server-0.1.0/src/drupal_mcp_server/services/migration_service.py +20 -0
  63. drupal_mcp_server-0.1.0/src/drupal_mcp_server/services/site_manager.py +43 -0
  64. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/__init__.py +1 -0
  65. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/_utils.py +239 -0
  66. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/accessibility.py +264 -0
  67. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/ai.py +57 -0
  68. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/analysis.py +460 -0
  69. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/code_audit.py +227 -0
  70. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/comments.py +224 -0
  71. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/commerce.py +443 -0
  72. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/config.py +248 -0
  73. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/contact.py +189 -0
  74. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/content.py +561 -0
  75. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/database.py +79 -0
  76. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/devops.py +501 -0
  77. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/drupal_org.py +525 -0
  78. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/feeds.py +224 -0
  79. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/fields.py +227 -0
  80. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/headless.py +231 -0
  81. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/layout_builder.py +569 -0
  82. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/media_enhanced.py +193 -0
  83. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/menus.py +233 -0
  84. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/metatag.py +229 -0
  85. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/migration.py +326 -0
  86. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/moderation.py +547 -0
  87. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/module_dev.py +1510 -0
  88. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/multilingual.py +620 -0
  89. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/paragraphs.py +235 -0
  90. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/pathauto.py +186 -0
  91. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/performance.py +153 -0
  92. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/redirects.py +197 -0
  93. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/revisions.py +196 -0
  94. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/scheduler.py +309 -0
  95. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/search_api.py +280 -0
  96. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/security_audit.py +265 -0
  97. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/taxonomy.py +170 -0
  98. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/testing.py +249 -0
  99. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/theme_dev.py +755 -0
  100. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/users.py +209 -0
  101. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/views.py +336 -0
  102. drupal_mcp_server-0.1.0/src/drupal_mcp_server/tools/webform.py +564 -0
  103. drupal_mcp_server-0.1.0/src/drupal_mcp_server/validation.py +173 -0
  104. drupal_mcp_server-0.1.0/tests/__init__.py +0 -0
  105. drupal_mcp_server-0.1.0/tests/integration/test_server.py +247 -0
  106. drupal_mcp_server-0.1.0/tests/live/README.md +301 -0
  107. drupal_mcp_server-0.1.0/tests/live/bootstrap.py +258 -0
  108. drupal_mcp_server-0.1.0/tests/live/filter_check.py +186 -0
  109. drupal_mcp_server-0.1.0/tests/live/install_modules.py +258 -0
  110. drupal_mcp_server-0.1.0/tests/live/layout_builder_test.py +198 -0
  111. drupal_mcp_server-0.1.0/tests/live/mcp_call.py +72 -0
  112. drupal_mcp_server-0.1.0/tests/live/queue_test.py +276 -0
  113. drupal_mcp_server-0.1.0/tests/live/resource_check.py +150 -0
  114. drupal_mcp_server-0.1.0/tests/live/seed.php +96 -0
  115. drupal_mcp_server-0.1.0/tests/live/smoke_test.py +106 -0
  116. drupal_mcp_server-0.1.0/tests/live/sweep.py +764 -0
  117. drupal_mcp_server-0.1.0/tests/unit/__init__.py +0 -0
  118. drupal_mcp_server-0.1.0/tests/unit/test_auth.py +336 -0
  119. drupal_mcp_server-0.1.0/tests/unit/test_clients.py +731 -0
  120. drupal_mcp_server-0.1.0/tests/unit/test_content_tools.py +154 -0
  121. drupal_mcp_server-0.1.0/tests/unit/test_drupal_org.py +418 -0
  122. drupal_mcp_server-0.1.0/tests/unit/test_errors.py +99 -0
  123. drupal_mcp_server-0.1.0/tests/unit/test_handler_and_translation_tools.py +386 -0
  124. drupal_mcp_server-0.1.0/tests/unit/test_logging_config.py +41 -0
  125. drupal_mcp_server-0.1.0/tests/unit/test_moderation_transitions.py +467 -0
  126. drupal_mcp_server-0.1.0/tests/unit/test_resource_names.py +121 -0
  127. drupal_mcp_server-0.1.0/tests/unit/test_revisions.py +248 -0
  128. drupal_mcp_server-0.1.0/tests/unit/test_scheduler_tools.py +229 -0
  129. drupal_mcp_server-0.1.0/tests/unit/test_schemas.py +94 -0
  130. drupal_mcp_server-0.1.0/tests/unit/test_sweep_harness.py +440 -0
  131. drupal_mcp_server-0.1.0/tests/unit/test_tool_utils.py +950 -0
  132. drupal_mcp_server-0.1.0/tests/unit/test_tools.py +6 -0
  133. drupal_mcp_server-0.1.0/tests/unit/test_validation.py +237 -0
@@ -0,0 +1,38 @@
1
+ # Drupal MCP Server Configuration
2
+ # Copy to .env and fill in your values
3
+
4
+ # === Drupal Connection ===
5
+ DRUPAL_BASE_URL=https://your-drupal-site.com
6
+ DRUPAL_AUTH_TYPE=token
7
+ DRUPAL_AUTH_TOKEN=your-api-token-here
8
+
9
+ # For basic auth:
10
+ # DRUPAL_AUTH_TYPE=basic
11
+ # DRUPAL_USERNAME=admin
12
+ # DRUPAL_PASSWORD=your-password
13
+
14
+ # For OAuth2:
15
+ # DRUPAL_AUTH_TYPE=oauth2
16
+ # DRUPAL_OAUTH_CLIENT_ID=your-client-id
17
+ # DRUPAL_OAUTH_CLIENT_SECRET=your-client-secret
18
+ # DRUPAL_OAUTH_TOKEN_URL=https://your-site.com/oauth/token
19
+
20
+ # === AI Provider (choose one) ===
21
+ AI_PROVIDER=ollama
22
+ OLLAMA_BASE_URL=http://localhost:11434
23
+
24
+ # AI_PROVIDER=openai
25
+ # OPENAI_API_KEY=sk-...
26
+
27
+ # AI_PROVIDER=anthropic
28
+ # ANTHROPIC_API_KEY=sk-ant-...
29
+
30
+ # AI_PROVIDER=google
31
+ # GOOGLE_API_KEY=...
32
+
33
+ # === Drush Configuration ===
34
+ DRUSH_PATH=/usr/local/bin/drush
35
+ DRUSH_ROOT=/var/www/html
36
+
37
+ # === DDEV Configuration ===
38
+ DDEV_PROJECT_PATH=/path/to/your/drupal-project
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: '[BUG] '
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Describe the bug
10
+ A clear and concise description of what the bug is.
11
+
12
+ ## To Reproduce
13
+ Steps to reproduce the behavior:
14
+ 1. Configure ... with '...'
15
+ 2. Run tool '...'
16
+ 3. See error
17
+
18
+ ## Expected behavior
19
+ A clear and concise description of what you expected to happen.
20
+
21
+ ## Environment
22
+ - OS: [e.g., macOS 15, Ubuntu 22.04]
23
+ - Python version: [e.g., 3.11.0]
24
+ - Drupal version: [e.g., 10.2]
25
+ - MCP transport: [stdio / http / sse / streamable-http]
26
+ - Installation method: [pip install / source]
27
+
28
+ ## Logs
29
+ ```
30
+ Paste relevant error logs or tool output here
31
+ ```
32
+
33
+ ## Additional context
34
+ Add any other context about the problem here.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: '[FEAT] '
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Is your feature request related to a problem?
10
+ A clear and concise description of what the problem is.
11
+
12
+ ## Describe the solution you'd like
13
+ A clear and concise description of what you want to happen.
14
+
15
+ ## Describe alternatives you've considered
16
+ A clear and concise description of any alternative solutions or features you've considered.
17
+
18
+ ## Which tool category does this belong to?
19
+ - [ ] Content management
20
+ - [ ] Users & permissions
21
+ - [ ] Taxonomy
22
+ - [ ] Media
23
+ - [ ] DevOps / Drush
24
+ - [ ] Migration
25
+ - [ ] AI-powered tools
26
+ - [ ] Security audit
27
+ - [ ] Code quality
28
+ - [ ] Other: ___________
29
+
30
+ ## Additional context
31
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,32 @@
1
+ ## Description
2
+
3
+ Please include a summary of the change and which issue is fixed.
4
+
5
+ Fixes # (issue number)
6
+
7
+ ## Type of change
8
+
9
+ - [ ] Bug fix (non-breaking change which fixes an issue)
10
+ - [ ] New feature (non-breaking change which adds functionality)
11
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12
+ - [ ] Documentation update
13
+ - [ ] Security fix
14
+ - [ ] Performance improvement
15
+
16
+ ## How has this been tested?
17
+
18
+ Please describe the tests that you ran to verify your changes.
19
+
20
+ - [ ] Unit tests pass (`pytest`)
21
+ - [ ] Linting passes (`ruff check src/ tests/`)
22
+ - [ ] Type checking passes (`mypy src/drupal_mcp_server/`)
23
+ - [ ] Integration tests pass (describe setup)
24
+
25
+ ## Checklist
26
+
27
+ - [ ] My code follows the project style guidelines
28
+ - [ ] I have added tests that prove my fix is effective or that my feature works
29
+ - [ ] New and existing unit tests pass locally with my changes
30
+ - [ ] I have updated the documentation accordingly
31
+ - [ ] I have added input validation for any new user-facing parameters
32
+ - [ ] Security considerations have been addressed
@@ -0,0 +1,75 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install -e ".[dev]"
28
+
29
+ - name: Lint with ruff
30
+ run: ruff check src/ tests/
31
+
32
+ - name: Type check with mypy
33
+ run: mypy src/drupal_mcp_server/
34
+
35
+ - name: Run tests with coverage
36
+ run: |
37
+ # Measure everything and report it, but gate only the code that can be
38
+ # tested without a live Drupal. The large majority of the statements
39
+ # are in the tool modules, which only tests/live/sweep.py can
40
+ # exercise, so a single total threshold is unreachable by construction:
41
+ # even 100% coverage of everything else would total just 25%.
42
+ pytest --cov=src/drupal_mcp_server --cov-report=term-missing --cov-report=xml -v
43
+ coverage report --omit="*/drupal_mcp_server/tools/*" --fail-under=70
44
+
45
+ - name: Upload coverage to Codecov
46
+ uses: codecov/codecov-action@v4
47
+ with:
48
+ files: ./coverage.xml
49
+ fail_ci_if_error: true
50
+
51
+ - name: Dependency vulnerability scan
52
+ run: pip-audit
53
+
54
+ build:
55
+ runs-on: ubuntu-latest
56
+ needs: test
57
+
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+
61
+ - name: Set up Python
62
+ uses: actions/setup-python@v5
63
+ with:
64
+ python-version: "3.11"
65
+
66
+ - name: Build package
67
+ run: |
68
+ pip install build
69
+ python -m build
70
+
71
+ - name: Upload artifacts
72
+ uses: actions/upload-artifact@v4
73
+ with:
74
+ name: dist
75
+ path: dist/
@@ -0,0 +1,40 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'docs/**'
8
+ - 'mkdocs.yml'
9
+ - 'README.md'
10
+ pull_request:
11
+ paths:
12
+ - 'docs/**'
13
+ - 'mkdocs.yml'
14
+ - 'README.md'
15
+
16
+ jobs:
17
+ docs:
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.11"
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ pip install mkdocs-material mkdocstrings[python]
31
+
32
+ - name: Build documentation
33
+ run: mkdocs build --strict
34
+
35
+ - name: Deploy to GitHub Pages
36
+ if: github.ref == 'refs/heads/main'
37
+ uses: peaceiris/actions-gh-pages@v4
38
+ with:
39
+ github_token: ${{ secrets.GITHUB_TOKEN }}
40
+ publish_dir: ./site
@@ -0,0 +1,38 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: write
11
+
12
+ jobs:
13
+ release:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.11"
23
+
24
+ - name: Install build tools
25
+ run: |
26
+ pip install build
27
+
28
+ - name: Build package
29
+ run: python -m build
30
+
31
+ - name: Create GitHub Release
32
+ uses: softprops/action-gh-release@v2
33
+ with:
34
+ files: dist/*
35
+ generate_release_notes: true
36
+
37
+ - name: Publish to PyPI
38
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,78 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .venv/
28
+
29
+ # Local DDEV test site (see tests/live/README.md to rebuild it)
30
+ drupal-live/
31
+
32
+ # Coverage output
33
+ coverage.xml
34
+ .coverage
35
+
36
+ # Live-test output
37
+ tests/live/sweep-results.json
38
+
39
+ # IDE
40
+ .idea/
41
+ .vscode/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+
46
+ # Testing
47
+ .pytest_cache/
48
+ .coverage
49
+ htmlcov/
50
+ .tox/
51
+ .nox/
52
+
53
+ # MyPy
54
+ .mypy_cache/
55
+
56
+ # Ruff
57
+ .ruff_cache/
58
+
59
+ # Environment
60
+ .env
61
+ .env.local
62
+ .env.*.local
63
+
64
+ # OS
65
+ .DS_Store
66
+ Thumbs.db
67
+
68
+ # Project specific
69
+ *.log
70
+ *.sql
71
+ *.tar.gz
72
+ config/export/
73
+ config/sync/
74
+
75
+ #agents
76
+ .omc
77
+ .pi
78
+ .mutagen-*
@@ -0,0 +1,306 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Security
11
+
12
+ - Fix command injection vulnerabilities in code audit and security audit tools
13
+ - Fix SQL injection in database query tools
14
+ - Fix path traversal in media upload tool
15
+ - Add input validation for URLs, queue names, and file paths
16
+ - Add authentication check to /mcp/tools/list endpoint
17
+
18
+ ### Added
19
+
20
+ - Add `moderation_transition_delete`, giving `moderation_transition_create` the
21
+ counterpart it never had. The tool count is now 367
22
+ - Add Lando support to SiteManager
23
+ - Add subprocess timeout to DrushClient
24
+ - Add OAuth token fetching to AuthManager
25
+ - Create shared tools/_utils.py with ensure_dict, validate_pagination, build_pagination_params
26
+ - Add pagination validation to list tools
27
+ - Add input validation to accessibility and performance tools
28
+ - Add LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md
29
+ - Add GitHub issue and PR templates
30
+
31
+ ### Changed
32
+
33
+ - Correct what the sweep asks for, not just how it reads the answer. Every
34
+ parameter ending in `id` had been answered with the fixture node's id, so
35
+ `view_id`, `menu_id`, `webform_id` and eighty-odd others were all sent one
36
+ node number and answered a lookup miss: **84 of 174 refusals** in the run
37
+ before this were the harness asking the wrong question, not tools refusing,
38
+ and a miss says nothing about whether a tool works. Of 367 tools, 192 declare
39
+ no id-shaped parameter, 93 read-only ones now get a real id for their own
40
+ resource — with the tool's own name deciding where one parameter covers
41
+ several, since `config_id` is a field config, a metatag config or a search
42
+ index depending on who is asking — and 82 mutating ones deliberately keep
43
+ junk ids, because `views_update` with a real view id would save the view back
44
+ with the harness's synthesised arguments and every later `views_execute`
45
+ would then run against a view this run broke. The remaining 8 read-only tools
46
+ keep junk because the site has no such entity to name. The fixture node also
47
+ re-checks and re-provisions itself: `delete_node` runs mid-sweep and took the
48
+ node with it, which alone 404'd `revision_list`, `revision_get`,
49
+ `revision_revert`, `revision_diff`, `translation_status`, `translation_add`,
50
+ `moderation_transition_execute`, `layout_builder_override_remove` and
51
+ `accessibility_aria_validate`
52
+ - Keep `feeds_log_list` an explicit refusal rather than implement it. Feeds 3.x
53
+ has no log table, no `feeds*` service and no log entity to read, so there is
54
+ nothing behind it to query; the tool names `imports_import_status`, which does
55
+ answer, instead of returning an empty list that would read as "no log entries"
56
+ - Definitive full re-sweep (OK 195 / REJECTED 156 / BLOCKED 7 /
57
+ UPSTREAM 4 / DEAD 0 / SKIPPED 5, 367 total); no tool that used to work
58
+ regressed and nothing is dead. Harness follow-up fixes (`entity_id` and
59
+ revision-id handling) pushed six more tools past their lookup so they reach
60
+ their own logic, where they report honestly.
61
+
62
+ - Re-sweep all 366 tools against the provisioned site. BLOCKED fell from 71 to
63
+ 5, OK rose from 156 to 181, and nothing is dead. 25 tools that could not be
64
+ reached at all now run; 41 reach Drupal and are turned away with a reason
65
+ instead of stopping at a 404. The five still BLOCKED are two `feeds_feed`
66
+ tools needing a feed type with a `feeds_item` field, two `moderation_state`
67
+ tools for a resource Drupal never exposes, and `split_config` needing a
68
+ `./config/sync` directory. The one UPSTREAM is `revision_delete`, since
69
+ JSON:API has no revision collection endpoint
70
+ - Correct the sweep's own accounting. Six tools — `translation_add`,
71
+ `translation_update`, the three `interface_translation_*` tools and
72
+ `config_translation_add` — were reported OK on every run while never being
73
+ called once. When arguments fail FastMCP's validation the answer is prose
74
+ rather than JSON, and nothing in the classifier matched it, so the
75
+ fall-through said pass; the server logged `missing_argument` for each and
76
+ the report showed a green result. A refused call now counts as refused,
77
+ through the SDK's own `isError` signal rather than a guess at wording. The
78
+ sweep also stopped filling `langcode` with a junk value (JSON:API
79
+ write-protects that field on a multilingual site whatever it says, so
80
+ `create_term` answered 403 about a permission instead of running),
81
+ started giving `ddev_exec` a command the container has, gave
82
+ `drush_watchdog` the enum and the number drush wants, and names a queue
83
+ that exists
84
+ - Provision the live test site with the modules the tools assume are there:
85
+ media, comment, contact, language, config_translation, workflows,
86
+ content_moderation and commerce plus its submodules from core and contrib,
87
+ and token, pathauto, redirect, metatag, paragraphs, search_api, facets,
88
+ webform, feeds and graphql from drupal.org, with phpstan, drupal-check,
89
+ phpcodesniffer and coder for the code-quality tools. Drupal's entity types
90
+ went from 26 to 78 and the JSON:API index from 27 resources to 76, which
91
+ turned most of the sweep's BLOCKED bucket into a real verdict. The site is
92
+ also multilingual now -- English and French -- because a single-language site
93
+ cannot exercise translation behaviour, and that is what surfaced the langcode
94
+ bug below
95
+ - Add three live suites. `tests/live/install_modules.py` provisions the modules
96
+ the sweep needs using the server's own `composer_require` and `drush_command`
97
+ tools, so bootstrapping the site is also a test of them; `resource_check.py`
98
+ asks Drupal for the entity types it knows and flags any resource name our
99
+ tools ask for that cannot exist, which is how the `views_view` and
100
+ `moderation_state` bugs were found; `mcp_call.py` is a one-call driver for
101
+ operating the site through the server instead of a shell
102
+
103
+ - Gate CI coverage on the code testable offline (non-tool core ≥70%) instead of
104
+ the total: 86% of statements live in the tool modules, which only a live sweep
105
+ can exercise, so a single total threshold is unreachable by construction.
106
+ `coverage.xml` is now actually generated, which the Codecov step already
107
+ required with `fail_ci_if_error: true`
108
+ - Extend `layout_builder_override_remove` with an optional `entity_id`, since an
109
+ override belongs to an entity and the old signature could never name one
110
+ - Document what live testing found, and how a failure arrives, in
111
+ `tests/live/README.md` and the README's Development section
112
+ - Extract _ensure_dict from 35+ tool modules into shared utility
113
+ - Remove duplicate service implementations (ai.py, migration.py, site.py)
114
+ - Clean up unused imports across all tool modules
115
+ - Update ARTIFACT.md to reflect actual project state
116
+
117
+ ### Fixed
118
+
119
+ - Make an issued OAuth token worth issuing. HTTP transport with OAuth enabled
120
+ refused everyone: `load_access_token` returned the dict the token was stored
121
+ in where the declared type is FastMCP's `AccessToken`, so the first thing the
122
+ bearer middleware did with a good token -- `auth_info.expires_at` -- raised
123
+ `AttributeError: 'dict' object has no attribute 'expires_at'` and the request
124
+ was refused exactly as a forged one would be. That made the defect invisible
125
+ by construction, because the fail-closed path and the fail-for-everyone path
126
+ are the same 401: the only OAuth behaviour anyone had ever observed was the
127
+ reassuring one, and the 401 that read as security working was also masking an
128
+ unusable transport. `expires_at` was stored as a float from `time.time()`
129
+ while `AccessToken.expires_at` is an int, so pydantic would have refused it
130
+ regardless. Verified three ways -- unit tests over authorize/exchange/load,
131
+ an ASGI test against FastMCP's real HTTP app (401 unauthenticated, 200 for
132
+ the issued token, 401 for a forged one), and a replay of the old
133
+ implementation that reproduces the AttributeError
134
+ - Implement twelve tools that refused or had never been written. The five
135
+ `moderation_transition_*` tools all read `$wf->get("transitions")`, a key the
136
+ workflow entity has never had — transitions live on
137
+ `Workflow::getTypePlugin()->getTransitions()` — so `list` answered
138
+ `count: 0` against a five-transition workflow, which reads as a pass because
139
+ an empty list is not an error, `get` reported "No transition with id 7" for a
140
+ transition present by name, and `execute`, `create` and the new `delete` were
141
+ unreachable behind the same mistake. The four `interface_translation_*` tools
142
+ read `TranslationString::getLanguage()` and `getTranslation()`, methods that
143
+ class does not have; they now go through `locale.storage`, and a live round
144
+ trip verified the list (13666 French rows, with search and context filters),
145
+ add, update and delete, including a source string carrying a non-ASCII
146
+ character and a `$`. Writing a translation also dispatches
147
+ `LocaleEvents::SAVE_TRANSLATION` through `_locale_refresh_translations()`,
148
+ which invalidates the `locale` cache tag every `LocaleLookup` row depends on
149
+ — `invalidateByPrefix` and `cache_clear_all` do not exist in Drupal 11. The
150
+ three `webform_handler_*` tools answered `absent_entity` because they had
151
+ never been written; they now use `plugin.manager.webform.handler` with
152
+ `Webform::addWebformHandler` / `updateWebformHandler` / `deleteWebformHandler`,
153
+ take `webform_id` to disambiguate a handler id that is unique per webform
154
+ rather than site-wide, replace configuration keys whole so omitted ones keep
155
+ their current values, and locate handlers through
156
+ `getHandlers()->getInstanceIds()` instead of `Webform::getHandler()`, which
157
+ throws `PluginNotFoundException` for an unseen id rather than returning NULL.
158
+ A `contact` form started this with two handlers and ended with two after a
159
+ full create/update/delete — the point of a round trip
160
+ - Hand structured values to `php:eval` as base64 rather than as JSON, which is
161
+ not PHP: it emits `\uXXXX` and PHP has no such escape, so non-ASCII mangles
162
+ silently; `$` interpolates into the code; and `{"k":1}` is a parse error.
163
+ `_utils.php_text()` and `php_json()` carry a string as `base64_decode(...)`
164
+ and an array as `json_decode(base64_decode(...), TRUE)`
165
+ - Stop chasing the login redirect to a URL the site invented. Drupal answers a
166
+ good login with 303 to the account page, built from the scheme and host it
167
+ saw the request arrive on, so when the site reported itself as plain `http`
168
+ while the client was on HTTPS the redirect was fetched as HTTP against a TLS
169
+ port, nginx answered 400, and the session cookie that same response had just
170
+ issued was discarded — leaving only a startup warning. Every write then fell
171
+ back to basic auth, which Drupal's mutating endpoints reject, on any site
172
+ whose self-issued URL disagrees with the client's behind a proxy or a
173
+ misconfigured base_url. The POST is no longer followed, because only its
174
+ status and its cookie were ever wanted; bad credentials still fail, since
175
+ Drupal refuses them with 200 and no cookie
176
+ - Make the sweep follow the site's links on the origin it was given. Drupal
177
+ answers `http://` when reached through a port-mapped HTTPS endpoint, and the
178
+ harness followed those verbatim — plain HTTP to a TLS listener, which nginx
179
+ refuses with 400 — so a whole run died before calling a single tool.
180
+ `filter_check.py` hardcoded its base URL while every other live script honours
181
+ `DRUPAL_BASE_URL`, so it could not be pointed at all
182
+ - Ask whether a node exists in a way JSON:API can answer. The check built the
183
+ resource *name* into the path (`/jsonapi/node--article/7`, where the resource
184
+ lives at `/jsonapi/node/article`) and asked for an internal id where rows are
185
+ addressed by uuid, so it 404'd for nodes that exist and reported every fixture
186
+ as gone — which would have had the harness re-provisioning on every check
187
+ instead of only when `delete_node` removed one
188
+ - Make three tools answer with the reason instead of a bare error.
189
+ `views_execute` could never have run: it asked for a `views.views` service
190
+ that does not exist (the executable is built from the view's config entity by
191
+ `views.executable`), read the row's entity through a magic property that
192
+ `isset()` always reports as absent, and took no care over the display. It now
193
+ loads the view, checks the display against the ones the view actually has --
194
+ reporting the rest when the caller's is wrong -- and reads `_entity`.
195
+ `graphql_query` and `graphql_schema_get` returned a raw 404 whenever the site
196
+ had no GraphQL server, which is the state a plain install leaves it in, since
197
+ the module ships no default; they now say that, and name the config entity
198
+ and the permission that fix it. `paragraphs_item_list` returned a bare 404
199
+ for a bundle that does not exist, which reads as "paragraphs are missing"
200
+ rather than "that type is not configured here"; it now lists the types the
201
+ site does have
202
+ - Fix eleven hard-coded JSON:API filters that Drupal rejected outright, each
203
+ returning an empty result on every call on every site. `commerce_product`,
204
+ `commerce_promotion` and `commerce_shipping_method` filtered on `store` where
205
+ the field is the plural `stores`, and `commerce_order` on `store` where it is
206
+ the `store_id` reference; all four needed the `.id` specifier that entity
207
+ references require. `webform_submission` filtered on `webform` where the field
208
+ is `webform_id`, also a reference. `paragraphs_item_list` filtered on
209
+ `paragraphs_type`, which is not a field at all -- a paragraph's type is its
210
+ bundle, so it now addresses `paragraph--{type}`. `pathauto_pattern_list`
211
+ filtered on `entity_type` and `bundle`, which live in a pattern's id as
212
+ `{entity_type}--{bundle}`, so it now filters on `id` when it has both.
213
+ `comments_thread_list` filtered on `pid` for top-level comments, but `pid` is
214
+ a reference to the parent comment, so it 400'd -- and `pid.id = 0` would have
215
+ returned nothing, since no comment has id 0; the whole thread is returned now,
216
+ with each row's `pid` available for building the tree. `facets_facet_list`
217
+ filtered on `index`, but a facet attaches to a facet source rather than to an
218
+ index, so it lists facets and reports their `facet_source_id`
219
+ - Stop sending `langcode: "en"` in create payloads. Four tools did this. It is
220
+ harmless on a single-language site, but once a second language exists the
221
+ field is writable only with translation permission, so every create failed
222
+ 403 "the current user is not allowed to POST the selected field (langcode)" --
223
+ including an ordinary authenticated user creating a term in the site's own
224
+ language. The field is now sent only when the caller asks for a language
225
+ - Fix `drush_command` option rendering, which made every confirmation-answering
226
+ call fail. Boolean options were emitted as `--key=value`, but drush flags take
227
+ no value, so `options={"yes": True}` -- the documented way to install a module
228
+ -- was rejected with "The --yes option does not accept a value" and nothing
229
+ was installed while the tool reported success. A `False` was worse: it emitted
230
+ the bare flag, so asking *not* to confirm silently suppressed confirmation on
231
+ destructive commands. Zero-valued options were also mangled, `--count=0`
232
+ becoming a bare `--count`. Booleans are now bare flags, `False` omits the
233
+ option, and everything else keeps its value
234
+ - Return an `error` from `drush_command` when Drush exits non-zero. It handed
235
+ back the raw result with no `error` key, so the generic escape hatch -- the
236
+ tool an agent reaches for first -- still presented a failed command as an
237
+ empty success, even though `ensure_dict` already fixed this for the 224
238
+ specialised call sites
239
+ - Split several packages in `composer_require`. They arrived as one argv token,
240
+ so composer read `drupal/pathauto` as a version constraint on `drupal/token`
241
+ and refused the whole request with "Could not parse version constraint"
242
+ - Ask for `view`, not `views_view`: the entity type has been `view` since
243
+ Drupal 8, so all six views tools 404'd on a site with Views installed and
244
+ healthy, and read as "module not installed"
245
+ - Ask for `content_moderation_state`, not `moderation_state`, in the moderation
246
+ tools. The node *field* is still called `moderation_state` and is unchanged
247
+ - Ask for `facets_facet`, not `search_api_facet`, in the facet tools
248
+ - Rewrite `config_translation_list` and `config_translation_add`, which targeted
249
+ three mechanisms that do not exist between them: a `config_translation`
250
+ JSON:API resource, a `config:translation:add` Drush command (Drush has no
251
+ translation command category at all), and `$entity->addTranslation()` on a
252
+ config entity, which implements no `TranslatableInterface`. The list reports
253
+ the mappers this site has. The add resolves `{entity_type}/{entity_id}`
254
+ through the config translation mapper to the config object the entity is
255
+ stored as -- `node_type` and `article` become `node.type.article` -- and
256
+ writes a language config override for the target language, which is where
257
+ Drupal keeps config translations. Verified live: the French `name` and
258
+ `description` land on the override while the base config keeps its own
259
+ - Drop a dead `get_resource("jsonapi")` call from `nextjs_helpers_generate`. It
260
+ requested `/jsonapi/jsonapi`, a path that does not exist, and the 404 aborted
261
+ the whole tool for a value the generator never used
262
+
263
+ - Implement the six `layout_builder_*` tools against the real API; they asked
264
+ JSON:API for a resource that has never existed and returned an opaque 404 on
265
+ every call, so reading, writing and reverting layouts now go through Drush to
266
+ `entity_view_display` and `OverridesSectionStorage`
267
+ - Fix `DrushClient._flatten` shlex-splitting list arguments, which made
268
+ `db_tables`, `db_query`, `db_count`, `drush_php_eval` and every `ev` tool fail
269
+ with "Too many arguments"
270
+ - Report a failed Drush run as an `error`: `ensure_dict` passed
271
+ `{returncode: 1, data: {}, stderr}` through untouched, byte for byte an empty
272
+ success, so 224 call sites presented broken commands as empty results
273
+ - Return real tool schemas from `/mcp/tools/list`, which reported
274
+ `input_schema: {}` for all 366 tools
275
+ - Fix `cron_list` (called Drupal 7's `drupal_get_cron_jobs()`, removed in
276
+ Drupal 8), `queue_create` (a shell escape in PHP that never passes through a
277
+ shell, so PHP died with a ParseError) and `queue_delete` (a second argument on
278
+ all three paths when `queue:delete` accepts exactly one)
279
+ - Route `queue_create` and `queue_delete` around `drush queue:delete`, which
280
+ resolves only registered queue services and answered "Queue not found" for a
281
+ queue this server had just created
282
+ - Fix `permission_audit` and `menu_link_list` filtering on fields Drupal does
283
+ not expose (`filter[name]` on `user_role`, `filter[menu]` on
284
+ `menu_link_content`), a 400 on every call on every site
285
+ - Validate whole statements in `db_query`; a `SELECT 1; DROP TABLE x` passed the
286
+ `startswith("SELECT")` guard and dropped the table
287
+ - Stop stacking log handlers on repeated `setup_logging` calls
288
+
289
+ ### Removed
290
+
291
+ - Remove dead register_all_tools function from tools/__init__.py
292
+ - Remove duplicate _find_tool_path definition in security_audit.py
293
+
294
+ ## [0.1.0] - 2026-10-13
295
+
296
+ ### Added
297
+
298
+ - Initial release with 35+ MCP tools across all major Drupal categories
299
+ - FastMCP server with stdio and HTTP transports
300
+ - JSON:API and GraphQL clients with authentication
301
+ - Drush command execution via subprocess
302
+ - Multi-provider AI support (Ollama, OpenAI, Anthropic, NVIDIA NIM)
303
+ - Content management, user management, taxonomy, views, paragraphs tools
304
+ - DevOps tools for DDEV/Lando, Composer, database queries
305
+ - Migration tools for D7 to D10 upgrades
306
+ - Security audit and code quality tools