drupal-mcp-connector 2.15.0 → 2.15.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-get-paragraph.md +3 -3
- package/.agents/commands/drupal-list-nodes.md +2 -2
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/lib/backends/jsonapi.js +81 -17
- package/src/tools/nodes.js +6 -3
- package/src/tools/paragraphs.js +73 -6
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy."
|
|
2
|
+
description: "Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass revisionId (the host pin) to read that revision via JSON:API resourceVersion=id:<vid>; a mismatch or miss is an error, not a silent default-revision fallback. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy."
|
|
3
3
|
argument-hint: "<paragraphType> <id> [site] [langcode] [revisionId]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_get_paragraph`.
|
|
7
7
|
|
|
8
|
-
Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.
|
|
8
|
+
Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass revisionId (the host pin) to read that revision via JSON:API resourceVersion=id:<vid>; a mismatch or miss is an error, not a silent default-revision fallback. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -16,6 +16,6 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
16
16
|
**Optional:**
|
|
17
17
|
- `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
|
|
18
18
|
- `langcode` (string): Target language for the unpublished working translation (e.g. 'es').
|
|
19
|
-
- `revisionId` (string): Paragraph revision id the host already pins.
|
|
19
|
+
- `revisionId` (string): Paragraph revision id the host already pins (ERR meta.target_revision_id). Honored on the read; a different served vid is an error.
|
|
20
20
|
|
|
21
21
|
If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors."
|
|
2
|
+
description: "List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors. Drupal core JSON:API caps page[limit] at 50; a larger requested limit is filled by following links.next (up to 1000). When the site does not expose meta.count, total is exact only if this window reached the end of the collection; otherwise approximate is true and hasNext is set."
|
|
3
3
|
argument-hint: "<type> [site] [status] [limit] [offset] [filters] [sort]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_list_nodes`.
|
|
7
7
|
|
|
8
|
-
List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors.
|
|
8
|
+
List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors. Drupal core JSON:API caps page[limit] at 50; a larger requested limit is filled by following links.next (up to 1000). When the site does not expose meta.count, total is exact only if this window reached the end of the collection; otherwise approximate is true and hasNext is set.
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.15.1] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`drupal_get_paragraph` honors `revisionId` and refuses a default-revision fallback (#292).**
|
|
14
|
+
Passing the host pin without `langcode` now reads JSON:API
|
|
15
|
+
`?resourceVersion=id:<vid>`. If Drupal serves a different
|
|
16
|
+
`drupal_internal__revision_id` (or nothing), the tool errors instead of
|
|
17
|
+
returning the live default revision. `ref.meta.target_revision_id` is the
|
|
18
|
+
served vid. `langcode` still uses Sentinel's draft read.
|
|
19
|
+
- **`drupal_list_nodes` no longer reports a Drupal-capped page as the exact collection total (#291).**
|
|
20
|
+
Core JSON:API silently caps `page[limit]` at 50 and does not send `meta.count`.
|
|
21
|
+
The connector was treating that page length as `total` with `approximate: false`,
|
|
22
|
+
so a `limit: 100` call on a 54-item collection returned 50 and looked complete.
|
|
23
|
+
`listEntities` now follows `links.next` to fill a larger requested limit (up to
|
|
24
|
+
1000) and only reports an exact `total` when `meta.count` is present or the
|
|
25
|
+
window reached the end. Otherwise `approximate` is true and `hasNext` is
|
|
26
|
+
returned on the tool result.
|
|
27
|
+
|
|
10
28
|
## [2.15.0] - 2026-09-10
|
|
11
29
|
|
|
12
30
|
### Fixed
|
|
@@ -1553,6 +1571,8 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
|
|
|
1553
1571
|
- User tools gained explicit PII-access assertions.
|
|
1554
1572
|
- Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
|
|
1555
1573
|
|
|
1574
|
+
[2.15.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.1
|
|
1575
|
+
[2.15.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.0
|
|
1556
1576
|
[2.14.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.2
|
|
1557
1577
|
[2.14.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.1
|
|
1558
1578
|
[2.14.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.0
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# drupal-mcp-connector
|
|
2
2
|
|
|
3
|
-
> A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL access, governed content tools, audit reports, and an SSH Drush bridge.
|
|
3
|
+
> A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL access, governed draft translations, content tools, audit reports, and an SSH Drush bridge.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.15.
|
|
4
|
-
"description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
|
|
3
|
+
"version": "2.15.1",
|
|
4
|
+
"description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL access, governed draft translations, content tools, audit reports, and an SSH Drush bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -24,14 +24,30 @@ import { isPositiveNid, normalizeAlias, PATH_ALIAS_ENTITY_TYPE } from "../path-a
|
|
|
24
24
|
// governed read/write workflows explicitly need.
|
|
25
25
|
const INTERNAL_ATTR_RE = /^drupal_internal__/;
|
|
26
26
|
|
|
27
|
-
// countEntities() pagination. Drupal core JSON:API returns no
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// GraphQL backend's MAX_CLIENT_RECORDS) — past it the count is approximate.
|
|
27
|
+
// countEntities() / listEntities() pagination. Drupal core JSON:API returns no
|
|
28
|
+
// total in `meta` and silently caps `page[limit]` at OffsetPage::SIZE_MAX
|
|
29
|
+
// (50 by default). COUNT_PAGE_SIZE matches that default; COUNT_MAX_RECORDS
|
|
30
|
+
// bounds a walk so a huge collection can't issue unbounded requests (mirrors
|
|
31
|
+
// the GraphQL backend's MAX_CLIENT_RECORDS) — past it the count is approximate.
|
|
32
32
|
const COUNT_PAGE_SIZE = 50;
|
|
33
33
|
const COUNT_MAX_RECORDS = 1000;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Whether a JSON:API collection document advertises another page.
|
|
37
|
+
* `links.next` may be a string href or a `{ href }` link object.
|
|
38
|
+
* @param {?object} data JSON:API document.
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
function jsonApiHasNext(data) {
|
|
42
|
+
const next = data?.links?.next;
|
|
43
|
+
if (next === undefined || next === null || next === false) return false;
|
|
44
|
+
if (typeof next === "string") return next.length > 0;
|
|
45
|
+
if (typeof next === "object" && next.href !== undefined && next.href !== null) {
|
|
46
|
+
return String(next.href).length > 0;
|
|
47
|
+
}
|
|
48
|
+
return Boolean(next);
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
/**
|
|
36
52
|
* Detect the JSON:API error Drupal returns when a write attempts to set the
|
|
37
53
|
* `status` (published) field on a content_moderation-governed entity. Such
|
|
@@ -268,24 +284,72 @@ export class JsonApiBackend extends Backend {
|
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
/**
|
|
271
|
-
* List entities for a descriptor.
|
|
272
|
-
*
|
|
287
|
+
* List entities for a descriptor.
|
|
288
|
+
*
|
|
289
|
+
* Drupal core JSON:API has no `meta.count` (jsonapi_extras can add it) and
|
|
290
|
+
* silently caps `page[limit]` at OffsetPage::SIZE_MAX (50 by default). When
|
|
291
|
+
* the caller asks for more rows than one Drupal page returns and
|
|
292
|
+
* `links.next` is present, this method follows that link until the requested
|
|
293
|
+
* window is filled, the collection ends, or COUNT_MAX_RECORDS is hit.
|
|
294
|
+
* `page.total` is exact when `meta.count` is present or this window reached
|
|
295
|
+
* the end; otherwise it is the number of rows seen so far and `approximate`
|
|
296
|
+
* is true. Never report a single page's length as an exact collection total.
|
|
297
|
+
*
|
|
273
298
|
* @param {import("../canonical.js").QueryDescriptor} descriptor
|
|
274
299
|
* @returns {Promise<import("./backend-interface.js").ListResult>}
|
|
275
300
|
*/
|
|
276
301
|
async listEntities(descriptor) {
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
302
|
+
const requestedLimit = descriptor.page?.limit;
|
|
303
|
+
const startOffset = descriptor.page?.offset ?? 0;
|
|
304
|
+
const fillTo = typeof requestedLimit === "number"
|
|
305
|
+
? Math.min(Math.max(0, requestedLimit), COUNT_MAX_RECORDS)
|
|
306
|
+
: null;
|
|
307
|
+
|
|
308
|
+
const entities = [];
|
|
309
|
+
let offset = startOffset;
|
|
310
|
+
let hasNext = false;
|
|
311
|
+
let metaCount = null;
|
|
312
|
+
|
|
313
|
+
for (;;) {
|
|
314
|
+
const remaining = fillTo === null ? requestedLimit : fillTo - entities.length;
|
|
315
|
+
const page = {
|
|
316
|
+
...descriptor.page,
|
|
317
|
+
offset,
|
|
318
|
+
...(typeof remaining === "number" ? { limit: remaining } : {}),
|
|
319
|
+
};
|
|
320
|
+
const params = this.compileQuery({ ...descriptor, page });
|
|
321
|
+
const qs = params.toString();
|
|
322
|
+
const base = this.resourcePath(descriptor.entityType, descriptor.bundle);
|
|
323
|
+
const path = qs ? `${base}?${qs}` : base;
|
|
324
|
+
const data = await drupalFetch(this.site, path);
|
|
325
|
+
if (metaCount === null && typeof data?.meta?.count === "number") {
|
|
326
|
+
metaCount = data.meta.count;
|
|
327
|
+
}
|
|
328
|
+
const pageEntities = (data.data || []).map((r) => this.toCanonical(r));
|
|
329
|
+
entities.push(...pageEntities);
|
|
330
|
+
hasNext = jsonApiHasNext(data);
|
|
331
|
+
|
|
332
|
+
if (fillTo === null) break;
|
|
333
|
+
if (!hasNext || pageEntities.length === 0) break;
|
|
334
|
+
if (entities.length >= fillTo) break;
|
|
335
|
+
offset += pageEntities.length;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const truncated = fillTo !== null
|
|
339
|
+
&& typeof requestedLimit === "number"
|
|
340
|
+
&& entities.length < requestedLimit
|
|
341
|
+
&& hasNext;
|
|
342
|
+
const seen = startOffset + entities.length;
|
|
343
|
+
const exact = typeof metaCount === "number" || !hasNext;
|
|
284
344
|
return {
|
|
285
345
|
entities,
|
|
286
|
-
page: {
|
|
287
|
-
|
|
288
|
-
|
|
346
|
+
page: {
|
|
347
|
+
total: typeof metaCount === "number" ? metaCount : seen,
|
|
348
|
+
hasNext,
|
|
349
|
+
cursor: null,
|
|
350
|
+
},
|
|
351
|
+
approximate: !exact,
|
|
352
|
+
truncated,
|
|
289
353
|
};
|
|
290
354
|
}
|
|
291
355
|
|
package/src/tools/nodes.js
CHANGED
|
@@ -280,8 +280,9 @@ async function getNode({ site: siteName, type, id, langcode, resourceVersion })
|
|
|
280
280
|
*
|
|
281
281
|
* @param {object} args - { site?, type, status?, filters?, limit?, offset?, sort? }.
|
|
282
282
|
* A `status` boolean is appended to `filters` as a status equality descriptor.
|
|
283
|
-
* @returns {Promise<{total: number, approximate: boolean,
|
|
284
|
-
* nextOffset: number, nodes: object[]}>}
|
|
283
|
+
* @returns {Promise<{total: number, approximate: boolean, truncated: boolean,
|
|
284
|
+
* offset: number, nextOffset: number, hasNext: boolean, nodes: object[]}>}
|
|
285
|
+
* Paged, redacted node list. `hasNext` is the backend's `links.next` signal.
|
|
285
286
|
*/
|
|
286
287
|
async function listNodes({ site: siteName, type, status, filters = [], limit = 20, offset = 0, sort = [{ field: "changed", dir: "desc" }] }) {
|
|
287
288
|
const site = getSiteConfig(siteName);
|
|
@@ -295,8 +296,10 @@ async function listNodes({ site: siteName, type, status, filters = [], limit = 2
|
|
|
295
296
|
return {
|
|
296
297
|
total: res.page?.total ?? nodes.length,
|
|
297
298
|
approximate: res.approximate ?? false,
|
|
299
|
+
truncated: res.truncated ?? false,
|
|
298
300
|
offset,
|
|
299
301
|
nextOffset: offset + nodes.length,
|
|
302
|
+
hasNext: Boolean(res.page?.hasNext),
|
|
300
303
|
nodes,
|
|
301
304
|
};
|
|
302
305
|
}
|
|
@@ -528,7 +531,7 @@ export const definitions = [
|
|
|
528
531
|
},
|
|
529
532
|
{
|
|
530
533
|
name: "drupal_list_nodes",
|
|
531
|
-
description: "List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors.",
|
|
534
|
+
description: "List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors. Drupal core JSON:API caps page[limit] at 50; a larger requested limit is filled by following links.next (up to 1000). When the site does not expose meta.count, total is exact only if this window reached the end of the collection; otherwise approximate is true and hasNext is set.",
|
|
532
535
|
inputSchema: {
|
|
533
536
|
type: "object", required: ["type"],
|
|
534
537
|
properties: {
|
package/src/tools/paragraphs.js
CHANGED
|
@@ -145,24 +145,85 @@ async function updateParagraph({ site: siteName, paragraphType, id, attributes =
|
|
|
145
145
|
return { paragraph, ref, relationshipData: ref, note: EMBED_NOTE };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* JSON:API `resourceVersion` selector for a paragraph host pin.
|
|
150
|
+
* Accepts a numeric vid, a numeric string, or an explicit `id:<vid>`.
|
|
151
|
+
* @param {string|number} revisionId Host pin / paragraph revision id.
|
|
152
|
+
* @returns {string} e.g. `id:6654`.
|
|
153
|
+
* @throws {Error} If the value is not a usable revision id.
|
|
154
|
+
*/
|
|
155
|
+
export function paragraphResourceVersion(revisionId) {
|
|
156
|
+
if (typeof revisionId === "number" && Number.isFinite(revisionId) && revisionId > 0) {
|
|
157
|
+
return `id:${revisionId}`;
|
|
158
|
+
}
|
|
159
|
+
if (typeof revisionId === "string" && revisionId.length) {
|
|
160
|
+
if (/^id:[1-9]\d*$/.test(revisionId)) return revisionId;
|
|
161
|
+
if (/^[1-9]\d*$/.test(revisionId)) return `id:${revisionId}`;
|
|
162
|
+
}
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Invalid paragraph revisionId "${revisionId}". Use a numeric vid (the host pin).`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Numeric vid from a host pin (`6654` or `id:6654`).
|
|
170
|
+
* @param {string|number} revisionId
|
|
171
|
+
* @returns {?number}
|
|
172
|
+
*/
|
|
173
|
+
function requestedParagraphVid(revisionId) {
|
|
174
|
+
const raw = String(revisionId).replace(/^id:/, "");
|
|
175
|
+
const n = Number(raw);
|
|
176
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Fail closed when a requested paragraph revision was not the one served.
|
|
181
|
+
* Silent substitution of the default revision is the defect this guards (#292).
|
|
182
|
+
* @param {?object} entity Canonical paragraph, or null if the read missed.
|
|
183
|
+
* @param {string|number} requested Host pin that was asked for.
|
|
184
|
+
* @param {string} id Paragraph UUID.
|
|
185
|
+
* @returns {object} `entity` when the served vid matches.
|
|
186
|
+
* @throws {Error} On a miss or a mismatched vid.
|
|
187
|
+
*/
|
|
188
|
+
function requireServedParagraphRevision(entity, requested, id) {
|
|
189
|
+
const wanted = requestedParagraphVid(requested);
|
|
190
|
+
const served = paragraphRevisionId(entity);
|
|
191
|
+
if (!entity || wanted === null || served === null || served !== wanted) {
|
|
192
|
+
const got = !entity || served === null ? "no revision" : String(served);
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Requested paragraph revision ${wanted ?? requested} for ${id} but the backend served ${got}. ` +
|
|
195
|
+
"Refusing to substitute a different revision.",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return entity;
|
|
199
|
+
}
|
|
200
|
+
|
|
148
201
|
/**
|
|
149
202
|
* Fetch a single paragraph by bundle + UUID, redacted per the site policy, and
|
|
150
203
|
* annotate it with the embedding ref (including `meta.target_revision_id`).
|
|
151
204
|
*
|
|
152
|
-
*
|
|
205
|
+
* When `revisionId` is set (the host's `meta.target_revision_id`), the read
|
|
206
|
+
* addresses that revision via JSON:API `?resourceVersion=id:<vid>` and errors
|
|
207
|
+
* if Drupal serves a different vid. Omitting `revisionId` still reads the
|
|
208
|
+
* default revision.
|
|
209
|
+
*
|
|
210
|
+
* @param {object} args - { site?, paragraphType, id, langcode?, revisionId? }.
|
|
153
211
|
* @returns {Promise<(object & {ref: object})|null>}
|
|
154
|
-
* The redacted paragraph with an embedding `ref`, or null if not found
|
|
212
|
+
* The redacted paragraph with an embedding `ref`, or null if not found and
|
|
213
|
+
* no `revisionId` was requested.
|
|
155
214
|
* @throws {SecurityError} If reading paragraphs of this bundle is not permitted.
|
|
215
|
+
* @throws {Error} If a requested `revisionId` cannot be served.
|
|
156
216
|
*/
|
|
157
217
|
async function getParagraph({ site: siteName, paragraphType, id, langcode, revisionId }) {
|
|
158
218
|
const site = getSiteConfig(siteName);
|
|
159
219
|
const sec = resolveSecurityConfig(site);
|
|
160
220
|
assertReadAllowed(sec, "paragraph", paragraphType);
|
|
161
221
|
const backend = await resolveBackend(site);
|
|
222
|
+
const pinRequested = revisionId !== undefined && revisionId !== null && revisionId !== "";
|
|
162
223
|
let entity;
|
|
163
224
|
if (langcode) {
|
|
164
225
|
const targetLang = assertDraftLangcode(langcode);
|
|
165
|
-
const pinned = revisionId
|
|
226
|
+
const pinned = pinRequested ? revisionId : paragraphRevisionId(
|
|
166
227
|
await backend.getEntity({ entityType: "paragraph", bundle: paragraphType, id }),
|
|
167
228
|
);
|
|
168
229
|
if (pinned === null || pinned === undefined || pinned === "") {
|
|
@@ -172,12 +233,18 @@ async function getParagraph({ site: siteName, paragraphType, id, langcode, revis
|
|
|
172
233
|
entityType: "paragraph", bundle: paragraphType, id, langcode: targetLang,
|
|
173
234
|
draftRevision: { revisionId: pinned },
|
|
174
235
|
});
|
|
236
|
+
} else if (pinRequested) {
|
|
237
|
+
const resourceVersion = paragraphResourceVersion(revisionId);
|
|
238
|
+
entity = await backend.getEntity({
|
|
239
|
+
entityType: "paragraph", bundle: paragraphType, id, resourceVersion,
|
|
240
|
+
});
|
|
241
|
+
requireServedParagraphRevision(entity, revisionId, id);
|
|
175
242
|
} else {
|
|
176
243
|
entity = await backend.getEntity({ entityType: "paragraph", bundle: paragraphType, id });
|
|
177
244
|
}
|
|
178
245
|
if (!entity) return null;
|
|
179
246
|
const redacted = redactCanonicalEntity(entity, sec, "paragraph");
|
|
180
|
-
const resolvedRevisionId = paragraphRevisionId(entity) ?? paragraphRevisionId(redacted)
|
|
247
|
+
const resolvedRevisionId = paragraphRevisionId(entity) ?? paragraphRevisionId(redacted);
|
|
181
248
|
return { ...redacted, ref: embedRef(redacted.bundle || paragraphType, redacted.id, resolvedRevisionId) };
|
|
182
249
|
}
|
|
183
250
|
|
|
@@ -219,7 +286,7 @@ export const definitions = [
|
|
|
219
286
|
{
|
|
220
287
|
name: "drupal_get_paragraph",
|
|
221
288
|
description:
|
|
222
|
-
"Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.",
|
|
289
|
+
"Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass revisionId (the host pin) to read that revision via JSON:API resourceVersion=id:<vid>; a mismatch or miss is an error, not a silent default-revision fallback. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.",
|
|
223
290
|
inputSchema: {
|
|
224
291
|
type: "object", required: ["paragraphType", "id"],
|
|
225
292
|
properties: {
|
|
@@ -227,7 +294,7 @@ export const definitions = [
|
|
|
227
294
|
paragraphType: { type: "string", description: "Paragraph type / bundle machine name" },
|
|
228
295
|
id: { type: "string", description: "Paragraph UUID" },
|
|
229
296
|
langcode: { type: "string", description: "Target language for the unpublished working translation (e.g. 'es')." },
|
|
230
|
-
revisionId: { type: "string", description: "Paragraph revision id the host already pins." },
|
|
297
|
+
revisionId: { type: "string", description: "Paragraph revision id the host already pins (ERR meta.target_revision_id). Honored on the read; a different served vid is an error." },
|
|
231
298
|
},
|
|
232
299
|
},
|
|
233
300
|
},
|