drupal-mcp-connector 2.15.2 → 2.17.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-create-translation.md +3 -3
- package/.agents/commands/drupal-describe-fields.md +2 -2
- package/.agents/commands/drupal-get-media.md +4 -3
- package/.agents/commands/drupal-get-node.md +4 -3
- package/.agents/commands/drupal-get-taxonomy-term.md +4 -3
- package/.agents/commands/drupal-list-translations.md +3 -3
- 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-media.md +5 -4
- package/.agents/commands/drupal-update-menu-link.md +2 -1
- package/.agents/commands/drupal-update-taxonomy-term.md +4 -3
- package/CHANGELOG.md +57 -0
- package/README.md +3 -3
- package/bin/drupal-mcp-agent.js +2 -2
- package/package.json +3 -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/canonical.js +10 -5
- package/src/lib/config.js +18 -1
- package/src/lib/draft-write.js +5 -5
- package/src/lib/err-relationships.js +22 -0
- package/src/lib/mcp-server.js +1 -1
- package/src/lib/node-draft-inventory.js +3 -2
- package/src/lib/server-tools.js +2 -2
- package/src/lib/translation-rows.js +59 -0
- package/src/tools/codegen.js +150 -0
- package/src/tools/fields.js +12 -4
- package/src/tools/index.js +2 -1
- package/src/tools/media.js +65 -8
- package/src/tools/moderation.js +82 -13
- package/src/tools/nodes.js +56 -6
- package/src/tools/reports-content.js +117 -28
- package/src/tools/structure.js +6 -3
- package/src/tools/taxonomy.js +15 -6
- package/src/tools/translations.js +31 -27
package/src/tools/taxonomy.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolveSecurityConfig, redactCanonicalEntity,
|
|
13
13
|
assertReadAllowed, assertWriteAllowed, assertDeleteAllowed,
|
|
14
14
|
} from "../lib/security.js";
|
|
15
|
+
import { assertDraftLangcode } from "../lib/draft-write.js";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Implementations
|
|
@@ -58,12 +59,15 @@ async function getTaxonomyTerms({ site: siteName, vocabulary, limit = 50, offset
|
|
|
58
59
|
* @param {object} args - { site?, vocabulary, id }.
|
|
59
60
|
* @returns {Promise<object|null>} The redacted term, or null if not found.
|
|
60
61
|
*/
|
|
61
|
-
async function getTaxonomyTerm({ site: siteName, vocabulary, id }) {
|
|
62
|
+
async function getTaxonomyTerm({ site: siteName, vocabulary, id, langcode }) {
|
|
62
63
|
const site = getSiteConfig(siteName);
|
|
63
64
|
const sec = resolveSecurityConfig(site);
|
|
64
65
|
assertReadAllowed(sec, "taxonomy_term", vocabulary);
|
|
65
66
|
const backend = await resolveBackend(site);
|
|
66
|
-
const entity = await backend.getEntity({
|
|
67
|
+
const entity = await backend.getEntity({
|
|
68
|
+
entityType: "taxonomy_term", bundle: vocabulary, id,
|
|
69
|
+
...(langcode ? { langcode: assertDraftLangcode(langcode) } : {}),
|
|
70
|
+
});
|
|
67
71
|
return entity ? redactCanonicalEntity(entity, sec, "taxonomy_term") : null;
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -93,7 +97,7 @@ async function createTaxonomyTerm({ site: siteName, vocabulary, name, descriptio
|
|
|
93
97
|
* @param {object} args - { site?, vocabulary, id, name?, description?, weight? }.
|
|
94
98
|
* @returns {Promise<object>} The updated term descriptor.
|
|
95
99
|
*/
|
|
96
|
-
async function updateTaxonomyTerm({ site: siteName, vocabulary, id, name, description, weight }) {
|
|
100
|
+
async function updateTaxonomyTerm({ site: siteName, vocabulary, id, name, description, weight, langcode }) {
|
|
97
101
|
const site = getSiteConfig(siteName);
|
|
98
102
|
assertWriteAllowed(resolveSecurityConfig(site), "update", "taxonomy_term", vocabulary);
|
|
99
103
|
const backend = await resolveBackend(site);
|
|
@@ -101,7 +105,10 @@ async function updateTaxonomyTerm({ site: siteName, vocabulary, id, name, descri
|
|
|
101
105
|
if (name !== undefined) attributes.name = name;
|
|
102
106
|
if (weight !== undefined) attributes.weight = weight;
|
|
103
107
|
if (description !== undefined) attributes.description = { value: description, format: "plain_text" };
|
|
104
|
-
return backend.updateEntity({
|
|
108
|
+
return backend.updateEntity({
|
|
109
|
+
entityType: "taxonomy_term", bundle: vocabulary, id, attributes,
|
|
110
|
+
...(langcode ? { langcode: assertDraftLangcode(langcode) } : {}),
|
|
111
|
+
});
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
/**
|
|
@@ -147,13 +154,14 @@ export const definitions = [
|
|
|
147
154
|
},
|
|
148
155
|
{
|
|
149
156
|
name: "drupal_get_taxonomy_term",
|
|
150
|
-
description: "Fetch a single taxonomy term by UUID.",
|
|
157
|
+
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.",
|
|
151
158
|
inputSchema: {
|
|
152
159
|
type: "object", required: ["vocabulary", "id"],
|
|
153
160
|
properties: {
|
|
154
161
|
site: { type: "string" },
|
|
155
162
|
vocabulary: { type: "string" },
|
|
156
163
|
id: { type: "string", description: "Term UUID" },
|
|
164
|
+
langcode: { type: "string", description: "Translation language (e.g. 'es'). Omit for the default language." },
|
|
157
165
|
},
|
|
158
166
|
},
|
|
159
167
|
},
|
|
@@ -174,7 +182,7 @@ export const definitions = [
|
|
|
174
182
|
},
|
|
175
183
|
{
|
|
176
184
|
name: "drupal_update_taxonomy_term",
|
|
177
|
-
description: "Update an existing taxonomy term's name, description, or weight.",
|
|
185
|
+
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.",
|
|
178
186
|
inputSchema: {
|
|
179
187
|
type: "object", required: ["vocabulary", "id"],
|
|
180
188
|
properties: {
|
|
@@ -184,6 +192,7 @@ export const definitions = [
|
|
|
184
192
|
name: { type: "string" },
|
|
185
193
|
description: { type: "string" },
|
|
186
194
|
weight: { type: "number" },
|
|
195
|
+
langcode: { type: "string", description: "Existing translation to update (e.g. 'es'). Omit for the default language." },
|
|
187
196
|
},
|
|
188
197
|
},
|
|
189
198
|
},
|
|
@@ -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";
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
readTranslationInventory,
|
|
35
38
|
resolveNodeTranslationPair,
|
|
36
39
|
} from "../lib/draft-write.js";
|
|
40
|
+
import { mapTranslationRow } from "../lib/translation-rows.js";
|
|
37
41
|
|
|
38
42
|
const LIST_NOTE =
|
|
39
43
|
"Live languages are those on the default revision. Working languages are the " +
|
|
@@ -61,7 +65,8 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
61
65
|
assertReadAllowed(sec, entityType, type);
|
|
62
66
|
|
|
63
67
|
const backend = await resolveBackend(site);
|
|
64
|
-
if ((entityType === "node" || entityType === "paragraph"
|
|
68
|
+
if ((entityType === "node" || entityType === "paragraph" || entityType === "media")
|
|
69
|
+
&& typeof backend.rawQuery === "function") {
|
|
65
70
|
try {
|
|
66
71
|
const meta = await readTranslationInventory(backend, { entityType, bundle: type, id });
|
|
67
72
|
const liveLangs = (meta.live?.translations ?? []).map((row) => row.langcode);
|
|
@@ -75,13 +80,7 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
75
80
|
langcodes,
|
|
76
81
|
live: meta.live ?? null,
|
|
77
82
|
working: meta.working ?? null,
|
|
78
|
-
translations: (meta.working?.translations ?? meta.live?.translations ?? []).map(
|
|
79
|
-
langcode: row.langcode,
|
|
80
|
-
default: Boolean(row.default),
|
|
81
|
-
status: row.status,
|
|
82
|
-
title: row.title,
|
|
83
|
-
moderation_state: row.moderation_state,
|
|
84
|
-
})),
|
|
83
|
+
translations: (meta.working?.translations ?? meta.live?.translations ?? []).map(mapTranslationRow),
|
|
85
84
|
note: LIST_NOTE,
|
|
86
85
|
};
|
|
87
86
|
} catch (error) {
|
|
@@ -132,8 +131,8 @@ async function createTranslation({
|
|
|
132
131
|
const sec = resolveSecurityConfig(site);
|
|
133
132
|
assertWriteAllowed(sec, "update", entityType, type);
|
|
134
133
|
|
|
135
|
-
if (entityType !== "node" && entityType !== "paragraph") {
|
|
136
|
-
throw new Error("Governed translation create is implemented for nodes and
|
|
134
|
+
if (entityType !== "node" && entityType !== "paragraph" && entityType !== "media") {
|
|
135
|
+
throw new Error("Governed translation create is implemented for nodes, paragraphs, and media.");
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
const backend = await resolveBackend(site);
|
|
@@ -156,12 +155,14 @@ async function createTranslation({
|
|
|
156
155
|
draftRevision = await resolveNodeTranslationPair(backend, {
|
|
157
156
|
entityType, bundle: type, id, existing,
|
|
158
157
|
});
|
|
159
|
-
if (
|
|
160
|
-
drafted.moderation_state
|
|
158
|
+
if (entityType === "node") {
|
|
159
|
+
if (drafted.status === undefined && drafted.moderation_state === undefined) {
|
|
160
|
+
drafted.moderation_state = "draft";
|
|
161
|
+
}
|
|
162
|
+
drafted = await applySafeDraftDefault({
|
|
163
|
+
backend, entityType, bundle: type, id, attributes: drafted, existingEntity: existing,
|
|
164
|
+
});
|
|
161
165
|
}
|
|
162
|
-
drafted = await applySafeDraftDefault({
|
|
163
|
-
backend, entityType, bundle: type, id, attributes: drafted, existingEntity: existing,
|
|
164
|
-
});
|
|
165
166
|
assertPublishAllowed(sec, drafted);
|
|
166
167
|
}
|
|
167
168
|
|
|
@@ -179,7 +180,7 @@ async function createTranslation({
|
|
|
179
180
|
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, relationships, draftRevision,
|
|
180
181
|
});
|
|
181
182
|
const redacted = omitLiveComputedMetatag(redactCanonicalEntity(created, sec, entityType));
|
|
182
|
-
if (entityType
|
|
183
|
+
if (entityType === "paragraph") return redacted;
|
|
183
184
|
const workingVid = entityRevisionId(created) ?? draftRevision.workingVid;
|
|
184
185
|
const liveVid = draftRevision.liveVid;
|
|
185
186
|
if (liveVid === undefined || liveVid === null) {
|
|
@@ -198,15 +199,16 @@ export const definitions = [
|
|
|
198
199
|
{
|
|
199
200
|
name: "drupal_list_translations",
|
|
200
201
|
description:
|
|
201
|
-
"List live and working translation langcodes for a Drupal node or
|
|
202
|
+
"List live and working translation langcodes for a Drupal node, paragraph, or media entity. Uses Sentinel's " +
|
|
202
203
|
"translation inventory when available (live default revision vs unpublished working " +
|
|
203
|
-
"draft)
|
|
204
|
+
"draft), including core content_translation_outdated and source when the server sends them. " +
|
|
205
|
+
"Core JSON:API alone serves one language and cannot prove others are absent. " +
|
|
204
206
|
"Defaults to node.",
|
|
205
207
|
inputSchema: {
|
|
206
208
|
type: "object", required: ["type", "id"],
|
|
207
209
|
properties: {
|
|
208
210
|
site: { type: "string", description: "Named site (omit for default)" },
|
|
209
|
-
entityType: { type: "string", description: "Entity type machine name. Default: 'node'." },
|
|
211
|
+
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' or 'media' when listing those." },
|
|
210
212
|
type: { type: "string", description: "Bundle machine name, e.g. 'basic_page'" },
|
|
211
213
|
id: { type: "string", description: "Entity UUID" },
|
|
212
214
|
},
|
|
@@ -223,15 +225,17 @@ export const definitions = [
|
|
|
223
225
|
"paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. " +
|
|
224
226
|
"The response includes `_revisions.live` / `_revisions.working` when known. " +
|
|
225
227
|
"Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. " +
|
|
226
|
-
"Continue a node draft with drupal_update_node and langcode;
|
|
227
|
-
"drupal_update_paragraph and langcode
|
|
228
|
-
"
|
|
229
|
-
"
|
|
228
|
+
"Continue a node draft with drupal_update_node and langcode; a paragraph with " +
|
|
229
|
+
"drupal_update_paragraph and langcode; media with drupal_update_media and langcode. " +
|
|
230
|
+
"Image alt is a relationship (same file UUID, meta.alt) — on media that is " +
|
|
231
|
+
"field_media_image, not a node image field. For paragraphs pass revisionId as the " +
|
|
232
|
+
"host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied " +
|
|
233
|
+
"for content-tier callers.",
|
|
230
234
|
inputSchema: {
|
|
231
235
|
type: "object", required: ["type", "id", "langcode"],
|
|
232
236
|
properties: {
|
|
233
237
|
site: { type: "string" },
|
|
234
|
-
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values." },
|
|
238
|
+
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values, 'media' for media (name, caption, image alt)." },
|
|
235
239
|
type: { type: "string", description: "Bundle machine name, e.g. 'basic_page' or 'p_hero'" },
|
|
236
240
|
id: { type: "string", description: "Entity UUID" },
|
|
237
241
|
langcode: { type: "string", description: "Target language code, e.g. 'es', 'de', 'pt-br'" },
|