drupal-mcp-connector 2.26.0 → 3.0.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.
Files changed (37) hide show
  1. package/.agents/commands/drupal-audit-site-health.md +1 -1
  2. package/.agents/commands/drupal-report-accessibility-audit.md +4 -2
  3. package/.agents/commands/drupal-report-alias-coverage.md +4 -2
  4. package/.agents/commands/drupal-report-broken-embeds.md +4 -2
  5. package/.agents/commands/drupal-report-broken-links.md +4 -2
  6. package/.agents/commands/drupal-report-content-by-author.md +4 -2
  7. package/.agents/commands/drupal-report-duplicate-content.md +4 -2
  8. package/.agents/commands/drupal-report-missing-field.md +2 -2
  9. package/.agents/commands/drupal-report-orphan-pages.md +4 -2
  10. package/.agents/commands/drupal-report-orphaned-references.md +4 -2
  11. package/.agents/commands/drupal-report-pii-exposure.md +4 -2
  12. package/.agents/commands/drupal-report-readability.md +4 -2
  13. package/.agents/commands/drupal-report-recently-published.md +1 -1
  14. package/.agents/commands/drupal-report-revision-hotspots.md +4 -2
  15. package/.agents/commands/drupal-report-scheduled-content.md +4 -4
  16. package/.agents/commands/drupal-report-seo-audit.md +4 -2
  17. package/.agents/commands/drupal-report-seo-meta-coverage.md +4 -2
  18. package/.agents/commands/drupal-report-stale-content.md +4 -2
  19. package/.agents/commands/drupal-report-taxonomy-usage.md +2 -2
  20. package/.agents/commands/drupal-report-translation-coverage.md +4 -2
  21. package/.agents/commands/drupal-report-unpublished.md +1 -1
  22. package/.agents/commands/drupal-report-workflow-bottlenecks.md +4 -2
  23. package/.agents/commands/drupal-schedule-publish.md +4 -4
  24. package/.agents/commands/drupal-search-content.md +1 -1
  25. package/.agents/commands/drupal-search.md +1 -1
  26. package/CHANGELOG.md +46 -0
  27. package/package.json +1 -1
  28. package/src/lib/backends/jsonapi.js +19 -7
  29. package/src/lib/reports-support.js +68 -0
  30. package/src/tools/audit-composite.js +21 -11
  31. package/src/tools/nodes.js +12 -6
  32. package/src/tools/reports-content.js +106 -38
  33. package/src/tools/reports-extra.js +56 -32
  34. package/src/tools/reports-links.js +13 -13
  35. package/src/tools/reports.js +65 -42
  36. package/src/tools/scheduler.js +90 -62
  37. package/src/tools/search.js +16 -12
@@ -11,7 +11,7 @@ Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Optional:**
13
13
  - `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).
14
- - `type` (string): Primary content type for content/link sections (default: article)
14
+ - `type` (string): Content type for the content/link sections. Omit it and those sections are reported unavailable; site-wide sections still run.
15
15
  - `sampleSize` (number): Per-section scan cap (kept small for a fast roll-up)
16
16
  - `sections` (array (pass as JSON)): Subset of sections to run (default: all). Available: content_summary, stale_content, seo_audit, accessibility, duplicate_content, readability, pii_exposure, broken_links, redirect_health, alias_coverage, log_404, config_best_practices, module_audit, permission_audit
17
17
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Accessibility audit for body content: images without alt text, inline H1 tags, non-descriptive link text ('click here', 'read more'), and tables without captions."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_accessibility_audit`.
@@ -9,9 +9,11 @@ Accessibility audit for body content: images without alt text, inline H1 tags, n
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number)
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "URL-alias coverage for a content type: nodes whose canonical URL is still /node/N (no alias / pathauto gap), plus conflicting aliases (one alias mapped to multiple system paths) when the path_alias entity is exposed."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_alias_coverage`.
@@ -9,9 +9,11 @@ URL-alias coverage for a content type: nodes whose canonical URL is still /node/
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number): Max nodes to scan
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Scan published body content for embedded entities (media/entity embeds) and report usage by type, flagging embeds with a missing/malformed data-entity-uuid. Full target-existence verification is gated on a future server-tool."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_broken_embeds`.
@@ -9,9 +9,11 @@ Scan published body content for embedded entities (media/entity embeds) and repo
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number): Max nodes to scan
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Inventory links in published body content (internal/external/images), aggregate external hosts, and flag malformed hrefs. With checkLive:true, verifies links via bounded, SSRF-guarded outbound HTTP (internal always; external only if includeExternal and host-allowlisted). No network egress unless checkLive is set."
3
- argument-hint: "[site] [type] [sampleSize] [checkLive] [includeExternal]"
3
+ argument-hint: "<type> [site] [sampleSize] [checkLive] [includeExternal]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_broken_links`.
@@ -9,9 +9,11 @@ Inventory links in published body content (internal/external/images), aggregate
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number): Max nodes to scan
16
18
  - `checkLive` (boolean (true/false)): Perform live HTTP checks (off by default)
17
19
  - `includeExternal` (boolean (true/false)): When checkLive, also check allowlisted external hosts
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Count nodes per author for a given content type. Returns author UUIDs and counts sorted by most prolific."
3
- argument-hint: "[site] [type] [limit]"
3
+ argument-hint: "<type> [site] [limit]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_content_by_author`.
@@ -9,9 +9,11 @@ Count nodes per author for a given content type. Returns author UUIDs and counts
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `limit` (number): Max nodes to scan
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Find duplicate / near-duplicate titles within a content type (normalized title grouping). Surfaces accidental re-publishing and content cannibalization."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_duplicate_content`.
@@ -9,9 +9,11 @@ Find duplicate / near-duplicate titles within a content type (normalized title g
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number): Max nodes to scan
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Find entities of a content type where a given field is empty (e.g. a missing meta description, image, or summary). Works for scalar fields and entity-reference fields. Sampling-bounded — flags 'approximate' when the scan is capped. An absent field is not an empty one: JSON:API leaves out a field this account may not view. When the field is absent from every sampled entity the result is `notVisible: true` with no findings (the field may be denied to this account, not exist on the bundle, or be misspelled) instead of every entity counted as missing. Otherwise each finding has `reason` 'empty' (key present, no value) or 'absent' (key missing, possibly access-denied)."
3
- argument-hint: "<field> [site] [type] [sampleSize]"
3
+ argument-hint: "<type> <field> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_missing_field`.
@@ -10,11 +10,11 @@ Find entities of a content type where a given field is empty (e.g. a missing met
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
13
14
  - `field` (string): Field machine name to check for emptiness, e.g. 'field_meta_description', 'field_image'
14
15
 
15
16
  **Optional:**
16
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).
17
- - `type` (string): Content type machine name (default: article)
18
18
  - `sampleSize` (number): Max entities to scan
19
19
 
20
20
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Find published pages with no inbound internal links from other sampled pages — content islands. Best-effort over the sampled set."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_orphan_pages`.
@@ -9,9 +9,11 @@ Find published pages with no inbound internal links from other sampled pages —
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number)
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Find entities whose entity-reference fields point at targets that no longer exist (orphaned references). Best-effort: samples entities and probes each distinct referenced target via JSON:API. A 404 (or unaddressable ref) is an orphan; 401/403 and connector policy denials are counted as unverifiable, not missing. uid/revision_uid are skipped when the policy denies user. Flags 'approximate' when sampling-bounded."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_orphaned_references`.
@@ -9,9 +9,11 @@ Find entities whose entity-reference fields point at targets that no longer exis
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type machine name to scan (default: article)
15
17
  - `sampleSize` (number): Max entities to scan for broken references
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Scan published body content for accidentally exposed PII (emails, US SSNs, phone numbers). Matched values are masked in the output so the report itself doesn't leak data."
3
- argument-hint: "[site] [type] [sampleSize] [kinds]"
3
+ argument-hint: "<type> [site] [sampleSize] [kinds]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_pii_exposure`.
@@ -9,9 +9,11 @@ Scan published body content for accidentally exposed PII (emails, US SSNs, phone
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number)
16
18
  - `kinds` (array (pass as JSON)): PII kinds to scan (default: all)
17
19
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Score body readability (Flesch Reading Ease) for a content type and flag hard-to-read content and structural issues (no H2 subheadings, multiple H1s)."
3
- argument-hint: "[site] [type] [sampleSize] [hardThreshold]"
3
+ argument-hint: "<type> [site] [sampleSize] [hardThreshold]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_readability`.
@@ -9,9 +9,11 @@ Score body readability (Flesch Reading Ease) for a content type and flag hard-to
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number)
16
18
  - `hardThreshold` (number): Flag content scoring below this
17
19
 
@@ -11,7 +11,7 @@ Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Optional:**
13
13
  - `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).
14
- - `type` (string): Content type (default: article)
14
+ - `type` (string): Content type machine name. Omit to read every node bundle (results merged, newest first; unreadable bundles listed in bundleErrors).
15
15
  - `limit` (number)
16
16
 
17
17
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Find nodes with the most revision activity — useful for spotting churn or content that needs editorial process review. Requires Drupal 9.3+ JSON:API revisions."
3
- argument-hint: "[site] [type] [limit]"
3
+ argument-hint: "<type> [site] [limit]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_revision_hotspots`.
@@ -9,9 +9,11 @@ Find nodes with the most revision activity — useful for spotting churn or cont
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `limit` (number)
16
18
 
17
19
  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,17 +1,17 @@
1
1
  ---
2
- description: "Report content with Scheduler publish/unpublish dates set, split into pending (future) and overdue (past, action not run). Gated when scheduler fields aren't exposed."
2
+ description: "Report content with Scheduler publish/unpublish dates set, split into pending (future) and overdue (past, action not run). Omit type to scan every node bundle in one call (one row per bundle in byContentType). A bundle is gated only when sampled nodes carry no publish_on/unpublish_on key at all; fields that exist but are empty report pending 0 / overdue 0. A bundle with no nodes reports zero counts with schedulerFields 'unknown'."
3
3
  argument-hint: "[site] [type] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_scheduled_content`.
7
7
 
8
- Report content with Scheduler publish/unpublish dates set, split into pending (future) and overdue (past, action not run). Gated when scheduler fields aren't exposed.
8
+ Report content with Scheduler publish/unpublish dates set, split into pending (future) and overdue (past, action not run). Omit type to scan every node bundle in one call (one row per bundle in byContentType). A bundle is gated only when sampled nodes carry no publish_on/unpublish_on key at all; fields that exist but are empty report pending 0 / overdue 0. A bundle with no nodes reports zero counts with schedulerFields 'unknown'.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Optional:**
13
13
  - `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).
14
- - `type` (string): Content type (default: article)
15
- - `sampleSize` (number)
14
+ - `type` (string): Content type machine name. Omit to scan every node bundle.
15
+ - `sampleSize` (number): Max nodes to scan per content type
16
16
 
17
17
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "SEO audit for a content type: missing meta descriptions, title length issues, and thin content (under 300 words). Returns node lists for each issue category. Meta descriptions use the rendered Metatag output via GraphQL when available (reported as `metaSource`); when no description source is readable it reports the meta check as unavailable rather than a false zero."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_seo_audit`.
@@ -9,9 +9,11 @@ SEO audit for a content type: missing meta descriptions, title length issues, an
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number)
16
18
 
17
19
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Report structured-meta (SEO) coverage for a content type: how many sampled nodes populate each meta field (metatag, meta description). Complements drupal_report_seo_audit with explicit per-field coverage. Reads scalar and entity-reference fields. A field absent from every sampled node has `present: false` and `coverage: null` (unknown, not 0), and is listed in `notVisible` when you named it. When none of the checked fields is visible, no node is flagged and `nodesMissingAllMeta` is null: JSON:API leaves out a field this account may not view."
3
- argument-hint: "[site] [type] [fields] [sampleSize]"
3
+ argument-hint: "<type> [site] [fields] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_seo_meta_coverage`.
@@ -9,9 +9,11 @@ Report structured-meta (SEO) coverage for a content type: how many sampled nodes
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `fields` (array (pass as JSON)): Meta field machine names to check
16
18
  - `sampleSize` (number)
17
19
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Find content that hasn't been updated in N days. Returns a sorted list with titles, status, and days-since-update."
3
- argument-hint: "[site] [type] [days] [status] [limit]"
3
+ argument-hint: "<type> [site] [days] [status] [limit]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_stale_content`.
@@ -9,9 +9,11 @@ Find content that hasn't been updated in N days. Returns a sorted list with titl
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `days` (number): Stale threshold in days
16
18
  - `status` (boolean (true/false)): Filter by publish status
17
19
  - `limit` (number)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Count how many nodes use each term in a vocabulary. Identifies over-used, under-used, and orphaned terms."
3
- argument-hint: "<vocabulary> [site] [contentType] [referenceField] [limit]"
3
+ argument-hint: "<vocabulary> <contentType> [site] [referenceField] [limit]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_taxonomy_usage`.
@@ -11,10 +11,10 @@ Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Required:**
13
13
  - `vocabulary` (string): Vocabulary machine name, e.g. 'tags', 'category'
14
+ - `contentType` (string): Content type machine name (required; see drupal_list_content_types)
14
15
 
15
16
  **Optional:**
16
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).
17
- - `contentType` (string): Content type to count references from (default: article)
18
18
  - `referenceField` (string): Field referencing the vocabulary (default: field_{vocabulary})
19
19
  - `limit` (number)
20
20
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Per-node translation coverage from Sentinel's inventory (missing non-default language, outdated core flag, language counts). Without Sentinel the report is unavailable — JSON:API only shows the default language, so a histogram would be misleading."
3
- argument-hint: "[site] [type] [sampleSize]"
3
+ argument-hint: "<type> [site] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_translation_coverage`.
@@ -9,9 +9,11 @@ Per-node translation coverage from Sentinel's inventory (missing non-default lan
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `sampleSize` (number): Max nodes to inventory
16
18
 
17
19
  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.
@@ -11,7 +11,7 @@ Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Optional:**
13
13
  - `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).
14
- - `type` (string): Content type machine name (default: article)
14
+ - `type` (string): Content type machine name. Omit to read every node bundle (results merged, newest first; unreadable bundles listed in bundleErrors).
15
15
  - `limit` (number): Max unpublished nodes to return
16
16
 
17
17
  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,6 +1,6 @@
1
1
  ---
2
2
  description: "Find content stuck in a non-published moderation state (draft/needs_review) beyond N days — editorial bottlenecks. Reads moderation_state; gated when content_moderation isn't exposed."
3
- argument-hint: "[site] [type] [days] [states] [langcode] [sampleSize]"
3
+ argument-hint: "<type> [site] [days] [states] [langcode] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_workflow_bottlenecks`.
@@ -9,9 +9,11 @@ Find content stuck in a non-published moderation state (draft/needs_review) beyo
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
+ **Required:**
13
+ - `type` (string): Content type machine name (required; see drupal_list_content_types)
14
+
12
15
  **Optional:**
13
16
  - `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).
14
- - `type` (string): Content type (default: article)
15
17
  - `days` (number): Days-in-state threshold
16
18
  - `states` (array (pass as JSON)): Moderation states to treat as bottlenecks
17
19
  - `langcode` (string): Limit to this translation (Sentinel inventory). Omit for the default-language field on each node.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Schedule a content node to publish and/or unpublish at a future time using the Drupal Scheduler module. Sets the publish_on and unpublish_on fields on the node. On a bundle under Content Moderation, also set publishState / unpublishState (written as publish_state / unpublish_state) — dates alone never go live. Requires the Scheduler module to be installed and enabled for the content type, with the publish_on / unpublish_on fields present on the bundle — otherwise the call fails with a clear capability error. Moderated bundles additionally need Scheduler Content Moderation Integration; when those state fields exist, the matching state is required whenever a date is set. Timestamps accept ISO 8601 (e.g. '2026-07-01T12:00:00Z') or a Unix epoch and are passed through unchanged. Provide at least one of publishOn or unpublishOn."
2
+ description: "Schedule a content node to publish and/or unpublish at a future time using the Drupal Scheduler module. Sets the publish_on and unpublish_on fields on the node. Provide at least one of publishOn or unpublishOn. Moderated bundles (Content Moderation) also need a target state: pass publishState / unpublishState (moderation state machine names, e.g. 'published', 'archived'), written as publish_state / unpublish_state. These fields exist only when the Scheduler Content Moderation Integration module is enabled. When the bundle exposes a state field and its date is set without the state, the call fails, because Drupal would accept a schedule that never runs. When a moderated bundle has no state field, the dates are written and the result carries a warning. Whether this account may schedule a given transition is decided by the site (Drupal validation and MCP Sentinel policy), not the connector; a refusal returns the site's reason verbatim. Without the Scheduler module on the content type, the call fails with a capability error. Timestamps accept ISO 8601 (e.g. '2026-07-01T12:00:00Z') or a Unix epoch and are passed through unchanged."
3
3
  argument-hint: "<type> <id> [site] [publishOn] [unpublishOn] [publishState] [unpublishState]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_schedule_publish`.
7
7
 
8
- Schedule a content node to publish and/or unpublish at a future time using the Drupal Scheduler module. Sets the publish_on and unpublish_on fields on the node. On a bundle under Content Moderation, also set publishState / unpublishState (written as publish_state / unpublish_state) — dates alone never go live. Requires the Scheduler module to be installed and enabled for the content type, with the publish_on / unpublish_on fields present on the bundle — otherwise the call fails with a clear capability error. Moderated bundles additionally need Scheduler Content Moderation Integration; when those state fields exist, the matching state is required whenever a date is set. Timestamps accept ISO 8601 (e.g. '2026-07-01T12:00:00Z') or a Unix epoch and are passed through unchanged. Provide at least one of publishOn or unpublishOn.
8
+ Schedule a content node to publish and/or unpublish at a future time using the Drupal Scheduler module. Sets the publish_on and unpublish_on fields on the node. Provide at least one of publishOn or unpublishOn. Moderated bundles (Content Moderation) also need a target state: pass publishState / unpublishState (moderation state machine names, e.g. 'published', 'archived'), written as publish_state / unpublish_state. These fields exist only when the Scheduler Content Moderation Integration module is enabled. When the bundle exposes a state field and its date is set without the state, the call fails, because Drupal would accept a schedule that never runs. When a moderated bundle has no state field, the dates are written and the result carries a warning. Whether this account may schedule a given transition is decided by the site (Drupal validation and MCP Sentinel policy), not the connector; a refusal returns the site's reason verbatim. Without the Scheduler module on the content type, the call fails with a capability error. Timestamps accept ISO 8601 (e.g. '2026-07-01T12:00:00Z') or a Unix epoch and are passed through unchanged.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -17,7 +17,7 @@ Parse the arguments supplied with this command into this tool's parameters:
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
  - `publishOn` (string): When to publish — ISO 8601 datetime or Unix epoch. Sets the Scheduler publish_on field.
19
19
  - `unpublishOn` (string): When to unpublish — ISO 8601 datetime or Unix epoch. Sets the Scheduler unpublish_on field.
20
- - `publishState` (string): Moderation state to enter at publishOn (e.g. 'published'). Required on moderated bundles when publishOn is set.
21
- - `unpublishState` (string): Moderation state to enter at unpublishOn (e.g. 'draft'). Required on moderated bundles when unpublishOn is set.
20
+ - `publishState` (string): Moderation state machine name to enter at publishOn (e.g. 'published'). Written as publish_state. Required when publishOn is set and the bundle has a publish_state field.
21
+ - `unpublishState` (string): Moderation state machine name to enter at unpublishOn (e.g. 'archived'). Written as unpublish_state. Required when unpublishOn is set and the bundle has an unpublish_state field.
22
22
 
23
23
  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.
@@ -14,7 +14,7 @@ Parse the arguments supplied with this command into this tool's parameters:
14
14
 
15
15
  **Optional:**
16
16
  - `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).
17
- - `type` (string): Limit to this content type (default: article)
17
+ - `type` (string): Content type machine name. Omit to search every node bundle (merged, newest first); if any bundle cannot be read the search fails and names it.
18
18
  - `status` (boolean (true/false)): Filter by publish status
19
19
  - `limit` (number)
20
20
 
@@ -14,7 +14,7 @@ Parse the arguments supplied with this command into this tool's parameters:
14
14
 
15
15
  **Optional:**
16
16
  - `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).
17
- - `type` (string): Content type machine name (default: article)
17
+ - `type` (string): Content type machine name. Omit to search every node bundle (merged, newest first); if any bundle cannot be read the search fails and names it.
18
18
  - `limit` (number)
19
19
 
20
20
  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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.0.0] - 2026-10-01
11
+
12
+ ### Fixed
13
+ - **`drupal_schedule_publish` site refusals (#402).** A 422 such as
14
+ `publish_state: You do not have access to transition from Draft to
15
+ Published` was rewritten as "Scheduler fields are not available", because
16
+ any message naming a Scheduler field counted as a missing field. Only the
17
+ "The attribute X does not exist on the Y resource type" form counts now. Other 4xx refusals,
18
+ including MCP Sentinel denials, return the site's reason verbatim with the
19
+ HTTP status.
20
+ - **`drupal_schedule_publish` no longer refuses on its own (#402).** A
21
+ moderated bundle without `publish_state` / `unpublish_state` is written and
22
+ the result carries a warning; Drupal and Sentinel decide. A date without
23
+ its state is still refused when the bundle has the state field, since
24
+ Drupal accepts that schedule and it never runs. That check now also applies
25
+ when `moderation_state` is not visible to the account.
26
+ - **`drupal_report_scheduled_content` on an empty bundle (#403).** A bundle
27
+ with no nodes reports pending 0 / overdue 0 with `schedulerFields:
28
+ "unknown"` instead of `gated: true`. A gated bundle that hit the sample cap
29
+ keeps `approximate: true`.
30
+ - **`listContentTypes` read only the first 50 node types (JSON:API).** It
31
+ now follows `links.next`, so all-bundle scans and
32
+ `drupal_report_content_summary` see every content type.
33
+
34
+ ### Changed
35
+ - **No node tool defaults to the `article` bundle any more (#403).** On a
36
+ site without that bundle the old default returned a 404 or scanned nothing.
37
+ - Require `type`: `drupal_report_duplicate_content`, `_workflow_bottlenecks`,
38
+ `_translation_coverage`, `_readability`, `_orphan_pages`, `_pii_exposure`,
39
+ `_seo_meta_coverage`, `_stale_content`, `_content_by_author`,
40
+ `_revision_hotspots`, `_seo_audit`, `_accessibility_audit`,
41
+ `_broken_links`, `_alias_coverage`, `_broken_embeds`, `_missing_field` and
42
+ `_orphaned_references`. `drupal_report_taxonomy_usage` requires
43
+ `contentType`. The error points to `drupal_list_content_types`.
44
+ - Scan every node bundle when `type` is omitted:
45
+ `drupal_report_scheduled_content` (one row per bundle in `byContentType`),
46
+ `drupal_report_recently_published` and `drupal_report_unpublished`
47
+ (merged newest first; unreadable bundles in `bundleErrors`), and
48
+ `drupal_search` / `drupal_search_content` (merged newest first; a bundle
49
+ that cannot be read fails the search and is named).
50
+ - `drupal_audit_site_health` without `type` reports its content-type
51
+ sections as unavailable and still runs the site-wide ones.
52
+ - **Local operator files stay untracked.** `config/config.*.json` is ignored
53
+ except `config/config.example.json`. One-off `scripts/*-staging.mjs`
54
+ loaders are ignored too.
55
+
10
56
  ## [2.26.0] - 2026-10-01
11
57
 
12
58
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.26.0",
3
+ "version": "3.0.0",
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",
@@ -20,6 +20,9 @@ import {
20
20
  } from "../canonical.js";
21
21
  import { isPositiveNid, normalizeAlias, PATH_ALIAS_ENTITY_TYPE } from "../path-alias.js";
22
22
 
23
+ /** Most node_type pages read by listContentTypes (50 per page). */
24
+ const CONTENT_TYPE_MAX_PAGES = 20;
25
+
23
26
  // Drupal exposes internal identifiers under drupal_internal__* attributes.
24
27
  // They are dropped from canonical `fields` except for the identifiers that
25
28
  // governed read/write workflows explicitly need.
@@ -666,13 +669,22 @@ export class JsonApiBackend extends Backend {
666
669
  * @returns {Promise<Array<{id: string, label: string, description: ?string}>>}
667
670
  */
668
671
  async listContentTypes() {
669
- // page[limit]=50 is an intentional cap; matches Drupal JSON:API's default page size.
670
- const data = await drupalFetch(this.site, "/jsonapi/node_type/node_type?page[limit]=50");
671
- return (data.data || []).map((ct) => ({
672
- id: ct.attributes.drupal_internal__type,
673
- label: ct.attributes.name,
674
- description: ct.attributes.description ?? null,
675
- }));
672
+ // JSON:API caps a page at 50. Follow `links.next` so callers that scan
673
+ // every bundle see them all (#403); stop after a fixed number of pages.
674
+ const out = [];
675
+ for (let page = 0, offset = 0; page < CONTENT_TYPE_MAX_PAGES; page += 1, offset += 50) {
676
+ const data = await drupalFetch(this.site, `/jsonapi/node_type/node_type?page[limit]=50&page[offset]=${offset}`);
677
+ const rows = data.data || [];
678
+ for (const ct of rows) {
679
+ out.push({
680
+ id: ct.attributes.drupal_internal__type,
681
+ label: ct.attributes.name,
682
+ description: ct.attributes.description ?? null,
683
+ });
684
+ }
685
+ if (!data.links?.next || rows.length === 0) break;
686
+ }
687
+ return out;
676
688
  }
677
689
 
678
690
  /**