drupal-mcp-connector 2.16.0 → 2.18.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-create-translation.md +3 -3
- package/.agents/commands/drupal-get-media.md +4 -3
- package/.agents/commands/drupal-list-translations.md +3 -3
- package/.agents/commands/drupal-update-media.md +5 -4
- package/CHANGELOG.md +52 -0
- package/README.md +5 -0
- package/package.json +4 -1
- package/src/index.js +16 -8
- package/src/lib/backends/backend-interface.js +3 -0
- package/src/lib/backends/graphql.js +1 -0
- package/src/lib/backends/jsonapi.js +1 -0
- package/src/lib/canonical.js +10 -5
- package/src/lib/dispatch.js +10 -0
- package/src/lib/draft-write.js +18 -429
- package/src/lib/module-tools.js +241 -0
- package/src/lib/node-draft-inventory.js +4 -46
- package/src/lib/operations.js +5 -0
- package/src/lib/patch-preflight.js +7 -15
- package/src/lib/sentinel-draft.js +530 -0
- package/src/lib/server-tools.js +50 -16
- package/src/lib/verify.js +7 -1
- package/src/tools/config.js +14 -4
- package/src/tools/media.js +61 -8
- package/src/tools/moderation.js +21 -21
- package/src/tools/nodes.js +1 -1
- package/src/tools/paragraphs.js +1 -1
- package/src/tools/reports-config.js +11 -1
- package/src/tools/reports-content.js +7 -6
- package/src/tools/revisions.js +24 -23
- package/src/tools/structure.js +1 -1
- package/src/tools/taxonomy.js +1 -1
- package/src/tools/translations.js +31 -21
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* - drupal_create_translation — add a language as an unpublished forward draft
|
|
10
10
|
*
|
|
11
11
|
* Continuation of an existing unpublished translation is drupal_update_node
|
|
12
|
-
* with `langcode
|
|
12
|
+
* with `langcode` (media: drupal_update_media; paragraph: drupal_update_paragraph).
|
|
13
|
+
* Reads of that draft are drupal_get_node / drupal_get_media / drupal_get_paragraph
|
|
13
14
|
* with `langcode`.
|
|
14
15
|
*
|
|
15
16
|
* Paragraph field-value translation uses the same surface with
|
|
16
|
-
* entityType "paragraph" and the pinned paragraph revision.
|
|
17
|
-
* node
|
|
17
|
+
* entityType "paragraph" and the pinned paragraph revision. Media uses the
|
|
18
|
+
* node live/working pair (`entityType: "media"`). Image alt on a media entity
|
|
19
|
+
* is the file relationship (`field_media_image` + `meta.alt`) with the shared
|
|
20
|
+
* file target unchanged.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import { getSiteConfig } from "../lib/config.js";
|
|
@@ -31,9 +34,11 @@ import { paragraphRevisionId } from "../lib/err-relationships.js";
|
|
|
31
34
|
import {
|
|
32
35
|
assertDraftLangcode,
|
|
33
36
|
createTranslationDraft,
|
|
37
|
+
isMissingTranslationEndpoint,
|
|
34
38
|
readTranslationInventory,
|
|
35
39
|
resolveNodeTranslationPair,
|
|
36
|
-
|
|
40
|
+
supportsSentinelDraft,
|
|
41
|
+
} from "../lib/sentinel-draft.js";
|
|
37
42
|
import { mapTranslationRow } from "../lib/translation-rows.js";
|
|
38
43
|
|
|
39
44
|
const LIST_NOTE =
|
|
@@ -62,7 +67,8 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
62
67
|
assertReadAllowed(sec, entityType, type);
|
|
63
68
|
|
|
64
69
|
const backend = await resolveBackend(site);
|
|
65
|
-
if ((entityType === "node" || entityType === "paragraph"
|
|
70
|
+
if ((entityType === "node" || entityType === "paragraph" || entityType === "media")
|
|
71
|
+
&& supportsSentinelDraft(backend)) {
|
|
66
72
|
try {
|
|
67
73
|
const meta = await readTranslationInventory(backend, { entityType, bundle: type, id });
|
|
68
74
|
const liveLangs = (meta.live?.translations ?? []).map((row) => row.langcode);
|
|
@@ -80,7 +86,7 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
80
86
|
note: LIST_NOTE,
|
|
81
87
|
};
|
|
82
88
|
} catch (error) {
|
|
83
|
-
if (
|
|
89
|
+
if (!isMissingTranslationEndpoint(error)) {
|
|
84
90
|
throw error;
|
|
85
91
|
}
|
|
86
92
|
}
|
|
@@ -127,8 +133,8 @@ async function createTranslation({
|
|
|
127
133
|
const sec = resolveSecurityConfig(site);
|
|
128
134
|
assertWriteAllowed(sec, "update", entityType, type);
|
|
129
135
|
|
|
130
|
-
if (entityType !== "node" && entityType !== "paragraph") {
|
|
131
|
-
throw new Error("Governed translation create is implemented for nodes and
|
|
136
|
+
if (entityType !== "node" && entityType !== "paragraph" && entityType !== "media") {
|
|
137
|
+
throw new Error("Governed translation create is implemented for nodes, paragraphs, and media.");
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
const backend = await resolveBackend(site);
|
|
@@ -151,12 +157,14 @@ async function createTranslation({
|
|
|
151
157
|
draftRevision = await resolveNodeTranslationPair(backend, {
|
|
152
158
|
entityType, bundle: type, id, existing,
|
|
153
159
|
});
|
|
154
|
-
if (
|
|
155
|
-
drafted.moderation_state
|
|
160
|
+
if (entityType === "node") {
|
|
161
|
+
if (drafted.status === undefined && drafted.moderation_state === undefined) {
|
|
162
|
+
drafted.moderation_state = "draft";
|
|
163
|
+
}
|
|
164
|
+
drafted = await applySafeDraftDefault({
|
|
165
|
+
backend, entityType, bundle: type, id, attributes: drafted, existingEntity: existing,
|
|
166
|
+
});
|
|
156
167
|
}
|
|
157
|
-
drafted = await applySafeDraftDefault({
|
|
158
|
-
backend, entityType, bundle: type, id, attributes: drafted, existingEntity: existing,
|
|
159
|
-
});
|
|
160
168
|
assertPublishAllowed(sec, drafted);
|
|
161
169
|
}
|
|
162
170
|
|
|
@@ -174,7 +182,7 @@ async function createTranslation({
|
|
|
174
182
|
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, relationships, draftRevision,
|
|
175
183
|
});
|
|
176
184
|
const redacted = omitLiveComputedMetatag(redactCanonicalEntity(created, sec, entityType));
|
|
177
|
-
if (entityType
|
|
185
|
+
if (entityType === "paragraph") return redacted;
|
|
178
186
|
const workingVid = entityRevisionId(created) ?? draftRevision.workingVid;
|
|
179
187
|
const liveVid = draftRevision.liveVid;
|
|
180
188
|
if (liveVid === undefined || liveVid === null) {
|
|
@@ -193,7 +201,7 @@ export const definitions = [
|
|
|
193
201
|
{
|
|
194
202
|
name: "drupal_list_translations",
|
|
195
203
|
description:
|
|
196
|
-
"List live and working translation langcodes for a Drupal node or
|
|
204
|
+
"List live and working translation langcodes for a Drupal node, paragraph, or media entity. Uses Sentinel's " +
|
|
197
205
|
"translation inventory when available (live default revision vs unpublished working " +
|
|
198
206
|
"draft), including core content_translation_outdated and source when the server sends them. " +
|
|
199
207
|
"Core JSON:API alone serves one language and cannot prove others are absent. " +
|
|
@@ -202,7 +210,7 @@ export const definitions = [
|
|
|
202
210
|
type: "object", required: ["type", "id"],
|
|
203
211
|
properties: {
|
|
204
212
|
site: { type: "string", description: "Named site (omit for default)" },
|
|
205
|
-
entityType: { type: "string", description: "Entity type machine name. Default: 'node'." },
|
|
213
|
+
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' or 'media' when listing those." },
|
|
206
214
|
type: { type: "string", description: "Bundle machine name, e.g. 'basic_page'" },
|
|
207
215
|
id: { type: "string", description: "Entity UUID" },
|
|
208
216
|
},
|
|
@@ -219,15 +227,17 @@ export const definitions = [
|
|
|
219
227
|
"paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. " +
|
|
220
228
|
"The response includes `_revisions.live` / `_revisions.working` when known. " +
|
|
221
229
|
"Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. " +
|
|
222
|
-
"Continue a node draft with drupal_update_node and langcode;
|
|
223
|
-
"drupal_update_paragraph and langcode
|
|
224
|
-
"
|
|
225
|
-
"
|
|
230
|
+
"Continue a node draft with drupal_update_node and langcode; a paragraph with " +
|
|
231
|
+
"drupal_update_paragraph and langcode; media with drupal_update_media and langcode. " +
|
|
232
|
+
"Image alt is a relationship (same file UUID, meta.alt) — on media that is " +
|
|
233
|
+
"field_media_image, not a node image field. For paragraphs pass revisionId as the " +
|
|
234
|
+
"host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied " +
|
|
235
|
+
"for content-tier callers.",
|
|
226
236
|
inputSchema: {
|
|
227
237
|
type: "object", required: ["type", "id", "langcode"],
|
|
228
238
|
properties: {
|
|
229
239
|
site: { type: "string" },
|
|
230
|
-
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values." },
|
|
240
|
+
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values, 'media' for media (name, caption, image alt)." },
|
|
231
241
|
type: { type: "string", description: "Bundle machine name, e.g. 'basic_page' or 'p_hero'" },
|
|
232
242
|
id: { type: "string", description: "Entity UUID" },
|
|
233
243
|
langcode: { type: "string", description: "Target language code, e.g. 'es', 'de', 'pt-br'" },
|