drupal-mcp-connector 2.23.2 → 2.23.3

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,11 +1,11 @@
1
1
  ---
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. Pass revise: true to open an unpublished draft over a language that is already published on the live revision and has no working copy (Sentinel X-MCP-Draft-Mode: revise). That requires MCP Sentinel 2.24.0 or later; an older host is refused before any write and the message names that version. dryRun uses the same translations endpoint and Sentinel's non-saving preflight. A working copy is refused. Paragraph revise is not supported. 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; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers."
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. Pass revise: true to open an unpublished draft over a language that is already published on the live revision (Sentinel X-MCP-Draft-Mode: revise). That requires MCP Sentinel 2.24.0 or later; an older host is refused before any write and the message names that version. When the node already has a working copy (for example an English draft), both revision IDs are sent and Sentinel drafts the language on that working copy; the other drafts carry forward. That needs MCP Sentinel 2.25.0 or later. A language that is already a draft on the working copy is continued with drupal_update_node and langcode instead. dryRun uses the same translations endpoint and Sentinel's non-saving preflight. Paragraph revise is not supported. 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; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers."
3
3
  argument-hint: "<type> <id> <langcode> [site] [entityType] [attributes] [relationships] [revisionId] [dryRun] [revise]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_create_translation`.
7
7
 
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. Pass revise: true to open an unpublished draft over a language that is already published on the live revision and has no working copy (Sentinel X-MCP-Draft-Mode: revise). That requires MCP Sentinel 2.24.0 or later; an older host is refused before any write and the message names that version. dryRun uses the same translations endpoint and Sentinel's non-saving preflight. A working copy is refused. Paragraph revise is not supported. 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; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers.
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. Pass revise: true to open an unpublished draft over a language that is already published on the live revision (Sentinel X-MCP-Draft-Mode: revise). That requires MCP Sentinel 2.24.0 or later; an older host is refused before any write and the message names that version. When the node already has a working copy (for example an English draft), both revision IDs are sent and Sentinel drafts the language on that working copy; the other drafts carry forward. That needs MCP Sentinel 2.25.0 or later. A language that is already a draft on the working copy is continued with drupal_update_node and langcode instead. dryRun uses the same translations endpoint and Sentinel's non-saving preflight. Paragraph revise is not supported. 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; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. 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
 
@@ -21,6 +21,6 @@ Parse the arguments supplied with this command into this tool's parameters:
21
21
  - `relationships` (object (pass as JSON)): JSON:API relationships. Use for image alt (same file UUID, meta.alt).
22
22
  - `revisionId` (string): Paragraph revision id the host already pins. Required for Home-shaped non-default pins.
23
23
  - `dryRun` (boolean (true/false)): Validate without saving. Sentinel's non-saving translation preflight receives the real fields, applies them through field access and validates the entity. The result's `checks` block says what was checked. With revise: true the preflight is the same translations endpoint.
24
- - `revise` (boolean (true/false)): Open an unpublished draft over a translation that is already published on the live default revision and has no working copy. Requires MCP Sentinel 2.24.0 or later. Omit it to create a language that does not exist yet.
24
+ - `revise` (boolean (true/false)): Open an unpublished draft over a translation that is already published on the live default revision. Requires MCP Sentinel 2.24.0 or later, and 2.25.0 or later when the node already has a working copy. Omit it to create a language that does not exist yet.
25
25
 
26
26
  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 a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. A published translation with no working copy is opened with drupal_create_translation and revise: true, not with this tool."
2
+ description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. A published translation is opened with drupal_create_translation and revise: true, not with this tool."
3
3
  argument-hint: "<type> <id> [site] [name] [status] [fields] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_media`.
7
7
 
8
- Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. A published translation with no working copy is opened with drupal_create_translation and revise: true, not with this tool.
8
+ Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. A published translation is opened with drupal_create_translation and revise: true, not with this tool.
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 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. Omit langcode to continue the default language: when the working revision holds several languages and the default language is its unpublished draft, the connector names it for Sentinel (changing shared fields or paragraphs there needs MCP Sentinel 2.24.2). A language that is already published and has no working copy is not continued here: call drupal_create_translation with revise: true. 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. A published language with no working copy is not converted into a draft by this tool; use drupal_create_translation with revise: true. dryRun uses the same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran."
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. Omit langcode to continue the default language: when the working revision holds several languages and the default language is its unpublished draft, the connector names it for Sentinel (changing shared fields or paragraphs there needs MCP Sentinel 2.24.2). A language that is still published (on live, or on the working copy) is not continued here: call drupal_create_translation with revise: true. 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. A published language is not converted into a draft by this tool; use drupal_create_translation with revise: true. dryRun uses the same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran."
3
3
  argument-hint: "<type> <id> [site] [title] [body] [summary] [format] [status] [moderationState] [langcode] [fields] [relationships] [dryRun] [returning]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_node`.
7
7
 
8
- Update an existing node. Only include fields you want to change. For moderated content types, use moderationState (e.g. 'published') rather than status. When the target is published and moderated and you omit moderationState, the connector defaults the write to moderation_state 'draft' (forward revision) so live default revisions are not mutated by accident. Pass langcode to continue an unpublished working translation (Sentinel X-MCP-Draft-Langcode); this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. Omit langcode to continue the default language: when the working revision holds several languages and the default language is its unpublished draft, the connector names it for Sentinel (changing shared fields or paragraphs there needs MCP Sentinel 2.24.2). A language that is already published and has no working copy is not continued here: call drupal_create_translation with revise: true. 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. A published language with no working copy is not converted into a draft by this tool; use drupal_create_translation with revise: true. dryRun uses the same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.
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. Omit langcode to continue the default language: when the working revision holds several languages and the default language is its unpublished draft, the connector names it for Sentinel (changing shared fields or paragraphs there needs MCP Sentinel 2.24.2). A language that is still published (on live, or on the working copy) is not continued here: call drupal_create_translation with revise: true. 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. A published language is not converted into a draft by this tool; use drupal_create_translation with revise: true. dryRun uses the same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.23.3] - 2026-09-28
11
+
12
+ ### Fixed
13
+ - **Revise a published translation on a node that has a working copy.**
14
+ `drupal_create_translation` with `revise: true` used to refuse
15
+ client-side when the node had a working copy (for example an English draft)
16
+ and told the caller to continue it. Continuing cannot draft a language that
17
+ is still published on the working copy, so that language had no path to a
18
+ draft. The tool now sends both revision IDs (`If-Match: "live:working"`),
19
+ and Sentinel drafts the language on that working copy while the other
20
+ drafts carry forward. This needs MCP Sentinel 2.25.0 or later, detected by
21
+ `revise_over_working_copy` in the translation inventory. Older hosts are
22
+ refused before any write, and the message names the version.
23
+ - **Refusals no longer point at each other.** A language that is already a
24
+ draft on the working copy is sent to `drupal_update_node` with `langcode`.
25
+ `drupal_update_node` with `langcode` on a language that is still published
26
+ on the working copy is sent to `drupal_create_translation` with
27
+ `revise: true`.
28
+
10
29
  ## [2.23.2] - 2026-09-28
11
30
 
12
31
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.23.2",
3
+ "version": "2.23.3",
4
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",
@@ -29,6 +29,10 @@ export const REVISE_PUBLISHED_TRANSLATION = "revise_published_translation";
29
29
  * Older hosts keep the create 409 and have no path in between.
30
30
  */
31
31
  export const MIN_SENTINEL_REVISE_VERSION = "2.24.0";
32
+ /** Inventory operation: revise accepts a named working copy (`If-Match: "live:working"`). */
33
+ export const REVISE_OVER_WORKING_COPY = "revise_over_working_copy";
34
+ /** First MCP Sentinel release that revises a published translation over a working copy. */
35
+ export const MIN_SENTINEL_REVISE_OVER_WORKING_VERSION = "2.25.0";
32
36
  /** First Sentinel release that keeps a default-language draft out of translation-write rules (#379). */
33
37
  export const MIN_SENTINEL_DEFAULT_LANGUAGE_DRAFT_VERSION = "2.24.2";
34
38
 
@@ -42,6 +46,30 @@ export function supportsRevisePublishedTranslation(meta) {
42
46
  return Array.isArray(meta?.operations) && meta.operations.includes(REVISE_PUBLISHED_TRANSLATION);
43
47
  }
44
48
 
49
+ /**
50
+ * Whether a translation inventory advertises revise over a named working copy.
51
+ * Absent on Sentinel releases before 2.25.0.
52
+ * @param {object} [meta] Inventory `meta`, or an object with `operations`.
53
+ * @returns {boolean}
54
+ */
55
+ export function supportsReviseOverWorkingCopy(meta) {
56
+ return Array.isArray(meta?.operations) && meta.operations.includes(REVISE_OVER_WORKING_COPY);
57
+ }
58
+
59
+ /**
60
+ * Refusal when a working copy exists and the host cannot revise over it.
61
+ * Continuing is not offered: it cannot draft a language that is still
62
+ * published on the working copy.
63
+ * @returns {Error}
64
+ */
65
+ export function reviseOverWorkingCapabilityError() {
66
+ return new Error(
67
+ "This node has a working copy, and this Sentinel host cannot revise a published translation over it " +
68
+ `(missing ${REVISE_OVER_WORKING_COPY}). Update MCP Sentinel ${MIN_SENTINEL_REVISE_OVER_WORKING_VERSION} or later, ` +
69
+ "or publish or discard the working copy first. No write was attempted.",
70
+ );
71
+ }
72
+
45
73
  /**
46
74
  * Refusal when the host cannot revise a published translation.
47
75
  * @returns {Error}
@@ -502,6 +530,12 @@ export function assertInventoryDraftLanguage(inventory, langcode) {
502
530
  throw new Error("This working revision contains translations. Pass an explicit langcode for an existing unpublished language; no draft was created.");
503
531
  }
504
532
  const row = langcode ? rows.find((item) => item.langcode === langcode) : rows[0];
533
+ if (row && row.status === true && langcode && langcode !== inventory.defaultLangcode && row.default !== true) {
534
+ throw new Error(
535
+ "This language is still published on the working copy, so it cannot be continued. " +
536
+ "Open a draft of it with drupal_create_translation and revise: true. Published languages and other drafts were left unchanged.",
537
+ );
538
+ }
505
539
  if (!row || row.status !== false) {
506
540
  throw new Error("The requested language is not an existing unpublished working draft. Published languages and other drafts were left unchanged.");
507
541
  }
@@ -319,7 +319,7 @@ export const definitions = [
319
319
  },
320
320
  {
321
321
  name: "drupal_update_media",
322
- description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. A published translation with no working copy is opened with drupal_create_translation and revise: true, not with this tool.",
322
+ description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first. A published translation is opened with drupal_create_translation and revise: true, not with this tool.",
323
323
  inputSchema: {
324
324
  type: "object", required: ["type", "id"],
325
325
  properties: {
@@ -644,7 +644,7 @@ export const definitions = [
644
644
  },
645
645
  {
646
646
  name: "drupal_update_node",
647
- 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. Omit langcode to continue the default language: when the working revision holds several languages and the default language is its unpublished draft, the connector names it for Sentinel (changing shared fields or paragraphs there needs MCP Sentinel 2.24.2). A language that is already published and has no working copy is not continued here: call drupal_create_translation with revise: true. 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. A published language with no working copy is not converted into a draft by this tool; use drupal_create_translation with revise: true. dryRun uses the same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.",
647
+ 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. Omit langcode to continue the default language: when the working revision holds several languages and the default language is its unpublished draft, the connector names it for Sentinel (changing shared fields or paragraphs there needs MCP Sentinel 2.24.2). A language that is still published (on live, or on the working copy) is not continued here: call drupal_create_translation with revise: true. 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. A published language is not converted into a draft by this tool; use drupal_create_translation with revise: true. dryRun uses the same target. workingCopy:null from drupal_list_revisions is not proof the node is writable (possiblyPatchBlocked / #201). Preflight here does not un-orphan paragraphs already created; probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.",
648
648
  inputSchema: {
649
649
  type: "object", required: ["type", "id"],
650
650
  properties: {
@@ -37,10 +37,13 @@ import {
37
37
  explainExistingTranslation,
38
38
  isMissingTranslationEndpoint,
39
39
  MIN_SENTINEL_REVISE_VERSION,
40
+ MIN_SENTINEL_REVISE_OVER_WORKING_VERSION,
40
41
  readTranslationInventory,
41
42
  resolveNodeTranslationPair,
42
43
  reviseCapabilityError,
44
+ reviseOverWorkingCapabilityError,
43
45
  supportsRevisePublishedTranslation,
46
+ supportsReviseOverWorkingCopy,
44
47
  supportsSentinelDraft,
45
48
  } from "../lib/sentinel-draft.js";
46
49
  import { mapTranslationRow } from "../lib/translation-rows.js";
@@ -171,12 +174,23 @@ async function createTranslation({
171
174
  if (!supportsRevisePublishedTranslation(draftRevision)) {
172
175
  throw reviseCapabilityError();
173
176
  }
174
- if (draftRevision.workingVid) {
175
- const continuer = entityType === "media" ? "drupal_update_media" : "drupal_update_node";
176
- throw new Error(
177
- "A working copy already exists. Continue that draft with " +
178
- `${continuer} and langcode. Revise was not attempted.`,
179
- );
177
+ const hasWorking = draftRevision.workingVid && draftRevision.liveVid
178
+ && String(draftRevision.workingVid) !== String(draftRevision.liveVid);
179
+ if (hasWorking) {
180
+ const workingRow = (draftRevision.inventory?.working?.translations ?? [])
181
+ .find((row) => row?.langcode === targetLang);
182
+ if (workingRow && workingRow.status === false) {
183
+ const continuer = entityType === "media" ? "drupal_update_media" : "drupal_update_node";
184
+ throw new Error(
185
+ "This language is already an unpublished draft on the working copy. " +
186
+ `Continue it with ${continuer} and langcode. Revise was not attempted.`,
187
+ );
188
+ }
189
+ // Sentinel 2.25.0+ builds the draft on the named working copy; the
190
+ // pair is sent as If-Match "live:working" by createTranslationDraft.
191
+ if (!supportsReviseOverWorkingCopy(draftRevision)) {
192
+ throw reviseOverWorkingCapabilityError();
193
+ }
180
194
  }
181
195
  }
182
196
  if (entityType === "node") {
@@ -275,10 +289,14 @@ export const definitions = [
275
289
  "draft (#282). English live title, body, status, alias, default revision, and " +
276
290
  "paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. " +
277
291
  "Pass revise: true to open an unpublished draft over a language that is already published " +
278
- "on the live revision and has no working copy (Sentinel X-MCP-Draft-Mode: revise). " +
292
+ "on the live revision (Sentinel X-MCP-Draft-Mode: revise). " +
279
293
  `That requires MCP Sentinel ${MIN_SENTINEL_REVISE_VERSION} or later; an older host is refused ` +
280
- "before any write and the message names that version. dryRun uses the same translations " +
281
- "endpoint and Sentinel's non-saving preflight. A working copy is refused. " +
294
+ "before any write and the message names that version. When the node already has a working " +
295
+ "copy (for example an English draft), both revision IDs are sent and Sentinel drafts the " +
296
+ "language on that working copy; the other drafts carry forward. That needs MCP Sentinel " +
297
+ `${MIN_SENTINEL_REVISE_OVER_WORKING_VERSION} or later. A language that is already a draft on the working copy is ` +
298
+ "continued with drupal_update_node and langcode instead. dryRun uses the same translations " +
299
+ "endpoint and Sentinel's non-saving preflight. " +
282
300
  "Paragraph revise is not supported. " +
283
301
  "The response includes `_revisions.live` / `_revisions.working` when known. " +
284
302
  "Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. " +
@@ -300,7 +318,7 @@ export const definitions = [
300
318
  relationships: { type: "object", description: "JSON:API relationships. Use for image alt (same file UUID, meta.alt)." },
301
319
  revisionId: { type: "string", description: "Paragraph revision id the host already pins. Required for Home-shaped non-default pins." },
302
320
  dryRun: { type: "boolean", description: "Validate without saving. Sentinel's non-saving translation preflight receives the real fields, applies them through field access and validates the entity. The result's `checks` block says what was checked. With revise: true the preflight is the same translations endpoint." },
303
- revise: { type: "boolean", description: "Open an unpublished draft over a translation that is already published on the live default revision and has no working copy. Requires MCP Sentinel 2.24.0 or later. Omit it to create a language that does not exist yet." },
321
+ revise: { type: "boolean", description: "Open an unpublished draft over a translation that is already published on the live default revision. Requires MCP Sentinel 2.24.0 or later, and 2.25.0 or later when the node already has a working copy. Omit it to create a language that does not exist yet." },
304
322
  },
305
323
  },
306
324
  },