drupal-mcp-connector 2.14.2 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,25 @@
1
1
  ---
2
- description: "Create a translation as an unpublished non-default draft revision (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. English live title, body, status, alias, and default revision stay unchanged. An existing translation is a conflict, not an overwrite. Continue the draft with drupal_update_node and langcode. Requires Sentinel's draft-translation endpoint and a translatable bundle. Paragraph field values are not translated on this path. Defaults to node. Publication stays denied for content-tier callers."
3
- argument-hint: "<type> <id> <langcode> [site] [entityType] [attributes] [dryRun]"
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."
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 revision (governed write). Adds the target language beside the default language; it does not PATCH langcode on the canonical entity. English live title, body, status, alias, and default revision stay unchanged. An existing translation is a conflict, not an overwrite. Continue the draft with drupal_update_node and langcode. Requires Sentinel's draft-translation endpoint and a translatable bundle. Paragraph field values are not translated on this path. Defaults to node. 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; 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.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
12
12
  **Required:**
13
- - `type` (string): Bundle machine name, e.g. 'basic_page'
13
+ - `type` (string): Bundle machine name, e.g. 'basic_page' or 'p_hero'
14
14
  - `id` (string): Entity UUID
15
15
  - `langcode` (string): Target language code, e.g. 'es', 'de', 'pt-br'
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'.
19
+ - `entityType` (string): Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values.
20
20
  - `attributes` (object (pass as JSON)): Translated field values keyed by Drupal machine name
21
+ - `relationships` (object (pass as JSON)): JSON:API relationships. Use for image alt (same file UUID, meta.alt).
22
+ - `revisionId` (string): Paragraph revision id the host already pins. Required for Home-shaped non-default pins.
21
23
  - `dryRun` (boolean (true/false)): Validate without saving
22
24
 
23
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.
@@ -19,7 +19,7 @@ Parse the arguments supplied with this command into this tool's parameters:
19
19
  - `langcode` (string): Target language for an unpublished working translation (nodes). Continues that translation via Sentinel.
20
20
  - `attributes` (object (pass as JSON))
21
21
  - `relationships` (object (pass as JSON))
22
- - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. Any refusal fails the dryRun.
22
+ - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. A published node with no distinct working copy whose changed timestamp is later than revision_timestamp (possiblyPatchBlocked) fails dryRun the same as the real write (#273). Any refusal fails the dryRun.
23
23
  - `returning` (string): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
24
24
 
25
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: "Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy."
3
- argument-hint: "<paragraphType> <id> [site]"
2
+ description: "Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass revisionId (the host pin) to read that revision via JSON:API resourceVersion=id:<vid>; a mismatch or miss is an error, not a silent default-revision fallback. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy."
3
+ argument-hint: "<paragraphType> <id> [site] [langcode] [revisionId]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_get_paragraph`.
7
7
 
8
- Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.
8
+ Fetch a single Paragraph entity by paragraph type (bundle) and UUID. Returns the redacted paragraph (fields include drupal_internal__revision_id) plus a `ref` ({ type: 'paragraph--<bundle>', id, meta: { target_revision_id } }) you can use to embed it in a host entity's paragraph / ERR field. Pass revisionId (the host pin) to read that revision via JSON:API resourceVersion=id:<vid>; a mismatch or miss is an error, not a silent default-revision fallback. Pass langcode to read an unpublished working translation via Sentinel. Paragraphs are referenced from a host field rather than queried standalone in production. Governed by the site security policy.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -15,5 +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): Target language for the unpublished working translation (e.g. 'es').
19
+ - `revisionId` (string): Paragraph revision id the host already pins (ERR meta.target_revision_id). Honored on the read; a different served vid is an error.
18
20
 
19
21
  If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors."
2
+ description: "List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors. Drupal core JSON:API caps page[limit] at 50; a larger requested limit is filled by following links.next (up to 1000). When the site does not expose meta.count, total is exact only if this window reached the end of the collection; otherwise approximate is true and hasNext is set."
3
3
  argument-hint: "<type> [site] [status] [limit] [offset] [filters] [sort]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_list_nodes`.
7
7
 
8
- List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors.
8
+ List nodes of a given content type. Supports status filtering, pagination, sorting, and structured filter descriptors. Drupal core JSON:API caps page[limit] at 50; a larger requested limit is filled by following links.next (up to 1000). When the site does not expose meta.count, total is exact only if this window reached the end of the collection; otherwise approximate is true and hasNext is set.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "List live and working translation langcodes for a Drupal node. 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). 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. 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). 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
 
@@ -23,8 +23,8 @@ Parse the arguments supplied with this command into this tool's parameters:
23
23
  - `moderationState` (string): Moderation state transition for content_moderation types, e.g. 'draft', 'published', 'archived'. Takes precedence over status. Required to keep or re-publish a live node — omitting it on a published moderated node defaults the write to 'draft'.
24
24
  - `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.
25
25
  - `fields` (object (pass as JSON)): Scalar/attribute field values keyed by machine name. Formatted text: a string or { value, format?, summary? }. format must be in the field's allowed_formats; a single allowed format is used when omitted. Entity-reference fields go in `relationships`, not here.
26
- - `relationships` (object (pass as JSON)): Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value uses { data: { type, id } }; multi-value uses { data: [{ type, id }, …] }. Paragraph / ERR items must carry meta.target_revision_id — the connector injects it when missing, and fails the write if it cannot.
27
- - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. Any refusal fails the dryRun.
26
+ - `relationships` (object (pass as JSON)): Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value uses { data: { type, id } }; multi-value uses { data: [{ type, id }, …] }. Paragraph / ERR items must carry meta.target_revision_id — the connector injects it when missing, and fails the write if it cannot. Image alt on a translation uses the existing file UUID plus meta.alt; replacing the file is refused.
27
+ - `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, and (on moderated targets) run the core PATCH-guard probe against Drupal, then return a preview without the real write. An existing node draft uses Sentinel's non-saving draft endpoint with the real payload and revision preconditions. Otherwise an id-mismatch core PATCH probes writability without saving. A published node with no distinct working copy whose changed timestamp is later than revision_timestamp (possiblyPatchBlocked) fails dryRun the same as the real write (#273). Any refusal fails the dryRun.
28
28
  - `returning` (string): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
29
29
 
30
30
  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 Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy."
3
- argument-hint: "<paragraphType> <id> [site] [attributes]"
2
+ description: "Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Pass langcode to continue an unpublished paragraph translation via Sentinel (the host pin, not a live English mutation). Omitting langcode still uses canonical JSON:API and remains gated on published-host children. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy."
3
+ argument-hint: "<paragraphType> <id> [site] [attributes] [langcode] [revisionId] [draftState]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_update_paragraph`.
7
7
 
8
- Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy.
8
+ Update an existing Paragraph entity's field values by paragraph type (bundle) and UUID. Only the attributes you pass are changed (partial update); the host entity's reference to the paragraph is unchanged (same UUID), so this maintains a component paragraph in place without re-embedding. Pass langcode to continue an unpublished paragraph translation via Sentinel (the host pin, not a live English mutation). Omitting langcode still uses canonical JSON:API and remains gated on published-host children. Returns relationshipData including meta.target_revision_id for a later host attach. Use drupal_get_entity_schema (entityType 'paragraph', the bundle) to discover fields. Governed by the site security policy.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
@@ -16,5 +16,8 @@ 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
  - `attributes` (object (pass as JSON)): Paragraph field values to change, keyed by Drupal machine name, e.g. { field_body: { value: '<p>..</p>', format: 'full_html' } }
19
+ - `langcode` (string): Target language for an unpublished paragraph translation (e.g. 'es'). Continues Sentinel /mcp-draft; does not create a missing translation.
20
+ - `revisionId` (string): Paragraph revision id the host already pins. Required when that pin is not the default revision.
21
+ - `draftState` (string): Required with langcode. Opaque draftState from the previous paragraph draft read/create/update; prevents overwriting newer edits.
19
22
 
20
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,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.15.1] - 2026-09-10
11
+
12
+ ### Fixed
13
+ - **`drupal_get_paragraph` honors `revisionId` and refuses a default-revision fallback (#292).**
14
+ Passing the host pin without `langcode` now reads JSON:API
15
+ `?resourceVersion=id:<vid>`. If Drupal serves a different
16
+ `drupal_internal__revision_id` (or nothing), the tool errors instead of
17
+ returning the live default revision. `ref.meta.target_revision_id` is the
18
+ served vid. `langcode` still uses Sentinel's draft read.
19
+ - **`drupal_list_nodes` no longer reports a Drupal-capped page as the exact collection total (#291).**
20
+ Core JSON:API silently caps `page[limit]` at 50 and does not send `meta.count`.
21
+ The connector was treating that page length as `total` with `approximate: false`,
22
+ so a `limit: 100` call on a 54-item collection returned 50 and looked complete.
23
+ `listEntities` now follows `links.next` to fill a larger requested limit (up to
24
+ 1000) and only reports an exact `total` when `meta.count` is present or the
25
+ window reached the end. Otherwise `approximate` is true and `hasNext` is
26
+ returned on the tool result.
27
+
28
+ ## [2.15.0] - 2026-09-10
29
+
30
+ ### Fixed
31
+ - Paragraph translation updates require the `draftState` returned by a previous draft read. Pass it unchanged to Sentinel for stale-write protection. Shared-status paragraphs remain pending for human editing (#280).
32
+ - **Omit computed `metatag` on unpublished working-translation bodies (#283).**
33
+ JSON:API's computed `metatag` array is resolved from the live default
34
+ revision, so a Spanish draft response could still show English title and
35
+ description. The stored override (`field_metatags`) is unchanged. Draft
36
+ translation create/read now drop `fields.metatag` and set `_metatagOmitted`.
37
+
38
+ - **`drupal_create_translation` sends live and working revision IDs when a draft exists (#282).**
39
+ Sentinel's translation POST 409s with "A working revision exists" if If-Match
40
+ is live-only. The connector now reads Sentinel's translation inventory for
41
+ the live/working pair (falling back to `rel:working-copy`) and sends
42
+ `If-Match: "live:working"`. The response includes `_revisions` so the caller
43
+ can see which revision the language landed on.
44
+
45
+ - **`dryRun` fails when a published-node write would hit Sentinel's stale-copy check (#273).**
46
+ The id-mismatch PATCH probe never reaches entity validation / presave, so it
47
+ could not see MCP Sentinel's default-revision stale-version refusal. When
48
+ `rel:latest-version` and `rel:working-copy` share a vid and the default
49
+ revision's `changed` is later than `revision_timestamp` (`possiblyPatchBlocked`),
50
+ `drupal_update_node` / `drupal_entity_update` (including `dryRun`) now refuse
51
+ with a stable `STALE_COPY` error. The same message is rewritten if Sentinel
52
+ still returns it on the saving write. Governance is unchanged: the draft and
53
+ publish gates are not bypassed.
54
+ - **Title-only node updates keep the existing URL alias; failed restorations error (#274).**
55
+ `getPathInfo` now fills `pid` / `aliasId` from the `path_alias` row when an
56
+ unpublished default or forward revision omits them on the computed `path`
57
+ field. After `drupal_update_node`, the connector re-reads the alias and, if
58
+ Pathauto still drifted it, PATCHes the existing `path_alias` entity. If the
59
+ alias still does not match, the tool throws instead of returning success or
60
+ creating a rename redirect. Title-only edits pin the live alias as before.
61
+
62
+ ### Added
63
+ - **Codex skill target for `npm run install:commands` (#263).**
64
+ The installer writes `$HOME/.agents/skills/drupal-mcp/SKILL.md` (plus
65
+ `references/tools.md`) so Drupal MCP tools are discoverable in Codex. Custom
66
+ prompts under `~/.codex/prompts` are not written — OpenAI deprecated that
67
+ surface in favor of skills. Default clients are now `claude,grok,codex`.
68
+ Test with `--home` on an isolated directory.
69
+
70
+ - **Unpublished paragraph field-value translation and image alt (#280).**
71
+ `drupal_create_translation` accepts `entityType: "paragraph"` and
72
+ `revisionId` (the host pin) and POSTs Sentinel's paragraph
73
+ `/mcp-draft/translations` surface. Continue with `drupal_update_paragraph`
74
+ + `langcode`; read with `drupal_get_paragraph` + `langcode`. Omitting
75
+ `langcode` on `drupal_update_paragraph` still uses canonical JSON:API.
76
+ Node translation writes forward image relationships so alt can change
77
+ while the file UUID stays the same. Replacing the file remains a
78
+ server refusal. Node-only translation create is unchanged.
79
+
10
80
  ## [2.14.2] - 2026-09-10
11
81
 
12
82
  ### Fixed
@@ -1501,6 +1571,8 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
1501
1571
  - User tools gained explicit PII-access assertions.
1502
1572
  - Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
1503
1573
 
1574
+ [2.15.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.1
1575
+ [2.15.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.0
1504
1576
  [2.14.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.2
1505
1577
  [2.14.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.1
1506
1578
  [2.14.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.14.0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # drupal-mcp-connector
2
2
 
3
- > A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL access, governed content tools, audit reports, and an SSH Drush bridge.
3
+ > A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL access, governed draft translations, content tools, audit reports, and an SSH Drush bridge.
4
4
 
5
5
  [![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)
@@ -129,13 +129,15 @@ Clients that scan `.agents/commands/` (including Grok Build when this repo is
129
129
  the project) pick them up automatically.
130
130
 
131
131
  Clients that only scan a vendor home path (Claude Code `~/.claude/commands`,
132
- Grok `~/.grok/commands`) install from that tree into the **operator home**, so
133
- no consuming project has to commit `.claude/` or `.grok/`:
132
+ Grok `~/.grok/commands`, Codex `$HOME/.agents/skills`) install from that tree
133
+ into the **operator home**, so no consuming project has to commit `.claude/` or
134
+ `.grok/`. Codex uses a **skill** (`drupal-mcp`), not deprecated custom prompts:
134
135
 
135
136
  ```bash
136
137
  npm run generate:commands # refresh .agents/commands/ (from a clone)
137
- npm run install:commands # copies into ~/.claude/commands and ~/.grok/commands
138
+ npm run install:commands # copies into ~/.claude/commands, ~/.grok/commands, and ~/.agents/skills/drupal-mcp/
138
139
  # npm run install:commands -- --clients claude
140
+ # npm run install:commands -- --clients codex
139
141
  # npm run install:commands -- --home /path/to/fake-home # tests / CI
140
142
  ```
141
143
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.14.2",
4
- "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
3
+ "version": "2.15.1",
4
+ "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL access, governed draft translations, content tools, audit reports, and an SSH Drush bridge.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -8,14 +8,17 @@
8
8
  * Build, when this repo is the project) pick them up as `/drupal-<tool>`.
9
9
  *
10
10
  * Clients that only scan a vendor home directory (Claude Code `~/.claude/commands`,
11
- * Grok `~/.grok/commands`) get the same files via `npm run install:commands`.
11
+ * Grok `~/.grok/commands`, Codex `$HOME/.agents/skills`) get the same files via
12
+ * `npm run install:commands`. Codex receives one skill (`drupal-mcp`), not
13
+ * per-tool custom prompts.
12
14
  *
13
15
  * Driven from the same tool definitions as the server (src/tools/index.js), so the
14
16
  * command set never drifts from the tools. Run: `npm run generate:commands`.
15
17
  *
16
- * Exports `renderCommandMarkdown`, `renderClaudeCommandMarkdown`, `commandFileName`,
17
- * `COMMANDS_DIR`, and `generate` for tests; the file-writing side effect runs only
18
- * when executed directly.
18
+ * Exports `renderCommandMarkdown`, `renderClaudeCommandMarkdown`,
19
+ * `renderCodexSkillMarkdown`, `renderCodexToolsReference`, `CODEX_SKILL_NAME`,
20
+ * `commandFileName`, `COMMANDS_DIR`, and `generate` for tests; the file-writing
21
+ * side effect runs only when executed directly.
19
22
  */
20
23
 
21
24
  import { mkdirSync, readdirSync, rmSync, writeFileSync, realpathSync } from "fs";
@@ -113,6 +116,62 @@ export function renderClaudeCommandMarkdown(def) {
113
116
  });
114
117
  }
115
118
 
119
+ /** Codex user-skill directory name under `$HOME/.agents/skills/`. */
120
+ export const CODEX_SKILL_NAME = "drupal-mcp";
121
+
122
+ /**
123
+ * Codex skill adapter (OpenAI Skills). Custom `~/.codex/prompts` slash
124
+ * commands are deprecated; Codex discovers `$HOME/.agents/skills/<name>/SKILL.md`.
125
+ * One skill covers the whole Drupal MCP tool surface so the skills list stays
126
+ * inside Codex's discovery budget.
127
+ *
128
+ * @param {Array<object>} [definitions]
129
+ * @returns {string} SKILL.md contents.
130
+ */
131
+ export function renderCodexSkillMarkdown(definitions = allDefinitions) {
132
+ const count = definitions.length;
133
+ return [
134
+ "---",
135
+ `name: ${yamlString(CODEX_SKILL_NAME)}`,
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.",
138
+ )}`,
139
+ "---",
140
+ "",
141
+ "Call Drupal MCP tools by their protocol names (`drupal_list_nodes`, `drupal_update_node`, …).",
142
+ "The MCP server is configured separately (`codex mcp add drupal`); this skill does not replace that connection and does not use deprecated Codex custom prompts.",
143
+ "",
144
+ "Parse the user's request into the tool's parameters. If a required parameter is missing, ask before calling.",
145
+ "Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the tool call and summarize the result.",
146
+ "",
147
+ "Destructive tools permanently change or delete data — confirm with the user before calling.",
148
+ "Do not invent REST paths, Drush commands, or Drupal admin URLs when an MCP tool exists.",
149
+ "",
150
+ `The connector currently exposes **${count}** tools. Full names and descriptions: [references/tools.md](references/tools.md).`,
151
+ "",
152
+ ].join("\n");
153
+ }
154
+
155
+ /**
156
+ * Progressive-disclosure catalog for the Codex Drupal skill.
157
+ * @param {Array<object>} [definitions]
158
+ * @returns {string} references/tools.md contents.
159
+ */
160
+ export function renderCodexToolsReference(definitions = allDefinitions) {
161
+ const lines = [
162
+ "# Drupal MCP tools",
163
+ "",
164
+ "Protocol tool names for `drupal-mcp-connector`. Call these via MCP; do not invent REST paths.",
165
+ "",
166
+ ];
167
+ for (const def of definitions) {
168
+ const desc = String(def.description || "").replace(/\s+/g, " ").trim();
169
+ lines.push(`- \`${def.name}\`${desc ? ` — ${desc}` : ""}`);
170
+ }
171
+ lines.push("");
172
+ return lines.join("\n");
173
+ }
174
+
116
175
  /**
117
176
  * Write one command file per tool, pruning stale `drupal-*.md` first so removed
118
177
  * tools leave no orphans.
@@ -9,11 +9,12 @@
9
9
  * copies so `/drupal-*` works in every project without committing a vendor
10
10
  * folder to this repo or to a consuming application.
11
11
  *
12
- * Default targets: `~/.claude/commands` (Claude adapter) and `~/.grok/commands`
13
- * (canonical files). Pass `--clients` to subset. Never writes into a project
14
- * tree.
12
+ * Default targets: `~/.claude/commands` (Claude adapter), `~/.grok/commands`
13
+ * (canonical files), and `$HOME/.agents/skills/drupal-mcp/` (Codex skill).
14
+ * Pass `--clients` to subset. Never writes into a project tree. Codex custom
15
+ * prompts (`~/.codex/prompts`) are not written — they are deprecated.
15
16
  *
16
- * Run: `npm run install:commands -- [--home DIR] [--clients claude,grok,agents]`
17
+ * Run: `npm run install:commands -- [--home DIR] [--clients claude,grok,codex,agents]`
17
18
  */
18
19
 
19
20
  import { mkdirSync, readdirSync, rmSync, writeFileSync, realpathSync } from "fs";
@@ -26,6 +27,9 @@ import {
26
27
  commandFileName,
27
28
  renderCommandMarkdown,
28
29
  renderClaudeCommandMarkdown,
30
+ renderCodexSkillMarkdown,
31
+ renderCodexToolsReference,
32
+ CODEX_SKILL_NAME,
29
33
  } from "./generate-commands.js";
30
34
 
31
35
  /** Whitelisted install targets. `rel` is under `--home` (default: os.homedir()). */
@@ -42,9 +46,18 @@ export const CLIENTS = {
42
46
  rel: ".agents/commands",
43
47
  render: renderCommandMarkdown,
44
48
  },
49
+ // Codex discovers user skills at `$HOME/.agents/skills/<name>/SKILL.md`.
50
+ // One skill for the whole tool surface — not one skill per tool.
51
+ codex: {
52
+ rel: ".agents/skills",
53
+ kind: "skill",
54
+ skillName: CODEX_SKILL_NAME,
55
+ renderSkill: renderCodexSkillMarkdown,
56
+ renderReference: renderCodexToolsReference,
57
+ },
45
58
  };
46
59
 
47
- const DEFAULT_CLIENTS = ["claude", "grok"];
60
+ const DEFAULT_CLIENTS = ["claude", "grok", "codex"];
48
61
 
49
62
  /**
50
63
  * Parse CLI flags. Unknown flags throw.
@@ -111,6 +124,15 @@ export function install(options = {}) {
111
124
  if (!client) {
112
125
  throw new Error(`Unknown client "${name}". Allowed: ${Object.keys(CLIENTS).join(", ")}`);
113
126
  }
127
+ if (client.kind === "skill") {
128
+ const dir = join(home, client.rel, client.skillName);
129
+ rmSync(dir, { recursive: true, force: true });
130
+ mkdirSync(join(dir, "references"), { recursive: true });
131
+ writeFileSync(join(dir, "SKILL.md"), client.renderSkill(definitions));
132
+ writeFileSync(join(dir, "references", "tools.md"), client.renderReference(definitions));
133
+ results.push({ client: name, dir, written: ["SKILL.md", "references/tools.md"] });
134
+ continue;
135
+ }
114
136
  const dir = join(home, client.rel);
115
137
  mkdirSync(dir, { recursive: true });
116
138
  for (const f of readdirSync(dir)) {
@@ -127,14 +149,15 @@ export function install(options = {}) {
127
149
  return results;
128
150
  }
129
151
 
130
- const HELP = `Usage: node scripts/install-commands.js [--home DIR] [--clients claude,grok,agents]
152
+ const HELP = `Usage: node scripts/install-commands.js [--home DIR] [--clients claude,grok,codex,agents]
131
153
 
132
- Copy generated /drupal-* command stubs into operator home directories.
133
- Does not write into a project tree.
154
+ Copy generated /drupal-* command stubs (and the Codex skill) into operator
155
+ home directories. Does not write into a project tree. Does not write
156
+ deprecated Codex custom prompts (~/.codex/prompts).
134
157
 
135
158
  --home DIR Install root (default: the current user's home)
136
- --clients LIST Comma-separated subset of: claude, grok, agents
137
- (default: claude,grok)
159
+ --clients LIST Comma-separated subset of: claude, grok, codex, agents
160
+ (default: claude,grok,codex)
138
161
  `;
139
162
 
140
163
  const invokedDirectly =
@@ -79,10 +79,12 @@ export class Backend {
79
79
  * for callers that must round-trip the alias `pid` on an in-place update (the
80
80
  * canonical entity only exposes `path.alias` as `url`). Optional capability:
81
81
  * the default returns nulls so read-only/path-less backends are safe. See the 1.5.1 alias fix.
82
- * @param {{entityType: string, bundle: string, id: string}} _ref
83
- * @returns {Promise<{alias: ?string, pid: ?(number|string), langcode: ?string, drupalId: ?(number|string)}>}
82
+ * @param {{entityType: string, bundle: string, id: string, resourceVersion?: string}} _ref
83
+ * @returns {Promise<{alias: ?string, pid: ?(number|string), langcode: ?string, drupalId: ?(number|string), aliasId: ?string}>}
84
84
  */
85
- async getPathInfo(_ref) { return { alias: null, pid: null, langcode: null, drupalId: null }; }
85
+ async getPathInfo(_ref) {
86
+ return { alias: null, pid: null, langcode: null, drupalId: null, aliasId: null };
87
+ }
86
88
 
87
89
  /**
88
90
  * Create an entity.