drupal-mcp-connector 2.13.0 → 2.14.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.
@@ -2,7 +2,7 @@
2
2
  * Relay frame codec (#232).
3
3
  *
4
4
  * Length-prefixed JSON frames for the edge/agent channel, promoted from the
5
- * DEV-293 lab harness (`lab/outbound-relay/harness.js`). Mechanism only — no
5
+ * outbound-relay lab harness (`lab/outbound-relay/harness.js`). Mechanism only — no
6
6
  * authentication, entitlement, or header policy lives here.
7
7
  *
8
8
  * Wire format: 4-byte big-endian payload length, then UTF-8 JSON. A frame
package/src/lib/usage.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Attributable usage, quotas, and abuse signals on the relay edge (#256 /
3
- * DEV-126).
2
+ * Attributable usage, quotas, and abuse signals on the relay edge (#256).
4
3
  *
5
4
  * Metering at the seam, lab bounds. Every edge decision (allow or deny) and
6
5
  * every fan-down receipt is a record keyed by the grant-resolved tenant and
package/src/tools/bulk.js CHANGED
@@ -119,13 +119,14 @@ async function bulkUpdate({ site: siteName, entityType, bundle, items = [] }) {
119
119
  assertPublishAllowed(sec, attributes);
120
120
  const resolvedRelationships = await resolveErrRelationships(backend, item.relationships ?? {});
121
121
  const patchTarget = await prepareGuardedPatch(backend, {
122
- entityType, bundle, id: item.id, existing, attributes,
122
+ entityType, bundle, id: item.id, existing, attributes, relationships: resolvedRelationships,
123
123
  });
124
124
  const entity = await updateEntityGuarded(backend, {
125
125
  entityType, bundle, id: item.id,
126
126
  attributes,
127
127
  relationships: resolvedRelationships,
128
128
  ...(patchTarget.resourceVersion ? { resourceVersion: patchTarget.resourceVersion } : {}),
129
+ ...(patchTarget.draftRevision ? { draftRevision: patchTarget.draftRevision } : {}),
129
130
  });
130
131
  updated += 1;
131
132
  results.push({ index, success: true, id: entity?.id ?? item.id });
@@ -118,7 +118,7 @@ async function updateEntity({ site: siteName, entityType, bundle, id, attributes
118
118
  assertPublishAllowed(sec, safeAttributes);
119
119
  const resolvedRelationships = await resolveErrRelationships(backend, relationships);
120
120
  const patchTarget = await prepareGuardedPatch(backend, {
121
- entityType, bundle, id, existing, attributes: safeAttributes,
121
+ entityType, bundle, id, existing, attributes: safeAttributes, relationships: resolvedRelationships,
122
122
  });
123
123
  if (dryRun) {
124
124
  return {
@@ -129,6 +129,7 @@ async function updateEntity({ site: siteName, entityType, bundle, id, attributes
129
129
  const result = await updateEntityGuarded(backend, {
130
130
  entityType, bundle, id, attributes: safeAttributes, relationships: resolvedRelationships,
131
131
  ...(patchTarget.resourceVersion ? { resourceVersion: patchTarget.resourceVersion } : {}),
132
+ ...(patchTarget.draftRevision ? { draftRevision: patchTarget.draftRevision } : {}),
132
133
  });
133
134
  const written = await readWrittenRevision({
134
135
  backend, entityType, bundle, id,
@@ -280,7 +281,7 @@ export const definitions = [
280
281
  },
281
282
  {
282
283
  name: "drupal_entity_update",
283
- description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets an id-mismatch PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable working copy is PATCHed via ?resourceVersion=rel:working-copy (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents.",
284
+ description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including 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); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents.",
284
285
  inputSchema: {
285
286
  type: "object", required: ["entityType", "bundle", "id"],
286
287
  properties: {
@@ -290,7 +291,7 @@ export const definitions = [
290
291
  id: { type: "string" },
291
292
  attributes: { type: "object" },
292
293
  relationships: { type: "object" },
293
- 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. The probe uses a non-matching data.id so Drupal does not save, and hits the same URL as the real write (canonical, or ?resourceVersion=rel:working-copy when a draft is addressable). A working-copy 400 fails the dryRun." },
294
+ 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." },
294
295
  returning: RETURNING_SCHEMA,
295
296
  },
296
297
  },
@@ -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 (DEV-116): JSON:API deserializes `{ alias, pathauto }` onto the node's
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. DEV-114's path-omitted "preserve" had the same defect (no `pid`),
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 (DEV-116).
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 same URL the write will hit. An addressable
348
- // working copy is PATCHed via rel:working-copy; a stray revision still
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 (DEV-116): an explicit `path.alias` is set in place by
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 an id-mismatch PATCH preflight runs first — including on dryRun — against the same URL the write will hit. An addressable working copy is PATCHed via ?resourceVersion=rel:working-copy (#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.",
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. The probe uses a non-matching data.id so Drupal does not save, and hits the same URL as the real write (canonical, or ?resourceVersion=rel:working-copy when a draft is addressable). A working-copy 400 fails the dryRun." },
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
  },