drupal-mcp-connector 2.25.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/commands/drupal-audit-site-health.md +1 -1
- package/.agents/commands/drupal-entity-update.md +1 -1
- package/.agents/commands/drupal-report-accessibility-audit.md +4 -2
- package/.agents/commands/drupal-report-alias-coverage.md +4 -2
- package/.agents/commands/drupal-report-broken-embeds.md +4 -2
- package/.agents/commands/drupal-report-broken-links.md +4 -2
- package/.agents/commands/drupal-report-content-by-author.md +4 -2
- package/.agents/commands/drupal-report-duplicate-content.md +4 -2
- package/.agents/commands/drupal-report-missing-field.md +2 -2
- package/.agents/commands/drupal-report-orphan-pages.md +4 -2
- package/.agents/commands/drupal-report-orphaned-references.md +4 -2
- package/.agents/commands/drupal-report-pii-exposure.md +4 -2
- package/.agents/commands/drupal-report-readability.md +4 -2
- package/.agents/commands/drupal-report-recently-published.md +1 -1
- package/.agents/commands/drupal-report-revision-hotspots.md +4 -2
- package/.agents/commands/drupal-report-scheduled-content.md +4 -4
- package/.agents/commands/drupal-report-seo-audit.md +4 -2
- package/.agents/commands/drupal-report-seo-meta-coverage.md +4 -2
- package/.agents/commands/drupal-report-stale-content.md +4 -2
- package/.agents/commands/drupal-report-taxonomy-usage.md +2 -2
- package/.agents/commands/drupal-report-translation-coverage.md +4 -2
- package/.agents/commands/drupal-report-unpublished.md +1 -1
- package/.agents/commands/drupal-report-workflow-bottlenecks.md +4 -2
- package/.agents/commands/drupal-schedule-publish.md +5 -3
- package/.agents/commands/drupal-search-content.md +1 -1
- package/.agents/commands/drupal-search.md +1 -1
- package/.agents/commands/drupal-update-node.md +1 -1
- package/CHANGELOG.md +114 -0
- package/README.md +57 -1
- package/bin/drupal-mcp-doctor.js +16 -0
- package/bin/drupal-mcp-wizard.js +17 -0
- package/package.json +10 -5
- package/src/index.js +19 -19
- package/src/lib/backends/jsonapi.js +19 -7
- package/src/lib/dispatch.js +22 -0
- package/src/lib/doctor.js +645 -0
- package/src/lib/module-tools.js +33 -2
- package/src/lib/operations.js +1 -1
- package/src/lib/patch-preflight.js +32 -32
- package/src/lib/principal.js +13 -5
- package/src/lib/reports-support.js +68 -0
- package/src/lib/security.js +42 -44
- package/src/lib/sentinel-draft.js +59 -3
- package/src/lib/server-tools.js +3 -3
- package/src/lib/transports.js +153 -0
- package/src/lib/version-skew.js +193 -0
- package/src/lib/wizard.js +902 -0
- package/src/lib/workflow-prompts.js +5 -5
- package/src/tools/audit-composite.js +21 -11
- package/src/tools/codegen.js +2 -2
- package/src/tools/config.js +11 -6
- package/src/tools/entities.js +1 -1
- package/src/tools/index.js +1 -1
- package/src/tools/media.js +2 -2
- package/src/tools/nodes.js +17 -9
- package/src/tools/reports-config.js +3 -3
- package/src/tools/reports-content.js +109 -39
- package/src/tools/reports-extra.js +56 -32
- package/src/tools/reports-links.js +13 -13
- package/src/tools/reports.js +65 -42
- package/src/tools/revisions.js +2 -1
- package/src/tools/scheduler.js +142 -27
- package/src/tools/search.js +16 -12
- package/src/tools/translations.js +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,120 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.0.0] - 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`drupal_schedule_publish` site refusals (#402).** A 422 such as
|
|
14
|
+
`publish_state: You do not have access to transition from Draft to
|
|
15
|
+
Published` was rewritten as "Scheduler fields are not available", because
|
|
16
|
+
any message naming a Scheduler field counted as a missing field. Only the
|
|
17
|
+
"The attribute X does not exist on the Y resource type" form counts now. Other 4xx refusals,
|
|
18
|
+
including MCP Sentinel denials, return the site's reason verbatim with the
|
|
19
|
+
HTTP status.
|
|
20
|
+
- **`drupal_schedule_publish` no longer refuses on its own (#402).** A
|
|
21
|
+
moderated bundle without `publish_state` / `unpublish_state` is written and
|
|
22
|
+
the result carries a warning; Drupal and Sentinel decide. A date without
|
|
23
|
+
its state is still refused when the bundle has the state field, since
|
|
24
|
+
Drupal accepts that schedule and it never runs. That check now also applies
|
|
25
|
+
when `moderation_state` is not visible to the account.
|
|
26
|
+
- **`drupal_report_scheduled_content` on an empty bundle (#403).** A bundle
|
|
27
|
+
with no nodes reports pending 0 / overdue 0 with `schedulerFields:
|
|
28
|
+
"unknown"` instead of `gated: true`. A gated bundle that hit the sample cap
|
|
29
|
+
keeps `approximate: true`.
|
|
30
|
+
- **`listContentTypes` read only the first 50 node types (JSON:API).** It
|
|
31
|
+
now follows `links.next`, so all-bundle scans and
|
|
32
|
+
`drupal_report_content_summary` see every content type.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- **No node tool defaults to the `article` bundle any more (#403).** On a
|
|
36
|
+
site without that bundle the old default returned a 404 or scanned nothing.
|
|
37
|
+
- Require `type`: `drupal_report_duplicate_content`, `_workflow_bottlenecks`,
|
|
38
|
+
`_translation_coverage`, `_readability`, `_orphan_pages`, `_pii_exposure`,
|
|
39
|
+
`_seo_meta_coverage`, `_stale_content`, `_content_by_author`,
|
|
40
|
+
`_revision_hotspots`, `_seo_audit`, `_accessibility_audit`,
|
|
41
|
+
`_broken_links`, `_alias_coverage`, `_broken_embeds`, `_missing_field` and
|
|
42
|
+
`_orphaned_references`. `drupal_report_taxonomy_usage` requires
|
|
43
|
+
`contentType`. The error points to `drupal_list_content_types`.
|
|
44
|
+
- Scan every node bundle when `type` is omitted:
|
|
45
|
+
`drupal_report_scheduled_content` (one row per bundle in `byContentType`),
|
|
46
|
+
`drupal_report_recently_published` and `drupal_report_unpublished`
|
|
47
|
+
(merged newest first; unreadable bundles in `bundleErrors`), and
|
|
48
|
+
`drupal_search` / `drupal_search_content` (merged newest first; a bundle
|
|
49
|
+
that cannot be read fails the search and is named).
|
|
50
|
+
- `drupal_audit_site_health` without `type` reports its content-type
|
|
51
|
+
sections as unavailable and still runs the site-wide ones.
|
|
52
|
+
- **Local operator files stay untracked.** `config/config.*.json` is ignored
|
|
53
|
+
except `config/config.example.json`. One-off `scripts/*-staging.mjs`
|
|
54
|
+
loaders are ignored too.
|
|
55
|
+
|
|
56
|
+
## [2.26.0] - 2026-10-01
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- **Nightly leftover cleanup (#399).** CI pins Node 20 (no 1-cell matrix) and
|
|
60
|
+
runs `npm run syntax-check` recursively over `src/`. The two
|
|
61
|
+
`resources/read` URIs (`content-types`, `security-policy`) go through
|
|
62
|
+
`invokeReadTool` → `callTool`. AGENTS, CONTRIBUTING, threat-model, and
|
|
63
|
+
ops docs point middleware at `src/lib/dispatch.js`.
|
|
64
|
+
|
|
65
|
+
### Removed
|
|
66
|
+
- **Production-dead `redactResource`.** Canonical `redactCanonicalEntity` is
|
|
67
|
+
the only redaction path; JSON:API-shape tests folded into it.
|
|
68
|
+
- **Leftover public exports** from `workflow-prompts.js`, `server-tools.js`,
|
|
69
|
+
and `allModules` (module-private after #395).
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- **Terminal install wizard (`wizard` / `init`).**
|
|
73
|
+
`npx drupal-mcp-connector init` (also `wizard`, `npm run init`,
|
|
74
|
+
`drupal-mcp-wizard`) collects transport (stdio vs remote HTTPS), auth
|
|
75
|
+
(OAuth vs sealed token), and agent client id, then prints Cursor
|
|
76
|
+
`.cursor/mcp.json` and Claude Code `mcpServers` snippets. Files are
|
|
77
|
+
written only with `--write` and never clobbered without confirm unless
|
|
78
|
+
`--yes`. Post-config `whoami` + `contract_ready` print pass/fail with a
|
|
79
|
+
next fix, or a clear skip when the host is unreachable. Remote HTTPS is
|
|
80
|
+
Tailscale / local-VPN only — not a public SaaS path.
|
|
81
|
+
- **`doctor` CLI.** `npx drupal-mcp-connector doctor` runs ordered
|
|
82
|
+
gates (reachability → auth/401 → agent client → allowlist →
|
|
83
|
+
`contract_ready` → version skew), prints one primary failure plus the
|
|
84
|
+
exact next fix, exits non-zero on fail, and supports `--json` with no
|
|
85
|
+
secrets.
|
|
86
|
+
- **Transport presets.** Wizard `--preset local-stdio|tailscale|public-https`
|
|
87
|
+
and docs table. Public HTTPS is listed as a preset and is not a hosted service.
|
|
88
|
+
- **Version skew.** Doctor and wizard print “you are on X;
|
|
89
|
+
site needs Y” when readiness reports versions. No auto-upgrade.
|
|
90
|
+
- **Happy-path recipe.** README walkthrough using
|
|
91
|
+
`drupal_mcp_whoami` → `drupal_list_sites` → unpublished
|
|
92
|
+
`drupal_list_nodes` / `dryRun` `drupal_create_node`.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
- **A `changed` / `revision_timestamp` gap on the default revision no longer blocks the next draft (#405).**
|
|
96
|
+
`#273` treated `possiblyPatchBlocked` as local proof of a hidden revision and
|
|
97
|
+
refused before Sentinel ran. When inventory `working` is null and both
|
|
98
|
+
JSON:API aliases report the same published vid, a `moderationState: draft`
|
|
99
|
+
write now proceeds to the core probe / Sentinel. An actual Sentinel
|
|
100
|
+
stale-copy refusal is still rewritten. `drupal_list_revisions` still reports
|
|
101
|
+
the warning.
|
|
102
|
+
- **`drupal_report_scheduled_content` no longer reports Scheduler fields missing when every date is empty (#403).**
|
|
103
|
+
Field presence is the key on the JSON:API attributes, not a non-empty value.
|
|
104
|
+
Keys present and all null: `summary: { pending: 0, overdue: 0 }`. `gated`
|
|
105
|
+
only when the keys are actually absent.
|
|
106
|
+
- **`drupal_schedule_publish` writes `publish_state` / `unpublish_state` on moderated bundles (#402).**
|
|
107
|
+
Optional `publishState` / `unpublishState` are accepted. When the bundle is
|
|
108
|
+
moderated and those fields exist, the matching state is required whenever a
|
|
109
|
+
date is set. Dates alone were accepted by Drupal and then failed at cron.
|
|
110
|
+
- **Revise guard uses `moderation_state`, not `status: false`, to decide "already a draft" (#400).**
|
|
111
|
+
After revising a translation over a working copy, Sentinel copies the live
|
|
112
|
+
published moderation record onto the carried default language (`status: false`
|
|
113
|
+
+ `moderation_state: published`). Neither `drupal_update_node` nor
|
|
114
|
+
`drupal_create_translation` revise can open a draft of that language while
|
|
115
|
+
the other translation draft exists (drupal.org #3626919). Both tools now
|
|
116
|
+
say so and name the manual path: publish or discard the translation draft,
|
|
117
|
+
or edit in the UI.
|
|
118
|
+
- **Config reads accept the read-only `mcp_config_read` scope (#397).**
|
|
119
|
+
`drupal_config_get` / `drupal_config_list`, config reports, and codegen
|
|
120
|
+
inspect/diff accept `mcp_config` or `mcp_config_read`. Writes still require
|
|
121
|
+
`mcp_config`. `drupal_mcp_whoami` reports `configRead: true` for a
|
|
122
|
+
read-scope token. A token with neither scope is still refused.
|
|
123
|
+
|
|
10
124
|
## [2.25.0] - 2026-10-01
|
|
11
125
|
|
|
12
126
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Drupal MCP Connector
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Multi-site MCP server for Drupal over JSON:API and GraphQL, with governed writes and an SSH Drush bridge. `init` writes a stdio or Tailscale client config from a transport preset. `doctor` checks reachability, auth, and readiness, and reports version skew. A timestamp gap on a published revision no longer blocks the next draft.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
@@ -205,6 +205,62 @@ Full detail: **[docs/security.md](docs/security.md)** and **[docs/security-harde
|
|
|
205
205
|
|
|
206
206
|
## Quick Start
|
|
207
207
|
|
|
208
|
+
### One-liner (recommended for operators)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npx -y drupal-mcp-connector init
|
|
212
|
+
# aliases: npx -y drupal-mcp-connector wizard
|
|
213
|
+
# npx -y drupal-mcp-wizard
|
|
214
|
+
# CI / dogfood: npx -y drupal-mcp-connector init --yes --json
|
|
215
|
+
npx -y drupal-mcp-connector doctor
|
|
216
|
+
# CI: npx -y drupal-mcp-connector doctor --json
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The wizard prints Cursor (`.cursor/mcp.json`) and Claude Code `mcpServers`
|
|
220
|
+
snippets, optional `auth.grants` allowlist hints, and a whoami / contract_ready
|
|
221
|
+
check. It writes files only with `--write` (never clobbers without confirm
|
|
222
|
+
unless `--yes`). Doctor prints **one** primary failing gate and the exact next
|
|
223
|
+
fix (exit non-zero on fail; `--json` is secret-free).
|
|
224
|
+
|
|
225
|
+
### Transport presets
|
|
226
|
+
|
|
227
|
+
| Preset | URL shape | Notes |
|
|
228
|
+
|--------|-----------|--------|
|
|
229
|
+
| **Local stdio** (`--preset local-stdio`) | `https://drupal.ddev.site` | Client launches this connector as a subprocess. Drupal must be reachable from this machine. |
|
|
230
|
+
| **Tailscale VPN** (`--preset tailscale`) | `https://drupal.<tailnet>.ts.net` | VPN-required. The MCP client host must be on the same tailnet. Not a public internet path. |
|
|
231
|
+
| **Public HTTPS** (`--preset public-https`) | `https://mcp.example.com/mcp` | **Gated / later.** Operator-run `MCP_TRANSPORT=https` + TLS + inbound OAuth. There is **no** hosted Wilkes & Liberty SaaS MCP URL. |
|
|
232
|
+
|
|
233
|
+
**Remote HTTPS is Tailscale / local-VPN only.** This package does not ship a
|
|
234
|
+
public SaaS remote endpoint. stdio is the default: the client launches the
|
|
235
|
+
connector on your machine and inherits VPN/Tailscale/localhost reachability to
|
|
236
|
+
Drupal. See **[docs/mcp-clients.md](docs/mcp-clients.md)**.
|
|
237
|
+
|
|
238
|
+
`requireGovernance: true` remains the listing default for a later marketplace
|
|
239
|
+
entry — this repo does not submit listings. If the client only shows
|
|
240
|
+
`drupal_list_sites` and `drupal_governance_status`, run `doctor`.
|
|
241
|
+
|
|
242
|
+
### Two-minute happy path
|
|
243
|
+
|
|
244
|
+
After `init` and a green `doctor`, in the MCP client (real tool names):
|
|
245
|
+
|
|
246
|
+
1. **Who am I?** Call `drupal_mcp_whoami`. Confirm `tier`, `preset`, and
|
|
247
|
+
`capabilities.publish` (false on `production-strict` / `content-editor`).
|
|
248
|
+
2. **What can I see?** Call `drupal_list_sites`, then `drupal_list_nodes` with
|
|
249
|
+
`{ "type": "article", "status": false, "limit": 5 }` (unpublished only) or
|
|
250
|
+
`drupal_get_node` on a staging UUID. Prefer `drupal_search_content` for a
|
|
251
|
+
title substring.
|
|
252
|
+
3. **Safe unpublished write (optional, non-prod):** Preview with
|
|
253
|
+
`drupal_create_node`
|
|
254
|
+
`{ "type": "article", "title": "MCP draft", "status": false, "dryRun": true }`.
|
|
255
|
+
Commit without `dryRun` only on staging, still `status: false` (or
|
|
256
|
+
`moderationState: "draft"`). Never set `status: true` or
|
|
257
|
+
`moderationState: "published"` unless `drupal_mcp_whoami` shows
|
|
258
|
+
`capabilities.publish: true` **and** you intend to go live.
|
|
259
|
+
|
|
260
|
+
Fail-closed: default `production-strict` is read-only. A deny is expected when
|
|
261
|
+
the allowlist or Sentinel profile excludes the type — run `doctor` and
|
|
262
|
+
`drupal_governance_status`; do not auto-widen allowlists.
|
|
263
|
+
|
|
208
264
|
### From npm (recommended for operators)
|
|
209
265
|
|
|
210
266
|
```bash
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Connection doctor (DEV-759).
|
|
4
|
+
*
|
|
5
|
+
* npx drupal-mcp-connector doctor
|
|
6
|
+
* node bin/drupal-mcp-doctor.js --json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runDoctorCli } from "../src/lib/doctor.js";
|
|
10
|
+
|
|
11
|
+
runDoctorCli(process.argv.slice(2))
|
|
12
|
+
.then((code) => process.exit(code))
|
|
13
|
+
.catch((err) => {
|
|
14
|
+
process.stderr.write(`doctor: ${err?.message ?? err}\n`);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Operator install wizard (DEV-758).
|
|
4
|
+
*
|
|
5
|
+
* npx drupal-mcp-connector init
|
|
6
|
+
* npx drupal-mcp-connector wizard
|
|
7
|
+
* node bin/drupal-mcp-wizard.js --yes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { runWizardCli } from "../src/lib/wizard.js";
|
|
11
|
+
|
|
12
|
+
runWizardCli(process.argv.slice(2))
|
|
13
|
+
.then((code) => process.exit(code))
|
|
14
|
+
.catch((err) => {
|
|
15
|
+
process.stderr.write(`wizard: ${err?.message ?? err}\n`);
|
|
16
|
+
process.exit(2);
|
|
17
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Multi-site MCP server for Drupal over JSON:API and GraphQL, with governed writes and an SSH Drush bridge. init writes a stdio or Tailscale client config from a transport preset. doctor checks reachability, auth, and readiness, and reports version skew. A timestamp gap on a published revision no longer blocks the next draft.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"drupal-mcp-verify": "bin/drupal-mcp-verify.js",
|
|
10
10
|
"drupal-mcp-edge": "bin/drupal-mcp-edge.js",
|
|
11
11
|
"drupal-mcp-agent": "bin/drupal-mcp-agent.js",
|
|
12
|
-
"drupal-mcp-anchor": "bin/drupal-mcp-anchor.js"
|
|
12
|
+
"drupal-mcp-anchor": "bin/drupal-mcp-anchor.js",
|
|
13
|
+
"drupal-mcp-wizard": "bin/drupal-mcp-wizard.js",
|
|
14
|
+
"drupal-mcp-doctor": "bin/drupal-mcp-doctor.js"
|
|
13
15
|
},
|
|
14
16
|
"files": [
|
|
15
17
|
"src/",
|
|
@@ -61,9 +63,12 @@
|
|
|
61
63
|
"test:watch": "vitest",
|
|
62
64
|
"audit": "npm audit --audit-level=high",
|
|
63
65
|
"check": "npm run lint && npm run audit",
|
|
64
|
-
"syntax-check": "
|
|
66
|
+
"syntax-check": "find src -name '*.js' -exec sh -c 'node --input-type=module --check < \"$1\" && echo \"$1 ✓\"' _ {} \\;",
|
|
65
67
|
"verify": "node bin/drupal-mcp-verify.js",
|
|
66
|
-
"verify:example": "node bin/drupal-mcp-verify.js --config config/config.example.json"
|
|
68
|
+
"verify:example": "node bin/drupal-mcp-verify.js --config config/config.example.json",
|
|
69
|
+
"wizard": "node bin/drupal-mcp-wizard.js",
|
|
70
|
+
"init": "node bin/drupal-mcp-wizard.js",
|
|
71
|
+
"doctor": "node bin/drupal-mcp-doctor.js"
|
|
67
72
|
},
|
|
68
73
|
"dependencies": {
|
|
69
74
|
"@modelcontextprotocol/node": "^2.0.0",
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* MCP_RATE_LIMIT Max /mcp requests per window per client IP (0/unset = off)
|
|
24
24
|
* MCP_RATE_WINDOW_SEC Rate-limit window in seconds (default: 60)
|
|
25
25
|
* MCP_LEGACY_TRANSPORT "serve" (default) | "reject" for 2025-era clients
|
|
26
|
+
*
|
|
27
|
+
* CLI subcommands (anything else starts the MCP server):
|
|
28
|
+
* wizard | init Operator install wizard — see src/lib/wizard.js
|
|
29
|
+
* doctor Connection doctor — see src/lib/doctor.js
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
32
|
import { createServer as createHttpsServer } from "https";
|
|
@@ -45,10 +49,9 @@ import {
|
|
|
45
49
|
import { createLegacySessionHandler, createMcpRequestHandler } from "./lib/http-handler.js";
|
|
46
50
|
import { createConnectorServerFactory } from "./lib/mcp-server.js";
|
|
47
51
|
import { createRateLimiter } from "./lib/rate-limit.js";
|
|
48
|
-
import { callTool, listResolvableSiteConfigs } from "./lib/dispatch.js";
|
|
52
|
+
import { callTool, invokeReadTool, listResolvableSiteConfigs } from "./lib/dispatch.js";
|
|
49
53
|
import { filterDiscoverableTools } from "./lib/governance.js";
|
|
50
54
|
import {
|
|
51
|
-
assertPrincipalEntitlement,
|
|
52
55
|
filterPromptsByPrincipal,
|
|
53
56
|
filterResourcesByPrincipal,
|
|
54
57
|
filterToolsByPrincipal,
|
|
@@ -57,7 +60,7 @@ import {
|
|
|
57
60
|
} from "./lib/principal.js";
|
|
58
61
|
|
|
59
62
|
// Tools — aggregated (single source of truth, side-effect-free) and per-tool prompts
|
|
60
|
-
import { allDefinitions,
|
|
63
|
+
import { allDefinitions, definitionsByName } from "./tools/index.js";
|
|
61
64
|
import { createModuleToolRegistry, isModuleTool } from "./lib/module-tools.js";
|
|
62
65
|
import { buildToolPrompts, createPromptSurface } from "./lib/tool-prompts.js";
|
|
63
66
|
import {
|
|
@@ -69,6 +72,16 @@ import {
|
|
|
69
72
|
replaceModuleWorkflows,
|
|
70
73
|
} from "./lib/workflow-prompts.js";
|
|
71
74
|
import { builtinWorkflowProvider } from "./lib/workflows/builtin.js";
|
|
75
|
+
import { runWizardCli } from "./lib/wizard.js";
|
|
76
|
+
import { runDoctorCli } from "./lib/doctor.js";
|
|
77
|
+
|
|
78
|
+
const cliCommand = process.argv[2];
|
|
79
|
+
if (cliCommand === "wizard" || cliCommand === "init") {
|
|
80
|
+
process.exit(await runWizardCli(process.argv.slice(3)));
|
|
81
|
+
}
|
|
82
|
+
if (cliCommand === "doctor") {
|
|
83
|
+
process.exit(await runDoctorCli(process.argv.slice(3)));
|
|
84
|
+
}
|
|
72
85
|
|
|
73
86
|
// Apply config/secrets.map (or the shipped example table) before any site
|
|
74
87
|
// resolution. MCP clients spawn this file directly; the shell launcher is
|
|
@@ -129,8 +142,7 @@ async function discoverableTools() {
|
|
|
129
142
|
/**
|
|
130
143
|
* Resolve a resource URI to its JSON payload. URIs are matched in order; the
|
|
131
144
|
* templated forms (content-types, security-policy) capture the site name and
|
|
132
|
-
*
|
|
133
|
-
* always return the same shape.
|
|
145
|
+
* go through {@link invokeReadTool} so resources and tools share middleware.
|
|
134
146
|
*
|
|
135
147
|
* @param {string} uri - A drupal:// resource URI.
|
|
136
148
|
* @returns {Promise<object>} The resource data (later JSON-serialized).
|
|
@@ -148,25 +160,13 @@ async function readResource(uri) {
|
|
|
148
160
|
// drupal://{site}/content-types
|
|
149
161
|
const ctMatch = uri.match(/^drupal:\/\/([^/]+)\/content-types$/);
|
|
150
162
|
if (ctMatch) {
|
|
151
|
-
|
|
152
|
-
toolName: "drupal_list_content_types",
|
|
153
|
-
args: { site: ctMatch[1] },
|
|
154
|
-
identity,
|
|
155
|
-
sites,
|
|
156
|
-
});
|
|
157
|
-
return allHandlers.drupal_list_content_types({ site: ctMatch[1] });
|
|
163
|
+
return invokeReadTool("drupal_list_content_types", { site: ctMatch[1] });
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
// drupal://{site}/security-policy
|
|
161
167
|
const spMatch = uri.match(/^drupal:\/\/([^/]+)\/security-policy$/);
|
|
162
168
|
if (spMatch) {
|
|
163
|
-
|
|
164
|
-
toolName: "drupal_security_info",
|
|
165
|
-
args: { site: spMatch[1] },
|
|
166
|
-
identity,
|
|
167
|
-
sites,
|
|
168
|
-
});
|
|
169
|
-
return allHandlers.drupal_security_info({ site: spMatch[1] });
|
|
169
|
+
return invokeReadTool("drupal_security_info", { site: spMatch[1] });
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
throw new Error(`Unknown resource URI: ${uri}`);
|
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
} from "../canonical.js";
|
|
21
21
|
import { isPositiveNid, normalizeAlias, PATH_ALIAS_ENTITY_TYPE } from "../path-alias.js";
|
|
22
22
|
|
|
23
|
+
/** Most node_type pages read by listContentTypes (50 per page). */
|
|
24
|
+
const CONTENT_TYPE_MAX_PAGES = 20;
|
|
25
|
+
|
|
23
26
|
// Drupal exposes internal identifiers under drupal_internal__* attributes.
|
|
24
27
|
// They are dropped from canonical `fields` except for the identifiers that
|
|
25
28
|
// governed read/write workflows explicitly need.
|
|
@@ -666,13 +669,22 @@ export class JsonApiBackend extends Backend {
|
|
|
666
669
|
* @returns {Promise<Array<{id: string, label: string, description: ?string}>>}
|
|
667
670
|
*/
|
|
668
671
|
async listContentTypes() {
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
672
|
+
// JSON:API caps a page at 50. Follow `links.next` so callers that scan
|
|
673
|
+
// every bundle see them all (#403); stop after a fixed number of pages.
|
|
674
|
+
const out = [];
|
|
675
|
+
for (let page = 0, offset = 0; page < CONTENT_TYPE_MAX_PAGES; page += 1, offset += 50) {
|
|
676
|
+
const data = await drupalFetch(this.site, `/jsonapi/node_type/node_type?page[limit]=50&page[offset]=${offset}`);
|
|
677
|
+
const rows = data.data || [];
|
|
678
|
+
for (const ct of rows) {
|
|
679
|
+
out.push({
|
|
680
|
+
id: ct.attributes.drupal_internal__type,
|
|
681
|
+
label: ct.attributes.name,
|
|
682
|
+
description: ct.attributes.description ?? null,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (!data.links?.next || rows.length === 0) break;
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
676
688
|
}
|
|
677
689
|
|
|
678
690
|
/**
|
package/src/lib/dispatch.js
CHANGED
|
@@ -236,3 +236,25 @@ export async function callTool(name, args, context = {}) {
|
|
|
236
236
|
return toolError(err);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Run a read tool through {@link callTool} and unwrap the MCP envelope.
|
|
242
|
+
* `resources/read` uses this so those URIs cannot skip middleware.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} name Tool name.
|
|
245
|
+
* @param {object} args Tool arguments.
|
|
246
|
+
* @param {object} [context] Optional inbound identity / grant overrides.
|
|
247
|
+
* @returns {Promise<*>} The tool payload.
|
|
248
|
+
* @throws {Error} When the tool is missing, denied, or returns isError.
|
|
249
|
+
*/
|
|
250
|
+
export async function invokeReadTool(name, args, context = {}) {
|
|
251
|
+
const result = await callTool(name, args, context);
|
|
252
|
+
if (result?.isError) {
|
|
253
|
+
throw new Error(result.content?.[0]?.text ?? `Tool "${name}" failed`);
|
|
254
|
+
}
|
|
255
|
+
const text = result?.content?.[0]?.text;
|
|
256
|
+
if (typeof text !== "string") {
|
|
257
|
+
throw new Error(`Tool "${name}" returned no payload`);
|
|
258
|
+
}
|
|
259
|
+
return JSON.parse(text);
|
|
260
|
+
}
|