drupal-mcp-connector 2.6.1 → 2.7.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/.claude/commands/drupal-bulk-create.md +2 -2
- package/.claude/commands/drupal-create-paragraph.md +2 -2
- package/.claude/commands/drupal-entity-update.md +3 -3
- package/.claude/commands/drupal-get-paragraph.md +2 -2
- package/.claude/commands/drupal-list-revisions.md +2 -2
- package/.claude/commands/drupal-list-sites.md +2 -2
- package/.claude/commands/drupal-update-node.md +4 -4
- package/.claude/commands/drupal-update-paragraph.md +2 -2
- package/CHANGELOG.md +80 -0
- package/README.md +2 -2
- package/config/config.example.json +16 -1
- package/package.json +4 -3
- package/src/index.js +91 -21
- package/src/lib/backends/backend-interface.js +4 -1
- package/src/lib/backends/jsonapi.js +16 -6
- package/src/lib/canonical.js +11 -1
- package/src/lib/config.js +23 -0
- package/src/lib/dispatch.js +36 -10
- package/src/lib/err-relationships.js +286 -0
- package/src/lib/http-auth.js +541 -3
- package/src/lib/http-handler.js +62 -9
- package/src/lib/mcp-server.js +32 -6
- package/src/lib/patch-preflight.js +176 -0
- package/src/lib/principal.js +372 -0
- package/src/lib/verify.js +40 -4
- package/src/lib/write-revision.js +72 -0
- package/src/tools/bulk.js +31 -6
- package/src/tools/config.js +6 -0
- package/src/tools/entities.js +37 -7
- package/src/tools/nodes.js +34 -10
- package/src/tools/paragraphs.js +56 -42
- package/src/tools/revisions.js +42 -7
- package/src/tools/site.js +19 -5
package/src/tools/revisions.js
CHANGED
|
@@ -112,6 +112,7 @@ function summarizeRevision(resource) {
|
|
|
112
112
|
return {
|
|
113
113
|
vid: attrs.get("drupal_internal__vid") ?? null,
|
|
114
114
|
revisionTimestamp: attrs.get("revision_timestamp") ?? null,
|
|
115
|
+
changed: attrs.get("changed") ?? null,
|
|
115
116
|
revisionLog: attrs.get("revision_log") ?? attrs.get("revision_log_message") ?? null,
|
|
116
117
|
status: attrs.get("status") ?? null,
|
|
117
118
|
title: attrs.get("title") ?? null,
|
|
@@ -119,6 +120,39 @@ function summarizeRevision(resource) {
|
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Whether the default revision's `changed` is later than its own
|
|
125
|
+
* `revision_timestamp` — the readable fingerprint of a #201 node.
|
|
126
|
+
* @param {?object} summary
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
function changedAheadOfRevision(summary) {
|
|
130
|
+
if (!summary?.changed || !summary?.revisionTimestamp) return false;
|
|
131
|
+
const changed = Date.parse(summary.changed);
|
|
132
|
+
const rev = Date.parse(summary.revisionTimestamp);
|
|
133
|
+
if (!Number.isFinite(changed) || !Number.isFinite(rev)) return false;
|
|
134
|
+
return changed > rev;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const LIST_REVISIONS_BASE_NOTE =
|
|
138
|
+
"JSON:API only addresses revisions by id (id:<vid>) or the rel:latest-version / " +
|
|
139
|
+
"rel:working-copy aliases; it cannot enumerate the full chronological history. " +
|
|
140
|
+
"Use drupal_get_revision with a specific vid to inspect a known revision, or the " +
|
|
141
|
+
"Drush bridge for complete revision-history enumeration. " +
|
|
142
|
+
"drupal_report_revision_hotspots can surface per-node revision counts.";
|
|
143
|
+
|
|
144
|
+
const LIST_REVISIONS_NULL_WC_NOTE =
|
|
145
|
+
" workingCopy: null is not proof the node is PATCH-able. Drupal core rejects " +
|
|
146
|
+
"PATCH when the stored entity is not the latest revision even when " +
|
|
147
|
+
"content_moderation exposes no pending revision (connector #201, core #2795279). " +
|
|
148
|
+
"Inspect possiblyPatchBlocked before creating dependent paragraphs; dryRun on " +
|
|
149
|
+
"the host update runs the same guard.";
|
|
150
|
+
|
|
151
|
+
const LIST_REVISIONS_FINGERPRINT_NOTE =
|
|
152
|
+
" The default revision's changed timestamp is later than its own " +
|
|
153
|
+
"revision_timestamp — a readable fingerprint that a newer revision row may " +
|
|
154
|
+
"exist without a content_moderation working copy.";
|
|
155
|
+
|
|
122
156
|
/**
|
|
123
157
|
* List the addressable revisions of an entity: the latest default revision and
|
|
124
158
|
* the working-copy (forward) revision. Full chronological enumeration is not
|
|
@@ -145,19 +179,20 @@ async function listRevisions({ site: siteName, type, id }) {
|
|
|
145
179
|
.then(summarizeRevision)
|
|
146
180
|
.catch(() => null);
|
|
147
181
|
|
|
182
|
+
const possiblyPatchBlocked = Boolean(latestVersion && changedAheadOfRevision(latestVersion));
|
|
183
|
+
let note = LIST_REVISIONS_BASE_NOTE;
|
|
184
|
+
if (!workingCopy) note += LIST_REVISIONS_NULL_WC_NOTE;
|
|
185
|
+
if (possiblyPatchBlocked) note += LIST_REVISIONS_FINGERPRINT_NOTE;
|
|
186
|
+
|
|
148
187
|
return {
|
|
149
188
|
entityType: "node",
|
|
150
189
|
bundle: type,
|
|
151
190
|
id,
|
|
152
191
|
latestVersion,
|
|
153
192
|
workingCopy,
|
|
193
|
+
possiblyPatchBlocked,
|
|
154
194
|
fullHistoryAvailable: false,
|
|
155
|
-
note
|
|
156
|
-
"JSON:API only addresses revisions by id (id:<vid>) or the rel:latest-version / " +
|
|
157
|
-
"rel:working-copy aliases; it cannot enumerate the full chronological history. " +
|
|
158
|
-
"Use drupal_get_revision with a specific vid to inspect a known revision, or the " +
|
|
159
|
-
"Drush bridge for complete revision-history enumeration. " +
|
|
160
|
-
"drupal_report_revision_hotspots can surface per-node revision counts.",
|
|
195
|
+
note,
|
|
161
196
|
};
|
|
162
197
|
}
|
|
163
198
|
|
|
@@ -266,7 +301,7 @@ export const definitions = [
|
|
|
266
301
|
{
|
|
267
302
|
name: "drupal_list_revisions",
|
|
268
303
|
description:
|
|
269
|
-
"Surface the addressable revisions of a content node: the latest default revision and the working-copy (forward) revision, with their version ids and links.
|
|
304
|
+
"Surface the addressable revisions of a content node: the latest default revision and the working-copy (forward) revision, with their version ids and links. workingCopy: null is not an all-clear — Drupal core can still reject PATCH when a revision row sits above the default without a content_moderation working copy (#201). The payload includes possiblyPatchBlocked (true when default changed is later than its revision_timestamp) plus changed and revisionTimestamp on latestVersion. Probe the host (this flag, then dryRun on the update) before creating dependent paragraphs. NOTE: JSON:API cannot enumerate full chronological revision history. Full history enumeration requires the Drush bridge.",
|
|
270
305
|
inputSchema: {
|
|
271
306
|
type: "object", required: ["type", "id"],
|
|
272
307
|
properties: {
|
package/src/tools/site.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { getSiteConfig, listSiteNames } from "../lib/config.js";
|
|
10
10
|
import { governanceStatus } from "../lib/governance.js";
|
|
11
11
|
import { resolveBackend } from "../lib/backends/index.js";
|
|
12
|
+
import { getRequestIdentity, resolveGrantedSiteNames, visibleSiteTargets } from "../lib/principal.js";
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Implementations
|
|
@@ -43,11 +44,21 @@ async function listContentTypes({ site: siteName }) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* List
|
|
47
|
-
*
|
|
47
|
+
* List named sites this principal may address. No backend call and no credentials.
|
|
48
|
+
* `sites` stays a name list for compatibility; `targets` is the authoritative
|
|
49
|
+
* resolved-target record.
|
|
50
|
+
* @returns {Promise<{sites: string[], targets: Array<object>}>}
|
|
48
51
|
*/
|
|
49
52
|
async function listConfiguredSites() {
|
|
50
|
-
|
|
53
|
+
const names = listSiteNames();
|
|
54
|
+
const resolvable = names.flatMap((name) => {
|
|
55
|
+
try {
|
|
56
|
+
return [getSiteConfig(name)];
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return visibleSiteTargets(getRequestIdentity(), resolvable, names);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
/**
|
|
@@ -71,7 +82,10 @@ export function classifySiteResolutionFailure(message) {
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
async function getGovernanceStatus({ site: siteName } = {}) {
|
|
74
|
-
const
|
|
85
|
+
const identity = getRequestIdentity();
|
|
86
|
+
const configured = listSiteNames();
|
|
87
|
+
const allowed = identity ? resolveGrantedSiteNames(identity, configured) : configured;
|
|
88
|
+
const names = siteName ? [siteName] : allowed;
|
|
75
89
|
const resolved = [];
|
|
76
90
|
const unresolved = [];
|
|
77
91
|
for (const name of names) {
|
|
@@ -123,7 +137,7 @@ export const definitions = [
|
|
|
123
137
|
},
|
|
124
138
|
{
|
|
125
139
|
name: "drupal_list_sites",
|
|
126
|
-
description: "List
|
|
140
|
+
description: "List the Drupal sites this principal may address. Each target includes the authoritative site name and base URL.",
|
|
127
141
|
inputSchema: {
|
|
128
142
|
type: "object",
|
|
129
143
|
properties: {},
|