drupal-mcp-connector 2.7.3 → 2.8.0

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 (229) hide show
  1. package/.agents/commands/drupal-audit-config-best-practices.md +15 -0
  2. package/{.claude → .agents}/commands/drupal-audit-site-health.md +4 -5
  3. package/.agents/commands/drupal-block-user.md +19 -0
  4. package/{.claude → .agents}/commands/drupal-bulk-create.md +4 -5
  5. package/{.claude → .agents}/commands/drupal-bulk-update.md +4 -5
  6. package/.agents/commands/drupal-config-get.md +18 -0
  7. package/.agents/commands/drupal-config-list.md +16 -0
  8. package/.agents/commands/drupal-config-set.md +19 -0
  9. package/.agents/commands/drupal-content-by-moderation-state.md +21 -0
  10. package/.agents/commands/drupal-create-block.md +20 -0
  11. package/{.claude → .agents}/commands/drupal-create-media.md +4 -5
  12. package/{.claude → .agents}/commands/drupal-create-menu-link.md +4 -5
  13. package/{.claude → .agents}/commands/drupal-create-node.md +7 -8
  14. package/{.claude → .agents}/commands/drupal-create-paragraph.md +4 -5
  15. package/{.claude → .agents}/commands/drupal-create-redirect.md +4 -5
  16. package/.agents/commands/drupal-create-taxonomy-term.md +22 -0
  17. package/{.claude → .agents}/commands/drupal-create-translation.md +4 -5
  18. package/.agents/commands/drupal-create-user.md +23 -0
  19. package/.agents/commands/drupal-delete-media.md +21 -0
  20. package/.agents/commands/drupal-delete-node.md +22 -0
  21. package/.agents/commands/drupal-delete-taxonomy-term.md +21 -0
  22. package/{.claude → .agents}/commands/drupal-describe-fields.md +4 -5
  23. package/.agents/commands/drupal-drush-cache-rebuild.md +15 -0
  24. package/.agents/commands/drupal-drush-config-export.md +15 -0
  25. package/.agents/commands/drupal-drush-config-import.md +15 -0
  26. package/.agents/commands/drupal-drush-config-status.md +15 -0
  27. package/.agents/commands/drupal-drush-cron.md +15 -0
  28. package/.agents/commands/drupal-drush-module-disable.md +20 -0
  29. package/.agents/commands/drupal-drush-module-enable.md +18 -0
  30. package/.agents/commands/drupal-drush-module-list.md +16 -0
  31. package/.agents/commands/drupal-drush-security-updates.md +15 -0
  32. package/{.claude → .agents}/commands/drupal-drush-sql-query.md +4 -5
  33. package/.agents/commands/drupal-drush-status.md +15 -0
  34. package/.agents/commands/drupal-drush-updatedb.md +15 -0
  35. package/.agents/commands/drupal-drush-user-create.md +21 -0
  36. package/.agents/commands/drupal-drush-user-list.md +18 -0
  37. package/.agents/commands/drupal-drush-watchdog.md +18 -0
  38. package/{.claude → .agents}/commands/drupal-entity-create.md +4 -5
  39. package/.agents/commands/drupal-entity-delete.md +23 -0
  40. package/.agents/commands/drupal-entity-get.md +21 -0
  41. package/{.claude → .agents}/commands/drupal-entity-list.md +4 -5
  42. package/{.claude → .agents}/commands/drupal-entity-update.md +7 -8
  43. package/.agents/commands/drupal-find-orphaned-media.md +17 -0
  44. package/.agents/commands/drupal-get-entity-schema.md +19 -0
  45. package/.agents/commands/drupal-get-media.md +19 -0
  46. package/.agents/commands/drupal-get-node.md +19 -0
  47. package/{.claude → .agents}/commands/drupal-get-paragraph.md +4 -5
  48. package/{.claude → .agents}/commands/drupal-get-revision.md +4 -5
  49. package/.agents/commands/drupal-get-taxonomy-term.md +19 -0
  50. package/.agents/commands/drupal-get-taxonomy-terms.md +20 -0
  51. package/.agents/commands/drupal-get-user-by-name.md +18 -0
  52. package/.agents/commands/drupal-get-user.md +18 -0
  53. package/{.claude → .agents}/commands/drupal-governance-status.md +4 -5
  54. package/.agents/commands/drupal-graphql-introspect.md +16 -0
  55. package/{.claude → .agents}/commands/drupal-graphql.md +4 -5
  56. package/{.claude → .agents}/commands/drupal-list-blocks.md +4 -5
  57. package/.agents/commands/drupal-list-content-types.md +15 -0
  58. package/.agents/commands/drupal-list-entity-types.md +15 -0
  59. package/.agents/commands/drupal-list-media-types.md +15 -0
  60. package/.agents/commands/drupal-list-media.md +20 -0
  61. package/{.claude → .agents}/commands/drupal-list-menu-links.md +4 -5
  62. package/.agents/commands/drupal-list-moderation-states.md +19 -0
  63. package/{.claude → .agents}/commands/drupal-list-nodes.md +4 -5
  64. package/{.claude → .agents}/commands/drupal-list-revisions.md +4 -5
  65. package/.agents/commands/drupal-list-roles.md +15 -0
  66. package/{.claude → .agents}/commands/drupal-list-sites.md +2 -3
  67. package/{.claude → .agents}/commands/drupal-list-translations.md +4 -5
  68. package/.agents/commands/drupal-list-users.md +19 -0
  69. package/.agents/commands/drupal-list-vocabularies.md +15 -0
  70. package/.agents/commands/drupal-mcp-whoami.md +15 -0
  71. package/.agents/commands/drupal-report-404-log.md +16 -0
  72. package/.agents/commands/drupal-report-accessibility-audit.md +17 -0
  73. package/.agents/commands/drupal-report-alias-coverage.md +17 -0
  74. package/.agents/commands/drupal-report-broken-embeds.md +17 -0
  75. package/{.claude → .agents}/commands/drupal-report-broken-links.md +4 -5
  76. package/.agents/commands/drupal-report-cache-config.md +15 -0
  77. package/.agents/commands/drupal-report-config-drift.md +15 -0
  78. package/.agents/commands/drupal-report-content-by-author.md +17 -0
  79. package/.agents/commands/drupal-report-content-summary.md +15 -0
  80. package/.agents/commands/drupal-report-duplicate-content.md +17 -0
  81. package/.agents/commands/drupal-report-field-completeness.md +20 -0
  82. package/.agents/commands/drupal-report-menu-integrity.md +16 -0
  83. package/{.claude → .agents}/commands/drupal-report-missing-field.md +4 -5
  84. package/.agents/commands/drupal-report-module-audit.md +15 -0
  85. package/.agents/commands/drupal-report-orphan-pages.md +17 -0
  86. package/{.claude → .agents}/commands/drupal-report-orphaned-references.md +4 -5
  87. package/.agents/commands/drupal-report-permission-audit.md +15 -0
  88. package/.agents/commands/drupal-report-pii-exposure.md +18 -0
  89. package/.agents/commands/drupal-report-readability.md +18 -0
  90. package/.agents/commands/drupal-report-recently-published.md +17 -0
  91. package/.agents/commands/drupal-report-redirect-health.md +16 -0
  92. package/.agents/commands/drupal-report-revision-hotspots.md +17 -0
  93. package/.agents/commands/drupal-report-scheduled-content.md +17 -0
  94. package/{.claude → .agents}/commands/drupal-report-seo-audit.md +4 -5
  95. package/.agents/commands/drupal-report-seo-meta-coverage.md +18 -0
  96. package/.agents/commands/drupal-report-stale-content.md +19 -0
  97. package/.agents/commands/drupal-report-status-report.md +16 -0
  98. package/.agents/commands/drupal-report-taxonomy-usage.md +21 -0
  99. package/.agents/commands/drupal-report-text-format-audit.md +15 -0
  100. package/.agents/commands/drupal-report-translation-coverage.md +18 -0
  101. package/.agents/commands/drupal-report-unpublished.md +17 -0
  102. package/.agents/commands/drupal-report-user-activity.md +17 -0
  103. package/.agents/commands/drupal-report-workflow-bottlenecks.md +19 -0
  104. package/{.claude → .agents}/commands/drupal-resolve-reference.md +4 -5
  105. package/{.claude → .agents}/commands/drupal-revert-revision.md +4 -5
  106. package/{.claude → .agents}/commands/drupal-schedule-publish.md +4 -5
  107. package/.agents/commands/drupal-search-content.md +21 -0
  108. package/.agents/commands/drupal-search.md +20 -0
  109. package/.agents/commands/drupal-security-info.md +15 -0
  110. package/.agents/commands/drupal-set-moderation-state.md +20 -0
  111. package/.agents/commands/drupal-site-info.md +15 -0
  112. package/.agents/commands/drupal-update-media.md +22 -0
  113. package/{.claude → .agents}/commands/drupal-update-menu-link.md +4 -5
  114. package/{.claude → .agents}/commands/drupal-update-node.md +10 -11
  115. package/{.claude → .agents}/commands/drupal-update-paragraph.md +4 -5
  116. package/{.claude → .agents}/commands/drupal-update-redirect.md +4 -5
  117. package/.agents/commands/drupal-update-taxonomy-term.md +22 -0
  118. package/.agents/commands/drupal-update-user.md +24 -0
  119. package/{.claude → .agents}/commands/drupal-upload-file-and-create-media.md +4 -5
  120. package/{.claude → .agents}/commands/drupal-upload-file.md +4 -5
  121. package/CHANGELOG.md +80 -0
  122. package/README.md +23 -12
  123. package/config/config.example.json +1 -1
  124. package/package.json +4 -2
  125. package/scripts/generate-commands.js +42 -21
  126. package/scripts/install-commands.js +157 -0
  127. package/src/lib/backends/backend-interface.js +20 -1
  128. package/src/lib/backends/jsonapi.js +47 -4
  129. package/src/lib/body-summary.js +60 -0
  130. package/src/lib/data-flow.js +437 -0
  131. package/src/lib/dispatch.js +99 -30
  132. package/src/lib/drupal-fetch.js +52 -6
  133. package/src/lib/field-definition.js +285 -0
  134. package/src/lib/operations.js +39 -2
  135. package/src/lib/patch-preflight.js +157 -66
  136. package/src/lib/security.js +15 -1
  137. package/src/lib/server-tools.js +9 -0
  138. package/src/lib/site-target.js +78 -0
  139. package/src/lib/tool-prompts.js +2 -1
  140. package/src/lib/write-revision.js +82 -2
  141. package/src/tools/bulk.js +4 -3
  142. package/src/tools/config.js +5 -2
  143. package/src/tools/entities.js +11 -7
  144. package/src/tools/index.js +24 -3
  145. package/src/tools/moderation.js +88 -9
  146. package/src/tools/nodes.js +55 -43
  147. package/.claude/commands/drupal-audit-config-best-practices.md +0 -16
  148. package/.claude/commands/drupal-block-user.md +0 -20
  149. package/.claude/commands/drupal-config-get.md +0 -19
  150. package/.claude/commands/drupal-config-list.md +0 -17
  151. package/.claude/commands/drupal-config-set.md +0 -20
  152. package/.claude/commands/drupal-content-by-moderation-state.md +0 -22
  153. package/.claude/commands/drupal-create-block.md +0 -21
  154. package/.claude/commands/drupal-create-taxonomy-term.md +0 -23
  155. package/.claude/commands/drupal-create-user.md +0 -24
  156. package/.claude/commands/drupal-delete-media.md +0 -22
  157. package/.claude/commands/drupal-delete-node.md +0 -23
  158. package/.claude/commands/drupal-delete-taxonomy-term.md +0 -22
  159. package/.claude/commands/drupal-drush-cache-rebuild.md +0 -16
  160. package/.claude/commands/drupal-drush-config-export.md +0 -16
  161. package/.claude/commands/drupal-drush-config-import.md +0 -16
  162. package/.claude/commands/drupal-drush-config-status.md +0 -16
  163. package/.claude/commands/drupal-drush-cron.md +0 -16
  164. package/.claude/commands/drupal-drush-module-disable.md +0 -21
  165. package/.claude/commands/drupal-drush-module-enable.md +0 -19
  166. package/.claude/commands/drupal-drush-module-list.md +0 -17
  167. package/.claude/commands/drupal-drush-security-updates.md +0 -16
  168. package/.claude/commands/drupal-drush-status.md +0 -16
  169. package/.claude/commands/drupal-drush-updatedb.md +0 -16
  170. package/.claude/commands/drupal-drush-user-create.md +0 -22
  171. package/.claude/commands/drupal-drush-user-list.md +0 -19
  172. package/.claude/commands/drupal-drush-watchdog.md +0 -19
  173. package/.claude/commands/drupal-entity-delete.md +0 -24
  174. package/.claude/commands/drupal-entity-get.md +0 -22
  175. package/.claude/commands/drupal-find-orphaned-media.md +0 -18
  176. package/.claude/commands/drupal-get-entity-schema.md +0 -20
  177. package/.claude/commands/drupal-get-media.md +0 -20
  178. package/.claude/commands/drupal-get-node.md +0 -20
  179. package/.claude/commands/drupal-get-taxonomy-term.md +0 -20
  180. package/.claude/commands/drupal-get-taxonomy-terms.md +0 -21
  181. package/.claude/commands/drupal-get-user-by-name.md +0 -19
  182. package/.claude/commands/drupal-get-user.md +0 -19
  183. package/.claude/commands/drupal-graphql-introspect.md +0 -17
  184. package/.claude/commands/drupal-list-content-types.md +0 -16
  185. package/.claude/commands/drupal-list-entity-types.md +0 -16
  186. package/.claude/commands/drupal-list-media-types.md +0 -16
  187. package/.claude/commands/drupal-list-media.md +0 -21
  188. package/.claude/commands/drupal-list-moderation-states.md +0 -20
  189. package/.claude/commands/drupal-list-roles.md +0 -16
  190. package/.claude/commands/drupal-list-users.md +0 -20
  191. package/.claude/commands/drupal-list-vocabularies.md +0 -16
  192. package/.claude/commands/drupal-mcp-whoami.md +0 -16
  193. package/.claude/commands/drupal-report-404-log.md +0 -17
  194. package/.claude/commands/drupal-report-accessibility-audit.md +0 -18
  195. package/.claude/commands/drupal-report-alias-coverage.md +0 -18
  196. package/.claude/commands/drupal-report-broken-embeds.md +0 -18
  197. package/.claude/commands/drupal-report-cache-config.md +0 -16
  198. package/.claude/commands/drupal-report-config-drift.md +0 -16
  199. package/.claude/commands/drupal-report-content-by-author.md +0 -18
  200. package/.claude/commands/drupal-report-content-summary.md +0 -16
  201. package/.claude/commands/drupal-report-duplicate-content.md +0 -18
  202. package/.claude/commands/drupal-report-field-completeness.md +0 -21
  203. package/.claude/commands/drupal-report-menu-integrity.md +0 -17
  204. package/.claude/commands/drupal-report-module-audit.md +0 -16
  205. package/.claude/commands/drupal-report-orphan-pages.md +0 -18
  206. package/.claude/commands/drupal-report-permission-audit.md +0 -16
  207. package/.claude/commands/drupal-report-pii-exposure.md +0 -19
  208. package/.claude/commands/drupal-report-readability.md +0 -19
  209. package/.claude/commands/drupal-report-recently-published.md +0 -18
  210. package/.claude/commands/drupal-report-redirect-health.md +0 -17
  211. package/.claude/commands/drupal-report-revision-hotspots.md +0 -18
  212. package/.claude/commands/drupal-report-scheduled-content.md +0 -18
  213. package/.claude/commands/drupal-report-seo-meta-coverage.md +0 -19
  214. package/.claude/commands/drupal-report-stale-content.md +0 -20
  215. package/.claude/commands/drupal-report-status-report.md +0 -17
  216. package/.claude/commands/drupal-report-taxonomy-usage.md +0 -22
  217. package/.claude/commands/drupal-report-text-format-audit.md +0 -16
  218. package/.claude/commands/drupal-report-translation-coverage.md +0 -19
  219. package/.claude/commands/drupal-report-unpublished.md +0 -18
  220. package/.claude/commands/drupal-report-user-activity.md +0 -18
  221. package/.claude/commands/drupal-report-workflow-bottlenecks.md +0 -20
  222. package/.claude/commands/drupal-search-content.md +0 -22
  223. package/.claude/commands/drupal-search.md +0 -21
  224. package/.claude/commands/drupal-security-info.md +0 -16
  225. package/.claude/commands/drupal-set-moderation-state.md +0 -21
  226. package/.claude/commands/drupal-site-info.md +0 -16
  227. package/.claude/commands/drupal-update-media.md +0 -23
  228. package/.claude/commands/drupal-update-taxonomy-term.md +0 -23
  229. package/.claude/commands/drupal-update-user.md +0 -25
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: "Get the base URL and the list of available resource/query types for a configured site (works for JSON:API and GraphQL backends)."
3
+ argument-hint: "[site]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_site_info`.
7
+
8
+ Get the base URL and the list of available resource/query types for a configured site (works for JSON:API and GraphQL backends).
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Optional:**
13
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
14
+
15
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched."
3
+ argument-hint: "<type> <id> [site] [name] [status] [fields]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_update_media`.
7
+
8
+ Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched.
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Required:**
13
+ - `type` (string)
14
+ - `id` (string)
15
+
16
+ **Optional:**
17
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
+ - `name` (string)
19
+ - `status` (boolean (true/false)): Published flag. Only sent when provided; requires allowPublish when true.
20
+ - `fields` (object (pass as JSON)): Field values. Entity-reference values in JSON:API linkage shape ({ data: { type, id } }) are sent as relationships automatically.
21
+
22
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,20 +1,19 @@
1
1
  ---
2
2
  description: "Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Checked against the site security config."
3
3
  argument-hint: "<id> [site] [title] [link] [menu] [weight] [parent] [enabled]"
4
- allowed-tools: mcp__drupal__drupal_update_menu_link
5
4
  ---
6
5
 
7
- Call the `mcp__drupal__drupal_update_menu_link` MCP tool.
6
+ Call the MCP tool `drupal_update_menu_link`.
8
7
 
9
8
  Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Checked against the site security config.
10
9
 
11
- Parse the request in `$ARGUMENTS` into this tool's parameters:
10
+ Parse the arguments supplied with this command into this tool's parameters:
12
11
 
13
12
  **Required:**
14
13
  - `id` (string): Menu link UUID
15
14
 
16
15
  **Optional:**
17
- - `site` (string): omit for the default site
16
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
17
  - `title` (string): New link label. Omit to leave unchanged.
19
18
  - `link` (string): New target URI (e.g. 'entity:node/42'). Omit to leave unchanged.
20
19
  - `menu` (string): Move the link to this menu. Omit to leave unchanged.
@@ -22,4 +21,4 @@ Parse the request in `$ARGUMENTS` into this tool's parameters:
22
21
  - `parent` (string): New parent link plugin id (e.g. 'menu_link_content:<uuid>'), or '' for top level. Omit to leave unchanged.
23
22
  - `enabled` (boolean (true/false)): Enable/disable the link. Omit to preserve the current state.
24
23
 
25
- If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
24
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,30 +1,29 @@
1
1
  ---
2
- description: "Update an existing node. Only include fields you want to change. For moderated content types, use moderationState (e.g. 'published') rather than status. When the target is published and moderated and you omit moderationState, the connector defaults the write to moderation_state 'draft' (forward revision) so live default revisions are not mutated by accident. Entity-reference fields go in `relationships`, not `fields`. Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved (an unresolved identifier persists as an empty field). On moderated targets an id-mismatch PATCH preflight runs first — including on dryRun — so a core working-copy guard failure is reported before the real write and no revision is saved by the probe. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents."
2
+ description: "Update an existing node. Only include fields you want to change. For moderated content types, use moderationState (e.g. 'published') rather than status. When the target is published and moderated and you omit moderationState, the connector defaults the write to moderation_state 'draft' (forward revision) so live default revisions are not mutated by accident. Entity-reference fields go in `relationships`, not `fields`. Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved (an unresolved identifier persists as an empty field). On moderated targets an id-mismatch PATCH preflight runs first — including on dryRun — against the same URL the write will hit. An addressable working copy is PATCHed via ?resourceVersion=rel:working-copy (#166); dryRun uses that same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents."
3
3
  argument-hint: "<type> <id> [site] [title] [body] [summary] [format] [status] [moderationState] [fields] [relationships] [dryRun] [returning]"
4
- allowed-tools: mcp__drupal__drupal_update_node
5
4
  ---
6
5
 
7
- Call the `mcp__drupal__drupal_update_node` MCP tool.
6
+ Call the MCP tool `drupal_update_node`.
8
7
 
9
- Update an existing node. Only include fields you want to change. For moderated content types, use moderationState (e.g. 'published') rather than status. When the target is published and moderated and you omit moderationState, the connector defaults the write to moderation_state 'draft' (forward revision) so live default revisions are not mutated by accident. Entity-reference fields go in `relationships`, not `fields`. Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved (an unresolved identifier persists as an empty field). On moderated targets an id-mismatch PATCH preflight runs first — including on dryRun — so a core working-copy guard failure is reported before the real write and no revision is saved by the probe. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents.
8
+ Update an existing node. Only include fields you want to change. For moderated content types, use moderationState (e.g. 'published') rather than status. When the target is published and moderated and you omit moderationState, the connector defaults the write to moderation_state 'draft' (forward revision) so live default revisions are not mutated by accident. Entity-reference fields go in `relationships`, not `fields`. Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved (an unresolved identifier persists as an empty field). On moderated targets an id-mismatch PATCH preflight runs first — including on dryRun — against the same URL the write will hit. An addressable working copy is PATCHed via ?resourceVersion=rel:working-copy (#166); dryRun uses that same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents.
10
9
 
11
- Parse the request in `$ARGUMENTS` into this tool's parameters:
10
+ Parse the arguments supplied with this command into this tool's parameters:
12
11
 
13
12
  **Required:**
14
13
  - `type` (string)
15
14
  - `id` (string): Node UUID
16
15
 
17
16
  **Optional:**
18
- - `site` (string): omit for the default site
17
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
19
18
  - `title` (string)
20
19
  - `body` (string)
21
- - `summary` (string): Body summary/teaser — writes the `summary` property of the body field (core `text_with_summary`). Many headless sites instead use a dedicated summary/deck field for teasers and meta descriptions; on those, set that field in `fields` a value written here will be stored but may never be rendered.
22
- - `format` (string): Text format machine name for the body, e.g. 'basic_html'. Defaults to the site config's `defaultTextFormat`, then 'full_html'. Set this when the site's formats do not include full_html, or to avoid writing content into a more permissive format than intended.
20
+ - `summary` (string): Body summary/teaser — writes body.summary on core text_with_summary only. Refused when the sampled body field has no summary property (text_long / text_formatted) or the schema cannot be determined. Prefer the site's dedicated deck/summary field via `fields`.
21
+ - `format` (string): Text format machine name for the body, e.g. 'basic_html'. When the body field's allowed_formats lists exactly one format, that is the default. A caller format outside that list is refused before write. When allowed_formats cannot be resolved, defaults to the site config's `defaultTextFormat`, then 'full_html'.
23
22
  - `status` (boolean (true/false)): Published flag for NON-moderated types: true = publish, false = unpublish. Ignored if moderationState is set.
24
23
  - `moderationState` (string): Moderation state transition for content_moderation types, e.g. 'draft', 'published', 'archived'. Takes precedence over status. Required to keep or re-publish a live node — omitting it on a published moderated node defaults the write to 'draft'.
25
- - `fields` (object (pass as JSON)): Scalar/attribute field values keyed by machine name. Entity-reference fields go in `relationships`, not here.
24
+ - `fields` (object (pass as JSON)): Scalar/attribute field values keyed by machine name. Formatted text: a string or { value, format?, summary? }. format must be in the field's allowed_formats; a single allowed format is used when omitted. Entity-reference fields go in `relationships`, not here.
26
25
  - `relationships` (object (pass as JSON)): Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value uses { data: { type, id } }; multi-value uses { data: [{ type, id }, …] }. Paragraph / ERR items must carry meta.target_revision_id — the connector injects it when missing, and fails the write if it cannot.
27
- - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. The probe uses a non-matching data.id so Drupal does not save. A working-copy 400 fails the dryRun.
26
+ - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. The probe uses a non-matching data.id so Drupal does not save, and hits the same URL as the real write (canonical, or ?resourceVersion=rel:working-copy when a draft is addressable). A working-copy 400 fails the dryRun.
28
27
  - `returning` (string): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
29
28
 
30
- If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
29
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,21 +1,20 @@
1
1
  ---
2
2
  description: "Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy."
3
3
  argument-hint: "<paragraphType> <id> [site] [attributes]"
4
- allowed-tools: mcp__drupal__drupal_update_paragraph
5
4
  ---
6
5
 
7
- Call the `mcp__drupal__drupal_update_paragraph` MCP tool.
6
+ Call the MCP tool `drupal_update_paragraph`.
8
7
 
9
8
  Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy.
10
9
 
11
- Parse the request in `$ARGUMENTS` into this tool's parameters:
10
+ Parse the arguments supplied with this command into this tool's parameters:
12
11
 
13
12
  **Required:**
14
13
  - `paragraphType` (string): Paragraph type / bundle machine name, e.g. 'text', 'image', 'cta'
15
14
  - `id` (string): Paragraph UUID
16
15
 
17
16
  **Optional:**
18
- - `site` (string): Named site (omit for default)
17
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
19
18
  - `attributes` (object (pass as JSON)): Paragraph field values to change, keyed by Drupal machine name, e.g. { field_body: { value: '<p>..</p>', format: 'full_html' } }
20
19
 
21
- If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
20
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,22 +1,21 @@
1
1
  ---
2
2
  description: "Update an existing redirect by UUID: repoint its source or target, or change its status code (e.g. 301↔302). Only the fields you pass are changed (partial update). Use this to activate/fix a redirect that isn't firing (e.g. one created with a stale source). Governed by the site security policy."
3
3
  argument-hint: "<id> [site] [source] [target] [statusCode]"
4
- allowed-tools: mcp__drupal__drupal_update_redirect
5
4
  ---
6
5
 
7
- Call the `mcp__drupal__drupal_update_redirect` MCP tool.
6
+ Call the MCP tool `drupal_update_redirect`.
8
7
 
9
8
  Update an existing redirect by UUID: repoint its source or target, or change its status code (e.g. 301↔302). Only the fields you pass are changed (partial update). Use this to activate/fix a redirect that isn't firing (e.g. one created with a stale source). Governed by the site security policy.
10
9
 
11
- Parse the request in `$ARGUMENTS` into this tool's parameters:
10
+ Parse the arguments supplied with this command into this tool's parameters:
12
11
 
13
12
  **Required:**
14
13
  - `id` (string): Redirect entity UUID
15
14
 
16
15
  **Optional:**
17
- - `site` (string): omit for the default site
16
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
17
  - `source` (string): New source/old path (leading slash optional). Omit to leave unchanged.
19
18
  - `target` (string): New destination path/URI. Omit to leave unchanged.
20
19
  - `statusCode` (number): New HTTP redirect status code (301/302/303/307/308). Omit to leave unchanged.
21
20
 
22
- If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
21
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: "Update an existing taxonomy term's name, description, or weight."
3
+ argument-hint: "<vocabulary> <id> [site] [name] [description] [weight]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_update_taxonomy_term`.
7
+
8
+ Update an existing taxonomy term's name, description, or weight.
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Required:**
13
+ - `vocabulary` (string)
14
+ - `id` (string)
15
+
16
+ **Optional:**
17
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
+ - `name` (string)
19
+ - `description` (string)
20
+ - `weight` (number)
21
+
22
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: "Update a Drupal user account. Only include fields you want to change. Can reassign roles by providing a full replacement role list."
3
+ argument-hint: "<id> [site] [name] [mail] [password] [status] [roles] [timezone]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_update_user`.
7
+
8
+ Update a Drupal user account. Only include fields you want to change. Can reassign roles by providing a full replacement role list.
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Required:**
13
+ - `id` (string): User UUID
14
+
15
+ **Optional:**
16
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
17
+ - `name` (string)
18
+ - `mail` (string)
19
+ - `password` (string)
20
+ - `status` (boolean (true/false))
21
+ - `roles` (array (pass as JSON)): Full replacement role UUID list
22
+ - `timezone` (string)
23
+
24
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  description: "Convenience tool: upload a local file and immediately create a Media entity in one step. Best for the common 'add an image' workflow. Media defaults to unpublished."
3
3
  argument-hint: "<filePath> <mediaType> <fieldName> [site] [mediaName] [altText] [status]"
4
- allowed-tools: mcp__drupal__drupal_upload_file_and_create_media
5
4
  ---
6
5
 
7
- Call the `mcp__drupal__drupal_upload_file_and_create_media` MCP tool.
6
+ Call the MCP tool `drupal_upload_file_and_create_media`.
8
7
 
9
8
  Convenience tool: upload a local file and immediately create a Media entity in one step. Best for the common 'add an image' workflow. Media defaults to unpublished.
10
9
 
11
- Parse the request in `$ARGUMENTS` into this tool's parameters:
10
+ Parse the arguments supplied with this command into this tool's parameters:
12
11
 
13
12
  **Required:**
14
13
  - `filePath` (string): Local path to the file (must resolve under MCP_UPLOAD_ROOT or the connector working directory)
@@ -16,9 +15,9 @@ Parse the request in `$ARGUMENTS` into this tool's parameters:
16
15
  - `fieldName` (string): Source field machine name, e.g. 'field_media_image'
17
16
 
18
17
  **Optional:**
19
- - `site` (string): omit for the default site
18
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
20
19
  - `mediaName` (string): Name for the media entity (defaults to filename)
21
20
  - `altText` (string): Alt text for image media
22
21
  - `status` (boolean (true/false)): Published flag. Defaults to false. Requires allowPublish when true.
23
22
 
24
- If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
23
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  description: "Upload a local file to Drupal and create a File entity. Returns the file UUID to use when creating a Media entity. For images, the typical flow is: drupal_upload_file → drupal_create_media."
3
3
  argument-hint: "<filePath> <bundle> <fieldName> [site] [entityType]"
4
- allowed-tools: mcp__drupal__drupal_upload_file
5
4
  ---
6
5
 
7
- Call the `mcp__drupal__drupal_upload_file` MCP tool.
6
+ Call the MCP tool `drupal_upload_file`.
8
7
 
9
8
  Upload a local file to Drupal and create a File entity. Returns the file UUID to use when creating a Media entity. For images, the typical flow is: drupal_upload_file → drupal_create_media.
10
9
 
11
- Parse the request in `$ARGUMENTS` into this tool's parameters:
10
+ Parse the arguments supplied with this command into this tool's parameters:
12
11
 
13
12
  **Required:**
14
13
  - `filePath` (string): Local path to the file (must resolve under MCP_UPLOAD_ROOT or the connector working directory)
@@ -16,7 +15,7 @@ Parse the request in `$ARGUMENTS` into this tool's parameters:
16
15
  - `fieldName` (string): Field machine name, e.g. 'field_media_image', 'field_image'
17
16
 
18
17
  **Optional:**
19
- - `site` (string): omit for the default site
18
+ - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
20
19
  - `entityType` (string): Drupal entity type (usually 'media' or 'node')
21
20
 
22
- If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
21
+ If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.8.0] - 2026-08-25
11
+
12
+ ### Fixed
13
+ - **Iterative updates PATCH an existing working copy (#166).**
14
+ `drupal_update_node`, `drupal_entity_update`, and `drupal_bulk_update`
15
+ resolve `rel:working-copy` before the write. When that alias is
16
+ addressable, both the dryRun probe and the real PATCH use
17
+ `?resourceVersion=rel:working-copy` instead of the canonical URL, so a
18
+ second edit lands on the same forward revision. dryRun can no longer
19
+ succeed when that write would 400. An addressable draft is edited in
20
+ place — the connector does not discard it or tell the caller to publish
21
+ first. The stray-revision case (alias does not resolve, core still
22
+ blocks) still refuses with revision-surgery language (#201). A stale or
23
+ concurrent working-copy 400 is refused without retrying the canonical
24
+ URL. Successful writes include `_revisions: { live, working }` when both
25
+ vids can be read.
26
+ - **`summary` is refused when body has no summary property (#163).**
27
+ `drupal_create_node` and `drupal_update_node` introspect the sampled body
28
+ field before writing `summary`. A `text_long` / `text_formatted` body (or
29
+ an undetermined schema) fails closed with an actionable message to set the
30
+ site's deck field via `fields`. Core `text_with_summary` still accepts
31
+ `summary` and returns `_warnings` with `summary_parameter_deprecated`.
32
+ dryRun uses the same check.
33
+ - **Node writes honor field `allowed_formats` (#168).** Create/update (including
34
+ `dryRun`) resolve Field API `allowed_formats` from JSON:API `field_config`
35
+ and, when that is unavailable, Drush `config:get`. A single allowed format
36
+ is the default when the caller omits `format`. A caller format outside the
37
+ list is refused before mutation. The historical `defaultTextFormat` /
38
+ `full_html` fallback applies only while the list cannot be resolved — never
39
+ when `full_html` is excluded by field config.
40
+
41
+ ### Added
42
+ - **Northbound data-flow budgets bind to principal and target (#179).** Every
43
+ governed tool call that resolves a site now carries request-scoped principal
44
+ and authoritative-target context. Row, byte, page, request, and chained-action
45
+ counters use the same finite defaults as mcp_sentinel (500 results / 8 MiB /
46
+ 600 req/60s / 120 pages/60s) and are keyed by inbound principal + target —
47
+ not by MCP session — so pagination, retries, batching, and a new chain id
48
+ cannot reset them. JSON:API, GraphQL, and the server-tool bridge send the
49
+ source wire contract (`X-MCP-Declared-Ceiling`, narrow-only;
50
+ `X-MCP-Declared-Destination` from the entitlement pair). Denials name a
51
+ stable reason plus a correlation id and do not echo restricted payload.
52
+ Optional `security.declaredCeiling` and `security.readBudgets` override the
53
+ defaults. Stdio / local-operator traffic still sends the declared headers;
54
+ connector-side counters enforce when an inbound OAuth principal is present.
55
+
56
+ ### Changed
57
+ - **Slash-command stubs moved to `.agents/commands/` (#218).** Generated
58
+ `/drupal-*` files are harness-agnostic (protocol tool names, no
59
+ `mcp__drupal__*` `allowed-tools`). `.claude/` is no longer tracked or
60
+ published. `npm run install:commands` copies stubs into operator home
61
+ directories (`~/.claude/commands` with the Claude Code adapter,
62
+ `~/.grok/commands` as-is) so the bare `/drupal-*` form still works
63
+ without a vendor folder in this repo or in a consuming project. The npm
64
+ `files` list now ships `.agents/commands/` instead of `.claude/commands/`.
65
+
66
+ ## [2.7.4] - 2026-08-18
67
+
68
+ ### Fixed
69
+ - **`drupal_content_by_moderation_state` no longer 500s on stock JSON:API
70
+ (#162).** `moderation_state` is a computed field and is not filterable
71
+ over core JSON:API. The tool still tries the server-side filter (so a
72
+ jsonapi_extras alias keeps working), and when Drupal rejects it the
73
+ connector samples recent nodes and filters client-side. The payload
74
+ reports `source: "sampled"` and `approximate` instead of a raw Drupal
75
+ 500. If the field is not exposed at all, the result is a gated
76
+ `unavailable` payload.
77
+ - **Every tool response names the resolved site (#167).** Omitting `site`
78
+ still defaults to `defaultSite`, but the payload now carries
79
+ `_target: { name, baseUrl, source }` — the same block `drupal_mcp_whoami`
80
+ already returned as `target`. `source` is `hint` when the caller named a
81
+ site, `default` when the configured default was used, or `grant` when a
82
+ principal had exactly one entitled site. A list_nodes-shaped success with
83
+ only `_backend` is no longer claimable as production. Writes (including
84
+ GraphQL mutations and generic `drupal_entity_*` writes) refuse a silent
85
+ default when more than one site is configured; a write on the wrong site
86
+ is not recoverable. Single-site configs are unchanged. Array-shaped
87
+ results are wrapped as `{ items, _target }` so the field survives
88
+ `JSON.stringify`.
89
+
10
90
  ## [2.7.3] - 2026-08-18
11
91
 
12
92
  ### Fixed
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  Built by **Jeremy Michael Cerda** (opensource@wilkesliberty.com). Maintained by [Wilkes & Liberty, LLC](https://github.com/Wilkes-Liberty).
11
11
 
12
- **If the client only shows `drupal_list_sites` and `drupal_governance_status`**, the secret env vars named in `config.json` are unset. Upgrade to **2.7.3** (or at least 2.6.1), or stay on 2.6.0 and launch via `bin/drupal-mcp-launch.sh` with a `config/secrets.map` (`ENV_VAR=keychain-item`). Then restart the MCP server. See [#199](https://github.com/Wilkes-Liberty/drupal-mcp-connector/issues/199).
12
+ **If the client only shows `drupal_list_sites` and `drupal_governance_status`**, the secret env vars named in `config.json` are unset. Upgrade to **2.7.4** (or at least 2.6.1), or stay on 2.6.0 and launch via `bin/drupal-mcp-launch.sh` with a `config/secrets.map` (`ENV_VAR=keychain-item`). Then restart the MCP server. See [#199](https://github.com/Wilkes-Liberty/drupal-mcp-connector/issues/199).
13
13
 
14
14
  ---
15
15
 
@@ -44,11 +44,14 @@ Each site declares which backend(s) it exposes via the `api` key:
44
44
  - **`api` accepts** `"jsonapi"`, `"graphql"`, or a priority array like `["graphql","jsonapi"]`. Omit it to **auto-detect** (the connector probes both once and caches the result).
45
45
  - **One canonical shape.** Both backends return entities as
46
46
  `{ id, entityType, bundle, title, status, langcode, created, changed, url, fields, relationships, _backend }`, so tool output is identical regardless of protocol.
47
+ Every site-addressing response also includes `_target: { name, baseUrl, source }` so a defaulted call cannot be mistaken for another environment.
47
48
  - **Capability-aware.** Each backend advertises what it supports (read, write, delete, server-side filter/sort, revisions). GraphQL via GraphQL Compose is **read-only** (no mutations) and has no server-side field filter, so filters are applied client-side over a bounded fetch and flagged `approximate`/`truncated`. Write tools against a read-only backend return a clear capability error rather than failing silently.
48
49
  - **Writes go through JSON:API.** Use a JSON:API-enabled site as the write plane; keep GraphQL as a read plane where that suits your architecture.
49
50
  - **`defaultTextFormat` sets the body text format** used by the `body` convenience
50
51
  parameter on node writes, e.g. `{ "baseUrl": "…", "defaultTextFormat": "basic_html" }`.
51
- Individual calls can override it with `format`. Without either, the connector falls
52
+ Individual calls can override it with `format`. Field API `allowed_formats` wins
53
+ when it can be resolved: a single allowed format is the default, and a format
54
+ outside the list is refused. Without a resolved list, the connector falls
52
55
  back to `full_html`, which many governed sites deliberately do not define — and which
53
56
  is the most permissive core format, so setting this per site is the safer posture. A
54
57
  text format is Drupal's HTML-filtering boundary, so it is worth choosing deliberately
@@ -118,20 +121,27 @@ governed call to that tool. These are protocol-native, so they work everywhere t
118
121
  prompts capability is supported — the client renders them per its own convention
119
122
  (e.g. Claude Code shows `/mcp__drupal__drupal-create-node`).
120
123
 
121
- #### Claude Code slash commands (`/drupal-*`)
122
- For the literal bare `/drupal-<tool>` form in **Claude Code specifically**, the
123
- connector also ships generated command files under `.claude/commands/`. Because
124
- Claude Code project commands are per-project, copy them into your own project (or
125
- regenerate from the installed package):
124
+ #### Filesystem slash commands (`/drupal-*`)
125
+ MCP prompts (above) are the protocol-native slash surface. For the literal bare
126
+ `/drupal-<tool>` form, the connector ships generated stubs under
127
+ **`.agents/commands/`** a harness-agnostic tree, not a vendor rule directory.
128
+ Clients that scan `.agents/commands/` (including Grok Build when this repo is
129
+ the project) pick them up automatically.
130
+
131
+ Clients that only scan a vendor home path (Claude Code `~/.claude/commands`,
132
+ Grok `~/.grok/commands`) install from that tree into the **operator home**, so
133
+ no consuming project has to commit `.claude/` or `.grok/`:
126
134
 
127
135
  ```bash
128
- mkdir -p .claude/commands
129
- cp node_modules/drupal-mcp-connector/.claude/commands/drupal-*.md .claude/commands/
130
- # …or, from a clone of the connector: npm run generate:commands
136
+ npm run generate:commands # refresh .agents/commands/ (from a clone)
137
+ npm run install:commands # copies into ~/.claude/commands and ~/.grok/commands
138
+ # npm run install:commands -- --clients claude
139
+ # npm run install:commands -- --home /path/to/fake-home # tests / CI
131
140
  ```
132
141
 
133
- Other MCP agents don't need this step — they get the same coverage from the per-tool
134
- prompts above.
142
+ From an npm install, the same files ship at
143
+ `node_modules/drupal-mcp-connector/.agents/commands/`. Do not copy them into an
144
+ application repo's `.claude/` or `.grok/` directory.
135
145
 
136
146
  ### Security Model
137
147
 
@@ -160,6 +170,7 @@ Additional connector-side gates (2.1+ / 2.2+):
160
170
  - **Uploads** only from `MCP_UPLOAD_ROOT` (or the process cwd); sensitive paths (`.env*`, `.ssh`, connector `config.json`) are refused.
161
171
  - **HTTPS:** non-loopback binds require an inbound OAuth resource server (`auth.issuer` + `auth.audience`), not `MCP_AUTH_TOKEN`. `MCP_AUTH_TOKEN` remains valid on loopback. `MCP_ALLOW_UNAUTHENTICATED=1` is only for a trusted proxy. Non-loopback TLS defaults to 120 req/min rate limiting. Inbound JWT scopes (and optional `auth.grants`) filter which tools, resources, prompts, and sites a principal can discover or invoke.
162
172
  - **GraphQL is off by default.** `drupal_graphql` / introspect require `security.allowGraphql` (true only on the `development` preset). Raw GraphQL results still bypass entity allowlists and field redaction — prefer JSON:API entity tools when connector policy must hold. Mutations also need `allowGraphqlMutations`.
173
+ - **Northbound data-flow budgets (2.8+):** row, byte, page, request, and chained-action counters keyed by inbound principal + authoritative target (same finite defaults as MCP Sentinel), so pagination, retries, batching, or a new chain id cannot reset them. Governed reads carry the declared-ceiling / declared-destination wire contract; tune with `security.declaredCeiling` and `security.readBudgets`.
163
174
 
164
175
  Full detail: **[docs/security.md](docs/security.md)** and **[docs/security-hardening.md](docs/security-hardening.md)**.
165
176
 
@@ -7,7 +7,7 @@
7
7
  },
8
8
 
9
9
  "_security_options": {
10
- "_comment": "apiTokenEnv / oauth.clientSecretEnv: read credentials from the environment, never from this file. requireSecureAuth: reject anon/basic, require HTTPS+Bearer — set it on every remote site. requireGovernance: deny every governed path unless the source governance contract (GET /drupal-mcp/readiness, mcp_sentinel) verifies — no ungoverned JSON:API/GraphQL fallback. Env overrides: MCP_CLIENT_ID overrides or disables the outbound identity header; MCP_BIND_HOST restricts the listen interface (with TLS). Network-facing HTTPS /mcp requires the top-level auth block (issuer + audience), not MCP_AUTH_TOKEN. MCP_AUTH_TOKEN remains valid only on loopback. See docs/security-hardening.md."
10
+ "_comment": "apiTokenEnv / oauth.clientSecretEnv: read credentials from the environment, never from this file. requireSecureAuth: reject anon/basic, require HTTPS+Bearer — set it on every remote site. requireGovernance: deny every governed path unless the source governance contract (GET /drupal-mcp/readiness, mcp_sentinel) verifies — no ungoverned JSON:API/GraphQL fallback. Env overrides: MCP_CLIENT_ID overrides or disables the outbound identity header; MCP_BIND_HOST restricts the listen interface (with TLS). Network-facing HTTPS /mcp requires the top-level auth block (issuer + audience), not MCP_AUTH_TOKEN. MCP_AUTH_TOKEN remains valid only on loopback. Optional security.declaredCeiling (narrow-only X-MCP-Declared-Ceiling) and security.readBudgets (same finite classes as mcp_sentinel) bind northbound data-flow to the inbound principal and target. See docs/security.md and docs/security-hardening.md."
11
11
  },
12
12
 
13
13
  "_governance_tiers": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.7.3",
3
+ "version": "2.8.0",
4
4
  "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -11,7 +11,8 @@
11
11
  "files": [
12
12
  "src/",
13
13
  "scripts/generate-commands.js",
14
- ".claude/commands/",
14
+ "scripts/install-commands.js",
15
+ ".agents/commands/",
15
16
  "config/config.example.json",
16
17
  "README.md",
17
18
  "CHANGELOG.md",
@@ -50,6 +51,7 @@
50
51
  "start:https": "MCP_TRANSPORT=https node src/index.js",
51
52
  "start:dev": "MCP_TRANSPORT=https MCP_ALLOW_HTTP=1 MCP_PORT=3443 node src/index.js",
52
53
  "generate:commands": "node scripts/generate-commands.js",
54
+ "install:commands": "node scripts/install-commands.js",
53
55
  "lint": "eslint src/",
54
56
  "lint:fix": "eslint src/ --fix",
55
57
  "test": "vitest run",
@@ -1,22 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * generate-commands.js — generate Claude Code slash commands for every tool.
3
+ * generate-commands.js — generate harness-agnostic slash-command stubs for every tool.
4
4
  *
5
- * Writes one `.claude/commands/drupal-<tool>.md` per Drupal tool, giving the
6
- * literal `/drupal-<tool>` slash command in Claude Code (e.g.
7
- * `/drupal-create-node`). Each file is scoped via `allowed-tools` to only its own
8
- * `mcp__drupal__<tool>` and instructs the model to parse `$ARGUMENTS` into the
9
- * tool's parameters before making a single call.
5
+ * Writes one `.agents/commands/drupal-<tool>.md` per Drupal tool. The files use
6
+ * protocol tool names (`drupal_list_nodes`) so any MCP client can consume them;
7
+ * they are not a vendor rule tree. Clients that scan `.agents/commands/` (Grok
8
+ * Build, when this repo is the project) pick them up as `/drupal-<tool>`.
10
9
  *
11
- * These files are *client ergonomics* for operators using Claude Code as an MCP
12
- * client against this server not provider-specific project rules. Development
13
- * instructions for any agent live only in AGENTS.md.
10
+ * Clients that only scan a vendor home directory (Claude Code `~/.claude/commands`,
11
+ * Grok `~/.grok/commands`) get the same files via `npm run install:commands`.
14
12
  *
15
13
  * Driven from the same tool definitions as the server (src/tools/index.js), so the
16
14
  * command set never drifts from the tools. Run: `npm run generate:commands`.
17
15
  *
18
- * Exports `renderCommandMarkdown`, `commandFileName`, and `generate` for tests; the
19
- * file-writing side effect runs only when executed directly.
16
+ * Exports `renderCommandMarkdown`, `renderClaudeCommandMarkdown`, `commandFileName`,
17
+ * `COMMANDS_DIR`, and `generate` for tests; the file-writing side effect runs only
18
+ * when executed directly.
20
19
  */
21
20
 
22
21
  import { mkdirSync, readdirSync, rmSync, writeFileSync, realpathSync } from "fs";
@@ -26,7 +25,8 @@ import { allDefinitions } from "../src/tools/index.js";
26
25
  import { paramList, toolNameToPromptName } from "../src/lib/tool-prompts.js";
27
26
  import { isDestructiveTool } from "../src/lib/operations.js";
28
27
 
29
- const COMMANDS_DIR = new URL("../.claude/commands/", import.meta.url);
28
+ /** Canonical, harness-agnostic command tree shipped in the repo and the npm package. */
29
+ export const COMMANDS_DIR = new URL("../.agents/commands/", import.meta.url);
30
30
 
31
31
  /** Map a tool definition to its command filename: `drupal_create_node` → `drupal-create-node.md`. */
32
32
  export function commandFileName(def) {
@@ -49,19 +49,26 @@ function argumentHint(params) {
49
49
  * Render the markdown for one tool's slash command.
50
50
  *
51
51
  * @param {object} def - The tool definition ({name, description, inputSchema}).
52
+ * @param {object} [options]
53
+ * @param {string} [options.allowedTools] - Optional Claude Code `allowed-tools` value.
54
+ * @param {string} [options.argumentsPhrase="the arguments supplied with this command"]
55
+ * Phrase used in the parse-arguments instruction. Claude Code install rewrites
56
+ * this to `` `$ARGUMENTS` `` because that client substitutes the placeholder.
52
57
  * @returns {string} File contents (ends with a trailing newline).
53
58
  */
54
- export function renderCommandMarkdown(def) {
59
+ export function renderCommandMarkdown(def, options = {}) {
55
60
  const params = paramList(def.inputSchema);
56
61
  const required = params.filter((p) => p.required);
57
62
  const optional = params.filter((p) => !p.required);
58
63
  const line = (p) => `- \`${p.name}\` (${p.hint})${p.description ? `: ${p.description}` : ""}`;
64
+ const argumentsPhrase = options.argumentsPhrase ?? "the arguments supplied with this command";
59
65
 
60
66
  const frontmatter = ["---", `description: ${yamlString(def.description)}`];
61
67
  if (params.length) frontmatter.push(`argument-hint: ${yamlString(argumentHint(params))}`);
62
- frontmatter.push(`allowed-tools: mcp__drupal__${def.name}`, "---");
68
+ if (options.allowedTools) frontmatter.push(`allowed-tools: ${options.allowedTools}`);
69
+ frontmatter.push("---");
63
70
 
64
- const body = [`Call the \`mcp__drupal__${def.name}\` MCP tool.`, "", def.description];
71
+ const body = [`Call the MCP tool \`${def.name}\`.`, "", def.description];
65
72
 
66
73
  if (isDestructiveTool(def.name)) {
67
74
  body.push("", "> ⚠ **Destructive** — this permanently changes or deletes data. Confirm with the user before calling.");
@@ -69,9 +76,9 @@ export function renderCommandMarkdown(def) {
69
76
 
70
77
  body.push("");
71
78
  if (params.length === 0) {
72
- body.push("This tool takes no arguments — call it directly (ignore `$ARGUMENTS`).");
79
+ body.push("This tool takes no arguments — call it directly.");
73
80
  } else {
74
- body.push("Parse the request in `$ARGUMENTS` into this tool's parameters:", "");
81
+ body.push(`Parse ${argumentsPhrase} into this tool's parameters:`, "");
75
82
  if (required.length) {
76
83
  body.push("**Required:**");
77
84
  required.forEach((p) => body.push(line(p)));
@@ -83,15 +90,29 @@ export function renderCommandMarkdown(def) {
83
90
  body.push("");
84
91
  }
85
92
  body.push(
86
- "If a required parameter is missing from `$ARGUMENTS`, ask before calling — do not " +
87
- "invent values. Coerce each value to its JSON type (booleans → true/false, numbers → " +
88
- "numeric, object/array → parse JSON), then make the single tool call and summarize the result."
93
+ "If a required parameter is missing, ask before calling — do not invent values. " +
94
+ "Coerce each value to its JSON type (booleans → true/false, numbers → numeric, " +
95
+ "object/array → parse JSON), then make the single tool call and summarize the result."
89
96
  );
90
97
  }
91
98
 
92
99
  return `${frontmatter.join("\n")}\n\n${body.join("\n")}\n`;
93
100
  }
94
101
 
102
+ /**
103
+ * Claude Code adapter: same stub plus `allowed-tools` scoped to that client's
104
+ * MCP tool name, and `$ARGUMENTS` so typed `/drupal-*` args are not dropped.
105
+ *
106
+ * @param {object} def - The tool definition.
107
+ * @returns {string} File contents.
108
+ */
109
+ export function renderClaudeCommandMarkdown(def) {
110
+ return renderCommandMarkdown(def, {
111
+ allowedTools: `mcp__drupal__${def.name}`,
112
+ argumentsPhrase: "`$ARGUMENTS`",
113
+ });
114
+ }
115
+
95
116
  /**
96
117
  * Write one command file per tool, pruning stale `drupal-*.md` first so removed
97
118
  * tools leave no orphans.
@@ -118,5 +139,5 @@ const invokedDirectly =
118
139
  process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;
119
140
  if (invokedDirectly) {
120
141
  const written = generate();
121
- console.error(`[generate-commands] wrote ${written.length} command files to .claude/commands/`);
142
+ console.error(`[generate-commands] wrote ${written.length} command files to .agents/commands/`);
122
143
  }