drupal-mcp-connector 2.14.1 → 2.15.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.
@@ -1,22 +1,25 @@
1
1
  ---
2
- description: "Create or replace a translation of a Drupal entity for a target language (governed write). Sets the given langcode plus the supplied translated field values. Requires the content_translation module enabled and the bundle configured as translatable; otherwise Drupal rejects the write. Defaults to node."
3
- argument-hint: "<type> <id> <langcode> [site] [entityType] [attributes]"
2
+ description: "Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; continue a paragraph with drupal_update_paragraph and langcode. Image alt is a relationship (same file UUID, meta.alt). For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers."
3
+ argument-hint: "<type> <id> <langcode> [site] [entityType] [attributes] [relationships] [revisionId] [dryRun]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_create_translation`.
7
7
 
8
- Create or replace a translation of a Drupal entity for a target language (governed write). Sets the given langcode plus the supplied translated field values. Requires the content_translation module enabled and the bundle configured as translatable; otherwise Drupal rejects the write. Defaults to node.
8
+ Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; continue a paragraph with drupal_update_paragraph and langcode. Image alt is a relationship (same file UUID, meta.alt). For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Required:**
13
- - `type` (string): Bundle machine name, e.g. 'article'
13
+ - `type` (string): Bundle machine name, e.g. 'basic_page' or 'p_hero'
14
14
  - `id` (string): Entity UUID
15
- - `langcode` (string): Target language code, e.g. 'de', 'fr', 'pt_br'
15
+ - `langcode` (string): Target language code, e.g. 'es', 'de', 'pt-br'
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
- - `entityType` (string): Entity type machine name. Default: 'node'.
19
+ - `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values.
20
20
  - `attributes` (object (pass as JSON)): Translated field values keyed by Drupal machine name
21
+ - `relationships` (object (pass as JSON)): JSON:API relationships. Use for image alt (same file UUID, meta.alt).
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
21
24
 
22
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,6 +1,6 @@
1
1
  ---
2
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."
3
- argument-hint: "<entityType> <bundle> <id> [site] [attributes] [relationships] [dryRun] [returning]"
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`.
@@ -16,9 +16,10 @@ 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 language for an unpublished working translation (nodes). Continues that translation via Sentinel.
19
20
  - `attributes` (object (pass as JSON))
20
21
  - `relationships` (object (pass as JSON))
21
- - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. Any refusal fails the dryRun.
22
+ - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. 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.
22
23
  - `returning` (string): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
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: "Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes."
3
- argument-hint: "<type> <id> [site]"
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]"
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.
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).
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): Target language (e.g. 'es') to read the unpublished working translation instead of 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: "Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy."
3
- argument-hint: "<paragraphType> <id> [site]"
2
+ description: "Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy."
3
+ argument-hint: "<paragraphType> <id> [site] [langcode] [revisionId]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_get_paragraph`.
7
7
 
8
- Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.
8
+ Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -15,5 +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): Target language for the unpublished working translation (e.g. 'es').
19
+ - `revisionId` (string): Paragraph revision id the host already pins.
18
20
 
19
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 revision of a content node by version id or alias. `version` may be a numeric vid (e.g. 42), an explicit 'id:<vid>', or the relative aliases 'rel:latest-version' / 'rel:working-copy'. Read-only; attributes are redacted per security policy."
3
- argument-hint: "<type> <id> <version> [site]"
2
+ description: "Fetch a single revision of a content node by version id or alias. `version` may be a numeric vid (e.g. 42), an explicit 'id:<vid>', or the relative aliases 'rel:latest-version' / 'rel:working-copy'. Pass langcode to read the unpublished working translation of that node (Sentinel). Read-only; attributes are redacted per security policy."
3
+ argument-hint: "<type> <id> <version> [site] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_get_revision`.
7
7
 
8
- Fetch a single revision of a content node by version id or alias. `version` may be a numeric vid (e.g. 42), an explicit 'id:<vid>', or the relative aliases 'rel:latest-version' / 'rel:working-copy'. Read-only; attributes are redacted per security policy.
8
+ Fetch a single revision of a content node by version id or alias. `version` may be a numeric vid (e.g. 42), an explicit 'id:<vid>', or the relative aliases 'rel:latest-version' / 'rel:working-copy'. Pass langcode to read the unpublished working translation of that node (Sentinel). Read-only; attributes are redacted per security policy.
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 language for the unpublished working translation (e.g. 'es').
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,16 +1,16 @@
1
1
  ---
2
- description: "List the translation langcode(s) for a Drupal entity (multilingual / content_translation). Reports the language(s) observable on the resource. Core JSON:API serves one language per resource and does not enumerate all translations — see the returned note. 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). 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 the translation langcode(s) for a Drupal entity (multilingual / content_translation). Reports the language(s) observable on the resource. Core JSON:API serves one language per resource and does not enumerate all translations — see the returned note. 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). 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
 
12
12
  **Required:**
13
- - `type` (string): Bundle machine name, e.g. 'article'
13
+ - `type` (string): Bundle machine name, e.g. 'basic_page'
14
14
  - `id` (string): Entity UUID
15
15
 
16
16
  **Optional:**
@@ -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. 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. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); dryRun uses that same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents."
3
- argument-hint: "<type> <id> [site] [title] [body] [summary] [format] [status] [moderationState] [fields] [relationships] [dryRun] [returning]"
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. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); dryRun uses that same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents."
3
+ 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. 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. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); dryRun uses that same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents.
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. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); dryRun uses that same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -21,9 +21,10 @@ Parse the arguments supplied with this command into this tool's parameters:
21
21
  - `format` (string): Text format machine name for the body, e.g. 'basic_html'. When the body field's allowed_formats lists exactly one format, that is the default. A caller format outside that list is refused before write. When allowed_formats cannot be resolved, defaults to the site config's `defaultTextFormat`, then 'full_html'.
22
22
  - `status` (boolean (true/false)): Published flag for NON-moderated types: true = publish, false = unpublish. Ignored if moderationState is set.
23
23
  - `moderationState` (string): Moderation state transition for content_moderation types, e.g. 'draft', 'published', 'archived'. Takes precedence over status. Required to keep or re-publish a live node — omitting it on a published moderated node defaults the write to 'draft'.
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.
24
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.
25
- - `relationships` (object (pass as JSON)): Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value uses { data: { type, id } }; multi-value uses { data: [{ type, id }, …] }. Paragraph / ERR items must carry meta.target_revision_id — the connector injects it when missing, and fails the write if it cannot.
26
- - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. Any refusal fails the dryRun.
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, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. 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.
27
28
  - `returning` (string): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
28
29
 
29
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.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy."
3
- argument-hint: "<paragraphType> <id> [site] [attributes]"
2
+ description: "Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Pass langcode to continue an unpublished paragraph translation via Sentinel (the host pin, not a live English mutation). Omitting langcode still uses canonical JSON:API and remains gated on published-host children. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy."
3
+ argument-hint: "<paragraphType> <id> [site] [attributes] [langcode] [revisionId] [draftState]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_paragraph`.
7
7
 
8
- Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy.
8
+ Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Pass langcode to continue an unpublished paragraph translation via Sentinel (the host pin, not a live English mutation). Omitting langcode still uses canonical JSON:API and remains gated on published-host children. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -16,5 +16,8 @@ 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
  - `attributes` (object (pass as JSON)): Paragraph field values to change, keyed by Drupal machine name, e.g. { field_body: { value: '<p>..</p>', format: 'full_html' } }
19
+ - `langcode` (string): Target language for an unpublished paragraph translation (e.g. 'es'). Continues Sentinel /mcp-draft; does not create a missing translation.
20
+ - `revisionId` (string): Paragraph revision id the host already pins. Required when that pin is not the default revision.
21
+ - `draftState` (string): Required with langcode. Opaque draftState from the previous paragraph draft read/create/update; prevents overwriting newer edits.
19
22
 
20
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,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.15.0] - 2026-09-10
11
+
12
+ ### Fixed
13
+ - Paragraph translation updates require the `draftState` returned by a previous draft read. Pass it unchanged to Sentinel for stale-write protection. Shared-status paragraphs remain pending for human editing (#280).
14
+ - **Omit computed `metatag` on unpublished working-translation bodies (#283).**
15
+ JSON:API's computed `metatag` array is resolved from the live default
16
+ revision, so a Spanish draft response could still show English title and
17
+ description. The stored override (`field_metatags`) is unchanged. Draft
18
+ translation create/read now drop `fields.metatag` and set `_metatagOmitted`.
19
+
20
+ - **`drupal_create_translation` sends live and working revision IDs when a draft exists (#282).**
21
+ Sentinel's translation POST 409s with "A working revision exists" if If-Match
22
+ is live-only. The connector now reads Sentinel's translation inventory for
23
+ the live/working pair (falling back to `rel:working-copy`) and sends
24
+ `If-Match: "live:working"`. The response includes `_revisions` so the caller
25
+ can see which revision the language landed on.
26
+
27
+ - **`dryRun` fails when a published-node write would hit Sentinel's stale-copy check (#273).**
28
+ The id-mismatch PATCH probe never reaches entity validation / presave, so it
29
+ could not see MCP Sentinel's default-revision stale-version refusal. When
30
+ `rel:latest-version` and `rel:working-copy` share a vid and the default
31
+ revision's `changed` is later than `revision_timestamp` (`possiblyPatchBlocked`),
32
+ `drupal_update_node` / `drupal_entity_update` (including `dryRun`) now refuse
33
+ with a stable `STALE_COPY` error. The same message is rewritten if Sentinel
34
+ still returns it on the saving write. Governance is unchanged: the draft and
35
+ publish gates are not bypassed.
36
+ - **Title-only node updates keep the existing URL alias; failed restorations error (#274).**
37
+ `getPathInfo` now fills `pid` / `aliasId` from the `path_alias` row when an
38
+ unpublished default or forward revision omits them on the computed `path`
39
+ field. After `drupal_update_node`, the connector re-reads the alias and, if
40
+ Pathauto still drifted it, PATCHes the existing `path_alias` entity. If the
41
+ alias still does not match, the tool throws instead of returning success or
42
+ creating a rename redirect. Title-only edits pin the live alias as before.
43
+
44
+ ### Added
45
+ - **Codex skill target for `npm run install:commands` (#263).**
46
+ The installer writes `$HOME/.agents/skills/drupal-mcp/SKILL.md` (plus
47
+ `references/tools.md`) so Drupal MCP tools are discoverable in Codex. Custom
48
+ prompts under `~/.codex/prompts` are not written — OpenAI deprecated that
49
+ surface in favor of skills. Default clients are now `claude,grok,codex`.
50
+ Test with `--home` on an isolated directory.
51
+
52
+ - **Unpublished paragraph field-value translation and image alt (#280).**
53
+ `drupal_create_translation` accepts `entityType: "paragraph"` and
54
+ `revisionId` (the host pin) and POSTs Sentinel's paragraph
55
+ `/mcp-draft/translations` surface. Continue with `drupal_update_paragraph`
56
+ + `langcode`; read with `drupal_get_paragraph` + `langcode`. Omitting
57
+ `langcode` on `drupal_update_paragraph` still uses canonical JSON:API.
58
+ Node translation writes forward image relationships so alt can change
59
+ while the file UUID stays the same. Replacing the file remains a
60
+ server refusal. Node-only translation create is unchanged.
61
+
62
+ ## [2.14.2] - 2026-09-10
63
+
64
+ ### Fixed
65
+ - **`langcode` reads target only an unpublished working translation.**
66
+ After a Spanish draft exists, English on that working revision is still
67
+ the published copy. `drupal_get_node` / `drupal_get_revision` with
68
+ `langcode` now read the working translation only when that language is
69
+ unpublished, so they do not treat published English as a draft.
70
+ - **Spanish (and other) draft translations beside published English (#276).**
71
+ `drupal_create_translation` no longer PATCHes `langcode` on the canonical
72
+ entity. It POSTs Sentinel's `/mcp-draft/translations` endpoint so Drupal
73
+ calls `addTranslation()` on an unpublished forward revision. The live
74
+ default language, revision id, title, body, status, and alias stay
75
+ unchanged. An existing translation is a conflict. Continue the draft with
76
+ `drupal_update_node` + `langcode`; read it with `drupal_get_node` /
77
+ `drupal_list_translations` / `drupal_get_revision` + `langcode`. Requires
78
+ MCP Sentinel with the draft-translation contract. Paragraph field-value
79
+ translation and media photo-alt translation remain pending. A content-tier
80
+ caller still cannot publish through this path.
81
+
10
82
  ## [2.14.1] - 2026-09-09
11
83
 
12
84
  ### Changed
@@ -1481,6 +1553,7 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
1481
1553
  - User tools gained explicit PII-access assertions.
1482
1554
  - Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
1483
1555
 
1556
+ [2.14.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.2
1484
1557
  [2.14.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.1
1485
1558
  [2.14.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.0
1486
1559
  [2.13.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.13.1
package/README.md CHANGED
@@ -129,13 +129,15 @@ Clients that scan `.agents/commands/` (including Grok Build when this repo is
129
129
  the project) pick them up automatically.
130
130
 
131
131
  Clients that only scan a vendor home path (Claude Code `~/.claude/commands`,
132
- Grok `~/.grok/commands`) install from that tree into the **operator home**, so
133
- no consuming project has to commit `.claude/` or `.grok/`:
132
+ Grok `~/.grok/commands`, Codex `$HOME/.agents/skills`) install from that tree
133
+ into the **operator home**, so no consuming project has to commit `.claude/` or
134
+ `.grok/`. Codex uses a **skill** (`drupal-mcp`), not deprecated custom prompts:
134
135
 
135
136
  ```bash
136
137
  npm run generate:commands # refresh .agents/commands/ (from a clone)
137
- npm run install:commands # copies into ~/.claude/commands and ~/.grok/commands
138
+ npm run install:commands # copies into ~/.claude/commands, ~/.grok/commands, and ~/.agents/skills/drupal-mcp/
138
139
  # npm run install:commands -- --clients claude
140
+ # npm run install:commands -- --clients codex
139
141
  # npm run install:commands -- --home /path/to/fake-home # tests / CI
140
142
  ```
141
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.14.1",
3
+ "version": "2.15.0",
4
4
  "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,14 +8,17 @@
8
8
  * Build, when this repo is the project) pick them up as `/drupal-<tool>`.
9
9
  *
10
10
  * Clients that only scan a vendor home directory (Claude Code `~/.claude/commands`,
11
- * Grok `~/.grok/commands`) get the same files via `npm run install:commands`.
11
+ * Grok `~/.grok/commands`, Codex `$HOME/.agents/skills`) get the same files via
12
+ * `npm run install:commands`. Codex receives one skill (`drupal-mcp`), not
13
+ * per-tool custom prompts.
12
14
  *
13
15
  * Driven from the same tool definitions as the server (src/tools/index.js), so the
14
16
  * command set never drifts from the tools. Run: `npm run generate:commands`.
15
17
  *
16
- * Exports `renderCommandMarkdown`, `renderClaudeCommandMarkdown`, `commandFileName`,
17
- * `COMMANDS_DIR`, and `generate` for tests; the file-writing side effect runs only
18
- * when executed directly.
18
+ * Exports `renderCommandMarkdown`, `renderClaudeCommandMarkdown`,
19
+ * `renderCodexSkillMarkdown`, `renderCodexToolsReference`, `CODEX_SKILL_NAME`,
20
+ * `commandFileName`, `COMMANDS_DIR`, and `generate` for tests; the file-writing
21
+ * side effect runs only when executed directly.
19
22
  */
20
23
 
21
24
  import { mkdirSync, readdirSync, rmSync, writeFileSync, realpathSync } from "fs";
@@ -113,6 +116,62 @@ export function renderClaudeCommandMarkdown(def) {
113
116
  });
114
117
  }
115
118
 
119
+ /** Codex user-skill directory name under `$HOME/.agents/skills/`. */
120
+ export const CODEX_SKILL_NAME = "drupal-mcp";
121
+
122
+ /**
123
+ * Codex skill adapter (OpenAI Skills). Custom `~/.codex/prompts` slash
124
+ * commands are deprecated; Codex discovers `$HOME/.agents/skills/<name>/SKILL.md`.
125
+ * One skill covers the whole Drupal MCP tool surface so the skills list stays
126
+ * inside Codex's discovery budget.
127
+ *
128
+ * @param {Array<object>} [definitions]
129
+ * @returns {string} SKILL.md contents.
130
+ */
131
+ export function renderCodexSkillMarkdown(definitions = allDefinitions) {
132
+ const count = definitions.length;
133
+ return [
134
+ "---",
135
+ `name: ${yamlString(CODEX_SKILL_NAME)}`,
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.",
138
+ )}`,
139
+ "---",
140
+ "",
141
+ "Call Drupal MCP tools by their protocol names (`drupal_list_nodes`, `drupal_update_node`, …).",
142
+ "The MCP server is configured separately (`codex mcp add drupal`); this skill does not replace that connection and does not use deprecated Codex custom prompts.",
143
+ "",
144
+ "Parse the user's request into the tool's parameters. If a required parameter is missing, ask before calling.",
145
+ "Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the tool call and summarize the result.",
146
+ "",
147
+ "Destructive tools permanently change or delete data — confirm with the user before calling.",
148
+ "Do not invent REST paths, Drush commands, or Drupal admin URLs when an MCP tool exists.",
149
+ "",
150
+ `The connector currently exposes **${count}** tools. Full names and descriptions: [references/tools.md](references/tools.md).`,
151
+ "",
152
+ ].join("\n");
153
+ }
154
+
155
+ /**
156
+ * Progressive-disclosure catalog for the Codex Drupal skill.
157
+ * @param {Array<object>} [definitions]
158
+ * @returns {string} references/tools.md contents.
159
+ */
160
+ export function renderCodexToolsReference(definitions = allDefinitions) {
161
+ const lines = [
162
+ "# Drupal MCP tools",
163
+ "",
164
+ "Protocol tool names for `drupal-mcp-connector`. Call these via MCP; do not invent REST paths.",
165
+ "",
166
+ ];
167
+ for (const def of definitions) {
168
+ const desc = String(def.description || "").replace(/\s+/g, " ").trim();
169
+ lines.push(`- \`${def.name}\`${desc ? ` — ${desc}` : ""}`);
170
+ }
171
+ lines.push("");
172
+ return lines.join("\n");
173
+ }
174
+
116
175
  /**
117
176
  * Write one command file per tool, pruning stale `drupal-*.md` first so removed
118
177
  * tools leave no orphans.
@@ -9,11 +9,12 @@
9
9
  * copies so `/drupal-*` works in every project without committing a vendor
10
10
  * folder to this repo or to a consuming application.
11
11
  *
12
- * Default targets: `~/.claude/commands` (Claude adapter) and `~/.grok/commands`
13
- * (canonical files). Pass `--clients` to subset. Never writes into a project
14
- * tree.
12
+ * Default targets: `~/.claude/commands` (Claude adapter), `~/.grok/commands`
13
+ * (canonical files), and `$HOME/.agents/skills/drupal-mcp/` (Codex skill).
14
+ * Pass `--clients` to subset. Never writes into a project tree. Codex custom
15
+ * prompts (`~/.codex/prompts`) are not written — they are deprecated.
15
16
  *
16
- * Run: `npm run install:commands -- [--home DIR] [--clients claude,grok,agents]`
17
+ * Run: `npm run install:commands -- [--home DIR] [--clients claude,grok,codex,agents]`
17
18
  */
18
19
 
19
20
  import { mkdirSync, readdirSync, rmSync, writeFileSync, realpathSync } from "fs";
@@ -26,6 +27,9 @@ import {
26
27
  commandFileName,
27
28
  renderCommandMarkdown,
28
29
  renderClaudeCommandMarkdown,
30
+ renderCodexSkillMarkdown,
31
+ renderCodexToolsReference,
32
+ CODEX_SKILL_NAME,
29
33
  } from "./generate-commands.js";
30
34
 
31
35
  /** Whitelisted install targets. `rel` is under `--home` (default: os.homedir()). */
@@ -42,9 +46,18 @@ export const CLIENTS = {
42
46
  rel: ".agents/commands",
43
47
  render: renderCommandMarkdown,
44
48
  },
49
+ // Codex discovers user skills at `$HOME/.agents/skills/<name>/SKILL.md`.
50
+ // One skill for the whole tool surface — not one skill per tool.
51
+ codex: {
52
+ rel: ".agents/skills",
53
+ kind: "skill",
54
+ skillName: CODEX_SKILL_NAME,
55
+ renderSkill: renderCodexSkillMarkdown,
56
+ renderReference: renderCodexToolsReference,
57
+ },
45
58
  };
46
59
 
47
- const DEFAULT_CLIENTS = ["claude", "grok"];
60
+ const DEFAULT_CLIENTS = ["claude", "grok", "codex"];
48
61
 
49
62
  /**
50
63
  * Parse CLI flags. Unknown flags throw.
@@ -111,6 +124,15 @@ export function install(options = {}) {
111
124
  if (!client) {
112
125
  throw new Error(`Unknown client "${name}". Allowed: ${Object.keys(CLIENTS).join(", ")}`);
113
126
  }
127
+ if (client.kind === "skill") {
128
+ const dir = join(home, client.rel, client.skillName);
129
+ rmSync(dir, { recursive: true, force: true });
130
+ mkdirSync(join(dir, "references"), { recursive: true });
131
+ writeFileSync(join(dir, "SKILL.md"), client.renderSkill(definitions));
132
+ writeFileSync(join(dir, "references", "tools.md"), client.renderReference(definitions));
133
+ results.push({ client: name, dir, written: ["SKILL.md", "references/tools.md"] });
134
+ continue;
135
+ }
114
136
  const dir = join(home, client.rel);
115
137
  mkdirSync(dir, { recursive: true });
116
138
  for (const f of readdirSync(dir)) {
@@ -127,14 +149,15 @@ export function install(options = {}) {
127
149
  return results;
128
150
  }
129
151
 
130
- const HELP = `Usage: node scripts/install-commands.js [--home DIR] [--clients claude,grok,agents]
152
+ const HELP = `Usage: node scripts/install-commands.js [--home DIR] [--clients claude,grok,codex,agents]
131
153
 
132
- Copy generated /drupal-* command stubs into operator home directories.
133
- Does not write into a project tree.
154
+ Copy generated /drupal-* command stubs (and the Codex skill) into operator
155
+ home directories. Does not write into a project tree. Does not write
156
+ deprecated Codex custom prompts (~/.codex/prompts).
134
157
 
135
158
  --home DIR Install root (default: the current user's home)
136
- --clients LIST Comma-separated subset of: claude, grok, agents
137
- (default: claude,grok)
159
+ --clients LIST Comma-separated subset of: claude, grok, codex, agents
160
+ (default: claude,grok,codex)
138
161
  `;
139
162
 
140
163
  const invokedDirectly =
@@ -79,10 +79,12 @@ export class Backend {
79
79
  * for callers that must round-trip the alias `pid` on an in-place update (the
80
80
  * canonical entity only exposes `path.alias` as `url`). Optional capability:
81
81
  * the default returns nulls so read-only/path-less backends are safe. See the 1.5.1 alias fix.
82
- * @param {{entityType: string, bundle: string, id: string}} _ref
83
- * @returns {Promise<{alias: ?string, pid: ?(number|string), langcode: ?string, drupalId: ?(number|string)}>}
82
+ * @param {{entityType: string, bundle: string, id: string, resourceVersion?: string}} _ref
83
+ * @returns {Promise<{alias: ?string, pid: ?(number|string), langcode: ?string, drupalId: ?(number|string), aliasId: ?string}>}
84
84
  */
85
- async getPathInfo(_ref) { return { alias: null, pid: null, langcode: null, drupalId: null }; }
85
+ async getPathInfo(_ref) {
86
+ return { alias: null, pid: null, langcode: null, drupalId: null, aliasId: null };
87
+ }
86
88
 
87
89
  /**
88
90
  * Create an entity.