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.
- package/.agents/commands/drupal-audit-config-best-practices.md +15 -0
- package/{.claude → .agents}/commands/drupal-audit-site-health.md +4 -5
- package/.agents/commands/drupal-block-user.md +19 -0
- package/{.claude → .agents}/commands/drupal-bulk-create.md +4 -5
- package/{.claude → .agents}/commands/drupal-bulk-update.md +4 -5
- package/.agents/commands/drupal-config-get.md +18 -0
- package/.agents/commands/drupal-config-list.md +16 -0
- package/.agents/commands/drupal-config-set.md +19 -0
- package/.agents/commands/drupal-content-by-moderation-state.md +21 -0
- package/.agents/commands/drupal-create-block.md +20 -0
- package/{.claude → .agents}/commands/drupal-create-media.md +4 -5
- package/{.claude → .agents}/commands/drupal-create-menu-link.md +4 -5
- package/{.claude → .agents}/commands/drupal-create-node.md +7 -8
- package/{.claude → .agents}/commands/drupal-create-paragraph.md +4 -5
- package/{.claude → .agents}/commands/drupal-create-redirect.md +4 -5
- package/.agents/commands/drupal-create-taxonomy-term.md +22 -0
- package/{.claude → .agents}/commands/drupal-create-translation.md +4 -5
- package/.agents/commands/drupal-create-user.md +23 -0
- package/.agents/commands/drupal-delete-media.md +21 -0
- package/.agents/commands/drupal-delete-node.md +22 -0
- package/.agents/commands/drupal-delete-taxonomy-term.md +21 -0
- package/{.claude → .agents}/commands/drupal-describe-fields.md +4 -5
- package/.agents/commands/drupal-drush-cache-rebuild.md +15 -0
- package/.agents/commands/drupal-drush-config-export.md +15 -0
- package/.agents/commands/drupal-drush-config-import.md +15 -0
- package/.agents/commands/drupal-drush-config-status.md +15 -0
- package/.agents/commands/drupal-drush-cron.md +15 -0
- package/.agents/commands/drupal-drush-module-disable.md +20 -0
- package/.agents/commands/drupal-drush-module-enable.md +18 -0
- package/.agents/commands/drupal-drush-module-list.md +16 -0
- package/.agents/commands/drupal-drush-security-updates.md +15 -0
- package/{.claude → .agents}/commands/drupal-drush-sql-query.md +4 -5
- package/.agents/commands/drupal-drush-status.md +15 -0
- package/.agents/commands/drupal-drush-updatedb.md +15 -0
- package/.agents/commands/drupal-drush-user-create.md +21 -0
- package/.agents/commands/drupal-drush-user-list.md +18 -0
- package/.agents/commands/drupal-drush-watchdog.md +18 -0
- package/{.claude → .agents}/commands/drupal-entity-create.md +4 -5
- package/.agents/commands/drupal-entity-delete.md +23 -0
- package/.agents/commands/drupal-entity-get.md +21 -0
- package/{.claude → .agents}/commands/drupal-entity-list.md +4 -5
- package/{.claude → .agents}/commands/drupal-entity-update.md +7 -8
- package/.agents/commands/drupal-find-orphaned-media.md +17 -0
- package/.agents/commands/drupal-get-entity-schema.md +19 -0
- package/.agents/commands/drupal-get-media.md +19 -0
- package/.agents/commands/drupal-get-node.md +19 -0
- package/{.claude → .agents}/commands/drupal-get-paragraph.md +4 -5
- package/{.claude → .agents}/commands/drupal-get-revision.md +4 -5
- package/.agents/commands/drupal-get-taxonomy-term.md +19 -0
- package/.agents/commands/drupal-get-taxonomy-terms.md +20 -0
- package/.agents/commands/drupal-get-user-by-name.md +18 -0
- package/.agents/commands/drupal-get-user.md +18 -0
- package/{.claude → .agents}/commands/drupal-governance-status.md +4 -5
- package/.agents/commands/drupal-graphql-introspect.md +16 -0
- package/{.claude → .agents}/commands/drupal-graphql.md +4 -5
- package/{.claude → .agents}/commands/drupal-list-blocks.md +4 -5
- package/.agents/commands/drupal-list-content-types.md +15 -0
- package/.agents/commands/drupal-list-entity-types.md +15 -0
- package/.agents/commands/drupal-list-media-types.md +15 -0
- package/.agents/commands/drupal-list-media.md +20 -0
- package/{.claude → .agents}/commands/drupal-list-menu-links.md +4 -5
- package/.agents/commands/drupal-list-moderation-states.md +19 -0
- package/{.claude → .agents}/commands/drupal-list-nodes.md +4 -5
- package/{.claude → .agents}/commands/drupal-list-revisions.md +4 -5
- package/.agents/commands/drupal-list-roles.md +15 -0
- package/{.claude → .agents}/commands/drupal-list-sites.md +2 -3
- package/{.claude → .agents}/commands/drupal-list-translations.md +4 -5
- package/.agents/commands/drupal-list-users.md +19 -0
- package/.agents/commands/drupal-list-vocabularies.md +15 -0
- package/.agents/commands/drupal-mcp-whoami.md +15 -0
- package/.agents/commands/drupal-report-404-log.md +16 -0
- package/.agents/commands/drupal-report-accessibility-audit.md +17 -0
- package/.agents/commands/drupal-report-alias-coverage.md +17 -0
- package/.agents/commands/drupal-report-broken-embeds.md +17 -0
- package/{.claude → .agents}/commands/drupal-report-broken-links.md +4 -5
- package/.agents/commands/drupal-report-cache-config.md +15 -0
- package/.agents/commands/drupal-report-config-drift.md +15 -0
- package/.agents/commands/drupal-report-content-by-author.md +17 -0
- package/.agents/commands/drupal-report-content-summary.md +15 -0
- package/.agents/commands/drupal-report-duplicate-content.md +17 -0
- package/.agents/commands/drupal-report-field-completeness.md +20 -0
- package/.agents/commands/drupal-report-menu-integrity.md +16 -0
- package/{.claude → .agents}/commands/drupal-report-missing-field.md +4 -5
- package/.agents/commands/drupal-report-module-audit.md +15 -0
- package/.agents/commands/drupal-report-orphan-pages.md +17 -0
- package/{.claude → .agents}/commands/drupal-report-orphaned-references.md +4 -5
- package/.agents/commands/drupal-report-permission-audit.md +15 -0
- package/.agents/commands/drupal-report-pii-exposure.md +18 -0
- package/.agents/commands/drupal-report-readability.md +18 -0
- package/.agents/commands/drupal-report-recently-published.md +17 -0
- package/.agents/commands/drupal-report-redirect-health.md +16 -0
- package/.agents/commands/drupal-report-revision-hotspots.md +17 -0
- package/.agents/commands/drupal-report-scheduled-content.md +17 -0
- package/{.claude → .agents}/commands/drupal-report-seo-audit.md +4 -5
- package/.agents/commands/drupal-report-seo-meta-coverage.md +18 -0
- package/.agents/commands/drupal-report-stale-content.md +19 -0
- package/.agents/commands/drupal-report-status-report.md +16 -0
- package/.agents/commands/drupal-report-taxonomy-usage.md +21 -0
- package/.agents/commands/drupal-report-text-format-audit.md +15 -0
- package/.agents/commands/drupal-report-translation-coverage.md +18 -0
- package/.agents/commands/drupal-report-unpublished.md +17 -0
- package/.agents/commands/drupal-report-user-activity.md +17 -0
- package/.agents/commands/drupal-report-workflow-bottlenecks.md +19 -0
- package/{.claude → .agents}/commands/drupal-resolve-reference.md +4 -5
- package/{.claude → .agents}/commands/drupal-revert-revision.md +4 -5
- package/{.claude → .agents}/commands/drupal-schedule-publish.md +4 -5
- package/.agents/commands/drupal-search-content.md +21 -0
- package/.agents/commands/drupal-search.md +20 -0
- package/.agents/commands/drupal-security-info.md +15 -0
- package/.agents/commands/drupal-set-moderation-state.md +20 -0
- package/.agents/commands/drupal-site-info.md +15 -0
- package/.agents/commands/drupal-update-media.md +22 -0
- package/{.claude → .agents}/commands/drupal-update-menu-link.md +4 -5
- package/{.claude → .agents}/commands/drupal-update-node.md +10 -11
- package/{.claude → .agents}/commands/drupal-update-paragraph.md +4 -5
- package/{.claude → .agents}/commands/drupal-update-redirect.md +4 -5
- package/.agents/commands/drupal-update-taxonomy-term.md +22 -0
- package/.agents/commands/drupal-update-user.md +24 -0
- package/{.claude → .agents}/commands/drupal-upload-file-and-create-media.md +4 -5
- package/{.claude → .agents}/commands/drupal-upload-file.md +4 -5
- package/CHANGELOG.md +80 -0
- package/README.md +23 -12
- package/config/config.example.json +1 -1
- package/package.json +4 -2
- package/scripts/generate-commands.js +42 -21
- package/scripts/install-commands.js +157 -0
- package/src/lib/backends/backend-interface.js +20 -1
- package/src/lib/backends/jsonapi.js +47 -4
- package/src/lib/body-summary.js +60 -0
- package/src/lib/data-flow.js +437 -0
- package/src/lib/dispatch.js +99 -30
- package/src/lib/drupal-fetch.js +52 -6
- package/src/lib/field-definition.js +285 -0
- package/src/lib/operations.js +39 -2
- package/src/lib/patch-preflight.js +157 -66
- package/src/lib/security.js +15 -1
- package/src/lib/server-tools.js +9 -0
- package/src/lib/site-target.js +78 -0
- package/src/lib/tool-prompts.js +2 -1
- package/src/lib/write-revision.js +82 -2
- package/src/tools/bulk.js +4 -3
- package/src/tools/config.js +5 -2
- package/src/tools/entities.js +11 -7
- package/src/tools/index.js +24 -3
- package/src/tools/moderation.js +88 -9
- package/src/tools/nodes.js +55 -43
- package/.claude/commands/drupal-audit-config-best-practices.md +0 -16
- package/.claude/commands/drupal-block-user.md +0 -20
- package/.claude/commands/drupal-config-get.md +0 -19
- package/.claude/commands/drupal-config-list.md +0 -17
- package/.claude/commands/drupal-config-set.md +0 -20
- package/.claude/commands/drupal-content-by-moderation-state.md +0 -22
- package/.claude/commands/drupal-create-block.md +0 -21
- package/.claude/commands/drupal-create-taxonomy-term.md +0 -23
- package/.claude/commands/drupal-create-user.md +0 -24
- package/.claude/commands/drupal-delete-media.md +0 -22
- package/.claude/commands/drupal-delete-node.md +0 -23
- package/.claude/commands/drupal-delete-taxonomy-term.md +0 -22
- package/.claude/commands/drupal-drush-cache-rebuild.md +0 -16
- package/.claude/commands/drupal-drush-config-export.md +0 -16
- package/.claude/commands/drupal-drush-config-import.md +0 -16
- package/.claude/commands/drupal-drush-config-status.md +0 -16
- package/.claude/commands/drupal-drush-cron.md +0 -16
- package/.claude/commands/drupal-drush-module-disable.md +0 -21
- package/.claude/commands/drupal-drush-module-enable.md +0 -19
- package/.claude/commands/drupal-drush-module-list.md +0 -17
- package/.claude/commands/drupal-drush-security-updates.md +0 -16
- package/.claude/commands/drupal-drush-status.md +0 -16
- package/.claude/commands/drupal-drush-updatedb.md +0 -16
- package/.claude/commands/drupal-drush-user-create.md +0 -22
- package/.claude/commands/drupal-drush-user-list.md +0 -19
- package/.claude/commands/drupal-drush-watchdog.md +0 -19
- package/.claude/commands/drupal-entity-delete.md +0 -24
- package/.claude/commands/drupal-entity-get.md +0 -22
- package/.claude/commands/drupal-find-orphaned-media.md +0 -18
- package/.claude/commands/drupal-get-entity-schema.md +0 -20
- package/.claude/commands/drupal-get-media.md +0 -20
- package/.claude/commands/drupal-get-node.md +0 -20
- package/.claude/commands/drupal-get-taxonomy-term.md +0 -20
- package/.claude/commands/drupal-get-taxonomy-terms.md +0 -21
- package/.claude/commands/drupal-get-user-by-name.md +0 -19
- package/.claude/commands/drupal-get-user.md +0 -19
- package/.claude/commands/drupal-graphql-introspect.md +0 -17
- package/.claude/commands/drupal-list-content-types.md +0 -16
- package/.claude/commands/drupal-list-entity-types.md +0 -16
- package/.claude/commands/drupal-list-media-types.md +0 -16
- package/.claude/commands/drupal-list-media.md +0 -21
- package/.claude/commands/drupal-list-moderation-states.md +0 -20
- package/.claude/commands/drupal-list-roles.md +0 -16
- package/.claude/commands/drupal-list-users.md +0 -20
- package/.claude/commands/drupal-list-vocabularies.md +0 -16
- package/.claude/commands/drupal-mcp-whoami.md +0 -16
- package/.claude/commands/drupal-report-404-log.md +0 -17
- package/.claude/commands/drupal-report-accessibility-audit.md +0 -18
- package/.claude/commands/drupal-report-alias-coverage.md +0 -18
- package/.claude/commands/drupal-report-broken-embeds.md +0 -18
- package/.claude/commands/drupal-report-cache-config.md +0 -16
- package/.claude/commands/drupal-report-config-drift.md +0 -16
- package/.claude/commands/drupal-report-content-by-author.md +0 -18
- package/.claude/commands/drupal-report-content-summary.md +0 -16
- package/.claude/commands/drupal-report-duplicate-content.md +0 -18
- package/.claude/commands/drupal-report-field-completeness.md +0 -21
- package/.claude/commands/drupal-report-menu-integrity.md +0 -17
- package/.claude/commands/drupal-report-module-audit.md +0 -16
- package/.claude/commands/drupal-report-orphan-pages.md +0 -18
- package/.claude/commands/drupal-report-permission-audit.md +0 -16
- package/.claude/commands/drupal-report-pii-exposure.md +0 -19
- package/.claude/commands/drupal-report-readability.md +0 -19
- package/.claude/commands/drupal-report-recently-published.md +0 -18
- package/.claude/commands/drupal-report-redirect-health.md +0 -17
- package/.claude/commands/drupal-report-revision-hotspots.md +0 -18
- package/.claude/commands/drupal-report-scheduled-content.md +0 -18
- package/.claude/commands/drupal-report-seo-meta-coverage.md +0 -19
- package/.claude/commands/drupal-report-stale-content.md +0 -20
- package/.claude/commands/drupal-report-status-report.md +0 -17
- package/.claude/commands/drupal-report-taxonomy-usage.md +0 -22
- package/.claude/commands/drupal-report-text-format-audit.md +0 -16
- package/.claude/commands/drupal-report-translation-coverage.md +0 -19
- package/.claude/commands/drupal-report-unpublished.md +0 -18
- package/.claude/commands/drupal-report-user-activity.md +0 -18
- package/.claude/commands/drupal-report-workflow-bottlenecks.md +0 -20
- package/.claude/commands/drupal-search-content.md +0 -22
- package/.claude/commands/drupal-search.md +0 -21
- package/.claude/commands/drupal-security-info.md +0 -16
- package/.claude/commands/drupal-set-moderation-state.md +0 -21
- package/.claude/commands/drupal-site-info.md +0 -16
- package/.claude/commands/drupal-update-media.md +0 -23
- package/.claude/commands/drupal-update-taxonomy-term.md +0 -23
- 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
|
|
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
|
|
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):
|
|
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
|
|
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 —
|
|
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
|
|
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 —
|
|
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
|
|
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):
|
|
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
|
|
22
|
-
- `format` (string): Text format machine name for the body, e.g. 'basic_html'.
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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):
|
|
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
|
|
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
|
|
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
|
|
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):
|
|
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
|
|
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
|
|
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
|
|
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):
|
|
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
|
|
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.
|
|
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`.
|
|
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
|
-
####
|
|
122
|
-
|
|
123
|
-
connector
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
#
|
|
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
|
-
|
|
134
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
3
|
+
* generate-commands.js — generate harness-agnostic slash-command stubs for every tool.
|
|
4
4
|
*
|
|
5
|
-
* Writes one `.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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`, `
|
|
19
|
-
* file-writing side effect runs only
|
|
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
|
-
|
|
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:
|
|
68
|
+
if (options.allowedTools) frontmatter.push(`allowed-tools: ${options.allowedTools}`);
|
|
69
|
+
frontmatter.push("---");
|
|
63
70
|
|
|
64
|
-
const body = [`Call the
|
|
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
|
|
79
|
+
body.push("This tool takes no arguments — call it directly.");
|
|
73
80
|
} else {
|
|
74
|
-
body.push(
|
|
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
|
|
87
|
-
"
|
|
88
|
-
"
|
|
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 .
|
|
142
|
+
console.error(`[generate-commands] wrote ${written.length} command files to .agents/commands/`);
|
|
122
143
|
}
|