drupal-mcp-connector 2.15.1 → 2.16.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-codegen-diff.md +17 -0
- package/.agents/commands/drupal-codegen-generate.md +17 -0
- package/.agents/commands/drupal-codegen-inspect.md +17 -0
- package/.agents/commands/drupal-content-by-moderation-state.md +4 -3
- package/.agents/commands/drupal-describe-fields.md +2 -2
- package/.agents/commands/drupal-get-node.md +4 -3
- package/.agents/commands/drupal-get-taxonomy-term.md +4 -3
- package/.agents/commands/drupal-list-revisions.md +2 -2
- package/.agents/commands/drupal-list-translations.md +2 -2
- package/.agents/commands/drupal-report-translation-coverage.md +4 -5
- package/.agents/commands/drupal-report-workflow-bottlenecks.md +2 -1
- package/.agents/commands/drupal-set-moderation-state.md +4 -3
- package/.agents/commands/drupal-update-menu-link.md +2 -1
- package/.agents/commands/drupal-update-node.md +2 -2
- package/.agents/commands/drupal-update-taxonomy-term.md +4 -3
- package/CHANGELOG.md +51 -0
- package/README.md +3 -3
- package/bin/drupal-mcp-agent.js +2 -2
- package/package.json +2 -2
- package/scripts/generate-commands.js +2 -2
- package/src/index.js +2 -2
- package/src/lib/backends/jsonapi.js +85 -9
- package/src/lib/config.js +18 -1
- package/src/lib/err-relationships.js +22 -0
- package/src/lib/mcp-server.js +1 -1
- package/src/lib/node-draft-inventory.js +47 -0
- package/src/lib/patch-preflight.js +23 -6
- package/src/lib/server-tools.js +2 -2
- package/src/lib/translation-rows.js +59 -0
- package/src/lib/write-revision.js +9 -2
- package/src/tools/codegen.js +150 -0
- package/src/tools/entities.js +1 -0
- package/src/tools/fields.js +12 -4
- package/src/tools/index.js +2 -1
- package/src/tools/moderation.js +82 -13
- package/src/tools/nodes.js +60 -8
- package/src/tools/reports-content.js +117 -28
- package/src/tools/revisions.js +18 -2
- package/src/tools/structure.js +6 -3
- package/src/tools/taxonomy.js +15 -6
- package/src/tools/translations.js +4 -8
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff."
|
|
3
|
+
argument-hint: "[site] [bundles] [skipFields]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Call the MCP tool `drupal_codegen_diff`.
|
|
7
|
+
|
|
8
|
+
Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff.
|
|
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
|
+
- `bundles` (array (pass as JSON)): Node/paragraph bundle ids. Omit for every bundle graphql_compose exposes.
|
|
15
|
+
- `skipFields` (array (pass as JSON)): Extra field machine names to exclude from the scaffold.
|
|
16
|
+
|
|
17
|
+
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,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate."
|
|
3
|
+
argument-hint: "[site] [bundles] [skipFields]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Call the MCP tool `drupal_codegen_generate`.
|
|
7
|
+
|
|
8
|
+
Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate.
|
|
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
|
+
- `bundles` (array (pass as JSON)): Node/paragraph bundle ids. Omit for every bundle graphql_compose exposes.
|
|
15
|
+
- `skipFields` (array (pass as JSON)): Extra field machine names to exclude from the scaffold.
|
|
16
|
+
|
|
17
|
+
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,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files."
|
|
3
|
+
argument-hint: "[site] [bundles] [skipFields]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Call the MCP tool `drupal_codegen_inspect`.
|
|
7
|
+
|
|
8
|
+
List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files.
|
|
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
|
+
- `bundles` (array (pass as JSON)): Node/paragraph bundle ids. Omit for every bundle graphql_compose exposes.
|
|
15
|
+
- `skipFields` (array (pass as JSON)): Extra field machine names to exclude from the scaffold.
|
|
16
|
+
|
|
17
|
+
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: "List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Stock JSON:API cannot filter the computed moderation_state field; when the site rejects that filter the tool samples recent nodes client-side and marks the result approximate, instead of returning Drupal's 500."
|
|
3
|
-
argument-hint: "<type> <state> [site] [limit] [offset]"
|
|
2
|
+
description: "List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Pass langcode to match that translation via Sentinel inventory (the editorial work queue). Omit langcode for default-language JSON:API / sampled behavior. Stock JSON:API cannot filter the computed moderation_state field; when the site rejects that filter the tool samples recent nodes client-side and marks the result approximate, instead of returning Drupal's 500."
|
|
3
|
+
argument-hint: "<type> <state> [site] [langcode] [limit] [offset]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_content_by_moderation_state`.
|
|
7
7
|
|
|
8
|
-
List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Stock JSON:API cannot filter the computed moderation_state field; when the site rejects that filter the tool samples recent nodes client-side and marks the result approximate, instead of returning Drupal's 500.
|
|
8
|
+
List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Pass langcode to match that translation via Sentinel inventory (the editorial work queue). Omit langcode for default-language JSON:API / sampled behavior. Stock JSON:API cannot filter the computed moderation_state field; when the site rejects that filter the tool samples recent nodes client-side and marks the result approximate, instead of returning Drupal's 500.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -15,6 +15,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
15
15
|
|
|
16
16
|
**Optional:**
|
|
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
|
+
- `langcode` (string): Match this translation (e.g. 'es'). Requires Sentinel. Omit for default-language listing.
|
|
18
19
|
- `limit` (number)
|
|
19
20
|
- `offset` (number)
|
|
20
21
|
|
|
@@ -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?, 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. 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. 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?, 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. 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. 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: "Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English)."
|
|
3
|
-
argument-hint: "<type> <id> [site] [langcode]"
|
|
2
|
+
description: "Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English). Pass includeComponents true to attach pinned paragraph translations under `components` (default false)."
|
|
3
|
+
argument-hint: "<type> <id> [site] [langcode] [includeComponents]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_get_node`.
|
|
7
7
|
|
|
8
|
-
Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English).
|
|
8
|
+
Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English). Pass includeComponents true to attach pinned paragraph translations under `components` (default false).
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -16,5 +16,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
16
16
|
**Optional:**
|
|
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
|
- `langcode` (string): Target language (e.g. 'es') to read the unpublished working translation instead of the default language.
|
|
19
|
+
- `includeComponents` (boolean (true/false)): If true, include pinned paragraph translations under `components` (empty array when the host has no ERR fields). Default false so existing callers are unchanged.
|
|
19
20
|
|
|
20
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.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Fetch a single taxonomy term by UUID."
|
|
3
|
-
argument-hint: "<vocabulary> <id> [site]"
|
|
2
|
+
description: "Fetch a single taxonomy term by UUID. Pass langcode to request that translation; omit for the default language. JSON:API must negotiate language or the tool errors if a different language is served."
|
|
3
|
+
argument-hint: "<vocabulary> <id> [site] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_get_taxonomy_term`.
|
|
7
7
|
|
|
8
|
-
Fetch a single taxonomy term by UUID.
|
|
8
|
+
Fetch a single taxonomy term by UUID. Pass langcode to request that translation; omit for the default language. JSON:API must negotiate language or the tool errors if a different language is served.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -15,5 +15,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
15
15
|
|
|
16
16
|
**Optional:**
|
|
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
|
+
- `langcode` (string): Translation language (e.g. 'es'). Omit for the default language.
|
|
18
19
|
|
|
19
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,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Surface the
|
|
2
|
+
description: "Surface the latest default and working node revisions. When the core working-copy alias is absent or echoes live, consult Sentinel translation inventory; workingCopy then includes source and language-specific translations instead of a misleading aggregate status. Continue an unpublished translation with explicit langcode. workingCopy: null is not an all-clear — Drupal core can still reject PATCH when a revision row sits above the default without a content_moderation working copy (#201). The payload includes possiblyPatchBlocked (true when default changed is later than its revision_timestamp) plus changed and revisionTimestamp on latestVersion. Probe the host (this flag, then dryRun on the update) before creating dependent paragraphs. NOTE: JSON:API cannot enumerate full chronological revision history. Full history enumeration requires the Drush bridge."
|
|
3
3
|
argument-hint: "<type> <id> [site]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_list_revisions`.
|
|
7
7
|
|
|
8
|
-
Surface the
|
|
8
|
+
Surface the latest default and working node revisions. When the core working-copy alias is absent or echoes live, consult Sentinel translation inventory; workingCopy then includes source and language-specific translations instead of a misleading aggregate status. Continue an unpublished translation with explicit langcode. workingCopy: null is not an all-clear — Drupal core can still reject PATCH when a revision row sits above the default without a content_moderation working copy (#201). The payload includes possiblyPatchBlocked (true when default changed is later than its revision_timestamp) plus changed and revisionTimestamp on latestVersion. Probe the host (this flag, then dryRun on the update) before creating dependent paragraphs. NOTE: JSON:API cannot enumerate full chronological revision history. Full history enumeration requires the Drush bridge.
|
|
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: "List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft). Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node."
|
|
2
|
+
description: "List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft), including core content_translation_outdated and source when the server sends them. Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node."
|
|
3
3
|
argument-hint: "<type> <id> [site] [entityType]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_list_translations`.
|
|
7
7
|
|
|
8
|
-
List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft). Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node.
|
|
8
|
+
List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft), including core content_translation_outdated and source when the server sends them. Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
3
|
-
argument-hint: "[site] [type] [
|
|
2
|
+
description: "Per-node translation coverage from Sentinel's inventory (missing non-default language, outdated core flag, language counts). Without Sentinel the report is unavailable — JSON:API only shows the default language, so a histogram would be misleading."
|
|
3
|
+
argument-hint: "[site] [type] [sampleSize]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_report_translation_coverage`.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Per-node translation coverage from Sentinel's inventory (missing non-default language, outdated core flag, language counts). Without Sentinel the report is unavailable — JSON:API only shows the default language, so a histogram would be misleading.
|
|
9
9
|
|
|
10
10
|
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
14
|
- `type` (string): Content type (default: article)
|
|
15
|
-
- `
|
|
16
|
-
- `sampleSize` (number)
|
|
15
|
+
- `sampleSize` (number): Max nodes to inventory
|
|
17
16
|
|
|
18
17
|
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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Find content stuck in a non-published moderation state (draft/needs_review) beyond N days — editorial bottlenecks. Reads moderation_state; gated when content_moderation isn't exposed."
|
|
3
|
-
argument-hint: "[site] [type] [days] [states] [sampleSize]"
|
|
3
|
+
argument-hint: "[site] [type] [days] [states] [langcode] [sampleSize]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_report_workflow_bottlenecks`.
|
|
@@ -14,6 +14,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
14
14
|
- `type` (string): Content type (default: article)
|
|
15
15
|
- `days` (number): Days-in-state threshold
|
|
16
16
|
- `states` (array (pass as JSON)): Moderation states to treat as bottlenecks
|
|
17
|
+
- `langcode` (string): Limit to this translation (Sentinel inventory). Omit for the default-language field on each node.
|
|
17
18
|
- `sampleSize` (number)
|
|
18
19
|
|
|
19
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,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write."
|
|
3
|
-
argument-hint: "<type> <id> <state> [site]"
|
|
2
|
+
description: "Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write. Pass langcode to change one translation via Sentinel; omit it for the default-language / shared-state write. If moderation_state is not translatable, a langcode write is refused."
|
|
3
|
+
argument-hint: "<type> <id> <state> [site] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_set_moderation_state`.
|
|
7
7
|
|
|
8
|
-
Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write.
|
|
8
|
+
Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write. Pass langcode to change one translation via Sentinel; omit it for the default-language / shared-state write. If moderation_state is not translatable, a langcode write is refused.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -16,5 +16,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
16
16
|
|
|
17
17
|
**Optional:**
|
|
18
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).
|
|
19
|
+
- `langcode` (string): Target translation (e.g. 'es'). Omit for the default language. Requires Sentinel.
|
|
19
20
|
|
|
20
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
argument-hint: "<id> [site] [title] [link] [menu] [weight] [parent] [enabled]"
|
|
3
|
+
argument-hint: "<id> [site] [title] [link] [menu] [weight] [parent] [enabled] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_update_menu_link`.
|
|
@@ -20,5 +20,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
20
20
|
- `weight` (number): New ordering weight. Omit to leave unchanged.
|
|
21
21
|
- `parent` (string): New parent link plugin id (e.g. 'menu_link_content:<uuid>'), or '' for top level. Omit to leave unchanged.
|
|
22
22
|
- `enabled` (boolean (true/false)): Enable/disable the link. Omit to preserve the current state.
|
|
23
|
+
- `langcode` (string): Existing translation to update (e.g. 'es'). Omit for the default language. Does not create a missing translation.
|
|
23
24
|
|
|
24
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: "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.
|
|
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."
|
|
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.
|
|
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.
|
|
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: "Update an existing taxonomy term's name, description, or weight."
|
|
3
|
-
argument-hint: "<vocabulary> <id> [site] [name] [description] [weight]"
|
|
2
|
+
description: "Update an existing taxonomy term's name, description, or weight. Pass langcode to update an existing translation; this does not create a missing translation."
|
|
3
|
+
argument-hint: "<vocabulary> <id> [site] [name] [description] [weight] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_update_taxonomy_term`.
|
|
7
7
|
|
|
8
|
-
Update an existing taxonomy term's name, description, or weight.
|
|
8
|
+
Update an existing taxonomy term's name, description, or weight. Pass langcode to update an existing translation; this does not create a missing translation.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -18,5 +18,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
18
18
|
- `name` (string)
|
|
19
19
|
- `description` (string)
|
|
20
20
|
- `weight` (number)
|
|
21
|
+
- `langcode` (string): Existing translation to update (e.g. 'es'). Omit for the default language.
|
|
21
22
|
|
|
22
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.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.16.0] - 2026-09-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Optional graphql_compose_codegen tools (#308).**
|
|
14
|
+
`drupal_codegen_inspect`, `drupal_codegen_diff`, and `drupal_codegen_generate`
|
|
15
|
+
call `drush graphql-compose-codegen:*` over the existing SSH bridge.
|
|
16
|
+
Generate is `--dry-run` only (artefacts as text; no `--output-dir`).
|
|
17
|
+
Missing module or unknown command fails loud. If `allowedCommands` is set,
|
|
18
|
+
the matching `graphql-compose-codegen:*` subcommand must be listed.
|
|
19
|
+
- **Field `translatable`, taxonomy/menu `langcode`, and composed node reads (#303).**
|
|
20
|
+
`drupal_describe_fields` copies Field API `translatable` when JSON:API
|
|
21
|
+
`field_config` is readable (omitted means unknown). Taxonomy get/update and
|
|
22
|
+
menu-link update accept optional `langcode` (existing translations only).
|
|
23
|
+
`drupal_get_node` accepts `includeComponents` (default false) to attach
|
|
24
|
+
pinned paragraph translations.
|
|
25
|
+
- **Optional `langcode` on the editorial queue (#303).**
|
|
26
|
+
`drupal_content_by_moderation_state` and `drupal_set_moderation_state` accept
|
|
27
|
+
a translation language. Omitting it keeps the previous default-language
|
|
28
|
+
behavior. A langcode write uses Sentinel's draft-translation surface (no
|
|
29
|
+
canonical `langcode` PATCH). A langcode list uses the translation inventory;
|
|
30
|
+
without Sentinel the result is `unavailable`. `drupal_report_workflow_bottlenecks`
|
|
31
|
+
has the same optional filter.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **`drupal_list_translations` passes through core outdated/source (#303).**
|
|
35
|
+
When Sentinel sends `content_translation_outdated` / `content_translation_source`,
|
|
36
|
+
those keys appear on each row. They are omitted when absent.
|
|
37
|
+
- **`drupal_report_translation_coverage` uses Sentinel inventory (#303).**
|
|
38
|
+
Per-node missing non-default language and outdated flags replace the
|
|
39
|
+
default-language histogram. Without Sentinel the report is `unavailable`
|
|
40
|
+
rather than a misleading 100% default-language chart.
|
|
41
|
+
- **Public product name is Drupal MCP Connector (#301).** Document titles and
|
|
42
|
+
the MCP handshake `serverInfo.title` now use the title-case product name.
|
|
43
|
+
The machine identifiers are unchanged: npm package, GitHub repo, binaries,
|
|
44
|
+
`X-MCP-Client`, log prefixes, and `serverInfo.name` remain
|
|
45
|
+
`drupal-mcp-connector`.
|
|
46
|
+
|
|
47
|
+
## [2.15.2] - 2026-09-11
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- **Discover and continue translation-only forward revisions (#297).** When core's
|
|
51
|
+
working-copy alias is absent or echoes live, node revision listing and guarded
|
|
52
|
+
updates consult Sentinel's translation inventory. Existing unpublished languages
|
|
53
|
+
continue through the governed endpoint with explicit language and live/working
|
|
54
|
+
preconditions. Published languages and ambiguous multilingual edits remain refused.
|
|
55
|
+
Permission and transport failures stop discovery rather than becoming “no draft”.
|
|
56
|
+
Update responses preserve the saved translation instead of replacing it with live
|
|
57
|
+
English. An unresolved revision conflict no longer prescribes revision surgery.
|
|
58
|
+
|
|
10
59
|
## [2.15.1] - 2026-09-10
|
|
11
60
|
|
|
12
61
|
### Fixed
|
|
@@ -1571,6 +1620,8 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
|
|
|
1571
1620
|
- User tools gained explicit PII-access assertions.
|
|
1572
1621
|
- Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
|
|
1573
1622
|
|
|
1623
|
+
[2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
|
|
1624
|
+
[2.15.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.2
|
|
1574
1625
|
[2.15.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.1
|
|
1575
1626
|
[2.15.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.0
|
|
1576
1627
|
[2.14.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.2
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Drupal MCP Connector
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Drupal MCP Connector — multi-site MCP server for Drupal with JSON:API and GraphQL, governed writes, draft translations, content tools, audit reports, and an SSH Drush bridge.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
@@ -15,7 +15,7 @@ Built by **[Jeremy Michael Cerda](https://www.drupal.org/u/jmcerda)** (opensourc
|
|
|
15
15
|
|
|
16
16
|
## What It Does
|
|
17
17
|
|
|
18
|
-
`drupal-mcp-connector` connects any [Model Context Protocol](https://modelcontextprotocol.io) client to one or more Drupal sites. It exposes Drupal content and configuration as a set of MCP **tools**, **resources**, and **prompts**, so an MCP client can read, audit, and (where permitted) write content through structured, governed operations instead of the admin UI:
|
|
18
|
+
**Drupal MCP Connector** (`drupal-mcp-connector`) connects any [Model Context Protocol](https://modelcontextprotocol.io) client to one or more Drupal sites. It exposes Drupal content and configuration as a set of MCP **tools**, **resources**, and **prompts**, so an MCP client can read, audit, and (where permitted) write content through structured, governed operations instead of the admin UI:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
"Find all articles missing a meta description and list them."
|
package/bin/drupal-mcp-agent.js
CHANGED
|
@@ -25,7 +25,7 @@ import { readFileSync } from "node:fs";
|
|
|
25
25
|
import { connect as netConnect } from "node:net";
|
|
26
26
|
import { connect as tlsConnect } from "node:tls";
|
|
27
27
|
import process from "node:process";
|
|
28
|
-
import { CLIENT_VERSION, listSiteNames, loadConfig } from "../src/lib/config.js";
|
|
28
|
+
import { CLIENT_VERSION, SERVER_INFO, listSiteNames, loadConfig } from "../src/lib/config.js";
|
|
29
29
|
import { callTool, listResolvableSiteConfigs } from "../src/lib/dispatch.js";
|
|
30
30
|
import { filterDiscoverableTools } from "../src/lib/governance.js";
|
|
31
31
|
import {
|
|
@@ -110,7 +110,7 @@ async function discoverableTools() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const surface = {
|
|
113
|
-
serverInfo:
|
|
113
|
+
serverInfo: SERVER_INFO,
|
|
114
114
|
tools: {
|
|
115
115
|
definitions: allDefinitions,
|
|
116
116
|
list: discoverableTools,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.16.0",
|
|
4
|
+
"description": "Drupal MCP Connector — multi-site MCP server for Drupal with JSON:API and GraphQL, governed writes, draft translations, content tools, audit reports, and an SSH Drush bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -134,7 +134,7 @@ export function renderCodexSkillMarkdown(definitions = allDefinitions) {
|
|
|
134
134
|
"---",
|
|
135
135
|
`name: ${yamlString(CODEX_SKILL_NAME)}`,
|
|
136
136
|
`description: ${yamlString(
|
|
137
|
-
"Use Drupal MCP tools (drupal_*) via drupal-mcp-connector to read or write Drupal content, media, taxonomy, reports, and config. Trigger when the user asks to work with a Drupal site over MCP.",
|
|
137
|
+
"Use Drupal MCP tools (drupal_*) via Drupal MCP Connector (drupal-mcp-connector) to read or write Drupal content, media, taxonomy, reports, and config. Trigger when the user asks to work with a Drupal site over MCP.",
|
|
138
138
|
)}`,
|
|
139
139
|
"---",
|
|
140
140
|
"",
|
|
@@ -161,7 +161,7 @@ export function renderCodexToolsReference(definitions = allDefinitions) {
|
|
|
161
161
|
const lines = [
|
|
162
162
|
"# Drupal MCP tools",
|
|
163
163
|
"",
|
|
164
|
-
"Protocol tool names for
|
|
164
|
+
"Protocol tool names for Drupal MCP Connector. Call these via MCP; do not invent REST paths.",
|
|
165
165
|
"",
|
|
166
166
|
];
|
|
167
167
|
for (const def of definitions) {
|
package/src/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
|
33
33
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
34
34
|
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
35
35
|
|
|
36
|
-
import { listSiteNames, getTlsConfig, loadConfig, CLIENT_VERSION } from "./lib/config.js";
|
|
36
|
+
import { listSiteNames, getTlsConfig, loadConfig, CLIENT_VERSION, SERVER_INFO } from "./lib/config.js";
|
|
37
37
|
import { loadLocalSecrets, secretLoadFatalMessage, secretTableMismatchMessage } from "./lib/load-secrets.js";
|
|
38
38
|
import {
|
|
39
39
|
makeBearerCheck,
|
|
@@ -291,7 +291,7 @@ function getPromptMessages(name, args) {
|
|
|
291
291
|
// ---------------------------------------------------------------------------
|
|
292
292
|
|
|
293
293
|
const buildConnectorServer = createConnectorServerFactory({
|
|
294
|
-
serverInfo:
|
|
294
|
+
serverInfo: SERVER_INFO,
|
|
295
295
|
tools: {
|
|
296
296
|
definitions: allDefinitions,
|
|
297
297
|
list: discoverableTools,
|
|
@@ -32,6 +32,38 @@ const INTERNAL_ATTR_RE = /^drupal_internal__/;
|
|
|
32
32
|
const COUNT_PAGE_SIZE = 50;
|
|
33
33
|
const COUNT_MAX_RECORDS = 1000;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* JSON:API language headers. Drupal negotiates Content-Language / Accept-Language
|
|
37
|
+
* when URL prefixes are not used on /jsonapi.
|
|
38
|
+
* @param {?string} langcode
|
|
39
|
+
* @returns {object}
|
|
40
|
+
*/
|
|
41
|
+
function languageFetchOptions(langcode) {
|
|
42
|
+
if (!langcode) return {};
|
|
43
|
+
return { headers: { "Content-Language": langcode, "Accept-Language": langcode } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fail if JSON:API served a different language than requested.
|
|
48
|
+
* @param {?object} entity
|
|
49
|
+
* @param {?string} langcode
|
|
50
|
+
*/
|
|
51
|
+
function assertServedLanguage(entity, langcode) {
|
|
52
|
+
if (!langcode || !entity) return;
|
|
53
|
+
if (!entity.langcode) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`JSON:API did not report a language for requested langcode "${langcode}". ` +
|
|
56
|
+
"The translation may not exist, or this backend does not negotiate language.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (entity.langcode !== langcode) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`JSON:API served language "${entity.langcode}" for requested langcode "${langcode}". ` +
|
|
62
|
+
"The translation may not exist, or this site does not negotiate JSON:API by language.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
/**
|
|
36
68
|
* Whether a JSON:API collection document advertises another page.
|
|
37
69
|
* `links.next` may be a string href or a `{ href }` link object.
|
|
@@ -358,14 +390,16 @@ export class JsonApiBackend extends Backend {
|
|
|
358
390
|
* @param {{entityType: string, bundle: string, id: string, resourceVersion?: string}} ref
|
|
359
391
|
* @returns {Promise<?import("../canonical.js").CanonicalEntity>} Entity, or null.
|
|
360
392
|
*/
|
|
361
|
-
async getEntity({ entityType, bundle, id, resourceVersion }) {
|
|
393
|
+
async getEntity({ entityType, bundle, id, resourceVersion, langcode }) {
|
|
362
394
|
validateUuid(id);
|
|
363
395
|
let path = `${this.resourcePath(entityType, bundle)}/${encodeURIComponent(id)}`;
|
|
364
396
|
if (resourceVersion) {
|
|
365
397
|
path += `?resourceVersion=${encodeURIComponent(resourceVersion)}`;
|
|
366
398
|
}
|
|
367
|
-
const data = await drupalFetch(this.site, path);
|
|
368
|
-
|
|
399
|
+
const data = await drupalFetch(this.site, path, languageFetchOptions(langcode));
|
|
400
|
+
const entity = data?.data ? this.toCanonical(data.data) : null;
|
|
401
|
+
assertServedLanguage(entity, langcode);
|
|
402
|
+
return entity;
|
|
369
403
|
}
|
|
370
404
|
|
|
371
405
|
/**
|
|
@@ -478,14 +512,15 @@ export class JsonApiBackend extends Backend {
|
|
|
478
512
|
* @param {object} attributes Entity attributes (may include `status`).
|
|
479
513
|
* @returns {Promise<object>} The JSON:API response body.
|
|
480
514
|
*/
|
|
481
|
-
async writeWithModerationFallback(path, method, buildPayload, attributes) {
|
|
515
|
+
async writeWithModerationFallback(path, method, buildPayload, attributes, langcode) {
|
|
516
|
+
const lang = languageFetchOptions(langcode);
|
|
482
517
|
try {
|
|
483
|
-
return await drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(attributes)) });
|
|
518
|
+
return await drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(attributes)), ...lang });
|
|
484
519
|
} catch (err) {
|
|
485
520
|
if (!isModeratedStatusError(err) || !("status" in attributes)) throw err;
|
|
486
521
|
const withoutStatus = { ...attributes };
|
|
487
522
|
delete withoutStatus.status;
|
|
488
|
-
return drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(withoutStatus)) });
|
|
523
|
+
return drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(withoutStatus)), ...lang });
|
|
489
524
|
}
|
|
490
525
|
}
|
|
491
526
|
|
|
@@ -515,7 +550,7 @@ export class JsonApiBackend extends Backend {
|
|
|
515
550
|
* canonical default (#166 / Drupal #2795279).
|
|
516
551
|
* @returns {Promise<import("../canonical.js").CanonicalEntity>} The updated entity.
|
|
517
552
|
*/
|
|
518
|
-
async updateEntity({ entityType, bundle, id, attributes = {}, relationships, resourceVersion }) {
|
|
553
|
+
async updateEntity({ entityType, bundle, id, attributes = {}, relationships, resourceVersion, langcode }) {
|
|
519
554
|
validateUuid(id);
|
|
520
555
|
const buildPayload = (attrs) => {
|
|
521
556
|
const payload = { data: { type: `${entityType}--${bundle}`, id, attributes: attrs } };
|
|
@@ -527,8 +562,10 @@ export class JsonApiBackend extends Backend {
|
|
|
527
562
|
if (resourceVersion) {
|
|
528
563
|
path += `?resourceVersion=${encodeURIComponent(resourceVersion)}`;
|
|
529
564
|
}
|
|
530
|
-
const data = await this.writeWithModerationFallback(path, "PATCH", buildPayload, attributes);
|
|
531
|
-
|
|
565
|
+
const data = await this.writeWithModerationFallback(path, "PATCH", buildPayload, attributes, langcode);
|
|
566
|
+
const entity = this.toCanonical(data.data);
|
|
567
|
+
assertServedLanguage(entity, langcode);
|
|
568
|
+
return entity;
|
|
532
569
|
}
|
|
533
570
|
|
|
534
571
|
/**
|
|
@@ -704,6 +741,45 @@ export class JsonApiBackend extends Backend {
|
|
|
704
741
|
* @param {{entityType: string, bundle: string, fieldName: string}} ref
|
|
705
742
|
* @returns {Promise<?{fieldName: string, fieldType: ?string, allowedFormats: string[]}>}
|
|
706
743
|
*/
|
|
744
|
+
/**
|
|
745
|
+
* Map of field machine name → translatable from JSON:API field_config.
|
|
746
|
+
* Base fields (title, moderation_state) are omitted unless exposed as
|
|
747
|
+
* base_field_override. Missing keys must not be treated as false.
|
|
748
|
+
* @param {string} entityType
|
|
749
|
+
* @param {string} bundle
|
|
750
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
751
|
+
*/
|
|
752
|
+
async listFieldTranslatability(entityType, bundle) {
|
|
753
|
+
validateMachineName(entityType, "entityType");
|
|
754
|
+
validateMachineName(bundle, "bundle");
|
|
755
|
+
const map = new Map();
|
|
756
|
+
const params = new URLSearchParams();
|
|
757
|
+
params.set("filter[entity_type]", entityType);
|
|
758
|
+
params.set("filter[bundle]", bundle);
|
|
759
|
+
params.set("page[limit]", "50");
|
|
760
|
+
const ingest = (rows) => {
|
|
761
|
+
for (const row of rows) {
|
|
762
|
+
const name = row?.attributes?.field_name;
|
|
763
|
+
if (typeof name === "string" && typeof row.attributes?.translatable === "boolean") {
|
|
764
|
+
map.set(name, row.attributes.translatable);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
try {
|
|
769
|
+
const data = await drupalFetch(this.site, `/jsonapi/field_config/field_config?${params}`);
|
|
770
|
+
ingest(Array.isArray(data?.data) ? data.data : []);
|
|
771
|
+
} catch {
|
|
772
|
+
// field_config may be unexposed.
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const data = await drupalFetch(this.site, `/jsonapi/base_field_override/base_field_override?${params}`);
|
|
776
|
+
ingest(Array.isArray(data?.data) ? data.data : []);
|
|
777
|
+
} catch {
|
|
778
|
+
// base_field_override is often not JSON:API-exposed.
|
|
779
|
+
}
|
|
780
|
+
return Object.fromEntries(map);
|
|
781
|
+
}
|
|
782
|
+
|
|
707
783
|
async getFieldDefinition({ entityType, bundle, fieldName }) {
|
|
708
784
|
validateMachineName(entityType, "entityType");
|
|
709
785
|
validateMachineName(bundle, "bundle");
|