drupal-mcp-connector 2.1.1 → 2.2.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.
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type."
2
+ description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type. Requires security.allowGraphql (off outside the development preset by default)."
3
3
  argument-hint: "[site] [typeName]"
4
4
  allowed-tools: mcp__drupal__drupal_graphql_introspect
5
5
  ---
6
6
 
7
7
  Call the `mcp__drupal__drupal_graphql_introspect` MCP tool.
8
8
 
9
- Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type.
9
+ Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type. Requires security.allowGraphql (off outside the development preset by default).
10
10
 
11
11
  Parse the request in `$ARGUMENTS` into this tool's parameters:
12
12
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Execute a GraphQL query against a Drupal site. Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which exposes a read-only schema; mutations are gated by \"allowGraphqlMutations\". Use drupal_graphql_introspect first to discover available types and fields. Example query: query GetArticle($id: String!) { nodeById(id: $id) { title ... on NodeArticle { body { value } } } } Example mutation (only if your GraphQL Compose schema enables mutations): mutation CreateArticle($title: String!, $body: String!) { createNodeArticle(data: { title: $title, body: { value: $body, format: \"full_html\" } }) { entity { title uuid } errors { message } } }"
2
+ description: "Execute a GraphQL query against a Drupal site. Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which exposes a read-only schema. GraphQL is off unless security.allowGraphql is true (development preset only by default) because raw results bypass entity allowlists and field redaction. Mutations also require allowGraphqlMutations. Use drupal_graphql_introspect first to discover available types and fields. Example query: query GetArticle($id: String!) { nodeById(id: $id) { title ... on NodeArticle { body { value } } } } Example mutation (only if your GraphQL Compose schema enables mutations): mutation CreateArticle($title: String!, $body: String!) { createNodeArticle(data: { title: $title, body: { value: $body, format: \"full_html\" } }) { entity { title uuid } errors { message } } }"
3
3
  argument-hint: "<query> [site] [variables] [operationName]"
4
4
  allowed-tools: mcp__drupal__drupal_graphql
5
5
  ---
@@ -8,7 +8,9 @@ Call the `mcp__drupal__drupal_graphql` MCP tool.
8
8
 
9
9
  Execute a GraphQL query against a Drupal site.
10
10
  Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which
11
- exposes a read-only schema; mutations are gated by "allowGraphqlMutations".
11
+ exposes a read-only schema. GraphQL is off unless security.allowGraphql is true
12
+ (development preset only by default) because raw results bypass entity allowlists
13
+ and field redaction. Mutations also require allowGraphqlMutations.
12
14
  Use drupal_graphql_introspect first to discover available types and fields.
13
15
 
14
16
  Example query:
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-07-30
11
+
12
+ ### Security
13
+ - **Default security preset is `production-strict` (#140).** Omitting `security`
14
+ or passing `{}` no longer opens the site under the `development` preset.
15
+ Local and integration work must set `"preset": "development"` (or another
16
+ explicit preset) deliberately. Migration: add
17
+ `"security": { "preset": "development" }` to any site config that relied on
18
+ the old open default.
19
+ - **GraphQL tools fail closed (#142).** `drupal_graphql` and
20
+ `drupal_graphql_introspect` require `security.allowGraphql` (true only on the
21
+ `development` preset). Mutations still need `allowGraphqlMutations` as well.
22
+ When GraphQL is opted in, results remain raw (no entity allowlist/redaction
23
+ on that path); prefer JSON:API entity tools for policy-bound reads.
24
+ - **Dependency advisories cleared (#128).** Bump
25
+ `@modelcontextprotocol/sdk` to `^1.30.0` and pin overrides for
26
+ `@hono/node-server` `^2.0.5` and `postcss` `^8.5.18` so `npm audit` reports
27
+ zero vulnerabilities.
28
+
29
+ ### Documentation
30
+ - README, security guide, threat model, architecture, tools reference, and
31
+ SECURITY.md updated for the secure default, GraphQL gate, and residual-risk
32
+ table.
33
+
10
34
  ## [2.1.1] - 2026-07-31
11
35
 
12
36
  ### Documentation
package/README.md CHANGED
@@ -127,9 +127,9 @@ prompts above.
127
127
  ### Security Model
128
128
 
129
129
  Defense-in-depth presets, enforced connector-side and complemented by Drupal-side
130
- governance (MCP Sentinel). **Set an explicit preset on every non-dev site** —
131
- if `security` is omitted, the connector still defaults to `development` (tracked
132
- for change in issue #140).
130
+ governance (MCP Sentinel). **Default when `security` is omitted:**
131
+ `production-strict` (read-only, sensitive types denied). Local development must
132
+ set `"preset": "development"` explicitly.
133
133
 
134
134
  ```json
135
135
  "security": { "preset": "auditor" }
@@ -137,20 +137,20 @@ for change in issue #140).
137
137
 
138
138
  | Preset | What it does |
139
139
  |--------|-------------|
140
- | `development` | Everything allowed — local development only |
140
+ | `development` | Everything allowed, including GraphQL — local development only |
141
141
  | `content-editor` | Create/edit content + structural entities; no deletes; no publishing; secrets/governance/account types denied |
142
142
  | `config-editor` | content-editor + site-building config read + governed config write (developer tier) |
143
143
  | `auditor` | Read-only; secrets/governance/account types denied; user PII redacted when user is allowed |
144
- | `production-strict` | Read-only; same sensitive denylist as auditor; broad PII field redaction |
145
- | `write-plane` | Create/update content set for agents; no deletes; no GraphQL mutations; publish off by default |
144
+ | `production-strict` | Read-only; same sensitive denylist as auditor; broad PII field redaction (**default**) |
145
+ | `write-plane` | Create/update content set for agents; no deletes; no GraphQL; publish off by default |
146
146
 
147
- Additional connector-side gates (2.1+):
147
+ Additional connector-side gates (2.1+ / 2.2+):
148
148
 
149
149
  - **Entity allowlists** apply to specialized tools (`drupal_*_node`, media, taxonomy), not only `drupal_entity_*`.
150
150
  - **Publish gate:** `status: true` and `moderation_state: "published"` require `allowPublish`. Media create defaults **unpublished**. Published moderated node updates without a moderation state default to **draft** (forward revision).
151
151
  - **Uploads** only from `MCP_UPLOAD_ROOT` (or the process cwd); sensitive paths (`.env*`, `.ssh`, connector `config.json`) are refused.
152
152
  - **HTTPS:** non-loopback binds require `MCP_AUTH_TOKEN` (or `MCP_ALLOW_UNAUTHENTICATED=1` behind a trusted proxy); non-loopback TLS defaults to 120 req/min rate limiting.
153
- - **GraphQL caveat:** `drupal_graphql` returns raw query data connector allowlists/redaction do not apply to that path (issue #142).
153
+ - **GraphQL is off by default.** `drupal_graphql` / introspect require `security.allowGraphql` (true only on the `development` preset). Raw GraphQL results still bypass entity allowlists and field redaction prefer JSON:API entity tools when connector policy must hold. Mutations also need `allowGraphqlMutations`.
154
154
 
155
155
  Full detail: **[docs/security.md](docs/security.md)** and **[docs/security-hardening.md](docs/security-hardening.md)**.
156
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -58,16 +58,20 @@
58
58
  "syntax-check": "for f in src/lib/*.js src/tools/*.js src/index.js; do node --input-type=module --check < $f && echo \"$f ✓\"; done"
59
59
  },
60
60
  "dependencies": {
61
- "@modelcontextprotocol/sdk": "^1.0.0",
61
+ "@modelcontextprotocol/sdk": "^1.30.0",
62
62
  "graphql": "^17.0.0",
63
63
  "node-fetch": "^3.3.2",
64
64
  "ssh2": "^1.16.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "eslint": "^10.4.1",
68
- "eslint-plugin-security": "^4.0.0",
69
68
  "eslint-plugin-n": "^18.1.0",
69
+ "eslint-plugin-security": "^4.0.0",
70
70
  "globals": "^17.6.0",
71
71
  "vitest": "^4.1.8"
72
+ },
73
+ "overrides": {
74
+ "@hono/node-server": "^2.0.5",
75
+ "postcss": "^8.5.18"
72
76
  }
73
77
  }
@@ -12,13 +12,14 @@ import { parse } from "graphql";
12
12
  *
13
13
  * ─── Quick presets ────────────────────────────────────────────────────────
14
14
  *
15
- * "preset": "development" Everything allowed. Default if no security key.
15
+ * "preset": "development" Everything allowed. Opt-in only set explicitly.
16
16
  * "preset": "content-editor" Create/edit content (nodes, media, terms, paragraphs, blocks,
17
17
  * menu links, redirects, aliases, files). No deletes. Config read-only.
18
18
  * "preset": "config-editor" content-editor + site-building config READ + governed config
19
19
  * read/write (Developer tier). Model changes go via the config bridge.
20
- * "preset": "auditor" Read-only. All entity types. User fields redacted.
21
- * "preset": "production-strict" Read-only. Explicit allowlist required. Redacts PII.
20
+ * "preset": "auditor" Read-only. Sensitive entity types denied. User fields redacted.
21
+ * "preset": "production-strict" Read-only. Sensitive types denied. Redacts PII. **Default** when
22
+ * security is omitted or has no preset (#140).
22
23
  * "preset": "write-plane" Governed writes (no delete/mutations) on the content set
23
24
  * (node, term, media + structural content entities).
24
25
  *
@@ -31,7 +32,8 @@ import { parse } from "graphql";
31
32
  *
32
33
  * readOnly true → reject all create/update/delete/graphql-mutation calls
33
34
  * allowDestructive false → reject all delete operations
34
- * allowPublish false → reject a write carrying status:true (publishing)
35
+ * allowPublish false → reject a write carrying status:true / moderation_state:published
36
+ * allowGraphql false → reject drupal_graphql + introspect (raw GraphQL is policy-free, #142)
35
37
  * allowGraphqlMutations false → reject drupal_graphql when mutation is detected
36
38
  * allowConfigRead false → reject drupal_config_get / drupal_config_list
37
39
  * allowConfigWrite false → reject drupal_config_set
@@ -118,6 +120,7 @@ const PRESETS = {
118
120
  readOnly: false,
119
121
  allowDestructive: true,
120
122
  allowPublish: true, // mirrors allowDestructive: everything allowed
123
+ allowGraphql: true, // raw GraphQL allowed only in open mode (#142)
121
124
  allowGraphqlMutations: true,
122
125
  allowConfigRead: true,
123
126
  allowConfigWrite: true,
@@ -130,6 +133,7 @@ const PRESETS = {
130
133
  "content-editor": {
131
134
  readOnly: false,
132
135
  allowDestructive: false, // no deletes
136
+ allowGraphql: false, // freeform GraphQL bypasses entity denylists (#142)
133
137
  allowGraphqlMutations: false,
134
138
  allowConfigRead: true, // config read-only
135
139
  allowConfigWrite: false,
@@ -151,6 +155,7 @@ const PRESETS = {
151
155
  // The Drupal-side governance layer remains authoritative; this is defence in depth.
152
156
  readOnly: false,
153
157
  allowDestructive: false, // no deletes
158
+ allowGraphql: false,
154
159
  allowGraphqlMutations: false,
155
160
  allowConfigRead: true,
156
161
  allowConfigWrite: true, // governed config writes via drupal_config_set
@@ -171,6 +176,7 @@ const PRESETS = {
171
176
  auditor: {
172
177
  readOnly: true,
173
178
  allowDestructive: false,
179
+ allowGraphql: false,
174
180
  allowGraphqlMutations: false,
175
181
  allowConfigRead: true, // read-only inspection of config
176
182
  allowConfigWrite: false,
@@ -189,6 +195,7 @@ const PRESETS = {
189
195
  "production-strict": {
190
196
  readOnly: true,
191
197
  allowDestructive: false,
198
+ allowGraphql: false,
192
199
  allowGraphqlMutations: false,
193
200
  allowConfigRead: false, // nothing implicit; opt in per site
194
201
  allowConfigWrite: false,
@@ -203,6 +210,7 @@ const PRESETS = {
203
210
  // Drupal-side governance layer remains authoritative; this is defence in depth.
204
211
  readOnly: false,
205
212
  allowDestructive: false, // no deletes
213
+ allowGraphql: false, // JSON:API write plane; GraphQL is policy-free (#142)
206
214
  allowGraphqlMutations: false, // writes go through the JSON:API plane
207
215
  allowConfigRead: true, // config read-only
208
216
  allowConfigWrite: false,
@@ -225,15 +233,22 @@ const PRESETS = {
225
233
  * @param {object} site Site config (reads site.security).
226
234
  * @returns {object} Effective security config used by the assert/redact helpers.
227
235
  */
236
+ /** Default when `security.preset` is omitted — least privilege, not open mode (#140). */
237
+ export const DEFAULT_SECURITY_PRESET = "production-strict";
238
+
228
239
  export function resolveSecurityConfig(site) {
229
240
  const raw = site.security ?? {};
230
- const preset = PRESETS[raw.preset ?? "development"] ?? PRESETS.development;
241
+ // Explicit preset wins; bare {} / missing security → production-strict (#140).
242
+ // Open mode requires `preset: "development"` (or another named preset).
243
+ const presetName = raw.preset ?? DEFAULT_SECURITY_PRESET;
244
+ const preset = PRESETS[presetName] ?? PRESETS[DEFAULT_SECURITY_PRESET];
231
245
 
232
246
  // Merge: explicit keys in site.security override the preset
233
247
  return {
234
248
  readOnly: raw.readOnly ?? preset.readOnly,
235
249
  allowDestructive: raw.allowDestructive ?? preset.allowDestructive,
236
250
  allowPublish: raw.allowPublish ?? preset.allowPublish ?? false,
251
+ allowGraphql: raw.allowGraphql ?? preset.allowGraphql ?? false,
237
252
  allowGraphqlMutations: raw.allowGraphqlMutations ?? preset.allowGraphqlMutations,
238
253
  allowConfigRead: raw.allowConfigRead ?? preset.allowConfigRead ?? false,
239
254
  allowConfigWrite: raw.allowConfigWrite ?? preset.allowConfigWrite ?? false,
@@ -450,7 +465,30 @@ function graphqlHasMutation(query) {
450
465
  * @returns {void} No-op for read-only (query) documents.
451
466
  * @throws {SecurityError} if the document is a mutation and writes/mutations are disabled.
452
467
  */
468
+ /**
469
+ * Gate GraphQL queries and schema introspection. Freeform GraphQL returns raw
470
+ * data that does not pass through entity allowlists or field redaction (#142),
471
+ * so it is off by default outside the development preset. Opt in with
472
+ * `security.allowGraphql: true`.
473
+ *
474
+ * @param {object} secConfig Resolved security config.
475
+ * @param {string} [operationLabel] Label for the error message.
476
+ * @returns {void}
477
+ * @throws {SecurityError} if GraphQL is disabled for this site.
478
+ */
479
+ export function assertGraphqlAllowed(secConfig, operationLabel = "graphql") {
480
+ if (secConfig.allowGraphql) return;
481
+ throw new SecurityError(
482
+ "GraphQL tools are disabled for this site (allowGraphql: false). " +
483
+ "Raw GraphQL responses bypass connector entity allowlists and field redaction. " +
484
+ `Blocked: ${operationLabel}. ` +
485
+ "Set security.allowGraphql = true to enable (prefer development preset or an explicit opt-in)."
486
+ );
487
+ }
488
+
453
489
  export function assertGraphqlMutationAllowed(secConfig, query) {
490
+ // Queries still require allowGraphql (#142); mutations require both flags.
491
+ assertGraphqlAllowed(secConfig, "graphql mutation");
454
492
  const isMutation = graphqlHasMutation(query);
455
493
  if (!isMutation) return;
456
494
 
@@ -681,9 +719,11 @@ export function getSecuritySummary(site) {
681
719
  const cfg = resolveSecurityConfig(site);
682
720
  return {
683
721
  site: site._name,
684
- preset: site.security?.preset ?? "development (default)",
722
+ preset: site.security?.preset ?? `${DEFAULT_SECURITY_PRESET} (default)`,
685
723
  readOnly: cfg.readOnly,
686
724
  allowDestructive: cfg.allowDestructive,
725
+ allowPublish: cfg.allowPublish,
726
+ allowGraphql: cfg.allowGraphql,
687
727
  allowGraphqlMutations: cfg.allowGraphqlMutations,
688
728
  allowConfigRead: cfg.allowConfigRead,
689
729
  allowConfigWrite: cfg.allowConfigWrite,
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Raw GraphQL execution and schema introspection against a Drupal site. Require
5
5
  * the GraphQL Compose module (drupal.org/project/graphql_compose), which
6
- * exposes a read-only schema (no mutations); any mutation in a query is
7
- * additionally gated by the per-site "allowGraphqlMutations" security flag,
8
- * enforced by the middleware in index.js before the handler runs.
6
+ * exposes a read-only schema (no mutations).
7
+ *
8
+ * Policy: responses are raw GraphQL data they do **not** pass connector entity
9
+ * allowlists or field redaction (#142). Tools are gated by `allowGraphql`
10
+ * (false outside the development preset unless opted in). Mutations also require
11
+ * `allowGraphqlMutations` (enforced in the handler and by index.js middleware).
9
12
  *
10
13
  * Per-site config: set "graphqlEndpoint" to override "/graphql".
11
14
  * Auth reuses the same credentials as the JSON:API tools.
@@ -13,6 +16,9 @@
13
16
 
14
17
  import { getSiteConfig } from "../lib/config.js";
15
18
  import { drupalGraphqlFetch } from "../lib/drupal-fetch.js";
19
+ import {
20
+ resolveSecurityConfig, assertGraphqlAllowed, assertGraphqlMutationAllowed,
21
+ } from "../lib/security.js";
16
22
 
17
23
  // ---------------------------------------------------------------------------
18
24
  // Implementations
@@ -29,6 +35,9 @@ import { drupalGraphqlFetch } from "../lib/drupal-fetch.js";
29
35
  */
30
36
  async function runGraphql({ site: siteName, query, variables = {}, operationName }) {
31
37
  const site = getSiteConfig(siteName);
38
+ const sec = resolveSecurityConfig(site);
39
+ assertGraphqlAllowed(sec, "drupal_graphql");
40
+ assertGraphqlMutationAllowed(sec, query);
32
41
  const json = await drupalGraphqlFetch(site, { query, variables, operationName });
33
42
 
34
43
  if (json.errors?.length) {
@@ -54,6 +63,7 @@ async function runGraphql({ site: siteName, query, variables = {}, operationName
54
63
  */
55
64
  async function introspectGraphql({ site: siteName, typeName }) {
56
65
  const site = getSiteConfig(siteName);
66
+ assertGraphqlAllowed(resolveSecurityConfig(site), "drupal_graphql_introspect");
57
67
 
58
68
  // If a specific type is requested, get detailed field info for it.
59
69
  if (typeName) {
@@ -128,7 +138,9 @@ export const definitions = [
128
138
  name: "drupal_graphql",
129
139
  description: `Execute a GraphQL query against a Drupal site.
130
140
  Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which
131
- exposes a read-only schema; mutations are gated by "allowGraphqlMutations".
141
+ exposes a read-only schema. GraphQL is off unless security.allowGraphql is true
142
+ (development preset only by default) because raw results bypass entity allowlists
143
+ and field redaction. Mutations also require allowGraphqlMutations.
132
144
  Use drupal_graphql_introspect first to discover available types and fields.
133
145
 
134
146
  Example query:
@@ -158,7 +170,7 @@ Example mutation (only if your GraphQL Compose schema enables mutations):
158
170
  },
159
171
  {
160
172
  name: "drupal_graphql_introspect",
161
- description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type.",
173
+ description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type. Requires security.allowGraphql (off outside the development preset by default).",
162
174
  inputSchema: {
163
175
  type: "object",
164
176
  properties: {