drupal-mcp-connector 2.15.2 → 2.17.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 (41) hide show
  1. package/.agents/commands/drupal-codegen-diff.md +17 -0
  2. package/.agents/commands/drupal-codegen-generate.md +17 -0
  3. package/.agents/commands/drupal-codegen-inspect.md +17 -0
  4. package/.agents/commands/drupal-content-by-moderation-state.md +4 -3
  5. package/.agents/commands/drupal-create-translation.md +3 -3
  6. package/.agents/commands/drupal-describe-fields.md +2 -2
  7. package/.agents/commands/drupal-get-media.md +4 -3
  8. package/.agents/commands/drupal-get-node.md +4 -3
  9. package/.agents/commands/drupal-get-taxonomy-term.md +4 -3
  10. package/.agents/commands/drupal-list-translations.md +3 -3
  11. package/.agents/commands/drupal-report-translation-coverage.md +4 -5
  12. package/.agents/commands/drupal-report-workflow-bottlenecks.md +2 -1
  13. package/.agents/commands/drupal-set-moderation-state.md +4 -3
  14. package/.agents/commands/drupal-update-media.md +5 -4
  15. package/.agents/commands/drupal-update-menu-link.md +2 -1
  16. package/.agents/commands/drupal-update-taxonomy-term.md +4 -3
  17. package/CHANGELOG.md +57 -0
  18. package/README.md +3 -3
  19. package/bin/drupal-mcp-agent.js +2 -2
  20. package/package.json +3 -2
  21. package/scripts/generate-commands.js +2 -2
  22. package/src/index.js +2 -2
  23. package/src/lib/backends/jsonapi.js +85 -9
  24. package/src/lib/canonical.js +10 -5
  25. package/src/lib/config.js +18 -1
  26. package/src/lib/draft-write.js +5 -5
  27. package/src/lib/err-relationships.js +22 -0
  28. package/src/lib/mcp-server.js +1 -1
  29. package/src/lib/node-draft-inventory.js +3 -2
  30. package/src/lib/server-tools.js +2 -2
  31. package/src/lib/translation-rows.js +59 -0
  32. package/src/tools/codegen.js +150 -0
  33. package/src/tools/fields.js +12 -4
  34. package/src/tools/index.js +2 -1
  35. package/src/tools/media.js +65 -8
  36. package/src/tools/moderation.js +82 -13
  37. package/src/tools/nodes.js +56 -6
  38. package/src/tools/reports-content.js +117 -28
  39. package/src/tools/structure.js +6 -3
  40. package/src/tools/taxonomy.js +15 -6
  41. package/src/tools/translations.js +31 -27
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: "Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff."
3
+ argument-hint: "[site] [bundles] [skipFields]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_codegen_diff`.
7
+
8
+ Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff.
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Optional:**
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
+ - `bundles` (array (pass as JSON)): Node/paragraph bundle ids. Omit for every bundle graphql_compose exposes.
15
+ - `skipFields` (array (pass as JSON)): Extra field machine names to exclude from the scaffold.
16
+
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.
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: "Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate."
3
+ argument-hint: "[site] [bundles] [skipFields]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_codegen_generate`.
7
+
8
+ Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate.
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Optional:**
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
+ - `bundles` (array (pass as JSON)): Node/paragraph bundle ids. Omit for every bundle graphql_compose exposes.
15
+ - `skipFields` (array (pass as JSON)): Extra field machine names to exclude from the scaffold.
16
+
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.
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: "List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files."
3
+ argument-hint: "[site] [bundles] [skipFields]"
4
+ ---
5
+
6
+ Call the MCP tool `drupal_codegen_inspect`.
7
+
8
+ List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files.
9
+
10
+ Parse the arguments supplied with this command into this tool's parameters:
11
+
12
+ **Optional:**
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
+ - `bundles` (array (pass as JSON)): Node/paragraph bundle ids. Omit for every bundle graphql_compose exposes.
15
+ - `skipFields` (array (pass as JSON)): Extra field machine names to exclude from the scaffold.
16
+
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,11 +1,11 @@
1
1
  ---
2
- 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."
3
- argument-hint: "<type> <state> [site] [limit] [offset]"
2
+ description: "List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Pass langcode to match that translation via Sentinel inventory (the editorial work queue). Omit langcode for default-language JSON:API / sampled behavior. 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."
3
+ argument-hint: "<type> <state> [site] [langcode] [limit] [offset]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_content_by_moderation_state`.
7
7
 
8
- 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.
8
+ List nodes of a content type currently in a given moderation state (e.g. what is in 'draft' or 'needs_review'). Pass langcode to match that translation via Sentinel inventory (the editorial work queue). Omit langcode for default-language JSON:API / sampled behavior. 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.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -15,6 +15,7 @@ Parse the arguments supplied with this command into this tool's parameters:
15
15
 
16
16
  **Optional:**
17
17
  - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
+ - `langcode` (string): Match this translation (e.g. 'es'). Requires Sentinel. Omit for default-language listing.
18
19
  - `limit` (number)
19
20
  - `offset` (number)
20
21
 
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; continue a paragraph with drupal_update_paragraph and langcode. Image alt is a relationship (same file UUID, meta.alt). For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers."
2
+ description: "Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers."
3
3
  argument-hint: "<type> <id> <langcode> [site] [entityType] [attributes] [relationships] [revisionId] [dryRun]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_create_translation`.
7
7
 
8
- Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; continue a paragraph with drupal_update_paragraph and langcode. Image alt is a relationship (same file UUID, meta.alt). For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers.
8
+ Create a translation as an unpublished non-default draft (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. When an English working draft already exists, both live and working revision IDs are sent (If-Match) so Sentinel will add the language on that draft (#282). English live title, body, status, alias, default revision, and paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. The response includes `_revisions.live` / `_revisions.working` when known. Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. Continue a node draft with drupal_update_node and langcode; a paragraph with drupal_update_paragraph and langcode; media with drupal_update_media and langcode. Image alt is a relationship (same file UUID, meta.alt) — on media that is field_media_image, not a node image field. For paragraphs pass revisionId as the host pin. Requires Sentinel's draft-translation endpoint. Publication stays denied for content-tier callers.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -16,7 +16,7 @@ Parse the arguments supplied with this command into this tool's parameters:
16
16
 
17
17
  **Optional:**
18
18
  - `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).
19
- - `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values.
19
+ - `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values, 'media' for media (name, caption, image alt).
20
20
  - `attributes` (object (pass as JSON)): Translated field values keyed by Drupal machine name
21
21
  - `relationships` (object (pass as JSON)): JSON:API relationships. Use for image alt (same file UUID, meta.alt).
22
22
  - `revisionId` (string): Paragraph revision id the host already pins. Required for Home-shaped non-default pins.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names."
2
+ description: "Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, translatable?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. When JSON:API field_config is readable, translatable is copied from Field API; omitted means unknown, not false. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names."
3
3
  argument-hint: "<site> [type] [entityType] [bundle]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_describe_fields`.
7
7
 
8
- Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names.
8
+ Introspect the fields of a Drupal entity type + bundle: returns a per-field list of { name, type, kind, cardinality?, translatable?, approximate }. Read-only. Built on schema SAMPLING (an existing entity), so results are approximate — only populated fields are visible and required/cardinality/allowedValues are inferred from value shape. When JSON:API field_config is readable, translatable is copied from Field API; omitted means unknown, not false. Authoritative field metadata comes from the Drush bridge (Field API). Use this before creating/updating entities to learn field names.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Fetch a single media entity by UUID and media type."
3
- argument-hint: "<type> <id> [site]"
2
+ description: "Fetch a single media entity by UUID and media type. Image alt/title are on the file relationship (meta.alt / meta.title). Pass langcode to read an unpublished working translation via Sentinel."
3
+ argument-hint: "<type> <id> [site] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_get_media`.
7
7
 
8
- Fetch a single media entity by UUID and media type.
8
+ Fetch a single media entity by UUID and media type. Image alt/title are on the file relationship (meta.alt / meta.title). Pass langcode to read an unpublished working translation via Sentinel.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -15,5 +15,6 @@ Parse the arguments supplied with this command into this tool's parameters:
15
15
 
16
16
  **Optional:**
17
17
  - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
+ - `langcode` (string): Target language (e.g. 'es') to read the unpublished working translation instead of the default language.
18
19
 
19
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,11 +1,11 @@
1
1
  ---
2
- description: "Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English)."
3
- argument-hint: "<type> <id> [site] [langcode]"
2
+ description: "Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English). Pass includeComponents true to attach pinned paragraph translations under `components` (default false)."
3
+ argument-hint: "<type> <id> [site] [langcode] [includeComponents]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_get_node`.
7
7
 
8
- Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English).
8
+ Fetch a single Drupal content node by UUID and content type. Returns title, body, status, path alias, and all attributes. Pass langcode to read a working translation draft via Sentinel (distinct from published English). Pass includeComponents true to attach pinned paragraph translations under `components` (default false).
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -16,5 +16,6 @@ Parse the arguments supplied with this command into this tool's parameters:
16
16
  **Optional:**
17
17
  - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
18
  - `langcode` (string): Target language (e.g. 'es') to read the unpublished working translation instead of the default language.
19
+ - `includeComponents` (boolean (true/false)): If true, include pinned paragraph translations under `components` (empty array when the host has no ERR fields). Default false so existing callers are unchanged.
19
20
 
20
21
  If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Fetch a single taxonomy term by UUID."
3
- argument-hint: "<vocabulary> <id> [site]"
2
+ description: "Fetch a single taxonomy term by UUID. Pass langcode to request that translation; omit for the default language. JSON:API must negotiate language or the tool errors if a different language is served."
3
+ argument-hint: "<vocabulary> <id> [site] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_get_taxonomy_term`.
7
7
 
8
- Fetch a single taxonomy term by UUID.
8
+ Fetch a single taxonomy term by UUID. Pass langcode to request that translation; omit for the default language. JSON:API must negotiate language or the tool errors if a different language is served.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -15,5 +15,6 @@ Parse the arguments supplied with this command into this tool's parameters:
15
15
 
16
16
  **Optional:**
17
17
  - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
+ - `langcode` (string): Translation language (e.g. 'es'). Omit for the default language.
18
19
 
19
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,11 +1,11 @@
1
1
  ---
2
- description: "List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft). Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node."
2
+ description: "List live and working translation langcodes for a Drupal node, paragraph, or media entity. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft), including core content_translation_outdated and source when the server sends them. Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node."
3
3
  argument-hint: "<type> <id> [site] [entityType]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_list_translations`.
7
7
 
8
- List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft). Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node.
8
+ List live and working translation langcodes for a Drupal node, paragraph, or media entity. Uses Sentinel's translation inventory when available (live default revision vs unpublished working draft), including core content_translation_outdated and source when the server sends them. Core JSON:API alone serves one language and cannot prove others are absent. Defaults to node.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -15,6 +15,6 @@ Parse the arguments supplied with this command into this tool's parameters:
15
15
 
16
16
  **Optional:**
17
17
  - `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
18
- - `entityType` (string): Entity type machine name. Default: 'node'.
18
+ - `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' or 'media' when listing those.
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,18 +1,17 @@
1
1
  ---
2
- description: "Report content distribution by language for a content type and flag languages lagging the most-populated language — a multilingual coverage signal."
3
- argument-hint: "[site] [type] [gapThreshold] [sampleSize]"
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]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_translation_coverage`.
7
7
 
8
- Report content distribution by language for a content type and flag languages lagging the most-populated language — a multilingual coverage signal.
8
+ 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.
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
14
  - `type` (string): Content type (default: article)
15
- - `gapThreshold` (number): Flag languages below this fraction of the top language
16
- - `sampleSize` (number)
15
+ - `sampleSize` (number): Max nodes to inventory
17
16
 
18
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] [sampleSize]"
3
+ argument-hint: "[site] [type] [days] [states] [langcode] [sampleSize]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_report_workflow_bottlenecks`.
@@ -14,6 +14,7 @@ Parse the arguments supplied with this command into this tool's parameters:
14
14
  - `type` (string): Content type (default: article)
15
15
  - `days` (number): Days-in-state threshold
16
16
  - `states` (array (pass as JSON)): Moderation states to treat as bottlenecks
17
+ - `langcode` (string): Limit to this translation (Sentinel inventory). Omit for the default-language field on each node.
17
18
  - `sampleSize` (number)
18
19
 
19
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,11 +1,11 @@
1
1
  ---
2
- description: "Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write."
3
- argument-hint: "<type> <id> <state> [site]"
2
+ description: "Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write. Pass langcode to change one translation via Sentinel; omit it for the default-language / shared-state write. If moderation_state is not translatable, a langcode write is refused."
3
+ argument-hint: "<type> <id> <state> [site] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_set_moderation_state`.
7
7
 
8
- Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write.
8
+ Transition a content node to a moderation state (content_moderation), e.g. 'draft', 'needs_review', 'published', 'archived'. Governed write. Pass langcode to change one translation via Sentinel; omit it for the default-language / shared-state write. If moderation_state is not translatable, a langcode write is refused.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -16,5 +16,6 @@ Parse the arguments supplied with this command into this tool's parameters:
16
16
 
17
17
  **Optional:**
18
18
  - `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).
19
+ - `langcode` (string): Target translation (e.g. 'es'). Omit for the default language. Requires Sentinel.
19
20
 
20
21
  If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched."
3
- argument-hint: "<type> <id> [site] [name] [status] [fields]"
2
+ description: "Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first."
3
+ argument-hint: "<type> <id> [site] [name] [status] [fields] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_media`.
7
7
 
8
- Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched.
8
+ Update a media entity's name, status, or field values. Partial: omitted fields (status included) are left untouched. Pass langcode to continue an unpublished working translation via Sentinel; this does not PATCH canonical langcode and will not create a missing translation — use drupal_create_translation first.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -17,6 +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
  - `name` (string)
19
19
  - `status` (boolean (true/false)): Published flag. Only sent when provided; requires allowPublish when true.
20
- - `fields` (object (pass as JSON)): Field values. Entity-reference values in JSON:API linkage shape ({ data: { type, id } }) are sent as relationships automatically.
20
+ - `fields` (object (pass as JSON)): Field values. Entity-reference values in JSON:API linkage shape ({ data: { type, id } }) are sent as relationships automatically. Image alt is meta.alt on field_media_image with the existing file UUID.
21
+ - `langcode` (string): Target language for an unpublished working translation (e.g. 'es'). Continues that translation via Sentinel; does not create a missing translation and does not PATCH canonical langcode.
21
22
 
22
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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Update a custom menu link by UUID (rename, re-weight, re-target, re-parent, enable/disable). Only the fields you pass change. The link's enabled state is preserved across edits — an unrelated change will not disable a live link — unless you pass 'enabled' explicitly. Checked against the site security config."
3
- argument-hint: "<id> [site] [title] [link] [menu] [weight] [parent] [enabled]"
3
+ argument-hint: "<id> [site] [title] [link] [menu] [weight] [parent] [enabled] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_menu_link`.
@@ -20,5 +20,6 @@ Parse the arguments supplied with this command into this tool's parameters:
20
20
  - `weight` (number): New ordering weight. Omit to leave unchanged.
21
21
  - `parent` (string): New parent link plugin id (e.g. 'menu_link_content:<uuid>'), or '' for top level. Omit to leave unchanged.
22
22
  - `enabled` (boolean (true/false)): Enable/disable the link. Omit to preserve the current state.
23
+ - `langcode` (string): Existing translation to update (e.g. 'es'). Omit for the default language. Does not create a missing translation.
23
24
 
24
25
  If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Update an existing taxonomy term's name, description, or weight."
3
- argument-hint: "<vocabulary> <id> [site] [name] [description] [weight]"
2
+ description: "Update an existing taxonomy term's name, description, or weight. Pass langcode to update an existing translation; this does not create a missing translation."
3
+ argument-hint: "<vocabulary> <id> [site] [name] [description] [weight] [langcode]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_taxonomy_term`.
7
7
 
8
- Update an existing taxonomy term's name, description, or weight.
8
+ Update an existing taxonomy term's name, description, or weight. Pass langcode to update an existing translation; this does not create a missing translation.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -18,5 +18,6 @@ Parse the arguments supplied with this command into this tool's parameters:
18
18
  - `name` (string)
19
19
  - `description` (string)
20
20
  - `weight` (number)
21
+ - `langcode` (string): Existing translation to update (e.g. 'es'). Omit for the default language.
21
22
 
22
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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.17.0] - 2026-09-11
11
+
12
+ ### Added
13
+ - **Governed media draft translation (#296).** `drupal_create_translation` and
14
+ `drupal_list_translations` accept `entityType: "media"`. Continue with
15
+ `drupal_update_media` + `langcode`; read with `drupal_get_media` + `langcode`.
16
+ Same Sentinel `/mcp-draft` live/working pair as nodes (d.o #3622412). Image
17
+ alt/title are kept on the file relationship (`meta.alt` / `meta.title`) so a
18
+ translator can read the source. Canonical media writes without `langcode` are
19
+ unchanged. Requires MCP Sentinel with media draft routes.
20
+
21
+ ### Security
22
+ - Bump transitive `hono` 4.13.1 → 4.13.7 (via `@modelcontextprotocol/node`)
23
+ and override it to `^4.13.5`, clearing three moderate advisories:
24
+ GHSA-gqvv-2mrq-wpjv (`toSSG()` path traversal), GHSA-g6gw-c38x-mqfc
25
+ (`parseBody()` unbounded nesting), and GHSA-crvj-82cr-hjcx (query parser
26
+ after URL fragment). `npm audit` reports 0 vulnerabilities (#299).
27
+
28
+ ## [2.16.0] - 2026-09-11
29
+
30
+ ### Added
31
+ - **Optional graphql_compose_codegen tools (#308).**
32
+ `drupal_codegen_inspect`, `drupal_codegen_diff`, and `drupal_codegen_generate`
33
+ call `drush graphql-compose-codegen:*` over the existing SSH bridge.
34
+ Generate is `--dry-run` only (artefacts as text; no `--output-dir`).
35
+ Missing module or unknown command fails loud. If `allowedCommands` is set,
36
+ the matching `graphql-compose-codegen:*` subcommand must be listed.
37
+ - **Field `translatable`, taxonomy/menu `langcode`, and composed node reads (#303).**
38
+ `drupal_describe_fields` copies Field API `translatable` when JSON:API
39
+ `field_config` is readable (omitted means unknown). Taxonomy get/update and
40
+ menu-link update accept optional `langcode` (existing translations only).
41
+ `drupal_get_node` accepts `includeComponents` (default false) to attach
42
+ pinned paragraph translations.
43
+ - **Optional `langcode` on the editorial queue (#303).**
44
+ `drupal_content_by_moderation_state` and `drupal_set_moderation_state` accept
45
+ a translation language. Omitting it keeps the previous default-language
46
+ behavior. A langcode write uses Sentinel's draft-translation surface (no
47
+ canonical `langcode` PATCH). A langcode list uses the translation inventory;
48
+ without Sentinel the result is `unavailable`. `drupal_report_workflow_bottlenecks`
49
+ has the same optional filter.
50
+
51
+ ### Changed
52
+ - **`drupal_list_translations` passes through core outdated/source (#303).**
53
+ When Sentinel sends `content_translation_outdated` / `content_translation_source`,
54
+ those keys appear on each row. They are omitted when absent.
55
+ - **`drupal_report_translation_coverage` uses Sentinel inventory (#303).**
56
+ Per-node missing non-default language and outdated flags replace the
57
+ default-language histogram. Without Sentinel the report is `unavailable`
58
+ rather than a misleading 100% default-language chart.
59
+ - **Public product name is Drupal MCP Connector (#301).** Document titles and
60
+ the MCP handshake `serverInfo.title` now use the title-case product name.
61
+ The machine identifiers are unchanged: npm package, GitHub repo, binaries,
62
+ `X-MCP-Client`, log prefixes, and `serverInfo.name` remain
63
+ `drupal-mcp-connector`.
64
+
10
65
  ## [2.15.2] - 2026-09-11
11
66
 
12
67
  ### Fixed
@@ -1583,6 +1638,8 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
1583
1638
  - User tools gained explicit PII-access assertions.
1584
1639
  - Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
1585
1640
 
1641
+ [2.17.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.17.0
1642
+ [2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
1586
1643
  [2.15.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.2
1587
1644
  [2.15.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.1
1588
1645
  [2.15.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # drupal-mcp-connector
1
+ # Drupal MCP Connector
2
2
 
3
- > A secure, multi-site Model Context Protocol (MCP) connector for Drupal dual-protocol JSON:API and GraphQL access, governed draft translations, content tools, audit reports, and an SSH Drush bridge.
3
+ > Drupal MCP Connector — multi-site MCP server for Drupal with JSON:API and GraphQL, governed writes, draft translations, content tools, audit reports, and an SSH Drush bridge.
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org)
@@ -15,7 +15,7 @@ Built by **[Jeremy Michael Cerda](https://www.drupal.org/u/jmcerda)** (opensourc
15
15
 
16
16
  ## What It Does
17
17
 
18
- `drupal-mcp-connector` connects any [Model Context Protocol](https://modelcontextprotocol.io) client to one or more Drupal sites. It exposes Drupal content and configuration as a set of MCP **tools**, **resources**, and **prompts**, so an MCP client can read, audit, and (where permitted) write content through structured, governed operations instead of the admin UI:
18
+ **Drupal MCP Connector** (`drupal-mcp-connector`) connects any [Model Context Protocol](https://modelcontextprotocol.io) client to one or more Drupal sites. It exposes Drupal content and configuration as a set of MCP **tools**, **resources**, and **prompts**, so an MCP client can read, audit, and (where permitted) write content through structured, governed operations instead of the admin UI:
19
19
 
20
20
  ```
21
21
  "Find all articles missing a meta description and list them."
@@ -25,7 +25,7 @@ import { readFileSync } from "node:fs";
25
25
  import { connect as netConnect } from "node:net";
26
26
  import { connect as tlsConnect } from "node:tls";
27
27
  import process from "node:process";
28
- import { CLIENT_VERSION, listSiteNames, loadConfig } from "../src/lib/config.js";
28
+ import { CLIENT_VERSION, SERVER_INFO, listSiteNames, loadConfig } from "../src/lib/config.js";
29
29
  import { callTool, listResolvableSiteConfigs } from "../src/lib/dispatch.js";
30
30
  import { filterDiscoverableTools } from "../src/lib/governance.js";
31
31
  import {
@@ -110,7 +110,7 @@ async function discoverableTools() {
110
110
  }
111
111
 
112
112
  const surface = {
113
- serverInfo: { name: "drupal-mcp-connector", version: CLIENT_VERSION },
113
+ serverInfo: SERVER_INFO,
114
114
  tools: {
115
115
  definitions: allDefinitions,
116
116
  list: discoverableTools,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.15.2",
4
- "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal dual-protocol JSON:API and GraphQL access, governed draft translations, content tools, audit reports, and an SSH Drush bridge.",
3
+ "version": "2.17.0",
4
+ "description": "Drupal MCP Connector — multi-site MCP server for Drupal with JSON:API and GraphQL, governed writes, draft translations, content tools, audit reports, and an SSH Drush bridge.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -83,6 +83,7 @@
83
83
  },
84
84
  "overrides": {
85
85
  "@hono/node-server": "^2.0.5",
86
+ "hono": "^4.13.5",
86
87
  "postcss": "^8.5.18"
87
88
  }
88
89
  }
@@ -134,7 +134,7 @@ export function renderCodexSkillMarkdown(definitions = allDefinitions) {
134
134
  "---",
135
135
  `name: ${yamlString(CODEX_SKILL_NAME)}`,
136
136
  `description: ${yamlString(
137
- "Use Drupal MCP tools (drupal_*) via drupal-mcp-connector to read or write Drupal content, media, taxonomy, reports, and config. Trigger when the user asks to work with a Drupal site over MCP.",
137
+ "Use Drupal MCP tools (drupal_*) via Drupal MCP Connector (drupal-mcp-connector) to read or write Drupal content, media, taxonomy, reports, and config. Trigger when the user asks to work with a Drupal site over MCP.",
138
138
  )}`,
139
139
  "---",
140
140
  "",
@@ -161,7 +161,7 @@ export function renderCodexToolsReference(definitions = allDefinitions) {
161
161
  const lines = [
162
162
  "# Drupal MCP tools",
163
163
  "",
164
- "Protocol tool names for `drupal-mcp-connector`. Call these via MCP; do not invent REST paths.",
164
+ "Protocol tool names for Drupal MCP Connector. Call these via MCP; do not invent REST paths.",
165
165
  "",
166
166
  ];
167
167
  for (const def of definitions) {
package/src/index.js CHANGED
@@ -33,7 +33,7 @@ import { createMcpHandler } from "@modelcontextprotocol/server";
33
33
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
34
34
  import { toNodeHandler } from "@modelcontextprotocol/node";
35
35
 
36
- import { listSiteNames, getTlsConfig, loadConfig, CLIENT_VERSION } from "./lib/config.js";
36
+ import { listSiteNames, getTlsConfig, loadConfig, CLIENT_VERSION, SERVER_INFO } from "./lib/config.js";
37
37
  import { loadLocalSecrets, secretLoadFatalMessage, secretTableMismatchMessage } from "./lib/load-secrets.js";
38
38
  import {
39
39
  makeBearerCheck,
@@ -291,7 +291,7 @@ function getPromptMessages(name, args) {
291
291
  // ---------------------------------------------------------------------------
292
292
 
293
293
  const buildConnectorServer = createConnectorServerFactory({
294
- serverInfo: { name: "drupal-mcp-connector", version: CLIENT_VERSION },
294
+ serverInfo: SERVER_INFO,
295
295
  tools: {
296
296
  definitions: allDefinitions,
297
297
  list: discoverableTools,