drupal-mcp-connector 2.15.2 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/commands/drupal-codegen-diff.md +17 -0
- package/.agents/commands/drupal-codegen-generate.md +17 -0
- package/.agents/commands/drupal-codegen-inspect.md +17 -0
- package/.agents/commands/drupal-content-by-moderation-state.md +4 -3
- package/.agents/commands/drupal-describe-fields.md +2 -2
- package/.agents/commands/drupal-get-node.md +4 -3
- package/.agents/commands/drupal-get-taxonomy-term.md +4 -3
- package/.agents/commands/drupal-list-translations.md +2 -2
- package/.agents/commands/drupal-report-translation-coverage.md +4 -5
- package/.agents/commands/drupal-report-workflow-bottlenecks.md +2 -1
- package/.agents/commands/drupal-set-moderation-state.md +4 -3
- package/.agents/commands/drupal-update-menu-link.md +2 -1
- package/.agents/commands/drupal-update-taxonomy-term.md +4 -3
- package/CHANGELOG.md +38 -0
- package/README.md +3 -3
- package/bin/drupal-mcp-agent.js +2 -2
- package/package.json +2 -2
- package/scripts/generate-commands.js +2 -2
- package/src/index.js +2 -2
- package/src/lib/backends/jsonapi.js +85 -9
- package/src/lib/config.js +18 -1
- package/src/lib/err-relationships.js +22 -0
- package/src/lib/mcp-server.js +1 -1
- package/src/lib/server-tools.js +2 -2
- package/src/lib/translation-rows.js +59 -0
- package/src/tools/codegen.js +150 -0
- package/src/tools/fields.js +12 -4
- package/src/tools/index.js +2 -1
- package/src/tools/moderation.js +82 -13
- package/src/tools/nodes.js +56 -6
- package/src/tools/reports-content.js +117 -28
- package/src/tools/structure.js +6 -3
- package/src/tools/taxonomy.js +15 -6
- package/src/tools/translations.js +4 -8
|
@@ -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: "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 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 or paragraph. 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 or paragraph. 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
|
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
3
|
-
argument-hint: "[site] [type] [
|
|
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
|
-
|
|
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
|
-
- `
|
|
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,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,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.16.0] - 2026-09-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Optional graphql_compose_codegen tools (#308).**
|
|
14
|
+
`drupal_codegen_inspect`, `drupal_codegen_diff`, and `drupal_codegen_generate`
|
|
15
|
+
call `drush graphql-compose-codegen:*` over the existing SSH bridge.
|
|
16
|
+
Generate is `--dry-run` only (artefacts as text; no `--output-dir`).
|
|
17
|
+
Missing module or unknown command fails loud. If `allowedCommands` is set,
|
|
18
|
+
the matching `graphql-compose-codegen:*` subcommand must be listed.
|
|
19
|
+
- **Field `translatable`, taxonomy/menu `langcode`, and composed node reads (#303).**
|
|
20
|
+
`drupal_describe_fields` copies Field API `translatable` when JSON:API
|
|
21
|
+
`field_config` is readable (omitted means unknown). Taxonomy get/update and
|
|
22
|
+
menu-link update accept optional `langcode` (existing translations only).
|
|
23
|
+
`drupal_get_node` accepts `includeComponents` (default false) to attach
|
|
24
|
+
pinned paragraph translations.
|
|
25
|
+
- **Optional `langcode` on the editorial queue (#303).**
|
|
26
|
+
`drupal_content_by_moderation_state` and `drupal_set_moderation_state` accept
|
|
27
|
+
a translation language. Omitting it keeps the previous default-language
|
|
28
|
+
behavior. A langcode write uses Sentinel's draft-translation surface (no
|
|
29
|
+
canonical `langcode` PATCH). A langcode list uses the translation inventory;
|
|
30
|
+
without Sentinel the result is `unavailable`. `drupal_report_workflow_bottlenecks`
|
|
31
|
+
has the same optional filter.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **`drupal_list_translations` passes through core outdated/source (#303).**
|
|
35
|
+
When Sentinel sends `content_translation_outdated` / `content_translation_source`,
|
|
36
|
+
those keys appear on each row. They are omitted when absent.
|
|
37
|
+
- **`drupal_report_translation_coverage` uses Sentinel inventory (#303).**
|
|
38
|
+
Per-node missing non-default language and outdated flags replace the
|
|
39
|
+
default-language histogram. Without Sentinel the report is `unavailable`
|
|
40
|
+
rather than a misleading 100% default-language chart.
|
|
41
|
+
- **Public product name is Drupal MCP Connector (#301).** Document titles and
|
|
42
|
+
the MCP handshake `serverInfo.title` now use the title-case product name.
|
|
43
|
+
The machine identifiers are unchanged: npm package, GitHub repo, binaries,
|
|
44
|
+
`X-MCP-Client`, log prefixes, and `serverInfo.name` remain
|
|
45
|
+
`drupal-mcp-connector`.
|
|
46
|
+
|
|
10
47
|
## [2.15.2] - 2026-09-11
|
|
11
48
|
|
|
12
49
|
### Fixed
|
|
@@ -1583,6 +1620,7 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
|
|
|
1583
1620
|
- User tools gained explicit PII-access assertions.
|
|
1584
1621
|
- Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
|
|
1585
1622
|
|
|
1623
|
+
[2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
|
|
1586
1624
|
[2.15.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.2
|
|
1587
1625
|
[2.15.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.1
|
|
1588
1626
|
[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
|
-
#
|
|
1
|
+
# Drupal MCP Connector
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](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."
|
package/bin/drupal-mcp-agent.js
CHANGED
|
@@ -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:
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.16.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": {
|
|
@@ -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
|
|
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:
|
|
294
|
+
serverInfo: SERVER_INFO,
|
|
295
295
|
tools: {
|
|
296
296
|
definitions: allDefinitions,
|
|
297
297
|
list: discoverableTools,
|
|
@@ -32,6 +32,38 @@ const INTERNAL_ATTR_RE = /^drupal_internal__/;
|
|
|
32
32
|
const COUNT_PAGE_SIZE = 50;
|
|
33
33
|
const COUNT_MAX_RECORDS = 1000;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* JSON:API language headers. Drupal negotiates Content-Language / Accept-Language
|
|
37
|
+
* when URL prefixes are not used on /jsonapi.
|
|
38
|
+
* @param {?string} langcode
|
|
39
|
+
* @returns {object}
|
|
40
|
+
*/
|
|
41
|
+
function languageFetchOptions(langcode) {
|
|
42
|
+
if (!langcode) return {};
|
|
43
|
+
return { headers: { "Content-Language": langcode, "Accept-Language": langcode } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fail if JSON:API served a different language than requested.
|
|
48
|
+
* @param {?object} entity
|
|
49
|
+
* @param {?string} langcode
|
|
50
|
+
*/
|
|
51
|
+
function assertServedLanguage(entity, langcode) {
|
|
52
|
+
if (!langcode || !entity) return;
|
|
53
|
+
if (!entity.langcode) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`JSON:API did not report a language for requested langcode "${langcode}". ` +
|
|
56
|
+
"The translation may not exist, or this backend does not negotiate language.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (entity.langcode !== langcode) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`JSON:API served language "${entity.langcode}" for requested langcode "${langcode}". ` +
|
|
62
|
+
"The translation may not exist, or this site does not negotiate JSON:API by language.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
/**
|
|
36
68
|
* Whether a JSON:API collection document advertises another page.
|
|
37
69
|
* `links.next` may be a string href or a `{ href }` link object.
|
|
@@ -358,14 +390,16 @@ export class JsonApiBackend extends Backend {
|
|
|
358
390
|
* @param {{entityType: string, bundle: string, id: string, resourceVersion?: string}} ref
|
|
359
391
|
* @returns {Promise<?import("../canonical.js").CanonicalEntity>} Entity, or null.
|
|
360
392
|
*/
|
|
361
|
-
async getEntity({ entityType, bundle, id, resourceVersion }) {
|
|
393
|
+
async getEntity({ entityType, bundle, id, resourceVersion, langcode }) {
|
|
362
394
|
validateUuid(id);
|
|
363
395
|
let path = `${this.resourcePath(entityType, bundle)}/${encodeURIComponent(id)}`;
|
|
364
396
|
if (resourceVersion) {
|
|
365
397
|
path += `?resourceVersion=${encodeURIComponent(resourceVersion)}`;
|
|
366
398
|
}
|
|
367
|
-
const data = await drupalFetch(this.site, path);
|
|
368
|
-
|
|
399
|
+
const data = await drupalFetch(this.site, path, languageFetchOptions(langcode));
|
|
400
|
+
const entity = data?.data ? this.toCanonical(data.data) : null;
|
|
401
|
+
assertServedLanguage(entity, langcode);
|
|
402
|
+
return entity;
|
|
369
403
|
}
|
|
370
404
|
|
|
371
405
|
/**
|
|
@@ -478,14 +512,15 @@ export class JsonApiBackend extends Backend {
|
|
|
478
512
|
* @param {object} attributes Entity attributes (may include `status`).
|
|
479
513
|
* @returns {Promise<object>} The JSON:API response body.
|
|
480
514
|
*/
|
|
481
|
-
async writeWithModerationFallback(path, method, buildPayload, attributes) {
|
|
515
|
+
async writeWithModerationFallback(path, method, buildPayload, attributes, langcode) {
|
|
516
|
+
const lang = languageFetchOptions(langcode);
|
|
482
517
|
try {
|
|
483
|
-
return await drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(attributes)) });
|
|
518
|
+
return await drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(attributes)), ...lang });
|
|
484
519
|
} catch (err) {
|
|
485
520
|
if (!isModeratedStatusError(err) || !("status" in attributes)) throw err;
|
|
486
521
|
const withoutStatus = { ...attributes };
|
|
487
522
|
delete withoutStatus.status;
|
|
488
|
-
return drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(withoutStatus)) });
|
|
523
|
+
return drupalFetch(this.site, path, { method, body: JSON.stringify(buildPayload(withoutStatus)), ...lang });
|
|
489
524
|
}
|
|
490
525
|
}
|
|
491
526
|
|
|
@@ -515,7 +550,7 @@ export class JsonApiBackend extends Backend {
|
|
|
515
550
|
* canonical default (#166 / Drupal #2795279).
|
|
516
551
|
* @returns {Promise<import("../canonical.js").CanonicalEntity>} The updated entity.
|
|
517
552
|
*/
|
|
518
|
-
async updateEntity({ entityType, bundle, id, attributes = {}, relationships, resourceVersion }) {
|
|
553
|
+
async updateEntity({ entityType, bundle, id, attributes = {}, relationships, resourceVersion, langcode }) {
|
|
519
554
|
validateUuid(id);
|
|
520
555
|
const buildPayload = (attrs) => {
|
|
521
556
|
const payload = { data: { type: `${entityType}--${bundle}`, id, attributes: attrs } };
|
|
@@ -527,8 +562,10 @@ export class JsonApiBackend extends Backend {
|
|
|
527
562
|
if (resourceVersion) {
|
|
528
563
|
path += `?resourceVersion=${encodeURIComponent(resourceVersion)}`;
|
|
529
564
|
}
|
|
530
|
-
const data = await this.writeWithModerationFallback(path, "PATCH", buildPayload, attributes);
|
|
531
|
-
|
|
565
|
+
const data = await this.writeWithModerationFallback(path, "PATCH", buildPayload, attributes, langcode);
|
|
566
|
+
const entity = this.toCanonical(data.data);
|
|
567
|
+
assertServedLanguage(entity, langcode);
|
|
568
|
+
return entity;
|
|
532
569
|
}
|
|
533
570
|
|
|
534
571
|
/**
|
|
@@ -704,6 +741,45 @@ export class JsonApiBackend extends Backend {
|
|
|
704
741
|
* @param {{entityType: string, bundle: string, fieldName: string}} ref
|
|
705
742
|
* @returns {Promise<?{fieldName: string, fieldType: ?string, allowedFormats: string[]}>}
|
|
706
743
|
*/
|
|
744
|
+
/**
|
|
745
|
+
* Map of field machine name → translatable from JSON:API field_config.
|
|
746
|
+
* Base fields (title, moderation_state) are omitted unless exposed as
|
|
747
|
+
* base_field_override. Missing keys must not be treated as false.
|
|
748
|
+
* @param {string} entityType
|
|
749
|
+
* @param {string} bundle
|
|
750
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
751
|
+
*/
|
|
752
|
+
async listFieldTranslatability(entityType, bundle) {
|
|
753
|
+
validateMachineName(entityType, "entityType");
|
|
754
|
+
validateMachineName(bundle, "bundle");
|
|
755
|
+
const map = new Map();
|
|
756
|
+
const params = new URLSearchParams();
|
|
757
|
+
params.set("filter[entity_type]", entityType);
|
|
758
|
+
params.set("filter[bundle]", bundle);
|
|
759
|
+
params.set("page[limit]", "50");
|
|
760
|
+
const ingest = (rows) => {
|
|
761
|
+
for (const row of rows) {
|
|
762
|
+
const name = row?.attributes?.field_name;
|
|
763
|
+
if (typeof name === "string" && typeof row.attributes?.translatable === "boolean") {
|
|
764
|
+
map.set(name, row.attributes.translatable);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
try {
|
|
769
|
+
const data = await drupalFetch(this.site, `/jsonapi/field_config/field_config?${params}`);
|
|
770
|
+
ingest(Array.isArray(data?.data) ? data.data : []);
|
|
771
|
+
} catch {
|
|
772
|
+
// field_config may be unexposed.
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const data = await drupalFetch(this.site, `/jsonapi/base_field_override/base_field_override?${params}`);
|
|
776
|
+
ingest(Array.isArray(data?.data) ? data.data : []);
|
|
777
|
+
} catch {
|
|
778
|
+
// base_field_override is often not JSON:API-exposed.
|
|
779
|
+
}
|
|
780
|
+
return Object.fromEntries(map);
|
|
781
|
+
}
|
|
782
|
+
|
|
707
783
|
async getFieldDefinition({ entityType, bundle, fieldName }) {
|
|
708
784
|
validateMachineName(entityType, "entityType");
|
|
709
785
|
validateMachineName(bundle, "bundle");
|
package/src/lib/config.js
CHANGED
|
@@ -16,9 +16,26 @@ import { getAccessToken } from "./oauth.js";
|
|
|
16
16
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- fixed path relative to this module (the package's own package.json), not user input
|
|
17
17
|
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
18
18
|
|
|
19
|
+
/** npm / protocol machine name. Public identifier — not the display name. */
|
|
20
|
+
export const CLIENT_NAME = "drupal-mcp-connector";
|
|
21
|
+
|
|
22
|
+
/** Human-readable product name for docs and MCP `serverInfo.title`. */
|
|
23
|
+
export const CLIENT_TITLE = "Drupal MCP Connector";
|
|
24
|
+
|
|
19
25
|
/** Connector version, sourced from package.json so it never drifts out of sync. */
|
|
20
26
|
export const CLIENT_VERSION = pkg.version;
|
|
21
27
|
|
|
28
|
+
/**
|
|
29
|
+
* MCP Implementation advertised in the handshake.
|
|
30
|
+
* `name` is the protocol identifier; `title` is the display name.
|
|
31
|
+
* @type {{name: string, title: string, version: string}}
|
|
32
|
+
*/
|
|
33
|
+
export const SERVER_INFO = {
|
|
34
|
+
name: CLIENT_NAME,
|
|
35
|
+
title: CLIENT_TITLE,
|
|
36
|
+
version: CLIENT_VERSION,
|
|
37
|
+
};
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* Identity headers sent on every outbound Drupal request. Lets governance layers
|
|
24
41
|
* label/identify connector traffic. ON by default; set MCP_CLIENT_ID to override
|
|
@@ -26,7 +43,7 @@ export const CLIENT_VERSION = pkg.version;
|
|
|
26
43
|
* @returns {Object<string,string>} Header map (empty when the identity is disabled).
|
|
27
44
|
*/
|
|
28
45
|
export function clientHeaders() {
|
|
29
|
-
const id = process.env.MCP_CLIENT_ID ??
|
|
46
|
+
const id = process.env.MCP_CLIENT_ID ?? `${CLIENT_NAME}/${CLIENT_VERSION}`;
|
|
30
47
|
if (!id) return {};
|
|
31
48
|
return { "X-MCP-Client": id, "User-Agent": id };
|
|
32
49
|
}
|
|
@@ -38,6 +38,28 @@ export function isParagraphResourceType(type) {
|
|
|
38
38
|
return typeof type === "string" && type.startsWith("paragraph--");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Paragraph ERR pins on a canonical host entity.
|
|
43
|
+
* @param {object} entity Canonical node (or other host).
|
|
44
|
+
* @returns {Array<{field: string, id: string, paragraphType: string, revisionId: ?string}>}
|
|
45
|
+
*/
|
|
46
|
+
export function paragraphPinsFromEntity(entity) {
|
|
47
|
+
const pins = [];
|
|
48
|
+
const rels = entity?.relationships && typeof entity.relationships === "object" ? entity.relationships : {};
|
|
49
|
+
for (const [field, value] of Object.entries(rels)) {
|
|
50
|
+
const list = Array.isArray(value) ? value : value ? [value] : [];
|
|
51
|
+
for (const ref of list) {
|
|
52
|
+
if (!ref?.id) continue;
|
|
53
|
+
const paragraphType = ref.bundle || parseResourceType(ref.type)?.bundle;
|
|
54
|
+
if (ref.entityType !== "paragraph" && !isParagraphResourceType(ref.type)) continue;
|
|
55
|
+
if (!paragraphType) continue;
|
|
56
|
+
const revisionId = ref.meta?.target_revision_id ?? null;
|
|
57
|
+
pins.push({ field, id: ref.id, paragraphType, revisionId });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return pins;
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
/**
|
|
42
64
|
* Split a JSON:API resource type into entity type + bundle.
|
|
43
65
|
* @param {string} type e.g. "paragraph--capability".
|
package/src/lib/mcp-server.js
CHANGED
|
@@ -29,7 +29,7 @@ function resourceUriIsListed(listed, requested) {
|
|
|
29
29
|
* Create the server factory shared by HTTP and stdio transports.
|
|
30
30
|
*
|
|
31
31
|
* @param {object} surface
|
|
32
|
-
* @param {{name: string, version: string}} surface.serverInfo
|
|
32
|
+
* @param {{name: string, title?: string, version: string}} surface.serverInfo
|
|
33
33
|
* @param {{definitions: Array<object>, list?: () => Promise<Array<object>>, call: (name: string, args: object, context: object) => Promise<object>}} surface.tools
|
|
34
34
|
* `definitions` is the full static surface (schema projection); the optional
|
|
35
35
|
* `list` hook decides what is DISCOVERABLE per request (governance + entitlement).
|