drupal-mcp-connector 2.7.3 → 2.8.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.
- package/.agents/commands/drupal-audit-config-best-practices.md +15 -0
- package/{.claude → .agents}/commands/drupal-audit-site-health.md +4 -5
- package/.agents/commands/drupal-block-user.md +19 -0
- package/{.claude → .agents}/commands/drupal-bulk-create.md +4 -5
- package/{.claude → .agents}/commands/drupal-bulk-update.md +4 -5
- package/.agents/commands/drupal-config-get.md +18 -0
- package/.agents/commands/drupal-config-list.md +16 -0
- package/.agents/commands/drupal-config-set.md +19 -0
- package/.agents/commands/drupal-content-by-moderation-state.md +21 -0
- package/.agents/commands/drupal-create-block.md +20 -0
- package/{.claude → .agents}/commands/drupal-create-media.md +4 -5
- package/{.claude → .agents}/commands/drupal-create-menu-link.md +4 -5
- package/{.claude → .agents}/commands/drupal-create-node.md +7 -8
- package/{.claude → .agents}/commands/drupal-create-paragraph.md +4 -5
- package/{.claude → .agents}/commands/drupal-create-redirect.md +4 -5
- package/.agents/commands/drupal-create-taxonomy-term.md +22 -0
- package/{.claude → .agents}/commands/drupal-create-translation.md +4 -5
- package/.agents/commands/drupal-create-user.md +23 -0
- package/.agents/commands/drupal-delete-media.md +21 -0
- package/.agents/commands/drupal-delete-node.md +22 -0
- package/.agents/commands/drupal-delete-taxonomy-term.md +21 -0
- package/{.claude → .agents}/commands/drupal-describe-fields.md +4 -5
- package/.agents/commands/drupal-drush-cache-rebuild.md +15 -0
- package/.agents/commands/drupal-drush-config-export.md +15 -0
- package/.agents/commands/drupal-drush-config-import.md +15 -0
- package/.agents/commands/drupal-drush-config-status.md +15 -0
- package/.agents/commands/drupal-drush-cron.md +15 -0
- package/.agents/commands/drupal-drush-module-disable.md +20 -0
- package/.agents/commands/drupal-drush-module-enable.md +18 -0
- package/.agents/commands/drupal-drush-module-list.md +16 -0
- package/.agents/commands/drupal-drush-security-updates.md +15 -0
- package/{.claude → .agents}/commands/drupal-drush-sql-query.md +4 -5
- package/.agents/commands/drupal-drush-status.md +15 -0
- package/.agents/commands/drupal-drush-updatedb.md +15 -0
- package/.agents/commands/drupal-drush-user-create.md +21 -0
- package/.agents/commands/drupal-drush-user-list.md +18 -0
- package/.agents/commands/drupal-drush-watchdog.md +18 -0
- package/{.claude → .agents}/commands/drupal-entity-create.md +4 -5
- package/.agents/commands/drupal-entity-delete.md +23 -0
- package/.agents/commands/drupal-entity-get.md +21 -0
- package/{.claude → .agents}/commands/drupal-entity-list.md +4 -5
- package/{.claude → .agents}/commands/drupal-entity-update.md +7 -8
- package/.agents/commands/drupal-find-orphaned-media.md +17 -0
- package/.agents/commands/drupal-get-entity-schema.md +19 -0
- package/.agents/commands/drupal-get-media.md +19 -0
- package/.agents/commands/drupal-get-node.md +19 -0
- package/{.claude → .agents}/commands/drupal-get-paragraph.md +4 -5
- package/{.claude → .agents}/commands/drupal-get-revision.md +4 -5
- package/.agents/commands/drupal-get-taxonomy-term.md +19 -0
- package/.agents/commands/drupal-get-taxonomy-terms.md +20 -0
- package/.agents/commands/drupal-get-user-by-name.md +18 -0
- package/.agents/commands/drupal-get-user.md +18 -0
- package/{.claude → .agents}/commands/drupal-governance-status.md +4 -5
- package/.agents/commands/drupal-graphql-introspect.md +16 -0
- package/{.claude → .agents}/commands/drupal-graphql.md +4 -5
- package/{.claude → .agents}/commands/drupal-list-blocks.md +4 -5
- package/.agents/commands/drupal-list-content-types.md +15 -0
- package/.agents/commands/drupal-list-entity-types.md +15 -0
- package/.agents/commands/drupal-list-media-types.md +15 -0
- package/.agents/commands/drupal-list-media.md +20 -0
- package/{.claude → .agents}/commands/drupal-list-menu-links.md +4 -5
- package/.agents/commands/drupal-list-moderation-states.md +19 -0
- package/{.claude → .agents}/commands/drupal-list-nodes.md +4 -5
- package/{.claude → .agents}/commands/drupal-list-revisions.md +4 -5
- package/.agents/commands/drupal-list-roles.md +15 -0
- package/{.claude → .agents}/commands/drupal-list-sites.md +2 -3
- package/{.claude → .agents}/commands/drupal-list-translations.md +4 -5
- package/.agents/commands/drupal-list-users.md +19 -0
- package/.agents/commands/drupal-list-vocabularies.md +15 -0
- package/.agents/commands/drupal-mcp-whoami.md +15 -0
- package/.agents/commands/drupal-report-404-log.md +16 -0
- package/.agents/commands/drupal-report-accessibility-audit.md +17 -0
- package/.agents/commands/drupal-report-alias-coverage.md +17 -0
- package/.agents/commands/drupal-report-broken-embeds.md +17 -0
- package/{.claude → .agents}/commands/drupal-report-broken-links.md +4 -5
- package/.agents/commands/drupal-report-cache-config.md +15 -0
- package/.agents/commands/drupal-report-config-drift.md +15 -0
- package/.agents/commands/drupal-report-content-by-author.md +17 -0
- package/.agents/commands/drupal-report-content-summary.md +15 -0
- package/.agents/commands/drupal-report-duplicate-content.md +17 -0
- package/.agents/commands/drupal-report-field-completeness.md +20 -0
- package/.agents/commands/drupal-report-menu-integrity.md +16 -0
- package/{.claude → .agents}/commands/drupal-report-missing-field.md +4 -5
- package/.agents/commands/drupal-report-module-audit.md +15 -0
- package/.agents/commands/drupal-report-orphan-pages.md +17 -0
- package/{.claude → .agents}/commands/drupal-report-orphaned-references.md +4 -5
- package/.agents/commands/drupal-report-permission-audit.md +15 -0
- package/.agents/commands/drupal-report-pii-exposure.md +18 -0
- package/.agents/commands/drupal-report-readability.md +18 -0
- package/.agents/commands/drupal-report-recently-published.md +17 -0
- package/.agents/commands/drupal-report-redirect-health.md +16 -0
- package/.agents/commands/drupal-report-revision-hotspots.md +17 -0
- package/.agents/commands/drupal-report-scheduled-content.md +17 -0
- package/{.claude → .agents}/commands/drupal-report-seo-audit.md +4 -5
- package/.agents/commands/drupal-report-seo-meta-coverage.md +18 -0
- package/.agents/commands/drupal-report-stale-content.md +19 -0
- package/.agents/commands/drupal-report-status-report.md +16 -0
- package/.agents/commands/drupal-report-taxonomy-usage.md +21 -0
- package/.agents/commands/drupal-report-text-format-audit.md +15 -0
- package/.agents/commands/drupal-report-translation-coverage.md +18 -0
- package/.agents/commands/drupal-report-unpublished.md +17 -0
- package/.agents/commands/drupal-report-user-activity.md +17 -0
- package/.agents/commands/drupal-report-workflow-bottlenecks.md +19 -0
- package/{.claude → .agents}/commands/drupal-resolve-reference.md +4 -5
- package/{.claude → .agents}/commands/drupal-revert-revision.md +4 -5
- package/{.claude → .agents}/commands/drupal-schedule-publish.md +4 -5
- package/.agents/commands/drupal-search-content.md +21 -0
- package/.agents/commands/drupal-search.md +20 -0
- package/.agents/commands/drupal-security-info.md +15 -0
- package/.agents/commands/drupal-set-moderation-state.md +20 -0
- package/.agents/commands/drupal-site-info.md +15 -0
- package/.agents/commands/drupal-update-media.md +22 -0
- package/{.claude → .agents}/commands/drupal-update-menu-link.md +4 -5
- package/{.claude → .agents}/commands/drupal-update-node.md +10 -11
- package/{.claude → .agents}/commands/drupal-update-paragraph.md +4 -5
- package/{.claude → .agents}/commands/drupal-update-redirect.md +4 -5
- package/.agents/commands/drupal-update-taxonomy-term.md +22 -0
- package/.agents/commands/drupal-update-user.md +24 -0
- package/{.claude → .agents}/commands/drupal-upload-file-and-create-media.md +4 -5
- package/{.claude → .agents}/commands/drupal-upload-file.md +4 -5
- package/CHANGELOG.md +80 -0
- package/README.md +23 -12
- package/config/config.example.json +1 -1
- package/package.json +4 -2
- package/scripts/generate-commands.js +42 -21
- package/scripts/install-commands.js +157 -0
- package/src/lib/backends/backend-interface.js +20 -1
- package/src/lib/backends/jsonapi.js +47 -4
- package/src/lib/body-summary.js +60 -0
- package/src/lib/data-flow.js +437 -0
- package/src/lib/dispatch.js +99 -30
- package/src/lib/drupal-fetch.js +52 -6
- package/src/lib/field-definition.js +285 -0
- package/src/lib/operations.js +39 -2
- package/src/lib/patch-preflight.js +157 -66
- package/src/lib/security.js +15 -1
- package/src/lib/server-tools.js +9 -0
- package/src/lib/site-target.js +78 -0
- package/src/lib/tool-prompts.js +2 -1
- package/src/lib/write-revision.js +82 -2
- package/src/tools/bulk.js +4 -3
- package/src/tools/config.js +5 -2
- package/src/tools/entities.js +11 -7
- package/src/tools/index.js +24 -3
- package/src/tools/moderation.js +88 -9
- package/src/tools/nodes.js +55 -43
- package/.claude/commands/drupal-audit-config-best-practices.md +0 -16
- package/.claude/commands/drupal-block-user.md +0 -20
- package/.claude/commands/drupal-config-get.md +0 -19
- package/.claude/commands/drupal-config-list.md +0 -17
- package/.claude/commands/drupal-config-set.md +0 -20
- package/.claude/commands/drupal-content-by-moderation-state.md +0 -22
- package/.claude/commands/drupal-create-block.md +0 -21
- package/.claude/commands/drupal-create-taxonomy-term.md +0 -23
- package/.claude/commands/drupal-create-user.md +0 -24
- package/.claude/commands/drupal-delete-media.md +0 -22
- package/.claude/commands/drupal-delete-node.md +0 -23
- package/.claude/commands/drupal-delete-taxonomy-term.md +0 -22
- package/.claude/commands/drupal-drush-cache-rebuild.md +0 -16
- package/.claude/commands/drupal-drush-config-export.md +0 -16
- package/.claude/commands/drupal-drush-config-import.md +0 -16
- package/.claude/commands/drupal-drush-config-status.md +0 -16
- package/.claude/commands/drupal-drush-cron.md +0 -16
- package/.claude/commands/drupal-drush-module-disable.md +0 -21
- package/.claude/commands/drupal-drush-module-enable.md +0 -19
- package/.claude/commands/drupal-drush-module-list.md +0 -17
- package/.claude/commands/drupal-drush-security-updates.md +0 -16
- package/.claude/commands/drupal-drush-status.md +0 -16
- package/.claude/commands/drupal-drush-updatedb.md +0 -16
- package/.claude/commands/drupal-drush-user-create.md +0 -22
- package/.claude/commands/drupal-drush-user-list.md +0 -19
- package/.claude/commands/drupal-drush-watchdog.md +0 -19
- package/.claude/commands/drupal-entity-delete.md +0 -24
- package/.claude/commands/drupal-entity-get.md +0 -22
- package/.claude/commands/drupal-find-orphaned-media.md +0 -18
- package/.claude/commands/drupal-get-entity-schema.md +0 -20
- package/.claude/commands/drupal-get-media.md +0 -20
- package/.claude/commands/drupal-get-node.md +0 -20
- package/.claude/commands/drupal-get-taxonomy-term.md +0 -20
- package/.claude/commands/drupal-get-taxonomy-terms.md +0 -21
- package/.claude/commands/drupal-get-user-by-name.md +0 -19
- package/.claude/commands/drupal-get-user.md +0 -19
- package/.claude/commands/drupal-graphql-introspect.md +0 -17
- package/.claude/commands/drupal-list-content-types.md +0 -16
- package/.claude/commands/drupal-list-entity-types.md +0 -16
- package/.claude/commands/drupal-list-media-types.md +0 -16
- package/.claude/commands/drupal-list-media.md +0 -21
- package/.claude/commands/drupal-list-moderation-states.md +0 -20
- package/.claude/commands/drupal-list-roles.md +0 -16
- package/.claude/commands/drupal-list-users.md +0 -20
- package/.claude/commands/drupal-list-vocabularies.md +0 -16
- package/.claude/commands/drupal-mcp-whoami.md +0 -16
- package/.claude/commands/drupal-report-404-log.md +0 -17
- package/.claude/commands/drupal-report-accessibility-audit.md +0 -18
- package/.claude/commands/drupal-report-alias-coverage.md +0 -18
- package/.claude/commands/drupal-report-broken-embeds.md +0 -18
- package/.claude/commands/drupal-report-cache-config.md +0 -16
- package/.claude/commands/drupal-report-config-drift.md +0 -16
- package/.claude/commands/drupal-report-content-by-author.md +0 -18
- package/.claude/commands/drupal-report-content-summary.md +0 -16
- package/.claude/commands/drupal-report-duplicate-content.md +0 -18
- package/.claude/commands/drupal-report-field-completeness.md +0 -21
- package/.claude/commands/drupal-report-menu-integrity.md +0 -17
- package/.claude/commands/drupal-report-module-audit.md +0 -16
- package/.claude/commands/drupal-report-orphan-pages.md +0 -18
- package/.claude/commands/drupal-report-permission-audit.md +0 -16
- package/.claude/commands/drupal-report-pii-exposure.md +0 -19
- package/.claude/commands/drupal-report-readability.md +0 -19
- package/.claude/commands/drupal-report-recently-published.md +0 -18
- package/.claude/commands/drupal-report-redirect-health.md +0 -17
- package/.claude/commands/drupal-report-revision-hotspots.md +0 -18
- package/.claude/commands/drupal-report-scheduled-content.md +0 -18
- package/.claude/commands/drupal-report-seo-meta-coverage.md +0 -19
- package/.claude/commands/drupal-report-stale-content.md +0 -20
- package/.claude/commands/drupal-report-status-report.md +0 -17
- package/.claude/commands/drupal-report-taxonomy-usage.md +0 -22
- package/.claude/commands/drupal-report-text-format-audit.md +0 -16
- package/.claude/commands/drupal-report-translation-coverage.md +0 -19
- package/.claude/commands/drupal-report-unpublished.md +0 -18
- package/.claude/commands/drupal-report-user-activity.md +0 -18
- package/.claude/commands/drupal-report-workflow-bottlenecks.md +0 -20
- package/.claude/commands/drupal-search-content.md +0 -22
- package/.claude/commands/drupal-search.md +0 -21
- package/.claude/commands/drupal-security-info.md +0 -16
- package/.claude/commands/drupal-set-moderation-state.md +0 -21
- package/.claude/commands/drupal-site-info.md +0 -16
- package/.claude/commands/drupal-update-media.md +0 -23
- package/.claude/commands/drupal-update-taxonomy-term.md +0 -23
- package/.claude/commands/drupal-update-user.md +0 -25
package/src/tools/config.js
CHANGED
|
@@ -107,7 +107,7 @@ function inferTier(site, sec) {
|
|
|
107
107
|
* @param {object} args - { site? }.
|
|
108
108
|
* @returns {Promise<object>} Effective identity + capability summary.
|
|
109
109
|
*/
|
|
110
|
-
async function whoami({ site: siteName }) {
|
|
110
|
+
async function whoami({ site: siteName, _resolvedSource }) {
|
|
111
111
|
const site = getSiteConfig(siteName);
|
|
112
112
|
const sec = resolveSecurityConfig(site);
|
|
113
113
|
const summary = getSecuritySummary(site);
|
|
@@ -119,9 +119,12 @@ async function whoami({ site: siteName }) {
|
|
|
119
119
|
const canWrite = !sec.readOnly && hasScope(site, "mcp_write");
|
|
120
120
|
const canConfig = hasScope(site, "mcp_config");
|
|
121
121
|
const identity = getRequestIdentity();
|
|
122
|
+
// Dispatch may inject `site` after a default/grant resolution. Prefer the
|
|
123
|
+
// authoritative source so `target` and `_target` cannot disagree (#167).
|
|
124
|
+
const source = _resolvedSource ?? (siteName ? "hint" : "default");
|
|
122
125
|
return {
|
|
123
126
|
site: site._name,
|
|
124
|
-
target: describeTarget(site,
|
|
127
|
+
target: describeTarget(site, source),
|
|
125
128
|
principal: identity
|
|
126
129
|
? { sub: identity.sub, clientId: identity.clientId, scopes: [...(identity.scopes ?? [])] }
|
|
127
130
|
: null,
|
package/src/tools/entities.js
CHANGED
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
resolveErrRelationships, relationshipsWereSent, embedParagraphRef,
|
|
17
17
|
resolveParagraphRevisionId, missingParagraphRevisionError,
|
|
18
18
|
} from "../lib/err-relationships.js";
|
|
19
|
-
import { readWrittenRevision } from "../lib/write-revision.js";
|
|
20
|
-
import {
|
|
19
|
+
import { attachWrittenRevisionPair, readWrittenRevision } from "../lib/write-revision.js";
|
|
20
|
+
import { prepareGuardedPatch, updateEntityGuarded } from "../lib/patch-preflight.js";
|
|
21
21
|
import {
|
|
22
22
|
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed, assertDeleteAllowed, assertPublishAllowed,
|
|
23
23
|
redactCanonicalEntity, getSecuritySummary,
|
|
@@ -117,8 +117,8 @@ async function updateEntity({ site: siteName, entityType, bundle, id, attributes
|
|
|
117
117
|
});
|
|
118
118
|
assertPublishAllowed(sec, safeAttributes);
|
|
119
119
|
const resolvedRelationships = await resolveErrRelationships(backend, relationships);
|
|
120
|
-
await
|
|
121
|
-
|
|
120
|
+
const patchTarget = await prepareGuardedPatch(backend, {
|
|
121
|
+
entityType, bundle, id, existing, attributes: safeAttributes,
|
|
122
122
|
});
|
|
123
123
|
if (dryRun) {
|
|
124
124
|
return {
|
|
@@ -128,6 +128,7 @@ async function updateEntity({ site: siteName, entityType, bundle, id, attributes
|
|
|
128
128
|
}
|
|
129
129
|
const result = await updateEntityGuarded(backend, {
|
|
130
130
|
entityType, bundle, id, attributes: safeAttributes, relationships: resolvedRelationships,
|
|
131
|
+
...(patchTarget.resourceVersion ? { resourceVersion: patchTarget.resourceVersion } : {}),
|
|
131
132
|
});
|
|
132
133
|
const written = await readWrittenRevision({
|
|
133
134
|
backend, entityType, bundle, id,
|
|
@@ -135,7 +136,10 @@ async function updateEntity({ site: siteName, entityType, bundle, id, attributes
|
|
|
135
136
|
patchResult: result,
|
|
136
137
|
preferCanonical: false,
|
|
137
138
|
});
|
|
138
|
-
|
|
139
|
+
const withRevs = await attachWrittenRevisionPair({
|
|
140
|
+
backend, entityType, bundle, id, entity: written, liveVid: patchTarget.liveVid,
|
|
141
|
+
});
|
|
142
|
+
return shapeWriteResponse(flagUnrequestedStatusChange(withRevs, existing, safeAttributes), returning);
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
/**
|
|
@@ -276,7 +280,7 @@ export const definitions = [
|
|
|
276
280
|
},
|
|
277
281
|
{
|
|
278
282
|
name: "drupal_entity_update",
|
|
279
|
-
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)
|
|
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.",
|
|
280
284
|
inputSchema: {
|
|
281
285
|
type: "object", required: ["entityType", "bundle", "id"],
|
|
282
286
|
properties: {
|
|
@@ -286,7 +290,7 @@ export const definitions = [
|
|
|
286
290
|
id: { type: "string" },
|
|
287
291
|
attributes: { type: "object" },
|
|
288
292
|
relationships: { type: "object" },
|
|
289
|
-
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. A working-copy 400 fails the dryRun." },
|
|
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." },
|
|
290
294
|
returning: RETURNING_SCHEMA,
|
|
291
295
|
},
|
|
292
296
|
},
|
package/src/tools/index.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Each tool module exports `definitions` (the MCP `{name, description, inputSchema}`
|
|
5
5
|
* descriptors) and `handlers` (a name → function dispatch map). This module flattens
|
|
6
6
|
* all of them into the runtime payloads consumed by the server (src/index.js) AND by
|
|
7
|
-
* the offline slash-command generator (scripts/generate-commands.js
|
|
8
|
-
* list, the per-tool MCP prompts, and the
|
|
7
|
+
* the offline slash-command generator (scripts/generate-commands.js →
|
|
8
|
+
* `.agents/commands/`), so the tool list, the per-tool MCP prompts, and the
|
|
9
|
+
* `/drupal-*` command files can never drift.
|
|
9
10
|
*
|
|
10
11
|
* Importing this module has NO side effects (unlike src/index.js, which boots the
|
|
11
12
|
* transport), so generators and tests can import it freely.
|
|
@@ -37,14 +38,34 @@ import * as reportsConfig from "./reports-config.js";
|
|
|
37
38
|
import * as reportsContent from "./reports-content.js";
|
|
38
39
|
import * as auditComposite from "./audit-composite.js";
|
|
39
40
|
import * as config from "./config.js";
|
|
41
|
+
import { SITE_PARAM } from "../lib/site-target.js";
|
|
40
42
|
|
|
41
43
|
export const allModules = [nodes, taxonomy, users, media, graphql, site, entities, reports, drush,
|
|
42
44
|
revisions, moderation, scheduler, fields, references, bulk, translations, paragraphs, structure, redirects, search, reportsExtra,
|
|
43
45
|
reportsLinks, reportsConfig, reportsContent, auditComposite, config];
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Stamp the shared `site` description onto every tool that accepts one so
|
|
49
|
+
* agents are not told "omit for the default" as if that meant production.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} def Tool definition.
|
|
52
|
+
* @returns {object}
|
|
53
|
+
*/
|
|
54
|
+
function withSiteParam(def) {
|
|
55
|
+
const props = def.inputSchema?.properties;
|
|
56
|
+
if (!props?.site) return def;
|
|
57
|
+
return {
|
|
58
|
+
...def,
|
|
59
|
+
inputSchema: {
|
|
60
|
+
...def.inputSchema,
|
|
61
|
+
properties: { ...props, site: { ...props.site, ...SITE_PARAM } },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
// Flatten every module's tool definitions into one ListTools payload, and merge
|
|
46
67
|
// their handler maps into a single closed dispatch table keyed by tool name.
|
|
47
|
-
export const allDefinitions = allModules.flatMap((m) => m.definitions);
|
|
68
|
+
export const allDefinitions = allModules.flatMap((m) => m.definitions).map(withSiteParam);
|
|
48
69
|
export const allHandlers = Object.assign({}, ...allModules.map((m) => m.handlers));
|
|
49
70
|
|
|
50
71
|
// Fast name → definition lookup for prompt/command rendering.
|
package/src/tools/moderation.js
CHANGED
|
@@ -20,6 +20,25 @@ import {
|
|
|
20
20
|
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed, assertPublishAllowed,
|
|
21
21
|
redactCanonicalEntity,
|
|
22
22
|
} from "../lib/security.js";
|
|
23
|
+
import { collectEntities } from "../lib/reports-support.js";
|
|
24
|
+
|
|
25
|
+
/** Cap for the client-side scan when JSON:API cannot filter the field. */
|
|
26
|
+
const SAMPLE_CAP = 500;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Drupal core JSON:API cannot filter on the computed `moderation_state`
|
|
30
|
+
* field. The error is a 500 whose detail is `'moderation_state' not found`
|
|
31
|
+
* (#162). jsonapi_extras (or a similar alias) can make the field filterable;
|
|
32
|
+
* those sites never hit this.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} err
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function isUnfilterableModerationState(err) {
|
|
38
|
+
const msg = String(err?.message || "");
|
|
39
|
+
if (!/moderation_state/i.test(msg)) return false;
|
|
40
|
+
return /not found/i.test(msg) || /not filterable/i.test(msg) || /invalid filter/i.test(msg);
|
|
41
|
+
}
|
|
23
42
|
|
|
24
43
|
/** Read a node's moderation_state from a canonical entity, tolerating shapes. */
|
|
25
44
|
function moderationStateOf(entity) {
|
|
@@ -49,6 +68,12 @@ async function setModerationState({ site: siteName, type, id, state }) {
|
|
|
49
68
|
|
|
50
69
|
/**
|
|
51
70
|
* List nodes of a type in a given moderation state, paged + redacted.
|
|
71
|
+
*
|
|
72
|
+
* Stock JSON:API cannot filter on the computed `moderation_state` field
|
|
73
|
+
* (#162). When the site accepts the filter (jsonapi_extras or similar) the
|
|
74
|
+
* result is exact. Otherwise we scan a bounded recent set and filter
|
|
75
|
+
* client-side rather than surfacing Drupal's 500 as a transient error.
|
|
76
|
+
*
|
|
52
77
|
* @param {object} args - { site?, type, state, limit?, offset? }.
|
|
53
78
|
*/
|
|
54
79
|
async function contentByModerationState({ site: siteName, type, state, limit = 20, offset = 0 }) {
|
|
@@ -56,14 +81,68 @@ async function contentByModerationState({ site: siteName, type, state, limit = 2
|
|
|
56
81
|
const sec = resolveSecurityConfig(site);
|
|
57
82
|
assertReadAllowed(sec, "node", type);
|
|
58
83
|
const backend = await resolveBackend(site);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
const sort = [{ field: "changed", dir: "desc" }];
|
|
85
|
+
const canFilter = typeof backend.capabilities === "function"
|
|
86
|
+
? Boolean(backend.capabilities()?.filter)
|
|
87
|
+
: true;
|
|
88
|
+
|
|
89
|
+
if (canFilter) {
|
|
90
|
+
try {
|
|
91
|
+
const res = await backend.listEntities({
|
|
92
|
+
entityType: "node", bundle: type,
|
|
93
|
+
filters: [{ field: "moderation_state", op: "eq", value: state }],
|
|
94
|
+
sort,
|
|
95
|
+
page: { limit, offset },
|
|
96
|
+
});
|
|
97
|
+
const nodes = res.entities.map((e) => redactCanonicalEntity(e, sec, "node"));
|
|
98
|
+
return {
|
|
99
|
+
type, state, source: "filter",
|
|
100
|
+
total: res.page?.total ?? nodes.length,
|
|
101
|
+
approximate: res.approximate ?? false,
|
|
102
|
+
offset, nextOffset: offset + nodes.length, nodes,
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (!isUnfilterableModerationState(err)) throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const scanned = await collectEntities(
|
|
110
|
+
backend,
|
|
111
|
+
{ entityType: "node", bundle: type, sort },
|
|
112
|
+
SAMPLE_CAP,
|
|
113
|
+
);
|
|
114
|
+
const wanted = String(state).toLowerCase();
|
|
115
|
+
const matches = [];
|
|
116
|
+
let sawState = false;
|
|
117
|
+
for (const entity of scanned) {
|
|
118
|
+
const got = moderationStateOf(entity);
|
|
119
|
+
if (got === null || got === undefined) continue;
|
|
120
|
+
sawState = true;
|
|
121
|
+
if (String(got).toLowerCase() === wanted) matches.push(entity);
|
|
122
|
+
}
|
|
123
|
+
if (!sawState && scanned.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
type, state, unavailable: true, source: "sampled", scanned: scanned.length,
|
|
126
|
+
reason:
|
|
127
|
+
"moderation_state is not filterable over JSON:API on this site (it is a " +
|
|
128
|
+
"computed field) and no sampled entity exposed the field. Enable " +
|
|
129
|
+
"content_moderation on the bundle, or expose a filterable alias " +
|
|
130
|
+
"(jsonapi_extras).",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const page = matches.slice(offset, offset + limit);
|
|
134
|
+
return {
|
|
135
|
+
type, state, source: "sampled",
|
|
136
|
+
approximate: scanned.length >= SAMPLE_CAP,
|
|
137
|
+
scanned: scanned.length,
|
|
138
|
+
total: matches.length,
|
|
139
|
+
offset, nextOffset: offset + page.length,
|
|
140
|
+
nodes: page.map((e) => redactCanonicalEntity(e, sec, "node")),
|
|
141
|
+
note:
|
|
142
|
+
"Stock JSON:API cannot filter on moderation_state (computed field). " +
|
|
143
|
+
"Results are a client-side sample of recent nodes, not a complete list. " +
|
|
144
|
+
"A filterable alias (jsonapi_extras) makes this exact.",
|
|
145
|
+
};
|
|
67
146
|
}
|
|
68
147
|
|
|
69
148
|
/**
|
|
@@ -101,7 +180,7 @@ export const definitions = [
|
|
|
101
180
|
},
|
|
102
181
|
{
|
|
103
182
|
name: "drupal_content_by_moderation_state",
|
|
104
|
-
description: "List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review').",
|
|
183
|
+
description: "List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Stock JSON:API cannot filter the computed moderation_state field; when the site rejects that filter the tool samples recent nodes client-side and marks the result approximate, instead of returning Drupal's 500.",
|
|
105
184
|
inputSchema: {
|
|
106
185
|
type: "object", required: ["type", "state"],
|
|
107
186
|
properties: {
|
package/src/tools/nodes.js
CHANGED
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
import { applySafeDraftDefault, hasExplicitModerationState } from "../lib/moderation-default.js";
|
|
17
17
|
import { shapeWriteResponse, flagUnrequestedStatusChange, RETURNING_SCHEMA } from "../lib/entity-response.js";
|
|
18
18
|
import { resolveErrRelationships, relationshipsWereSent } from "../lib/err-relationships.js";
|
|
19
|
-
import { readWrittenRevision } from "../lib/write-revision.js";
|
|
20
|
-
import {
|
|
19
|
+
import { attachWrittenRevisionPair, readWrittenRevision } from "../lib/write-revision.js";
|
|
20
|
+
import { prepareGuardedPatch, updateEntityGuarded } from "../lib/patch-preflight.js";
|
|
21
|
+
import { assertBodySummaryWritable, attachSummaryDeprecation } from "../lib/body-summary.js";
|
|
21
22
|
import { buildRedirectAttributes, REDIRECT_ENTITY_TYPE } from "./redirects.js";
|
|
23
|
+
import { applyAllowedFormatsToAttributes } from "../lib/field-definition.js";
|
|
22
24
|
|
|
23
25
|
/** Fallback language for an alias when the node exposes none. */
|
|
24
26
|
const DEFAULT_ALIAS_LANGCODE = "en";
|
|
@@ -130,22 +132,14 @@ async function createRenameRedirect(backend, sec, redirect) {
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
/**
|
|
134
|
-
* Fallback text format when neither the call nor the site config names one.
|
|
135
|
-
*
|
|
136
|
-
* Retained for backward compatibility only. A site whose text formats do not
|
|
137
|
-
* include `full_html` — or whose agent account may not use it — must set
|
|
138
|
-
* `defaultTextFormat` in its site config or pass `format` per call.
|
|
139
|
-
*/
|
|
140
|
-
const FALLBACK_TEXT_FORMAT = "full_html";
|
|
141
|
-
|
|
142
135
|
/**
|
|
143
136
|
* Build a Drupal body field descriptor from plain HTML + optional summary.
|
|
144
137
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
138
|
+
* Format is left omitted when the caller did not pass one so
|
|
139
|
+
* `applyAllowedFormatsToAttributes` can honor Field API `allowed_formats`
|
|
140
|
+
* before applying the historical `defaultTextFormat` / `full_html` chain.
|
|
141
|
+
* Applying that fallback first would treat `full_html` as a caller format
|
|
142
|
+
* and refuse a single-allowed-format field (#168).
|
|
149
143
|
*
|
|
150
144
|
* `summary` is only included when the caller supplied it. Sending an empty
|
|
151
145
|
* string on every write would blank an existing body summary whenever a
|
|
@@ -154,17 +148,14 @@ const FALLBACK_TEXT_FORMAT = "full_html";
|
|
|
154
148
|
* @param {string} [body] - Body HTML; when undefined the field is omitted.
|
|
155
149
|
* @param {string} [summary] - Teaser/summary text; omitted when undefined.
|
|
156
150
|
* @param {string} [format] - Text format machine name.
|
|
157
|
-
* @
|
|
158
|
-
* @returns {{value: string, format: string, summary?: string}|undefined}
|
|
151
|
+
* @returns {{value: string, format?: string, summary?: string}|undefined}
|
|
159
152
|
* A body attribute object, or undefined when no body was supplied (so callers
|
|
160
153
|
* can skip the field on update rather than blanking it).
|
|
161
154
|
*/
|
|
162
|
-
function buildBodyAttribute(body, summary, format
|
|
155
|
+
function buildBodyAttribute(body, summary, format) {
|
|
163
156
|
if (body === undefined) return undefined;
|
|
164
|
-
const attr = {
|
|
165
|
-
|
|
166
|
-
format: format ?? site?.defaultTextFormat ?? FALLBACK_TEXT_FORMAT,
|
|
167
|
-
};
|
|
157
|
+
const attr = { value: body };
|
|
158
|
+
if (format !== undefined) attr.format = format;
|
|
168
159
|
if (summary !== undefined) attr.summary = summary;
|
|
169
160
|
return attr;
|
|
170
161
|
}
|
|
@@ -263,18 +254,25 @@ async function createNode({ site: siteName, type, title, body, summary, format,
|
|
|
263
254
|
const site = getSiteConfig(siteName);
|
|
264
255
|
const sec = resolveSecurityConfig(site);
|
|
265
256
|
assertWriteAllowed(sec, "create", "node", type);
|
|
257
|
+
const backend = await resolveBackend(site);
|
|
266
258
|
const attributes = { title, ...fields };
|
|
267
259
|
if (moderationState !== undefined) {
|
|
268
260
|
attributes.moderation_state = moderationState;
|
|
269
261
|
} else {
|
|
270
262
|
attributes.status = status === undefined ? false : status;
|
|
271
263
|
}
|
|
272
|
-
const bodyAttr = buildBodyAttribute(body, summary, format
|
|
264
|
+
const bodyAttr = buildBodyAttribute(body, summary, format);
|
|
273
265
|
if (bodyAttr) attributes.body = bodyAttr;
|
|
266
|
+
await applyAllowedFormatsToAttributes({
|
|
267
|
+
backend, site, entityType: "node", bundle: type, attributes,
|
|
268
|
+
});
|
|
269
|
+
const summaryWrite = await assertBodySummaryWritable(backend, type, summary);
|
|
274
270
|
assertPublishAllowed(sec, attributes);
|
|
275
|
-
const backend = await resolveBackend(site);
|
|
276
271
|
const resolvedRelationships = await resolveErrRelationships(backend, relationships);
|
|
277
|
-
if (dryRun)
|
|
272
|
+
if (dryRun) {
|
|
273
|
+
const preview = { dryRun: true, operation: "create", entityType: "node", bundle: type, attributes, relationships: resolvedRelationships };
|
|
274
|
+
return summaryWrite.deprecated && bodyAttr ? attachSummaryDeprecation(preview) : preview;
|
|
275
|
+
}
|
|
278
276
|
// Alias handling: an explicit `path.alias` is set as a manual alias; otherwise
|
|
279
277
|
// `path` is omitted so pathauto generates the alias (DEV-116).
|
|
280
278
|
const { pathAttr } = await resolvePathWrite({ backend, type, id: null, providedPath: attributes.path, isCreate: true });
|
|
@@ -284,7 +282,8 @@ async function createNode({ site: siteName, type, title, body, summary, format,
|
|
|
284
282
|
// Honest response: re-read so the persisted alias (explicit or pathauto-generated)
|
|
285
283
|
// is reflected rather than the pre-alias write response.
|
|
286
284
|
const fresh = await backend.getEntity({ entityType: "node", bundle: type, id: created.id }).catch(() => null);
|
|
287
|
-
|
|
285
|
+
const shaped = shapeWriteResponse(fresh ?? created, returning);
|
|
286
|
+
return summaryWrite.deprecated && bodyAttr ? attachSummaryDeprecation(shaped) : shaped;
|
|
288
287
|
}
|
|
289
288
|
|
|
290
289
|
/**
|
|
@@ -316,12 +315,16 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
316
315
|
const sec = resolveSecurityConfig(site);
|
|
317
316
|
assertWriteAllowed(sec, "update", "node", type);
|
|
318
317
|
const backend = await resolveBackend(site);
|
|
318
|
+
const summaryWrite = await assertBodySummaryWritable(backend, type, summary);
|
|
319
319
|
let attributes = { ...fields };
|
|
320
320
|
if (title !== undefined) attributes.title = title;
|
|
321
321
|
if (moderationState !== undefined) attributes.moderation_state = moderationState;
|
|
322
322
|
else if (status !== undefined) attributes.status = status;
|
|
323
|
-
const bodyAttr = buildBodyAttribute(body, summary, format
|
|
323
|
+
const bodyAttr = buildBodyAttribute(body, summary, format);
|
|
324
324
|
if (bodyAttr) attributes.body = bodyAttr;
|
|
325
|
+
await applyAllowedFormatsToAttributes({
|
|
326
|
+
backend, site, entityType: "node", bundle: type, attributes,
|
|
327
|
+
});
|
|
325
328
|
// One pre-read serves the #131 draft default and the #171 unrequested-
|
|
326
329
|
// status-change flag. Skipped when the caller pinned the moderation state.
|
|
327
330
|
let existing = null;
|
|
@@ -341,16 +344,18 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
341
344
|
// #192: resolve paragraph ERR identifiers before any host PATCH. An unresolved
|
|
342
345
|
// list would persist empty — fail the whole write instead.
|
|
343
346
|
const resolvedRelationships = await resolveErrRelationships(backend, relationships);
|
|
344
|
-
// #201: the
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
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.
|
|
350
|
+
const patchTarget = await prepareGuardedPatch(backend, {
|
|
351
|
+
entityType: "node", bundle: type, id, existing, attributes,
|
|
348
352
|
});
|
|
349
353
|
if (dryRun) {
|
|
350
|
-
|
|
354
|
+
const preview = {
|
|
351
355
|
dryRun: true, operation: "update", entityType: "node", bundle: type, id,
|
|
352
356
|
attributes, relationships: resolvedRelationships,
|
|
353
357
|
};
|
|
358
|
+
return summaryWrite.deprecated && bodyAttr ? attachSummaryDeprecation(preview) : preview;
|
|
354
359
|
}
|
|
355
360
|
// Alias handling (DEV-116): an explicit `path.alias` is set in place by
|
|
356
361
|
// round-tripping the existing alias's pid (no duplicate); a path-less update
|
|
@@ -361,6 +366,7 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
361
366
|
else attributes.path = pathAttr;
|
|
362
367
|
const patched = await updateEntityGuarded(backend, {
|
|
363
368
|
entityType: "node", bundle: type, id, attributes, relationships: resolvedRelationships,
|
|
369
|
+
...(patchTarget.resourceVersion ? { resourceVersion: patchTarget.resourceVersion } : {}),
|
|
364
370
|
});
|
|
365
371
|
const redirectResult = redirect ? await createRenameRedirect(backend, sec, redirect) : null;
|
|
366
372
|
// #169: when relationships were sent, the canonical re-read is the published
|
|
@@ -370,12 +376,18 @@ async function updateNode({ site: siteName, type, id, title, body, summary, form
|
|
|
370
376
|
relationshipsSent: relationshipsWereSent(resolvedRelationships),
|
|
371
377
|
patchResult: patched,
|
|
372
378
|
preferCanonical: true,
|
|
379
|
+
resourceVersion: patchTarget.resourceVersion,
|
|
380
|
+
});
|
|
381
|
+
const withRevs = await attachWrittenRevisionPair({
|
|
382
|
+
backend, entityType: "node", bundle: type, id, entity: fresh, liveVid: patchTarget.liveVid,
|
|
373
383
|
});
|
|
374
384
|
// #171: an unrequested published-state flip in the persisted node is
|
|
375
385
|
// reported via _statusChanged rather than returned as a clean success.
|
|
376
|
-
const flagged = flagUnrequestedStatusChange(
|
|
377
|
-
|
|
378
|
-
|
|
386
|
+
const flagged = flagUnrequestedStatusChange(withRevs, existing, attributes);
|
|
387
|
+
const shaped = flagged && redirectResult
|
|
388
|
+
? shapeWriteResponse({ ...flagged, _redirect: redirectResult }, returning)
|
|
389
|
+
: shapeWriteResponse(flagged ?? { id }, returning);
|
|
390
|
+
return summaryWrite.deprecated && bodyAttr ? attachSummaryDeprecation(shaped) : shaped;
|
|
379
391
|
}
|
|
380
392
|
|
|
381
393
|
/**
|
|
@@ -452,11 +464,11 @@ export const definitions = [
|
|
|
452
464
|
type: { type: "string", description: "Content type machine name" },
|
|
453
465
|
title: { type: "string" },
|
|
454
466
|
body: { type: "string", description: "Body field HTML" },
|
|
455
|
-
summary: { type: "string", description: "Body summary/teaser — writes
|
|
456
|
-
format: { type: "string", description: "Text format machine name for the body, e.g. 'basic_html'.
|
|
467
|
+
summary: { type: "string", description: "Body summary/teaser — writes body.summary on core text_with_summary only. Refused when the sampled body field has no summary property (text_long / text_formatted) or the schema cannot be determined. Prefer the site's dedicated deck/summary field via `fields`." },
|
|
468
|
+
format: { type: "string", description: "Text format machine name for the body, e.g. 'basic_html'. When the body field's allowed_formats lists exactly one format, that is the default. A caller format outside that list is refused before write. When allowed_formats cannot be resolved, defaults to the site config's `defaultTextFormat`, then 'full_html'." },
|
|
457
469
|
status: { type: "boolean", default: false, description: "Published flag for NON-moderated types. true to publish immediately. Ignored if moderationState is set; on a moderated type it is dropped automatically." },
|
|
458
470
|
moderationState: { type: "string", description: "Moderation state for content_moderation types, e.g. 'draft' or 'published'. Takes precedence over status." },
|
|
459
|
-
fields: { type: "object", description: "Scalar/attribute field values keyed by Drupal machine name. Do NOT put entity-reference fields here — Drupal rejects them as attributes; use `relationships`." },
|
|
471
|
+
fields: { type: "object", description: "Scalar/attribute field values keyed by Drupal 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. Do NOT put entity-reference fields here — Drupal rejects them as attributes; use `relationships`." },
|
|
460
472
|
relationships: { type: "object", description: "Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value: { field_resource_type: { data: { type: 'taxonomy_term--resource_type', id: '<uuid>' } } }. Multi-value: { field_tags: { data: [{ type: 'taxonomy_term--tags', id: '<uuid>' }] } }." },
|
|
461
473
|
dryRun: { type: "boolean", default: false, description: "Validate and return a preview of the write without committing." },
|
|
462
474
|
returning: RETURNING_SCHEMA,
|
|
@@ -465,7 +477,7 @@ export const definitions = [
|
|
|
465
477
|
},
|
|
466
478
|
{
|
|
467
479
|
name: "drupal_update_node",
|
|
468
|
-
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 —
|
|
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.",
|
|
469
481
|
inputSchema: {
|
|
470
482
|
type: "object", required: ["type", "id"],
|
|
471
483
|
properties: {
|
|
@@ -474,13 +486,13 @@ export const definitions = [
|
|
|
474
486
|
id: { type: "string", description: "Node UUID" },
|
|
475
487
|
title: { type: "string" },
|
|
476
488
|
body: { type: "string" },
|
|
477
|
-
summary: { type: "string", description: "Body summary/teaser — writes
|
|
478
|
-
format: { type: "string", description: "Text format machine name for the body, e.g. 'basic_html'.
|
|
489
|
+
summary: { type: "string", description: "Body summary/teaser — writes body.summary on core text_with_summary only. Refused when the sampled body field has no summary property (text_long / text_formatted) or the schema cannot be determined. Prefer the site's dedicated deck/summary field via `fields`." },
|
|
490
|
+
format: { type: "string", description: "Text format machine name for the body, e.g. 'basic_html'. When the body field's allowed_formats lists exactly one format, that is the default. A caller format outside that list is refused before write. When allowed_formats cannot be resolved, defaults to the site config's `defaultTextFormat`, then 'full_html'." },
|
|
479
491
|
status: { type: "boolean", description: "Published flag for NON-moderated types: true = publish, false = unpublish. Ignored if moderationState is set." },
|
|
480
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'." },
|
|
481
|
-
fields: { type: "object", description: "Scalar/attribute field values keyed by machine name. Entity-reference fields go in `relationships`, not here." },
|
|
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." },
|
|
482
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." },
|
|
483
|
-
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. 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. 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." },
|
|
484
496
|
returning: RETURNING_SCHEMA,
|
|
485
497
|
},
|
|
486
498
|
},
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Lint key configuration for production-readiness and security: on-screen error display, CSS/JS aggregation, anonymous page cache, open user registration, missing 404/403 pages, and insecure file uploads. Severity-ranked. Requires config-read access and the server-tool bridge."
|
|
3
|
-
argument-hint: "[site]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_audit_config_best_practices
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_audit_config_best_practices` MCP tool.
|
|
8
|
-
|
|
9
|
-
Lint key configuration for production-readiness and security: on-screen error display, CSS/JS aggregation, anonymous page cache, open user registration, missing 404/403 pages, and insecure file uploads. Severity-ranked. Requires config-read access and the server-tool bridge.
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Optional:**
|
|
14
|
-
- `site` (string): omit for the default site
|
|
15
|
-
|
|
16
|
-
If a required parameter is missing from `$ARGUMENTS`, 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,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Block or unblock a Drupal user account without deleting it."
|
|
3
|
-
argument-hint: "<id> [site] [block]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_block_user
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_block_user` MCP tool.
|
|
8
|
-
|
|
9
|
-
Block or unblock a Drupal user account without deleting it.
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Required:**
|
|
14
|
-
- `id` (string): User UUID
|
|
15
|
-
|
|
16
|
-
**Optional:**
|
|
17
|
-
- `site` (string): omit for the default site
|
|
18
|
-
- `block` (boolean (true/false)): true = block, false = unblock
|
|
19
|
-
|
|
20
|
-
If a required parameter is missing from `$ARGUMENTS`, 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,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Read a single Drupal configuration object by name (e.g. \"system.site\") via the governed server-side config tool. Requires config read access."
|
|
3
|
-
argument-hint: "<name> [site]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_config_get
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_config_get` MCP tool.
|
|
8
|
-
|
|
9
|
-
Read a single Drupal configuration object by name (e.g. "system.site") via the governed server-side config tool. Requires config read access.
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Required:**
|
|
14
|
-
- `name` (string)
|
|
15
|
-
|
|
16
|
-
**Optional:**
|
|
17
|
-
- `site` (string): omit for the default site
|
|
18
|
-
|
|
19
|
-
If a required parameter is missing from `$ARGUMENTS`, 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,17 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "List Drupal configuration object names, optionally filtered by a name prefix, via the governed server-side config tool. Requires config read access."
|
|
3
|
-
argument-hint: "[site] [prefix]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_config_list
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_config_list` MCP tool.
|
|
8
|
-
|
|
9
|
-
List Drupal configuration object names, optionally filtered by a name prefix, via the governed server-side config tool. Requires config read access.
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Optional:**
|
|
14
|
-
- `site` (string): omit for the default site
|
|
15
|
-
- `prefix` (string)
|
|
16
|
-
|
|
17
|
-
If a required parameter is missing from `$ARGUMENTS`, 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,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Set a Drupal configuration value via the governed server-side config tool. Audited and gated server-side; requires the config-editor (Developer) tier. Then export to YAML for a PR."
|
|
3
|
-
argument-hint: "<name> <value> [site]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_config_set
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_config_set` MCP tool.
|
|
8
|
-
|
|
9
|
-
Set a Drupal configuration value via the governed server-side config tool. Audited and gated server-side; requires the config-editor (Developer) tier. Then export to YAML for a PR.
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Required:**
|
|
14
|
-
- `name` (string)
|
|
15
|
-
- `value` (object (pass as JSON)): A map of top-level config keys to their new values (e.g. { "slogan": "Information Technology" }). Other keys in the object are preserved server-side.
|
|
16
|
-
|
|
17
|
-
**Optional:**
|
|
18
|
-
- `site` (string): omit for the default site
|
|
19
|
-
|
|
20
|
-
If a required parameter is missing from `$ARGUMENTS`, 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,22 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review')."
|
|
3
|
-
argument-hint: "<type> <state> [site] [limit] [offset]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_content_by_moderation_state
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_content_by_moderation_state` MCP tool.
|
|
8
|
-
|
|
9
|
-
List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review').
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Required:**
|
|
14
|
-
- `type` (string): Content type machine name
|
|
15
|
-
- `state` (string): Moderation state machine name
|
|
16
|
-
|
|
17
|
-
**Optional:**
|
|
18
|
-
- `site` (string): omit for the default site
|
|
19
|
-
- `limit` (number)
|
|
20
|
-
- `offset` (number)
|
|
21
|
-
|
|
22
|
-
If a required parameter is missing from `$ARGUMENTS`, 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,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Create a custom content block of a given block type (bundle). 'info' is the administrative label; 'body' is optional rendered HTML. Checked against the site security config."
|
|
3
|
-
argument-hint: "<type> <info> [site] [body]"
|
|
4
|
-
allowed-tools: mcp__drupal__drupal_create_block
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Call the `mcp__drupal__drupal_create_block` MCP tool.
|
|
8
|
-
|
|
9
|
-
Create a custom content block of a given block type (bundle). 'info' is the administrative label; 'body' is optional rendered HTML. Checked against the site security config.
|
|
10
|
-
|
|
11
|
-
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
|
-
|
|
13
|
-
**Required:**
|
|
14
|
-
- `type` (string): Block type (bundle) machine name, e.g. 'basic'
|
|
15
|
-
- `info` (string): Administrative label / block description
|
|
16
|
-
|
|
17
|
-
**Optional:**
|
|
18
|
-
- `site` (string): omit for the default site
|
|
19
|
-
- `body` (string): Block body HTML (optional)
|
|
20
|
-
|
|
21
|
-
If a required parameter is missing from `$ARGUMENTS`, 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.
|