drupal-mcp-connector 2.12.0 → 2.13.1
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-entity-update.md +3 -3
- package/.agents/commands/drupal-update-node.md +3 -3
- package/CHANGELOG.md +52 -12
- package/bin/drupal-mcp-edge.js +55 -2
- package/config/config.example.json +3 -0
- package/package.json +1 -1
- package/src/lib/backends/backend-interface.js +1 -1
- package/src/lib/backends/jsonapi.js +2 -2
- package/src/lib/config.js +24 -0
- package/src/lib/draft-write.js +57 -0
- package/src/lib/patch-preflight.js +18 -7
- package/src/lib/policy-enforcement.js +1 -1
- package/src/lib/policy-promotion.js +1 -1
- package/src/lib/relay/agent.js +1 -1
- package/src/lib/relay/edge.js +315 -32
- package/src/lib/relay/frames.js +1 -1
- package/src/lib/usage.js +610 -0
- package/src/tools/bulk.js +2 -1
- package/src/tools/entities.js +4 -3
- package/src/tools/nodes.js +10 -10
package/src/tools/nodes.js
CHANGED
|
@@ -44,11 +44,11 @@ function normalizeAlias(value) {
|
|
|
44
44
|
* Resolve the `path` attribute to send on an alias-aware node write so the alias
|
|
45
45
|
* actually persists, and decide whether a rename redirect is needed.
|
|
46
46
|
*
|
|
47
|
-
* The bug (
|
|
47
|
+
* The bug (fixed in 1.5.1): JSON:API deserializes `{ alias, pathauto }` onto the node's
|
|
48
48
|
* `path` field, dropping the existing alias's `pid`; Drupal's `PathItem::postSave`
|
|
49
49
|
* then *creates a duplicate* `path_alias` (the older one stays canonical) instead
|
|
50
50
|
* of updating in place. The fix is to round-trip the existing `pid` so the update
|
|
51
|
-
* is in place.
|
|
51
|
+
* is in place. The 1.5.0 path-omitted "preserve" had the same defect (no `pid`),
|
|
52
52
|
* so it is fixed here too.
|
|
53
53
|
*
|
|
54
54
|
* @param {object} args - { backend, type, id, providedPath, isCreate }.
|
|
@@ -274,7 +274,7 @@ async function createNode({ site: siteName, type, title, body, summary, format,
|
|
|
274
274
|
return summaryWrite.deprecated && bodyAttr ? attachSummaryDeprecation(preview) : preview;
|
|
275
275
|
}
|
|
276
276
|
// Alias handling: an explicit `path.alias` is set as a manual alias; otherwise
|
|
277
|
-
// `path` is omitted so pathauto generates the alias (
|
|
277
|
+
// `path` is omitted so pathauto generates the alias (see the 1.5.1 alias fix).
|
|
278
278
|
const { pathAttr } = await resolvePathWrite({ backend, type, id: null, providedPath: attributes.path, isCreate: true });
|
|
279
279
|
if (pathAttr === undefined) delete attributes.path;
|
|
280
280
|
else attributes.path = pathAttr;
|
|
@@ -344,11 +344,10 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
344
344
|
// #192: resolve paragraph ERR identifiers before any host PATCH. An unresolved
|
|
345
345
|
// list would persist empty — fail the whole write instead.
|
|
346
346
|
const resolvedRelationships = await resolveErrRelationships(backend, relationships);
|
|
347
|
-
// #201 / #166: probe the
|
|
348
|
-
//
|
|
349
|
-
// fails the probe with revision-surgery language.
|
|
347
|
+
// #201 / #166: probe the write endpoint. Addressable drafts use Sentinel's
|
|
348
|
+
// governed continuation endpoint; stray revisions still fail the probe.
|
|
350
349
|
const patchTarget = await prepareGuardedPatch(backend, {
|
|
351
|
-
entityType: "node", bundle: type, id, existing, attributes,
|
|
350
|
+
entityType: "node", bundle: type, id, existing, attributes, relationships: resolvedRelationships,
|
|
352
351
|
});
|
|
353
352
|
if (dryRun) {
|
|
354
353
|
const preview = {
|
|
@@ -357,7 +356,7 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
357
356
|
};
|
|
358
357
|
return summaryWrite.deprecated && bodyAttr ? attachSummaryDeprecation(preview) : preview;
|
|
359
358
|
}
|
|
360
|
-
// Alias handling (
|
|
359
|
+
// Alias handling (1.5.1 alias fix): an explicit `path.alias` is set in place by
|
|
361
360
|
// round-tripping the existing alias's pid (no duplicate); a path-less update
|
|
362
361
|
// re-pins the current alias *with its pid* so the save can't revert/duplicate
|
|
363
362
|
// it. A rename (alias changed) also gets a 301 redirect from the old path.
|
|
@@ -367,6 +366,7 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
367
366
|
const patched = await updateEntityGuarded(backend, {
|
|
368
367
|
entityType: "node", bundle: type, id, attributes, relationships: resolvedRelationships,
|
|
369
368
|
...(patchTarget.resourceVersion ? { resourceVersion: patchTarget.resourceVersion } : {}),
|
|
369
|
+
...(patchTarget.draftRevision ? { draftRevision: patchTarget.draftRevision } : {}),
|
|
370
370
|
});
|
|
371
371
|
const redirectResult = redirect ? await createRenameRedirect(backend, sec, redirect) : null;
|
|
372
372
|
// #169: when relationships were sent, the canonical re-read is the published
|
|
@@ -477,7 +477,7 @@ export const definitions = [
|
|
|
477
477
|
},
|
|
478
478
|
{
|
|
479
479
|
name: "drupal_update_node",
|
|
480
|
-
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
|
|
480
|
+
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.",
|
|
481
481
|
inputSchema: {
|
|
482
482
|
type: "object", required: ["type", "id"],
|
|
483
483
|
properties: {
|
|
@@ -492,7 +492,7 @@ export const definitions = [
|
|
|
492
492
|
moderationState: { type: "string", description: "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'." },
|
|
493
493
|
fields: { type: "object", description: "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." },
|
|
494
494
|
relationships: { type: "object", description: "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." },
|
|
495
|
-
dryRun: { type: "boolean", default: false, description: "Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write.
|
|
495
|
+
dryRun: { type: "boolean", default: false, description: "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." },
|
|
496
496
|
returning: RETURNING_SCHEMA,
|
|
497
497
|
},
|
|
498
498
|
},
|