drupal-mcp-connector 2.19.0 → 2.20.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-config-set.md +2 -2
- package/.agents/commands/drupal-create-node.md +2 -2
- package/.agents/commands/drupal-create-translation.md +1 -1
- package/.agents/commands/drupal-delete-node.md +1 -1
- package/.agents/commands/drupal-describe-fields.md +2 -2
- package/.agents/commands/drupal-drush-config-import.md +2 -2
- package/.agents/commands/drupal-drush-module-disable.md +2 -2
- package/.agents/commands/drupal-drush-module-list.md +1 -1
- package/.agents/commands/drupal-drush-user-list.md +1 -1
- package/.agents/commands/drupal-drush-watchdog.md +1 -1
- package/.agents/commands/drupal-entity-create.md +2 -2
- package/.agents/commands/drupal-entity-delete.md +1 -1
- package/.agents/commands/drupal-entity-update.md +4 -4
- package/.agents/commands/drupal-report-field-completeness.md +2 -2
- package/.agents/commands/drupal-report-missing-field.md +2 -2
- package/.agents/commands/drupal-report-seo-meta-coverage.md +2 -2
- package/.agents/commands/drupal-report-status-report.md +1 -1
- package/.agents/commands/drupal-update-node.md +4 -4
- package/CHANGELOG.md +254 -0
- package/README.md +10 -3
- package/bin/drupal-mcp-verify.js +4 -3
- package/config/config.example.json +52 -2
- package/package.json +1 -1
- package/scripts/generate-commands.js +40 -5
- package/scripts/install-commands.js +148 -11
- package/src/index.js +9 -12
- package/src/lib/backends/graphql-schema.js +9 -1
- package/src/lib/backends/graphql.js +4 -2
- package/src/lib/backends/index.js +9 -3
- package/src/lib/backends/jsonapi.js +2 -0
- package/src/lib/dispatch.js +6 -6
- package/src/lib/drupal-fetch.js +97 -26
- package/src/lib/dry-run-checks.js +78 -0
- package/src/lib/error-body.js +448 -0
- package/src/lib/error-status.js +38 -0
- package/src/lib/errors.js +0 -11
- package/src/lib/evidence.js +0 -6
- package/src/lib/governance.js +2 -8
- package/src/lib/link-checker.js +3 -3
- package/src/lib/mcp-server.js +7 -1
- package/src/lib/metatag-audit.js +2 -1
- package/src/lib/module-tools.js +23 -2
- package/src/lib/operations.js +2 -2
- package/src/lib/patch-preflight.js +23 -5
- package/src/lib/policy-enforcement.js +4 -4
- package/src/lib/principal.js +3 -3
- package/src/lib/relay/edge.js +2 -2
- package/src/lib/reports-support.js +75 -0
- package/src/lib/security.js +234 -18
- package/src/lib/sentinel-draft.js +3 -2
- package/src/lib/server-tools.js +188 -27
- package/src/lib/tool-prompts.js +139 -8
- package/src/lib/usage.js +0 -9
- package/src/lib/verify.js +164 -61
- package/src/tools/config.js +70 -5
- package/src/tools/drush.js +191 -17
- package/src/tools/entities.js +18 -6
- package/src/tools/fields.js +40 -4
- package/src/tools/graphql.js +10 -5
- package/src/tools/nodes.js +16 -6
- package/src/tools/paragraphs.js +1 -1
- package/src/tools/reports-config.js +3 -3
- package/src/tools/reports-content.js +34 -26
- package/src/tools/reports-extra.js +46 -38
- package/src/tools/reports.js +50 -25
- package/src/tools/scheduler.js +1 -1
- package/src/tools/structure.js +12 -2
- package/src/tools/translations.js +5 -1
- package/src/lib/draft-write.js +0 -19
- package/src/lib/node-draft-inventory.js +0 -5
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Set a Drupal configuration value via the governed server-side config tool. Audited and gated server-side; requires the config-editor (Developer) tier. Then export to YAML for a PR."
|
|
2
|
+
description: "Set a Drupal configuration value via the governed server-side config tool. Audited and gated server-side; requires the config-editor (Developer) tier. Then export to YAML for a PR. Refused for `core.extension`, which lists installed modules and themes: use drupal_drush_module_enable or drupal_drush_module_disable instead. Only an operator can allow it, with `allowCoreExtensionChange` in site config (see drupal_security_info), and a value that removes a protected module is still refused."
|
|
3
3
|
argument-hint: "<name> <value> [site]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_config_set`.
|
|
7
7
|
|
|
8
|
-
Set a Drupal configuration value via the governed server-side config tool. Audited and gated server-side; requires the config-editor (Developer) tier. Then export to YAML for a PR.
|
|
8
|
+
Set a Drupal configuration value via the governed server-side config tool. Audited and gated server-side; requires the config-editor (Developer) tier. Then export to YAML for a PR. Refused for `core.extension`, which lists installed modules and themes: use drupal_drush_module_enable or drupal_drush_module_disable instead. Only an operator can allow it, with `allowCoreExtensionChange` in site config (see drupal_security_info), and a value that removes a protected module is still refused.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -22,7 +22,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
22
22
|
- `moderationState` (string): Moderation state for content_moderation types, e.g. 'draft' or 'published'. Takes precedence over status.
|
|
23
23
|
- `fields` (object (pass as JSON)): Scalar/attribute field values keyed by Drupal 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. Do NOT put entity-reference fields here — Drupal rejects them as attributes; use `relationships`.
|
|
24
24
|
- `relationships` (object (pass as JSON)): Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value: { field_resource_type: { data: { type: 'taxonomy_term--resource_type', id: '<uuid>' } } }. Multi-value: { field_tags: { data: [{ type: 'taxonomy_term--tags', id: '<uuid>' }] } }.
|
|
25
|
-
- `dryRun` (boolean (true/false)):
|
|
26
|
-
- `returning` (
|
|
25
|
+
- `dryRun` (boolean (true/false)): Return a preview of the payload without committing. Drupal does not evaluate the write: entity access, field access and entity validation are NOT checked, so the real create can still fail with a 403 or a validation 422. The result's `checks` block and `caveat` say what was and was not checked.
|
|
26
|
+
- `returning` (one of: full, minimal): 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.
|
|
27
27
|
|
|
28
28
|
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.
|
|
@@ -20,6 +20,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
20
20
|
- `attributes` (object (pass as JSON)): Translated field values keyed by Drupal machine name
|
|
21
21
|
- `relationships` (object (pass as JSON)): JSON:API relationships. Use for image alt (same file UUID, meta.alt).
|
|
22
22
|
- `revisionId` (string): Paragraph revision id the host already pins. Required for Home-shaped non-default pins.
|
|
23
|
-
- `dryRun` (boolean (true/false)): Validate without saving
|
|
23
|
+
- `dryRun` (boolean (true/false)): Validate without saving. Sentinel's non-saving translation preflight receives the real fields, applies them through field access and validates the entity. The result's `checks` block says what was checked.
|
|
24
24
|
|
|
25
25
|
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.
|
|
@@ -17,6 +17,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
17
17
|
|
|
18
18
|
**Optional:**
|
|
19
19
|
- `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
|
-
- `dryRun` (boolean (true/false)):
|
|
20
|
+
- `dryRun` (boolean (true/false)): Return a preview of the delete without committing. Drupal does not evaluate the delete: Drupal's delete access for the entity is NOT checked, only the connector's own policy. The result's `checks` block says so.
|
|
21
21
|
|
|
22
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,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, translatable?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. When JSON:API field_config is readable, translatable is copied from Field API; omitted means unknown, not false. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names."
|
|
2
|
+
description: "Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, translatable?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. When JSON:API field_config is readable, translatable is copied from Field API; omitted means unknown, not false. A field this account may not view is left out of the JSON:API resource, so it is NOT in `fields` and looks like a field that does not exist. When field_config is readable (`fieldDefinitions: 'available'`), fields defined for the bundle but absent from the sampled entity are listed apart as `notVisible`; when it is not, denied fields cannot be detected. Absent is not empty. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names."
|
|
3
3
|
argument-hint: "<site> [type] [entityType] [bundle]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_describe_fields`.
|
|
7
7
|
|
|
8
|
-
Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, translatable?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. When JSON:API field_config is readable, translatable is copied from Field API; omitted means unknown, not false. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names.
|
|
8
|
+
Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, translatable?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. When JSON:API field_config is readable, translatable is copied from Field API; omitted means unknown, not false. A field this account may not view is left out of the JSON:API resource, so it is NOT in `fields` and looks like a field that does not exist. When field_config is readable (`fieldDefinitions: 'available'`), fields defined for the bundle but absent from the sampled entity are listed apart as `notVisible`; when it is not, denied fields cannot be detected. Absent is not empty. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Import configuration from the sync directory into the database. Requires write access. Confirm with user before running on production."
|
|
2
|
+
description: "Import configuration from the sync directory into the database. Requires write access. Confirm with user before running on production. Reads `drush config:status` first and refuses the import when `core.extension` differs, because such an import installs and uninstalls modules and can remove a protected one; nothing is imported, and the status read must be allowed in `drushSsh.allowedCommands`. Use drupal_drush_module_enable or drupal_drush_module_disable for a module change. Only an operator can allow it, with `allowCoreExtensionChange` in site config (see drupal_security_info)."
|
|
3
3
|
argument-hint: "[site]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_drush_config_import`.
|
|
7
7
|
|
|
8
|
-
Import configuration from the sync directory into the database. Requires write access. Confirm with user before running on production.
|
|
8
|
+
Import configuration from the sync directory into the database. Requires write access. Confirm with user before running on production. Reads `drush config:status` first and refuses the import when `core.extension` differs, because such an import installs and uninstalls modules and can remove a protected one; nothing is imported, and the status read must be allowed in `drushSsh.allowedCommands`. Use drupal_drush_module_enable or drupal_drush_module_disable for a module change. Only an operator can allow it, with `allowCoreExtensionChange` in site config (see drupal_security_info).
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Uninstall a Drupal module. Irreversible for module-stored data. Confirm with user."
|
|
2
|
+
description: "Uninstall a Drupal module. Irreversible for module-stored data. Confirm with user. Refused for a protected module: governance, integrity, secrets, auth and API modules such as mcp_sentinel, audit_chain, key, simple_oauth and jsonapi (see `protectedModules` in drupal_security_info). Only an operator can change that list, in site config. Also refused when the uninstall would cascade to dependents: nothing is uninstalled, the dependents are named, and each must be uninstalled by name first."
|
|
3
3
|
argument-hint: "<moduleName> [site]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_drush_module_disable`.
|
|
7
7
|
|
|
8
|
-
Uninstall a Drupal module. Irreversible for module-stored data. Confirm with user.
|
|
8
|
+
Uninstall a Drupal module. Irreversible for module-stored data. Confirm with user. Refused for a protected module: governance, integrity, secrets, auth and API modules such as mcp_sentinel, audit_chain, key, simple_oauth and jsonapi (see `protectedModules` in drupal_security_info). Only an operator can change that list, in site config. Also refused when the uninstall would cascade to dependents: nothing is uninstalled, the dependents are named, and each must be uninstalled by name first.
|
|
9
9
|
|
|
10
10
|
> ⚠ **Destructive** — this permanently changes or deletes data. Confirm with the user before calling.
|
|
11
11
|
|
|
@@ -11,6 +11,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
11
11
|
|
|
12
12
|
**Optional:**
|
|
13
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
|
-
- `status` (
|
|
14
|
+
- `status` (one of: enabled, disabled)
|
|
15
15
|
|
|
16
16
|
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.
|
|
@@ -11,7 +11,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
11
11
|
|
|
12
12
|
**Optional:**
|
|
13
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
|
-
- `status` (
|
|
14
|
+
- `status` (one of: active, blocked)
|
|
15
15
|
- `role` (string)
|
|
16
16
|
- `limit` (number)
|
|
17
17
|
|
|
@@ -12,7 +12,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
12
12
|
**Optional:**
|
|
13
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
14
|
- `type` (string)
|
|
15
|
-
- `severity` (
|
|
15
|
+
- `severity` (one of: emergency, alert, critical, error, warning, notice, info, debug)
|
|
16
16
|
- `limit` (number)
|
|
17
17
|
|
|
18
18
|
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.
|
|
@@ -17,7 +17,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
17
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
18
|
- `attributes` (object (pass as JSON)): Field values keyed by Drupal machine name
|
|
19
19
|
- `relationships` (object (pass as JSON)): Relationship data keyed by field name
|
|
20
|
-
- `dryRun` (boolean (true/false)):
|
|
21
|
-
- `returning` (
|
|
20
|
+
- `dryRun` (boolean (true/false)): Return a preview of the payload without committing. Drupal does not evaluate the write: entity access, field access and entity validation are NOT checked, so the real create can still fail with a 403 or a validation 422. The result's `checks` block and `caveat` say what was and was not checked.
|
|
21
|
+
- `returning` (one of: full, minimal): 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.
|
|
22
22
|
|
|
23
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.
|
|
@@ -18,6 +18,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
18
18
|
|
|
19
19
|
**Optional:**
|
|
20
20
|
- `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).
|
|
21
|
-
- `dryRun` (boolean (true/false)):
|
|
21
|
+
- `dryRun` (boolean (true/false)): Return a preview of the delete without committing. Drupal does not evaluate the delete: Drupal's delete access for the entity is NOT checked, only the connector's own policy. The result's `checks` block says so.
|
|
22
22
|
|
|
23
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,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents."
|
|
2
|
+
description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran."
|
|
3
3
|
argument-hint: "<entityType> <bundle> <id> [site] [langcode] [attributes] [relationships] [dryRun] [returning]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_entity_update`.
|
|
7
7
|
|
|
8
|
-
Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents.
|
|
8
|
+
Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
19
19
|
- `langcode` (string): Target language for an unpublished working translation (nodes). Continues that translation via Sentinel.
|
|
20
20
|
- `attributes` (object (pass as JSON))
|
|
21
21
|
- `relationships` (object (pass as JSON))
|
|
22
|
-
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers,
|
|
23
|
-
- `returning` (
|
|
22
|
+
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, run the server-side preflight when one applies, and return a preview without the real write. The result's `checks` block says what was checked; `caveat` names what was not. Only an existing node draft (or a langcode translation draft) is checked with the real payload: Sentinel's non-saving draft endpoint applies the submitted fields through field access and validates the entity (`serverPreflight: sentinel_draft`). On other moderated targets an id-mismatch core PATCH with no fields checks entity update access and core's working-copy guard only; field access and entity validation are NOT checked (`core_patch_guard`), so the real write can still fail with a field-access 403 or a 422. Unmoderated targets get no server-side check at all (`none`). A published node with no distinct working copy whose changed timestamp is later than revision_timestamp (possiblyPatchBlocked) fails dryRun the same as the real write (#273). Any refusal fails the dryRun.
|
|
23
|
+
- `returning` (one of: full, minimal): 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.
|
|
24
24
|
|
|
25
25
|
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,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Score how completely optional fields are filled in for a content type. Finds nodes missing summaries, images, meta descriptions, tags, etc."
|
|
2
|
+
description: "Score how completely optional fields are filled in for a content type. Finds nodes missing summaries, images, meta descriptions, tags, etc. Reads scalar and entity-reference fields; a reference with no target is empty. Sampling-bounded — flags 'approximate' when the scan hits sampleSize. An absent field is not an empty one: JSON:API leaves out a field this account may not view. A node that omits the key is counted as `absent` and stays out of completenessPercent. A field you name that is absent from every sampled node is listed in `notVisible` and not scored (it may be denied to this account, not exist on the content type, or be misspelled). Default field guesses that are absent are dropped."
|
|
3
3
|
argument-hint: "<type> [site] [fields] [sampleSize]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_report_field_completeness`.
|
|
7
7
|
|
|
8
|
-
Score how completely optional fields are filled in for a content type. Finds nodes missing summaries, images, meta descriptions, tags, etc.
|
|
8
|
+
Score how completely optional fields are filled in for a content type. Finds nodes missing summaries, images, meta descriptions, tags, etc. Reads scalar and entity-reference fields; a reference with no target is empty. Sampling-bounded — flags 'approximate' when the scan hits sampleSize. An absent field is not an empty one: JSON:API leaves out a field this account may not view. A node that omits the key is counted as `absent` and stays out of completenessPercent. A field you name that is absent from every sampled node is listed in `notVisible` and not scored (it may be denied to this account, not exist on the content type, or be misspelled). Default field guesses that are absent are dropped.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Find entities of a content type where a given field is empty (e.g. a missing meta description, image, or summary). Works for scalar fields and entity-reference fields. Sampling-bounded — flags 'approximate' when the scan is capped."
|
|
2
|
+
description: "Find entities of a content type where a given field is empty (e.g. a missing meta description, image, or summary). Works for scalar fields and entity-reference fields. Sampling-bounded — flags 'approximate' when the scan is capped. An absent field is not an empty one: JSON:API leaves out a field this account may not view. When the field is absent from every sampled entity the result is `notVisible: true` with no findings (the field may be denied to this account, not exist on the bundle, or be misspelled) instead of every entity counted as missing. Otherwise each finding has `reason` 'empty' (key present, no value) or 'absent' (key missing, possibly access-denied)."
|
|
3
3
|
argument-hint: "<field> [site] [type] [sampleSize]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_report_missing_field`.
|
|
7
7
|
|
|
8
|
-
Find entities of a content type where a given field is empty (e.g. a missing meta description, image, or summary). Works for scalar fields and entity-reference fields. Sampling-bounded — flags 'approximate' when the scan is capped.
|
|
8
|
+
Find entities of a content type where a given field is empty (e.g. a missing meta description, image, or summary). Works for scalar fields and entity-reference fields. Sampling-bounded — flags 'approximate' when the scan is capped. An absent field is not an empty one: JSON:API leaves out a field this account may not view. When the field is absent from every sampled entity the result is `notVisible: true` with no findings (the field may be denied to this account, not exist on the bundle, or be misspelled) instead of every entity counted as missing. Otherwise each finding has `reason` 'empty' (key present, no value) or 'absent' (key missing, possibly access-denied).
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Report structured-meta (SEO) coverage for a content type: how many sampled nodes populate each meta field (metatag, meta description). Complements drupal_report_seo_audit with explicit per-field coverage."
|
|
2
|
+
description: "Report structured-meta (SEO) coverage for a content type: how many sampled nodes populate each meta field (metatag, meta description). Complements drupal_report_seo_audit with explicit per-field coverage. Reads scalar and entity-reference fields. A field absent from every sampled node has `present: false` and `coverage: null` (unknown, not 0), and is listed in `notVisible` when you named it. When none of the checked fields is visible, no node is flagged and `nodesMissingAllMeta` is null: JSON:API leaves out a field this account may not view."
|
|
3
3
|
argument-hint: "[site] [type] [fields] [sampleSize]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_report_seo_meta_coverage`.
|
|
7
7
|
|
|
8
|
-
Report structured-meta (SEO) coverage for a content type: how many sampled nodes populate each meta field (metatag, meta description). Complements drupal_report_seo_audit with explicit per-field coverage.
|
|
8
|
+
Report structured-meta (SEO) coverage for a content type: how many sampled nodes populate each meta field (metatag, meta description). Complements drupal_report_seo_audit with explicit per-field coverage. Reads scalar and entity-reference fields. A field absent from every sampled node has `present: false` and `coverage: null` (unknown, not 0), and is listed in `notVisible` when you named it. When none of the checked fields is visible, no node is flagged and `nodesMissingAllMeta` is null: JSON:API leaves out a field this account may not view.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -11,6 +11,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
11
11
|
|
|
12
12
|
**Optional:**
|
|
13
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
|
-
- `minSeverity` (
|
|
14
|
+
- `minSeverity` (one of: warning, error): Lowest severity to include
|
|
15
15
|
|
|
16
16
|
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,11 +1,11 @@
|
|
|
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. Pass langcode to continue an unpublished working translation (Sentinel X-MCP-Draft-Langcode); this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. 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 a non-saving PATCH preflight runs first — including on dryRun — against the same URL the write will hit. Existing node drafts use Sentinel's governed draft endpoint with verified live/working revision preconditions; translation-only drafts are discovered through Sentinel inventory (#297). Pass explicit langcode to continue an unpublished translation. Published languages are not converted into drafts; dryRun uses the 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."
|
|
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. Pass langcode to continue an unpublished working translation (Sentinel X-MCP-Draft-Langcode); this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. 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 a non-saving PATCH preflight runs first — including on dryRun — against the same URL the write will hit. Existing node drafts use Sentinel's governed draft endpoint with verified live/working revision preconditions; translation-only drafts are discovered through Sentinel inventory (#297). Pass explicit langcode to continue an unpublished translation. Published languages are not converted into drafts; dryRun uses the 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. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran."
|
|
3
3
|
argument-hint: "<type> <id> [site] [title] [body] [summary] [format] [status] [moderationState] [langcode] [fields] [relationships] [dryRun] [returning]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_update_node`.
|
|
7
7
|
|
|
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. Pass langcode to continue an unpublished working translation (Sentinel X-MCP-Draft-Langcode); this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. 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 a non-saving PATCH preflight runs first — including on dryRun — against the same URL the write will hit. Existing node drafts use Sentinel's governed draft endpoint with verified live/working revision preconditions; translation-only drafts are discovered through Sentinel inventory (#297). Pass explicit langcode to continue an unpublished translation. Published languages are not converted into drafts; dryRun uses the 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.
|
|
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. Pass langcode to continue an unpublished working translation (Sentinel X-MCP-Draft-Langcode); this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. 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 a non-saving PATCH preflight runs first — including on dryRun — against the same URL the write will hit. Existing node drafts use Sentinel's governed draft endpoint with verified live/working revision preconditions; translation-only drafts are discovered through Sentinel inventory (#297). Pass explicit langcode to continue an unpublished translation. Published languages are not converted into drafts; dryRun uses the 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. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
24
24
|
- `langcode` (string): Target language for an unpublished working translation (e.g. 'es'). Continues that translation via Sentinel; does not create a missing translation and does not PATCH canonical langcode.
|
|
25
25
|
- `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
26
|
- `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. Image alt on a translation uses the existing file UUID plus meta.alt; replacing the file is refused.
|
|
27
|
-
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers,
|
|
28
|
-
- `returning` (
|
|
27
|
+
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, run the server-side preflight when one applies, and return a preview without the real write. The result's `checks` block says what was checked; `caveat` names what was not. Only an existing node draft (or a langcode translation draft) is checked with the real payload: Sentinel's non-saving draft endpoint applies the submitted fields through field access and validates the entity (`serverPreflight: sentinel_draft`). On other moderated targets an id-mismatch core PATCH with no fields checks entity update access and core's working-copy guard only; field access and entity validation are NOT checked (`core_patch_guard`), so the real write can still fail with a field-access 403 or a 422. Unmoderated targets get no server-side check at all (`none`). A published node with no distinct working copy whose changed timestamp is later than revision_timestamp (possiblyPatchBlocked) fails dryRun the same as the real write (#273). Any refusal fails the dryRun.
|
|
28
|
+
- `returning` (one of: full, minimal): 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
29
|
|
|
30
30
|
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,257 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.20.0] - 2026-09-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Prompts for module-owned tools (#332).** Every module tool that discovery
|
|
14
|
+
returns for a request also gets a per-tool prompt
|
|
15
|
+
(`drupal-module-<operation>-<namespace>--<alias>`). Visibility follows the
|
|
16
|
+
tool: same caller scope, site grant and source checks, no cross-request
|
|
17
|
+
cache. The prompt lists the module's own parameters and explains the
|
|
18
|
+
`{ catalogRevision, arguments }` call shape without embedding a revision.
|
|
19
|
+
Write prompts say that module writes are not retried; delete prompts carry
|
|
20
|
+
the destructive warning.
|
|
21
|
+
- **Opt-in slash stubs for module-owned tools (#332).**
|
|
22
|
+
`npm run install:commands -- --modules` discovers the module tools your local
|
|
23
|
+
config approves and writes `drupal-<namespace>-<alias>.md` stubs next to the
|
|
24
|
+
built-in ones (and adds them to the Codex catalog). A name that matches a
|
|
25
|
+
built-in command, or that two module tools share, is refused. If a source
|
|
26
|
+
returns nothing the run fails and writes nothing; if it returns only some
|
|
27
|
+
configured tools, the missing ones are named and older stubs are kept. A
|
|
28
|
+
plain `install:commands` never writes module stubs and leaves installed ones
|
|
29
|
+
in place.
|
|
30
|
+
- **Example config documents module tools and bindings (#348).**
|
|
31
|
+
`config/config.example.json` now has a `_server_tools.example` block with
|
|
32
|
+
`serverTools.modules` (a namespace, two module-owned tools) and all seven
|
|
33
|
+
`serverTools.bindings`, plus notes on each key. The `drushSsh` SQL note names
|
|
34
|
+
the `sqlQuery` binding. A test runs the example through the module registry's
|
|
35
|
+
validation.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Prompts and command stubs show a parameter's allowed values when its schema
|
|
39
|
+
has a short string `enum` (`one of: full, minimal`). Eight built-in stubs
|
|
40
|
+
under `.agents/commands/` were regenerated.
|
|
41
|
+
- **`drupal_drush_module_disable` refuses protected modules (#346).** The tool
|
|
42
|
+
no longer uninstalls governance, integrity, secrets, auth and API modules
|
|
43
|
+
(`mcp_sentinel`, `audit_chain`, `field_guard`, `file_gate`, `key`, `encrypt`,
|
|
44
|
+
`simple_oauth`, `consumers`, `jsonapi`, `serialization`, `mcp_server`,
|
|
45
|
+
`mcp_server_tool_bridge`, `tool`, `content_moderation`, `workflows`). The list
|
|
46
|
+
is on for every preset. `security.protectedModules` adds modules;
|
|
47
|
+
`security.allowProtectedModuleUninstall` is the explicit opt-out. A malformed
|
|
48
|
+
value in either key blocks every uninstall. The bridge now answers the
|
|
49
|
+
`pm:uninstall` cascade prompt "no": an uninstall that would also remove
|
|
50
|
+
dependents is refused and names them. The handler also checks
|
|
51
|
+
`allowDestructive` itself. `drupal_security_info` shows the effective list.
|
|
52
|
+
One stub under `.agents/commands/` was regenerated.
|
|
53
|
+
- **`core.extension` can no longer be changed through config tools by default (#349).**
|
|
54
|
+
The protected-module list covered `drupal_drush_module_disable` only. Two
|
|
55
|
+
other tools could still uninstall a module. `drupal_config_set` now refuses a
|
|
56
|
+
write to `core.extension` and names `drupal_drush_module_enable` and
|
|
57
|
+
`drupal_drush_module_disable`; the name check ignores surrounding space and
|
|
58
|
+
case, and covers the binding path and the unbound path.
|
|
59
|
+
`drupal_drush_config_import` now runs `drush config:status` first and imports
|
|
60
|
+
nothing when `core.extension` differs, or when the status cannot be read or
|
|
61
|
+
understood. The connector cannot read the sync directory, so it refuses on
|
|
62
|
+
"core.extension differs" and does not name the modules. **Operators:** if
|
|
63
|
+
`drushSsh.allowedCommands` is set and lists `config:import`, add
|
|
64
|
+
`config:status`, or every import is refused. An import that leaves
|
|
65
|
+
`core.extension` alone, and `drupal_config_set` on any other object, work as
|
|
66
|
+
before. New per-site key `security.allowCoreExtensionChange` (`true` or
|
|
67
|
+
`false`, default `false` on every preset) opens both; any other value keeps
|
|
68
|
+
both refused. With it set, `drupal_config_set` still reads the current module
|
|
69
|
+
list and refuses a value that removes or alters an installed protected module,
|
|
70
|
+
and `drupal_drush_config_import` runs unchecked. `drupal_security_info` shows
|
|
71
|
+
the key. `docs/security.md` lists what stays out of the connector's reach (an
|
|
72
|
+
import run on the server, another client, the window between the status read
|
|
73
|
+
and the import) and points to MCP Sentinel's `denied_config_types` as the
|
|
74
|
+
source-side control. Two stubs under `.agents/commands/` were regenerated.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
- **Server-tool bridge errors no longer relay the response body or raw tool
|
|
78
|
+
text (#362).** `src/lib/server-tools.js` put four untrusted strings into its
|
|
79
|
+
errors with no cleaning and no bound: the body of a non-2xx `tools/call` or
|
|
80
|
+
`tools/list`, the body of a failed session handshake, a JSON-RPC
|
|
81
|
+
`error.message`, and the text of a tool result with `isError`. An HTML error
|
|
82
|
+
page, a server path or a backtrace reached the MCP client through the config
|
|
83
|
+
tools, the Drush-bound tools and the verifier's evidence. They now go through
|
|
84
|
+
`describeErrorBody()` and `cleanErrorText()`, as the JSON:API, GraphQL and
|
|
85
|
+
upload paths do. The prefix, the HTTP status and an integer JSON-RPC code are
|
|
86
|
+
unchanged (`Server-tool call <tool> failed <status>: …`,
|
|
87
|
+
`Server-tool <tool> error (<code>): …`,
|
|
88
|
+
`Server-tool <tool> reported an error: …`), so the verifier still classifies
|
|
89
|
+
them. A JSON-RPC error sent with a 4xx or 5xx reads
|
|
90
|
+
`failed <status>: JSON-RPC error <code>: <message>`. An empty body reads
|
|
91
|
+
`the server returned an empty body`. A JSON-RPC code that is not an integer
|
|
92
|
+
is no longer printed, and `error.data` is never read. A governed refusal's
|
|
93
|
+
text stays readable. Source budget codes are still looked for in the whole
|
|
94
|
+
text before it is cut.
|
|
95
|
+
- **A module tool's failure is cleaned before it is relayed (#362).** The
|
|
96
|
+
module registry relays a failed result (`isError`, or `success: false`)
|
|
97
|
+
because the module's message is what the caller needs. It now goes through
|
|
98
|
+
the new `cleanErrorData()`: the shape, keys, codes, numbers and booleans
|
|
99
|
+
stay, every string is cleaned like any other error detail, and the payload
|
|
100
|
+
is bounded (400 characters a string, 4,000 in total, 50 entries a level, 6
|
|
101
|
+
levels). A successful result is not changed. A transport failure behind the
|
|
102
|
+
registry was already replaced by a fixed message and still is.
|
|
103
|
+
- **The verifier reads the HTTP status from the response, not from its body
|
|
104
|
+
(#361).** `classifyBridgeError()` found the status of a failed bridge call
|
|
105
|
+
with a pattern that also matched the response body, so a 500 whose body held
|
|
106
|
+
"failed 403:" scored as a refusal and the config probe passed although the
|
|
107
|
+
tool never ran. The words "reported an error" and a quoted JSON-RPC code in a
|
|
108
|
+
body had the same effect. The bridge client now sets `status` on the error
|
|
109
|
+
for a non-2xx response, as `drupalFetch` does, and marks what failed
|
|
110
|
+
(`bridgeFailure`: `session`, `http`, `rpc` or `tool`, plus `rpcCode`). The
|
|
111
|
+
verifier reads those properties through `httpStatusOf()`. An error with no
|
|
112
|
+
marker is read only from the start of a documented message. A 401 or 403
|
|
113
|
+
from the token endpoint or the session handshake is no longer a refusal: no
|
|
114
|
+
tool was reached. A JSON-RPC error with no integer code is `skipped`.
|
|
115
|
+
- **A server failure no longer passes `probe_mass_read` or `principal_auth`
|
|
116
|
+
(#361).** Any failed mass read passed, including a 5xx, a 404 and a request
|
|
117
|
+
that never answered. The probe now passes on 401, 403 or 429, or on another
|
|
118
|
+
4xx (not 404) that carries the source's refusal code; anything else is
|
|
119
|
+
`skipped`. `principal_auth` is `skipped` when the anonymous request fails
|
|
120
|
+
with a 5xx or no answer, instead of counting that as "anonymous access is
|
|
121
|
+
refused".
|
|
122
|
+
- **Error details keep the URL paths the caller supplied (#357).** The error
|
|
123
|
+
cleaning added for #343 and #345 replaced every slash-led path of two or more
|
|
124
|
+
segments with `[path]`, so "The alias /about/team is already in use" arrived
|
|
125
|
+
as "The alias [path] is already in use". A path is now redacted only when it
|
|
126
|
+
looks like a filesystem path: it starts with a filesystem root (`/var`,
|
|
127
|
+
`/tmp`, `/home`, …), has a segment that marks a code tree or a file directory
|
|
128
|
+
(`vendor`, `modules`, `core`, `files`, …), or has a server-side file extension
|
|
129
|
+
(`.php`, `.yml`, `.log`, …). `/about/team`, `/node/12/edit` and
|
|
130
|
+
`/jsonapi/node/article/<id>` are kept. Windows drive and UNC paths,
|
|
131
|
+
`file://` and `phar://` URIs, and a filesystem path straight after a colon
|
|
132
|
+
(`include_path=.:/usr/share/php`) were not redacted before and now are.
|
|
133
|
+
Stream-wrapper redaction is unchanged. `docs/security.md` lists the rules and
|
|
134
|
+
their limits.
|
|
135
|
+
- **`dryRun` says what it checked (#336).** A preview could return without a
|
|
136
|
+
refusal and the real write then failed with a field-access 403. The core PATCH
|
|
137
|
+
probe sends no fields, and core rejects its id before it checks field access
|
|
138
|
+
or validation. Drupal never evaluated an unmoderated update, a create or a
|
|
139
|
+
delete preview. Every `dryRun` result on `drupal_create_node`, `drupal_update_node`,
|
|
140
|
+
`drupal_delete_node`, `drupal_entity_create`, `drupal_entity_update`,
|
|
141
|
+
`drupal_entity_delete` and `drupal_create_translation` now carries a `checks`
|
|
142
|
+
block (`serverPreflight`, `connectorPolicy`, `entityAccess`, `revisionGuard`,
|
|
143
|
+
`fieldAccess`, `entityValidation`, each `checked` or `not_checked`) and a
|
|
144
|
+
`caveat` when anything was not checked. `fieldAccess` and `entityValidation`
|
|
145
|
+
are `checked` only when Sentinel's non-saving draft endpoint evaluated the
|
|
146
|
+
real payload. No new probe was added, and existing preview fields are
|
|
147
|
+
unchanged. Tool descriptions, `docs/tools-reference.md` and the README state
|
|
148
|
+
the limit. Seven stubs under `.agents/commands/` were regenerated. The
|
|
149
|
+
internal `preflightPatchWritable` result drops `writable: true` for
|
|
150
|
+
`revisionGuardPassed` and `payloadEvaluated: false`.
|
|
151
|
+
- **Config tools call the wire name the source advertises (#335).** Without
|
|
152
|
+
`serverTools.bindings`, `drupal_config_get` / `_list` / `_set` and the config
|
|
153
|
+
reports called `tool_api.mcp_sentinel_config_*`. Current `mcp_server`
|
|
154
|
+
releases publish `tool_api__mcp_sentinel_config_*`. The connector now reads
|
|
155
|
+
the source's `tools/list` and uses the name it lists, accepting either form
|
|
156
|
+
and preferring the double underscore. If the source lists neither, the call
|
|
157
|
+
fails with "not advertised by the source" and nothing is sent. The
|
|
158
|
+
`SERVER_TOOLS` export in `src/lib/server-tools.js` is replaced by
|
|
159
|
+
`SERVER_TOOL_IDS` and `resolveServerToolName`. The bindings path is unchanged.
|
|
160
|
+
- **The verifier's config probe can no longer pass on a missing tool (#335).**
|
|
161
|
+
`probe_config_change` and `entitlement_filtering` read the source catalog
|
|
162
|
+
first and score a refusal only for a tool the catalog lists. A tool that is
|
|
163
|
+
not advertised, or a catalog that cannot be read, is `skipped` with the
|
|
164
|
+
reason, and the run is not ok. A served write still fails the probe whether
|
|
165
|
+
or not the tool was advertised. `verifyLive` takes a new `listTools`
|
|
166
|
+
dependency; without it the probe cannot pass. This applies to a configured
|
|
167
|
+
`configSet` binding too.
|
|
168
|
+
- **A field the account may not view is no longer reported as empty (#337).**
|
|
169
|
+
JSON:API leaves a view-denied field out of the resource and keeps the key of
|
|
170
|
+
an empty one. `drupal_report_missing_field` counted every entity as missing a
|
|
171
|
+
field it could not see. When the field is absent from every sampled entity
|
|
172
|
+
the report now returns `notVisible: true`, `totalMissing: null` and no
|
|
173
|
+
findings, and says the field may be denied, not on the bundle, or misspelled.
|
|
174
|
+
When only some entities omit it, each finding carries `reason` (`empty` or
|
|
175
|
+
`absent`) and the result adds `totalEmpty` and `totalAbsent`.
|
|
176
|
+
`drupal_describe_fields` reports `fieldDefinitions` (`available` or
|
|
177
|
+
`unavailable`) and, when `field_config` is readable, lists fields defined for
|
|
178
|
+
the bundle but absent from the sampled entity as `notVisible`. No request was
|
|
179
|
+
added per entity. Tool descriptions and `docs/tools-reference.md` state the
|
|
180
|
+
limit. Two stubs under `.agents/commands/` were regenerated.
|
|
181
|
+
- **Northbound Drupal HTTP timeouts.** JSON:API, GraphQL, and file-upload
|
|
182
|
+
`node-fetch` calls abort after 30s (`AbortSignal.timeout`), matching the
|
|
183
|
+
Drush SSH bound. A hung Drupal host no longer stalls the MCP process.
|
|
184
|
+
- **Upload failures no longer return the raw response body (#343).**
|
|
185
|
+
`drupal_upload_file` and `drupal_upload_file_and_create_media` threw
|
|
186
|
+
`File upload failed <status>: <body>`. The error now keeps the status and
|
|
187
|
+
surfaces Drupal's `errors[].detail` (or `title`, or a plain `message`), with
|
|
188
|
+
markup and control characters stripped, server paths and stream-wrapper URIs
|
|
189
|
+
redacted, and a 400-character bound. An HTML page or a JSON body with no
|
|
190
|
+
error detail is never shown. New helper: `src/lib/error-body.js`.
|
|
191
|
+
- **JSON:API and GraphQL failures no longer return the raw response body (#345).**
|
|
192
|
+
`drupalFetch()` fell back to the whole body when it was not a JSON:API error
|
|
193
|
+
document, and `drupalGraphqlFetch()` always sent the whole body, so an HTML
|
|
194
|
+
error page from Drupal, PHP or a proxy reached the MCP client. Both now use
|
|
195
|
+
`describeErrorBody()`, like uploads since #343. The messages keep their
|
|
196
|
+
shape, `Drupal <status> on <method> <path>: <detail>` and
|
|
197
|
+
`GraphQL request failed <status>: <detail>`, and keep `errors[].detail` and
|
|
198
|
+
GraphQL `errors[].message`. The detail has markup and control characters
|
|
199
|
+
stripped, server paths and stream-wrapper URIs redacted and any backtrace
|
|
200
|
+
removed. One detail is cut to 400 characters and a list of details to 1,200.
|
|
201
|
+
Each detail is cleaned on its own, so a backtrace in one error does not
|
|
202
|
+
remove the next. An OAuth error document surfaces `error` and
|
|
203
|
+
`error_description`, never `hint`. An empty or unreadable body reports the
|
|
204
|
+
status with `(empty response body)` or `(response body could not be read)`.
|
|
205
|
+
Path redaction was narrowed afterwards; see #357. Successful responses
|
|
206
|
+
are unchanged. Every matcher on these messages has a regression test in
|
|
207
|
+
`tests/lib/fetch-error-matchers.test.js`.
|
|
208
|
+
- **GraphQL errors on a 200 response are cleaned and bounded (#356).** GraphQL
|
|
209
|
+
reports a failed query as HTTP 200 with an `errors` array, so those errors
|
|
210
|
+
skipped the cleaning added for #345. `drupal_graphql` (the thrown message and
|
|
211
|
+
the `warnings` of a partial result), `drupal_graphql_introspect`, the GraphQL
|
|
212
|
+
backend, the backend probe and the SEO audit's metatag lookup joined
|
|
213
|
+
`errors[].message` as received, with no length limit. `drupalGraphqlFetch()`
|
|
214
|
+
now replaces the array with a cleaned form before any caller reads it:
|
|
215
|
+
`message`, `path`, `locations` and the machine values `extensions.code`,
|
|
216
|
+
`category` and `classification`. `extensions.trace`, `debugMessage`, `file`,
|
|
217
|
+
`line` and any stack are dropped. A message has markup and control characters
|
|
218
|
+
stripped, paths redacted, any backtrace removed, and is cut to 400 characters.
|
|
219
|
+
At most 50 errors and 4,000 characters of message are kept, and a last entry
|
|
220
|
+
says how many were left out. Thrown messages are cut to 1,200 characters.
|
|
221
|
+
`drupal_graphql_introspect` with a `typeName` now reports the server's errors
|
|
222
|
+
for a failed lookup instead of "Type not found in schema", and the GraphQL
|
|
223
|
+
backend's schema load reports them instead of a `TypeError`. `data` is
|
|
224
|
+
unchanged.
|
|
225
|
+
- **Field reports score reference fields and name fields they cannot see (#341).**
|
|
226
|
+
`drupal_report_field_completeness` skipped entity-reference fields and dropped
|
|
227
|
+
a requested field that was absent from every sampled node. It now reads
|
|
228
|
+
references, counts a node that omits the key as `absent`, lists such a
|
|
229
|
+
requested field in `notVisible`, and sets `approximate` from `sampleSize`. A
|
|
230
|
+
link value counts as populated. `drupal_report_seo_meta_coverage` follows the
|
|
231
|
+
same rules: `coverage: null` for an absent field, and no node flagged when no
|
|
232
|
+
checked field is visible. Two stubs under `.agents/commands/` were regenerated.
|
|
233
|
+
- **A status number in an error's text is no longer read as the response status (#355).**
|
|
234
|
+
`drupal_report_orphaned_references` reported a reference as an orphan when the
|
|
235
|
+
lookup failed with a 500 whose detail mentioned "404". Four other matchers had
|
|
236
|
+
the same fault. A 404 on `/jsonapi/node/article/401` read as an authentication
|
|
237
|
+
failure and cleared the cached OAuth token. A failed PATCH probe whose detail
|
|
238
|
+
mentioned "Drupal 422" read as a passed working-copy check. A 403 or 500 that
|
|
239
|
+
mentioned "422" and "inaccessible" triggered the menu-link retry. A 500 on a
|
|
240
|
+
Sentinel draft or translation endpoint whose detail mentioned "Drupal 404" was
|
|
241
|
+
reported as a missing endpoint. Errors thrown by `drupalFetch()`,
|
|
242
|
+
`drupalGraphqlFetch()` and the upload helper now carry the HTTP status on a
|
|
243
|
+
`status` property, and all five matchers read it through `httpStatusOf()`
|
|
244
|
+
(`src/lib/error-status.js`). An error with no `status` is read only from the
|
|
245
|
+
status token at the start of a documented message (`Drupal <status> …`,
|
|
246
|
+
`GraphQL request failed <status>`, `File upload failed <status>`). The orphan
|
|
247
|
+
report no longer treats a bare "404" elsewhere in a message as a missing
|
|
248
|
+
target; such a failure counts as unverifiable.
|
|
249
|
+
|
|
250
|
+
## [2.19.1] - 2026-09-17
|
|
251
|
+
|
|
252
|
+
### Changed
|
|
253
|
+
- **Remove leftover Sentinel facades (#328).** `draft-write.js` and
|
|
254
|
+
`node-draft-inventory.js` were re-exports of `sentinel-draft.js`. Tests
|
|
255
|
+
import the client directly. Unused exports (`toolMessage`, `redactResponse`)
|
|
256
|
+
are gone; `DIAGNOSTIC_TOOLS` and `POLICY_DIGEST` each live in one place.
|
|
257
|
+
- README and docs now report **123 tools across 27 modules** and **128 prompts**
|
|
258
|
+
(one per tool plus 5 workflow prompts). Codegen (`drupal_codegen_*`) shipped
|
|
259
|
+
in 2.16.0 but was missing from the public counts and the npm project page.
|
|
260
|
+
|
|
10
261
|
## [2.19.0] - 2026-09-16
|
|
11
262
|
|
|
12
263
|
### Added
|
|
@@ -1679,6 +1930,9 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
|
|
|
1679
1930
|
- User tools gained explicit PII-access assertions.
|
|
1680
1931
|
- Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
|
|
1681
1932
|
|
|
1933
|
+
[2.19.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.19.1
|
|
1934
|
+
[2.19.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.19.0
|
|
1935
|
+
[2.18.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.18.0
|
|
1682
1936
|
[2.17.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.17.0
|
|
1683
1937
|
[2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
|
|
1684
1938
|
[2.15.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.2
|