drupal-mcp-connector 3.2.1 → 3.2.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.2.3] - 2026-10-11
11
+
12
+ ### Added
13
+ - **HTTP transport eval harness skeleton (DEV-866, increment 1).**
14
+ `npm run eval:http` runs a test-only pack over the hosted HTTP request
15
+ handler with a stub token issuer (not Paladin), a fake loopback upstream, and
16
+ synthetic keys. T1 checks that a missing identity gets 401 before dispatch,
17
+ T7 is its allow twin, and T1n records the unauthenticated non-loopback start
18
+ (DEV-845) as an expected failure that fails the pack if it ever passes.
19
+ Nothing under `src/` changes.
20
+
21
+ ## [3.2.2] - 2026-10-07
22
+
23
+ ### Fixed
24
+ - **Library draft paragraph revisions (#420).** Entity Reference Revisions
25
+ saves a new paragraph revision when an existing host saves a new revision.
26
+ A library or custom-block draft matches when it pins that same paragraph,
27
+ in order, at the submitted revision or a newer one. An older revision, a
28
+ different paragraph, or a different order still fails. The published
29
+ revision's pins must still match exactly.
30
+
10
31
  ## [3.2.1] - 2026-10-07
11
32
 
12
33
  ### Fixed
package/README.md CHANGED
@@ -278,8 +278,8 @@ the allowlist or Sentinel profile excludes the type — run `doctor` and
278
278
  ### From npm (recommended for operators)
279
279
 
280
280
  ```bash
281
- npm install -g drupal-mcp-connector@3.2.1
282
- # or: npx -y drupal-mcp-connector@3.2.1
281
+ npm install -g drupal-mcp-connector@3.2.3
282
+ # or: npx -y drupal-mcp-connector@3.2.3
283
283
  ```
284
284
 
285
285
  Point your MCP client at the installed binary (path from `which drupal-mcp-connector`
@@ -294,16 +294,16 @@ unbounded range.
294
294
 
295
295
  1. Pin an exact version in the lockfile (`package-lock.json` or
296
296
  `npm-shrinkwrap.json`), or in `package.json` as
297
- `"drupal-mcp-connector": "3.2.1"` (use the version you intend to run).
297
+ `"drupal-mcp-connector": "3.2.3"` (use the version you intend to run).
298
298
  2. Confirm the registry checksum before you install.
299
- `npm view drupal-mcp-connector@3.2.1 dist.integrity` prints the integrity
299
+ `npm view drupal-mcp-connector@3.2.3 dist.integrity` prints the integrity
300
300
  npm checks. A lockfile install fails when the tarball does not match.
301
301
  3. Read [CHANGELOG.md](CHANGELOG.md) for that version.
302
302
  4. Run `npm audit --omit=dev` against the lockfile and read any advisory
303
303
  before you upgrade. There is no separate scanner to install.
304
304
 
305
305
  Restart the MCP client after the upgrade. Run `drupal-mcp-doctor` when the
306
- package is on your PATH, or `npx -y drupal-mcp-connector@3.2.1 doctor` when
306
+ package is on your PATH, or `npx -y drupal-mcp-connector@3.2.3 doctor` when
307
307
  you launch it with npx.
308
308
 
309
309
  ### From a git clone (development)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
4
4
  "description": "Multi-site MCP server for Drupal over JSON:API and GraphQL, with governed writes and an SSH Drush bridge. init writes a stdio or Tailscale client config from a transport preset. doctor checks reachability, auth, and readiness, and reports version skew. A timestamp gap on a published revision no longer blocks the next draft.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -61,6 +61,7 @@
61
61
  "lint:fix": "eslint src/ --fix",
62
62
  "test": "vitest run",
63
63
  "test:watch": "vitest",
64
+ "eval:http": "node tests/evals/http/run-pack.js",
64
65
  "audit": "npm audit --audit-level=high",
65
66
  "check": "npm run lint && npm run audit && npm run syntax-check && npm test",
66
67
  "syntax-check": "find src -name '*.js' -exec sh -c 'node --input-type=module --check < \"$1\" && echo \"$1 ✓\"' _ {} \\;",
@@ -23,10 +23,14 @@
23
23
  * timeout, or a missing revision id fails closed. A draft-inventory 404 or
24
24
  * 405 is absence. Any other inventory failure fails closed. After the write
25
25
  * the published revision, rel:working-copy, and the published paragraph pins
26
- * (including order) are re-read. A completed 4xx is returned when that
27
- * re-read matches the pre-write state. A lost response, or a 4xx whose
28
- * re-read shows a change, is reported from the re-read and is not called a
29
- * rollback. An unmoderated entity keeps the ordinary update.
26
+ * (including order) are re-read. Entity Reference Revisions saves a new
27
+ * paragraph revision when an existing host saves a new revision, so the draft
28
+ * may pin a later revision of the submitted paragraph. That later revision
29
+ * matches. An older revision, a different paragraph, or a different order
30
+ * does not. A completed 4xx is returned when that re-read matches the
31
+ * pre-write state. A lost response, or a 4xx whose re-read shows a change,
32
+ * is reported from the re-read and is not called a rollback. An unmoderated
33
+ * entity keeps the ordinary update.
30
34
  */
31
35
 
32
36
  import { entityRevisionId } from "./write-revision.js";
@@ -613,7 +617,7 @@ function assertPublishedPreserved(entityType, before, after, relationships) {
613
617
  const baseline = continuing ? normalizedPins(before.working) : before.pins;
614
618
  if (submitted) {
615
619
  const landed = forwardPins.filter((pin) => submitted.fields.has(pin.field));
616
- if (pinKey(landed) !== pinKey(submitted.pins)) {
620
+ if (!submittedPinsLanded(submitted.pins, landed)) {
617
621
  problems.push("the forward revision does not pin the submitted paragraphs");
618
622
  }
619
623
  } else if (pinKey(forwardPins) !== pinKey(baseline)) {
@@ -633,7 +637,7 @@ function assertSubmittedPins(entityType, latest, relationships) {
633
637
  const submitted = submittedParagraphPins(relationships);
634
638
  if (!submitted) return;
635
639
  const landed = normalizedPins(latest).filter((pin) => submitted.fields.has(pin.field));
636
- if (pinKey(landed) !== pinKey(submitted.pins)) {
640
+ if (!submittedPinsLanded(submitted.pins, landed)) {
637
641
  throw uncertainWriteError(entityType, "the written revision does not pin the submitted paragraphs");
638
642
  }
639
643
  }
@@ -793,3 +797,65 @@ function pinKey(pins) {
793
797
  return `${field}\0${rows.join("\n")}`;
794
798
  }).join("\n\n");
795
799
  }
800
+
801
+ /**
802
+ * Group pins by field, preserving submission and re-read order inside a field.
803
+ * @param {object[]} pins
804
+ * @returns {Map<string, object[]>}
805
+ */
806
+ function pinsByField(pins) {
807
+ const groups = new Map();
808
+ for (const pin of pins) {
809
+ if (!groups.has(pin.field)) groups.set(pin.field, []);
810
+ groups.get(pin.field).push(pin);
811
+ }
812
+ return groups;
813
+ }
814
+
815
+ /**
816
+ * Entity Reference Revisions creates a new paragraph revision when an existing
817
+ * host saves a new revision, then points the host at that revision. The
818
+ * submitted id is the earlier one. A missing revision on either side matches
819
+ * only another missing revision.
820
+ * @param {?string} submittedId
821
+ * @param {?string} landedId
822
+ * @returns {boolean}
823
+ */
824
+ function sameRevisionOrNewer(submittedId, landedId) {
825
+ const submittedMissing = submittedId === null || submittedId === undefined || submittedId === "";
826
+ const landedMissing = landedId === null || landedId === undefined || landedId === "";
827
+ if (submittedMissing || landedMissing) return submittedMissing && landedMissing;
828
+ const submitted = Number(submittedId);
829
+ const landed = Number(landedId);
830
+ if (!Number.isSafeInteger(submitted) || !Number.isSafeInteger(landed)) {
831
+ return String(submittedId) === String(landedId);
832
+ }
833
+ return landed >= submitted;
834
+ }
835
+
836
+ /**
837
+ * Whether the re-read pins the submitted paragraphs, in order. A later
838
+ * revision of the same paragraph counts. An older revision does not.
839
+ * @param {object[]} submittedPins
840
+ * @param {object[]} landedPins
841
+ * @returns {boolean}
842
+ */
843
+ function submittedPinsLanded(submittedPins, landedPins) {
844
+ const submitted = pinsByField(submittedPins);
845
+ const landed = pinsByField(landedPins);
846
+ const submittedFields = [...submitted.keys()].sort();
847
+ const landedFields = [...landed.keys()].sort();
848
+ if (submittedFields.length !== landedFields.length
849
+ || submittedFields.some((field, index) => field !== landedFields[index])) {
850
+ return false;
851
+ }
852
+ return submittedFields.every((field) => {
853
+ const left = submitted.get(field) ?? [];
854
+ const right = landed.get(field) ?? [];
855
+ if (left.length !== right.length) return false;
856
+ return left.every((pin, index) => {
857
+ const got = right[index];
858
+ return got.id === pin.id && sameRevisionOrNewer(pin.revisionId, got.revisionId);
859
+ });
860
+ });
861
+ }