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 +21 -0
- package/README.md +5 -5
- package/package.json +2 -1
- package/src/lib/canonical-draft.js +72 -6
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.
|
|
282
|
-
# or: npx -y drupal-mcp-connector@3.2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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 (
|
|
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 (
|
|
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
|
+
}
|