drupal-mcp-connector 2.24.2 → 2.25.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,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "List custom (content) menu links, optionally scoped to a single menu (e.g. 'main', 'footer'). Returns each link's title, target URI, menu, and
|
|
2
|
+
description: "List custom (content) menu links, optionally scoped to a single menu (e.g. 'main', 'footer'). Returns each link's title, target URI, menu, weight, and fields.drupal_internal__id (numeric id for /admin/structure/menu/item/{id}/edit). Note: this lists editable menu_link_content entities, not code-defined static links."
|
|
3
3
|
argument-hint: "[site] [menu] [limit] [offset] [sort]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_list_menu_links`.
|
|
7
7
|
|
|
8
|
-
List custom (content) menu links, optionally scoped to a single menu (e.g. 'main', 'footer'). Returns each link's title, target URI, menu, and
|
|
8
|
+
List custom (content) menu links, optionally scoped to a single menu (e.g. 'main', 'footer'). Returns each link's title, target URI, menu, weight, and fields.drupal_internal__id (numeric id for /admin/structure/menu/item/{id}/edit). Note: this lists editable menu_link_content entities, not code-defined static links.
|
|
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 a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Checked against the site security config."
|
|
2
|
+
description: "Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Pass langcode to update an existing translation via JSON:API language negotiation (URL prefix, then language headers). Does not create a missing translation. Responses include fields.drupal_internal__id. Checked against the site security config."
|
|
3
3
|
argument-hint: "<id> [site] [title] [link] [menu] [weight] [parent] [enabled] [langcode]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_update_menu_link`.
|
|
7
7
|
|
|
8
|
-
Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Checked against the site security config.
|
|
8
|
+
Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Pass langcode to update an existing translation via JSON:API language negotiation (URL prefix, then language headers). Does not create a missing translation. Responses include fields.drupal_internal__id. Checked against the site security config.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -20,6 +20,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
20
20
|
- `weight` (number): New ordering weight. Omit to leave unchanged.
|
|
21
21
|
- `parent` (string): New parent link plugin id (e.g. 'menu_link_content:<uuid>'), or '' for top level. Omit to leave unchanged.
|
|
22
22
|
- `enabled` (boolean (true/false)): Enable/disable the link. Omit to preserve the current state.
|
|
23
|
-
- `langcode` (string): Existing translation to update (e.g. 'es'). Omit for the default language. Does not create a missing translation.
|
|
23
|
+
- `langcode` (string): Existing translation to update (e.g. 'es'). Uses /{langcode}/jsonapi/… when the site prefixes JSON:API, then language headers. Omit for the default language. Does not create a missing translation.
|
|
24
24
|
|
|
25
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.
|
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.25.0] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Menu tools return `fields.drupal_internal__id`.** The numeric menu-link
|
|
14
|
+
id is kept on JSON:API canonicalization so list/create/update responses
|
|
15
|
+
can build `/admin/structure/menu/item/{id}/edit` (and `…/edit/{langcode}`).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`drupal_update_menu_link` with `langcode` targets the translation via a JSON:API URL prefix (#398).**
|
|
19
|
+
Stock Drupal negotiates content language from `/{langcode}/jsonapi/…` (the
|
|
20
|
+
same path that returns Spanish for nodes). The connector only sent
|
|
21
|
+
`Accept-Language` / `Content-Language` headers, which most production sites
|
|
22
|
+
ignore unless Browser detection is enabled — so a Spanish menu-link
|
|
23
|
+
translation was served as English and the write refused. Reads and writes
|
|
24
|
+
with `langcode` now try the prefixed path first and fall back to the
|
|
25
|
+
unprefixed path plus headers when the prefix 404s. Taxonomy get/update
|
|
26
|
+
with `langcode` use the same backend path. A mismatch after both attempts
|
|
27
|
+
still fails loud.
|
|
28
|
+
|
|
10
29
|
## [2.24.2] - 2026-09-30
|
|
11
30
|
|
|
12
31
|
### Removed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
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",
|
|
@@ -11,6 +11,7 @@ import { drupalFetch, drupalUploadFile } from "../drupal-fetch.js";
|
|
|
11
11
|
import { getRequestIdentity } from "../principal.js";
|
|
12
12
|
import { validateUuid, validateMachineName } from "../validate.js";
|
|
13
13
|
import { parseFieldConfigObject } from "../field-definition.js";
|
|
14
|
+
import { httpStatusOf } from "../error-status.js";
|
|
14
15
|
import { Backend } from "./backend-interface.js";
|
|
15
16
|
import {
|
|
16
17
|
makeCanonicalEntity,
|
|
@@ -33,8 +34,9 @@ const COUNT_PAGE_SIZE = 50;
|
|
|
33
34
|
const COUNT_MAX_RECORDS = 1000;
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* JSON:API language headers. Drupal
|
|
37
|
-
*
|
|
37
|
+
* JSON:API language headers. Stock Drupal ignores these unless Browser
|
|
38
|
+
* (Accept-Language) detection is enabled for content language. Production
|
|
39
|
+
* sites usually negotiate via URL prefixes instead — see jsonApiLanguagePath.
|
|
38
40
|
* @param {?string} langcode
|
|
39
41
|
* @returns {object}
|
|
40
42
|
*/
|
|
@@ -43,6 +45,59 @@ function languageFetchOptions(langcode) {
|
|
|
43
45
|
return { headers: { "Content-Language": langcode, "Accept-Language": langcode } };
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Prefix a JSON:API path with the language code stock Drupal URL negotiation
|
|
50
|
+
* expects (`/es/jsonapi/...`). Does not add `?langCode=` — core JSON:API
|
|
51
|
+
* rejects unknown query parameters unless jsonapi_multilingual is installed.
|
|
52
|
+
* @param {string} path JSON:API path, optionally with a query string.
|
|
53
|
+
* @param {?string} langcode
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function jsonApiLanguagePath(path, langcode) {
|
|
57
|
+
if (!langcode) return path;
|
|
58
|
+
const prefix = `/${encodeURIComponent(langcode)}`;
|
|
59
|
+
if (path === prefix || path.startsWith(`${prefix}/`)) return path;
|
|
60
|
+
return `${prefix}${path}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Merge language headers onto a drupalFetch options object.
|
|
65
|
+
* @param {object} [options]
|
|
66
|
+
* @param {?string} langcode
|
|
67
|
+
* @returns {object}
|
|
68
|
+
*/
|
|
69
|
+
function languageRequestOptions(options, langcode) {
|
|
70
|
+
const lang = languageFetchOptions(langcode);
|
|
71
|
+
if (!lang.headers) return { ...options };
|
|
72
|
+
return {
|
|
73
|
+
...options,
|
|
74
|
+
headers: { ...(options?.headers || {}), ...lang.headers },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetch a JSON:API resource in a requested language.
|
|
80
|
+
*
|
|
81
|
+
* Prefer the URL prefix (how `/es/jsonapi/node/...` works on multilingual
|
|
82
|
+
* Drupal). If that 404s — the site has no language prefixes — retry the
|
|
83
|
+
* unprefixed path with language headers only (#398 / #303).
|
|
84
|
+
* @param {object} site Resolved site config.
|
|
85
|
+
* @param {string} path Unprefixed JSON:API path.
|
|
86
|
+
* @param {object} [options] node-fetch options (method, body, extra headers).
|
|
87
|
+
* @param {?string} langcode
|
|
88
|
+
* @returns {Promise<object|null>}
|
|
89
|
+
*/
|
|
90
|
+
async function fetchJsonApiWithLanguage(site, path, options = {}, langcode) {
|
|
91
|
+
const opts = languageRequestOptions(options, langcode);
|
|
92
|
+
if (!langcode) return drupalFetch(site, path, opts);
|
|
93
|
+
try {
|
|
94
|
+
return await drupalFetch(site, jsonApiLanguagePath(path, langcode), opts);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (httpStatusOf(err) !== 404) throw err;
|
|
97
|
+
return drupalFetch(site, path, opts);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
46
101
|
/**
|
|
47
102
|
* Fail if JSON:API served a different language than requested.
|
|
48
103
|
* @param {?object} entity
|
|
@@ -59,7 +114,8 @@ function assertServedLanguage(entity, langcode) {
|
|
|
59
114
|
if (entity.langcode !== langcode) {
|
|
60
115
|
throw new Error(
|
|
61
116
|
`JSON:API served language "${entity.langcode}" for requested langcode "${langcode}". ` +
|
|
62
|
-
"The translation may not exist, or this site does not negotiate JSON:API by language
|
|
117
|
+
"The translation may not exist, or this site does not negotiate JSON:API by language " +
|
|
118
|
+
`(URL prefix /${langcode}/jsonapi/… or Accept-Language).`,
|
|
63
119
|
);
|
|
64
120
|
}
|
|
65
121
|
}
|
|
@@ -291,10 +347,12 @@ export class JsonApiBackend extends Backend {
|
|
|
291
347
|
// while the UUID remains the canonical top-level id (#237).
|
|
292
348
|
// Paragraph ERR attach needs the current revision id (#192).
|
|
293
349
|
// Node / revisionable writes need the working vs live vid (#166).
|
|
350
|
+
// Menu-link admin edit URLs need the numeric id (#398).
|
|
294
351
|
// Other drupal_internal__* attributes stay stripped.
|
|
295
352
|
return (entityType === "node" && k === "drupal_internal__nid")
|
|
296
353
|
|| k === "drupal_internal__vid"
|
|
297
|
-
|| (entityType === "paragraph" && k === "drupal_internal__revision_id")
|
|
354
|
+
|| (entityType === "paragraph" && k === "drupal_internal__revision_id")
|
|
355
|
+
|| (entityType === "menu_link_content" && k === "drupal_internal__id");
|
|
298
356
|
}
|
|
299
357
|
return true;
|
|
300
358
|
})
|
|
@@ -397,7 +455,7 @@ export class JsonApiBackend extends Backend {
|
|
|
397
455
|
if (resourceVersion) {
|
|
398
456
|
path += `?resourceVersion=${encodeURIComponent(resourceVersion)}`;
|
|
399
457
|
}
|
|
400
|
-
const data = await
|
|
458
|
+
const data = await fetchJsonApiWithLanguage(this.site, path, {}, langcode);
|
|
401
459
|
const entity = data?.data ? this.toCanonical(data.data) : null;
|
|
402
460
|
assertServedLanguage(entity, langcode);
|
|
403
461
|
return entity;
|
|
@@ -514,14 +572,19 @@ export class JsonApiBackend extends Backend {
|
|
|
514
572
|
* @returns {Promise<object>} The JSON:API response body.
|
|
515
573
|
*/
|
|
516
574
|
async writeWithModerationFallback(path, method, buildPayload, attributes, langcode) {
|
|
517
|
-
const
|
|
575
|
+
const send = (attrs) => fetchJsonApiWithLanguage(
|
|
576
|
+
this.site,
|
|
577
|
+
path,
|
|
578
|
+
{ method, body: JSON.stringify(buildPayload(attrs)) },
|
|
579
|
+
langcode,
|
|
580
|
+
);
|
|
518
581
|
try {
|
|
519
|
-
return await
|
|
582
|
+
return await send(attributes);
|
|
520
583
|
} catch (err) {
|
|
521
584
|
if (!isModeratedStatusError(err) || !("status" in attributes)) throw err;
|
|
522
585
|
const withoutStatus = { ...attributes };
|
|
523
586
|
delete withoutStatus.status;
|
|
524
|
-
return
|
|
587
|
+
return send(withoutStatus);
|
|
525
588
|
}
|
|
526
589
|
}
|
|
527
590
|
|
package/src/tools/structure.js
CHANGED
|
@@ -156,7 +156,7 @@ async function createMenuLink({ site: siteName, title, link, menu, weight, paren
|
|
|
156
156
|
* site — so an unrelated edit (rename, re-weight, re-parent) can never silently
|
|
157
157
|
* regress a live link to disabled. Set `parent` to re-nest a link.
|
|
158
158
|
*
|
|
159
|
-
* @param {object} args - { site?, id, title?, link?, menu?, weight?, parent?, enabled? }.
|
|
159
|
+
* @param {object} args - { site?, id, title?, link?, menu?, weight?, parent?, enabled?, langcode? }.
|
|
160
160
|
* @returns {Promise<object>} The updated menu-link descriptor from the backend.
|
|
161
161
|
* @throws {Error} If id is missing.
|
|
162
162
|
* @throws {SecurityError} If updating menu_link_content is not permitted.
|
|
@@ -243,7 +243,7 @@ async function createBlock({ site: siteName, type, info, body }) {
|
|
|
243
243
|
export const definitions = [
|
|
244
244
|
{
|
|
245
245
|
name: "drupal_list_menu_links",
|
|
246
|
-
description: "List custom (content) menu links, optionally scoped to a single menu (e.g. 'main', 'footer'). Returns each link's title, target URI, menu, and
|
|
246
|
+
description: "List custom (content) menu links, optionally scoped to a single menu (e.g. 'main', 'footer'). Returns each link's title, target URI, menu, weight, and fields.drupal_internal__id (numeric id for /admin/structure/menu/item/{id}/edit). Note: this lists editable menu_link_content entities, not code-defined static links.",
|
|
247
247
|
inputSchema: {
|
|
248
248
|
type: "object",
|
|
249
249
|
properties: {
|
|
@@ -273,7 +273,7 @@ export const definitions = [
|
|
|
273
273
|
},
|
|
274
274
|
{
|
|
275
275
|
name: "drupal_update_menu_link",
|
|
276
|
-
description: "Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Checked against the site security config.",
|
|
276
|
+
description: "Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Pass langcode to update an existing translation via JSON:API language negotiation (URL prefix, then language headers). Does not create a missing translation. Responses include fields.drupal_internal__id. Checked against the site security config.",
|
|
277
277
|
inputSchema: {
|
|
278
278
|
type: "object", required: ["id"],
|
|
279
279
|
properties: {
|
|
@@ -285,7 +285,7 @@ export const definitions = [
|
|
|
285
285
|
weight: { type: "number", description: "New ordering weight. Omit to leave unchanged." },
|
|
286
286
|
parent: { type: "string", description: "New parent link plugin id (e.g. 'menu_link_content:<uuid>'), or '' for top level. Omit to leave unchanged." },
|
|
287
287
|
enabled: { type: "boolean", description: "Enable/disable the link. Omit to preserve the current state." },
|
|
288
|
-
langcode: { type: "string", description: "Existing translation to update (e.g. 'es'). Omit for the default language. Does not create a missing translation." },
|
|
288
|
+
langcode: { type: "string", description: "Existing translation to update (e.g. 'es'). Uses /{langcode}/jsonapi/… when the site prefixes JSON:API, then language headers. Omit for the default language. Does not create a missing translation." },
|
|
289
289
|
},
|
|
290
290
|
},
|
|
291
291
|
},
|