drupal-mcp-connector 2.19.0 → 2.19.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.19.1] - 2026-09-17
11
+
12
+ ### Changed
13
+ - **Remove leftover Sentinel facades (#328).** `draft-write.js` and
14
+ `node-draft-inventory.js` were re-exports of `sentinel-draft.js`. Tests
15
+ import the client directly. Unused exports (`toolMessage`, `redactResponse`)
16
+ are gone; `DIAGNOSTIC_TOOLS` and `POLICY_DIGEST` each live in one place.
17
+ - README and docs now report **123 tools across 27 modules** and **128 prompts**
18
+ (one per tool plus 5 workflow prompts). Codegen (`drupal_codegen_*`) shipped
19
+ in 2.16.0 but was missing from the public counts and the npm project page.
20
+
10
21
  ## [2.19.0] - 2026-09-16
11
22
 
12
23
  ### Added
@@ -1679,6 +1690,9 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
1679
1690
  - User tools gained explicit PII-access assertions.
1680
1691
  - Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
1681
1692
 
1693
+ [2.19.1]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.19.1
1694
+ [2.19.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.19.0
1695
+ [2.18.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.18.0
1682
1696
  [2.17.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.17.0
1683
1697
  [2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
1684
1698
  [2.15.2]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.15.2
package/README.md CHANGED
@@ -68,7 +68,7 @@ See **[docs/architecture.md](docs/architecture.md)** for the backend abstraction
68
68
 
69
69
  ## Features
70
70
 
71
- ### 120 Tools Across 26 Modules
71
+ ### 123 Tools Across 27 Modules
72
72
 
73
73
  | Module | Tools |
74
74
  |--------|-------|
@@ -77,6 +77,7 @@ See **[docs/architecture.md](docs/architecture.md)** for the backend abstraction
77
77
  | **Users** | List, get, create, update, block/unblock, role management (PII-gated) |
78
78
  | **Media** | List types, CRUD, file upload, orphaned-media detection |
79
79
  | **GraphQL** | Execute a query, schema introspection (mutation-gated) |
80
+ | **Codegen** | Optional graphql_compose_codegen inspect/diff/generate (generate is `--dry-run` text) |
80
81
  | **Entities** | Generic CRUD for *any* Drupal entity type (paragraphs, commerce, webforms, …) |
81
82
  | **Site** | Site info, content-type discovery, configured-site listing |
82
83
  | **Reports** | Content summary, stale content, field completeness, SEO/accessibility audits, taxonomy usage, user activity, revision hotspots (10 read-only reports) |
@@ -308,7 +309,7 @@ an operator channel: keep the agent's credentials off it, and pin
308
309
  | [OAuth client_credentials](docs/oauth-client-credentials.md) | Production OAuth deploy: scope→role mapping, JSON:API writes, config persistence, secret handling, troubleshooting |
309
310
  | [Architecture](docs/architecture.md) | Backend abstraction, canonical model, and how to extend it |
310
311
  | [GraphQL Setup](docs/graphql-local-setup.md) | GraphQL Compose backend + local TLS notes |
311
- | [Tools Reference](docs/tools-reference.md) | Full reference for all 120 tools |
312
+ | [Tools Reference](docs/tools-reference.md) | Full reference for all 123 tools |
312
313
  | [Security Guide](docs/security.md) | Presets, entity access control, field redaction |
313
314
  | [Security Hardening](docs/security-hardening.md) | Optional transport, identity, and secrets controls |
314
315
  | [Verification](docs/verification.md) | Prove a secure install: static + live checks, negative probes, evidence artefact, managed residuals |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.19.0",
3
+ "version": "2.19.1",
4
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",
@@ -15,10 +15,10 @@ import { resolveSecurityConfig, assertNotReadOnly,
15
15
  import { toolError, toolResult } from "./errors.js";
16
16
  import { BackendCapabilityError, BackendResolutionError } from "./backends/errors.js";
17
17
  import { inferOperation } from "./operations.js";
18
- import { assertSourceGovernance, GovernanceError, GOVERNANCE_DIAGNOSTIC_TOOLS } from "./governance.js";
18
+ import { assertSourceGovernance, GovernanceError } from "./governance.js";
19
19
  import {
20
- assertPrincipalEntitlement, callerTargetHints, getRequestIdentity,
21
- principalHasScope, resolveAuthoritativeTarget,
20
+ assertPrincipalEntitlement, callerTargetHints, DIAGNOSTIC_TOOLS,
21
+ getRequestIdentity, principalHasScope, resolveAuthoritativeTarget,
22
22
  } from "./principal.js";
23
23
  import { assertExplicitSiteForWrite, withResolvedTarget } from "./site-target.js";
24
24
  import { buildDataFlowContext, consumeBudgetIfEnforced, runWithDataFlow } from "./data-flow.js";
@@ -55,7 +55,7 @@ export function listResolvableSiteConfigs() {
55
55
  * @returns {?{site: object, source: string, name: string}}
56
56
  * @throws {SecurityError}
57
57
  */
58
- export function resolveCallTarget(toolName, rawArgs, context = {}) {
58
+ function resolveCallTarget(toolName, rawArgs, context = {}) {
59
59
  if (toolName === "drupal_list_sites") return null;
60
60
  if (toolName === "drupal_governance_status" && callerTargetHints(rawArgs).length === 0) {
61
61
  return null;
@@ -157,7 +157,7 @@ export async function securityMiddleware(toolName, args, handler, context = {})
157
157
  const assertCallAllowed = async () => {
158
158
  // Source-governance gate (#176). The diagnostic tools stay callable while
159
159
  // governance fails — they are how an operator learns which condition failed.
160
- if (!GOVERNANCE_DIAGNOSTIC_TOOLS.has(toolName)) {
160
+ if (!DIAGNOSTIC_TOOLS.has(toolName)) {
161
161
  await assertSourceGovernance(site);
162
162
  }
163
163
 
@@ -188,7 +188,7 @@ export async function securityMiddleware(toolName, args, handler, context = {})
188
188
  await assertCallAllowed();
189
189
  // Charge only after governance and policy gates pass, so an outage or a
190
190
  // local deny cannot exhaust the window. Diagnostics do not consume.
191
- if (!GOVERNANCE_DIAGNOSTIC_TOOLS.has(toolName)) {
191
+ if (!DIAGNOSTIC_TOOLS.has(toolName)) {
192
192
  consumeBudgetIfEnforced("chained_action");
193
193
  }
194
194
  return handler(nextArgs);
package/src/lib/errors.js CHANGED
@@ -25,14 +25,3 @@ export function toolResult(data) {
25
25
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
26
26
  };
27
27
  }
28
-
29
- /**
30
- * Wrap a plain string message (for confirmations, warnings, etc.).
31
- * @param {string} text Message text.
32
- * @returns {{content: Array<{type: string, text: string}>}}
33
- */
34
- export function toolMessage(text) {
35
- return {
36
- content: [{ type: "text", text }],
37
- };
38
- }
@@ -36,12 +36,6 @@ export const EXECUTION_IDS = Object.freeze([
36
36
  "receiptId",
37
37
  ]);
38
38
 
39
- export const RECONCILE_STATES = Object.freeze([
40
- "settled",
41
- "incomplete",
42
- "mismatched",
43
- ]);
44
-
45
39
  const DEFAULT_MAX_RECORDS = 10_000;
46
40
 
47
41
  const FORBIDDEN_EXPORT_KEYS = new Set([
@@ -18,6 +18,7 @@
18
18
 
19
19
  import fetch from "node-fetch";
20
20
  import { authHeadersAsync, clientHeaders } from "./config.js";
21
+ import { DIAGNOSTIC_TOOLS } from "./principal.js";
21
22
 
22
23
  /** How long a passing verification stays fresh before it must be re-proven. */
23
24
  export const OK_TTL_MS = 60_000;
@@ -25,13 +26,6 @@ export const OK_TTL_MS = 60_000;
25
26
  /** How long a failed verification is held before the next attempt re-checks. */
26
27
  export const FAIL_TTL_MS = 5_000;
27
28
 
28
- /** Tools that stay discoverable and callable while governance is failing —
29
- * the diagnostic surface an operator needs to see WHY it is failing. */
30
- export const GOVERNANCE_DIAGNOSTIC_TOOLS = new Set([
31
- "drupal_list_sites",
32
- "drupal_governance_status",
33
- ]);
34
-
35
29
  /** Denial for a governed path whose source-governance contract is not verified. */
36
30
  export class GovernanceError extends Error {
37
31
  /**
@@ -203,5 +197,5 @@ export async function filterDiscoverableTools(definitions, sites) {
203
197
  const verdicts = await Promise.all(sites.map(async (site) =>
204
198
  !requiresGovernance(site) || (await verifySourceGovernance(site)).ok));
205
199
  if (verdicts.some(Boolean)) return definitions;
206
- return definitions.filter((d) => GOVERNANCE_DIAGNOSTIC_TOOLS.has(d.name));
200
+ return definitions.filter((d) => DIAGNOSTIC_TOOLS.has(d.name));
207
201
  }
@@ -18,11 +18,11 @@
18
18
  import nodeFetch from "node-fetch";
19
19
 
20
20
  /** Default per-request timeout (ms). */
21
- export const DEFAULT_TIMEOUT_MS = 5000;
21
+ const DEFAULT_TIMEOUT_MS = 5000;
22
22
  /** Default number of concurrent in-flight checks. */
23
- export const DEFAULT_CONCURRENCY = 5;
23
+ const DEFAULT_CONCURRENCY = 5;
24
24
  /** Default hard ceiling on URLs checked in one call. */
25
- export const DEFAULT_MAX_LINKS = 200;
25
+ const DEFAULT_MAX_LINKS = 200;
26
26
 
27
27
  /**
28
28
  * Hostnames that must never be probed regardless of allowlist — loopback and
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { graphqlHasMutation } from "./security.js";
14
14
 
15
- export const WRITE_PREFIXES = ["drupal_create_", "drupal_update_", "drupal_upload_",
15
+ const WRITE_PREFIXES = ["drupal_create_", "drupal_update_", "drupal_upload_",
16
16
  "drupal_block_", "drupal_drush_cache", "drupal_drush_cron",
17
17
  "drupal_drush_config_export", "drupal_drush_config_import",
18
18
  "drupal_drush_updatedb", "drupal_drush_module_enable",
@@ -22,7 +22,7 @@ export const WRITE_PREFIXES = ["drupal_create_", "drupal_update_", "drupal_uploa
22
22
  // Governed config write (also gated inside the handler by the config-write cap):
23
23
  "drupal_config_set"];
24
24
 
25
- export const DESTRUCTIVE_PREFIXES = ["drupal_delete_", "drupal_drush_module_disable"];
25
+ const DESTRUCTIVE_PREFIXES = ["drupal_delete_", "drupal_drush_module_disable"];
26
26
 
27
27
  /**
28
28
  * Classify a tool's operation intent from its name prefix.
@@ -11,11 +11,11 @@
11
11
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
12
12
  import { SEAL_PREFIX } from "./policy-promotion.js";
13
13
 
14
- export const POLICY_BUNDLE_VERSION = 1;
14
+ const POLICY_BUNDLE_VERSION = 1;
15
15
 
16
- export const DEFAULT_BUNDLE_TTL = 86400 * 30;
16
+ const DEFAULT_BUNDLE_TTL = 86400 * 30;
17
17
 
18
- export const EMERGENCY_DENY = "*";
18
+ const EMERGENCY_DENY = "*";
19
19
 
20
20
  export const EMERGENCY_DIGEST = "emergency-deny";
21
21
 
@@ -43,7 +43,7 @@ function normalize(value) {
43
43
  * @param {object} claims
44
44
  * @returns {string}
45
45
  */
46
- export function canonicalJson(claims) {
46
+ function canonicalJson(claims) {
47
47
  return JSON.stringify(normalize(claims));
48
48
  }
49
49
 
@@ -15,6 +15,7 @@
15
15
  import { AsyncLocalStorage } from "node:async_hooks";
16
16
  import { getDefaultSiteName, getInboundGrants } from "./config.js";
17
17
  import { inferOperation } from "./operations.js";
18
+ import { POLICY_DIGEST } from "./policy-promotion.js";
18
19
  import { resolveSecurityConfig, SecurityError } from "./security.js";
19
20
 
20
21
  const identityStore = new AsyncLocalStorage();
@@ -23,7 +24,6 @@ const identityStore = new AsyncLocalStorage();
23
24
  export const TARGET_HINT_KEYS = Object.freeze(["site", "environment", "tenant", "target"]);
24
25
 
25
26
  const ACTOR_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
26
- const POLICY_DIGEST = /^[0-9a-f]{64}$/i;
27
27
 
28
28
  function tableHasKeys(table) {
29
29
  return Object.keys(table).some((key) => {
@@ -129,7 +129,7 @@ function grantNameList(values) {
129
129
  * @param {object|null} actors
130
130
  * @returns {object|null}
131
131
  */
132
- export function normalizeActors(actors) {
132
+ function normalizeActors(actors) {
133
133
  if (!actors || typeof actors !== "object" || Array.isArray(actors)) return null;
134
134
  const entries = [];
135
135
  for (const [rawKey, value] of Object.entries(actors)) {
@@ -201,7 +201,7 @@ export function resolveActor({ identity = null, actors = null } = {}) {
201
201
  * @param {object|null} policies
202
202
  * @returns {object|null}
203
203
  */
204
- export function normalizePolicies(policies) {
204
+ function normalizePolicies(policies) {
205
205
  if (!policies || typeof policies !== "object" || Array.isArray(policies)) return null;
206
206
  const entries = [];
207
207
  for (const [rawKey, value] of Object.entries(policies)) {
@@ -348,7 +348,7 @@ function callerApprovalHint(args = {}) {
348
348
  * @param {unknown} raw
349
349
  * @returns {string[]|{invalid: true, reason: string}}
350
350
  */
351
- export function normalizeApprovalRequiredTools(raw) {
351
+ function normalizeApprovalRequiredTools(raw) {
352
352
  if (raw === null || raw === undefined) return [];
353
353
  if (Array.isArray(raw)) {
354
354
  return [...new Set(
@@ -415,7 +415,7 @@ export class EdgeStartupError extends Error {
415
415
  * @param {Record<string, string|string[]>} [headers]
416
416
  * @returns {Record<string, string>}
417
417
  */
418
- export function fanDownHeaders(headers = {}) {
418
+ function fanDownHeaders(headers = {}) {
419
419
  const entries = [];
420
420
  for (const [name, value] of Object.entries(forwardHeaders(headers))) {
421
421
  const key = String(name).toLowerCase();
@@ -387,7 +387,7 @@ export function hasScope(site, scope) {
387
387
  * @param {object} site Resolved site config.
388
388
  * @returns {boolean}
389
389
  */
390
- export function isGovernedSetup(site) {
390
+ function isGovernedSetup(site) {
391
391
  return site?.requireGovernance === true || Boolean(site?.oauth);
392
392
  }
393
393
 
@@ -723,23 +723,6 @@ export function redactCanonicalEntity(entity, secConfig, entityType) {
723
723
  return { ...entity, fields: redactedFields, ...baseOverrides };
724
724
  }
725
725
 
726
- /**
727
- * Redact a full JSON:API response by redacting each item under `.data`.
728
- * @param {?object} response JSON:API response with a `data` object or array.
729
- * @param {object} secConfig Resolved security config.
730
- * @param {string} entityType Entity type for the response data.
731
- * @returns {?object} New response; original is not mutated.
732
- */
733
- export function redactResponse(response, secConfig, entityType) {
734
- if (!response?.data) return response;
735
- return {
736
- ...response,
737
- data: Array.isArray(response.data)
738
- ? response.data.map((r) => redactResource(r, secConfig, entityType))
739
- : redactResource(response.data, secConfig, entityType),
740
- };
741
- }
742
-
743
726
  // ---------------------------------------------------------------------------
744
727
  // Security summary tool (exposed as drupal_security_info)
745
728
  // ---------------------------------------------------------------------------
@@ -29,7 +29,7 @@ export const promptNameToToolName = (name) => name.replace(/-/g, "_");
29
29
  * @param {object} spec - A JSON-Schema property spec.
30
30
  * @returns {string} e.g. "string", "boolean (true/false)", "object (pass as JSON)".
31
31
  */
32
- export function typeHint(spec) {
32
+ function typeHint(spec) {
33
33
  const t = Array.isArray(spec?.type) ? spec.type[0] : spec?.type;
34
34
  switch (t) {
35
35
  case "boolean": return "boolean (true/false)";
@@ -84,7 +84,7 @@ export function buildToolPrompts(definitions) {
84
84
  * @param {object} args - Arguments supplied to the prompt (all strings per MCP).
85
85
  * @returns {string} A user-role instruction message body.
86
86
  */
87
- export function renderToolInstruction(def, args = {}) {
87
+ function renderToolInstruction(def, args = {}) {
88
88
  const params = paramList(def.inputSchema);
89
89
  const required = params.filter((p) => p.required);
90
90
  const optional = params.filter((p) => !p.required);
package/src/lib/usage.js CHANGED
@@ -31,15 +31,6 @@ export const USAGE_DECISIONS = Object.freeze(["allow", "deny"]);
31
31
  */
32
32
  export const RECEIPT_OUTCOMES = Object.freeze(["ok", "failed", "unknown"]);
33
33
 
34
- /** Reconciliation states over one request / decision / receipt chain. */
35
- export const RECONCILE_STATES = Object.freeze([
36
- "settled",
37
- "denied",
38
- "missing",
39
- "duplicate",
40
- "uncertain",
41
- ]);
42
-
43
34
  const PHASE_SET = new Set(USAGE_PHASES);
44
35
  const DECISION_SET = new Set(USAGE_DECISIONS);
45
36
  const OUTCOME_SET = new Set(RECEIPT_OUTCOMES);
package/src/lib/verify.js CHANGED
@@ -30,9 +30,9 @@ import { resolveInboundAuthConfig, resolveInboundAuthMode } from "./http-auth.js
30
30
  * and does not fail it: a verifier a secure install can never pass is a
31
31
  * verifier people stop running.
32
32
  */
33
- export const PASS = "pass";
34
- export const FAIL = "fail";
35
- export const SKIPPED = "skipped";
33
+ const PASS = "pass";
34
+ const FAIL = "fail";
35
+ const SKIPPED = "skipped";
36
36
  export const NOT_APPLICABLE = "n/a";
37
37
 
38
38
  /** Every static check, in report order. */
@@ -39,7 +39,7 @@ const SAMPLE_CAP = 500;
39
39
  * @param {unknown} err
40
40
  * @returns {boolean}
41
41
  */
42
- export function isUnfilterableModerationState(err) {
42
+ function isUnfilterableModerationState(err) {
43
43
  const msg = String(err?.message || "");
44
44
  if (!/moderation_state/i.test(msg)) return false;
45
45
  return /not found/i.test(msg) || /not filterable/i.test(msg) || /invalid filter/i.test(msg);
@@ -57,7 +57,7 @@ import {
57
57
  * @param {number|string|null|undefined} [revisionId] Current revision id.
58
58
  * @returns {{type: string, id: string, meta?: {target_revision_id: number}}}
59
59
  */
60
- export function embedRef(bundle, id, revisionId) {
60
+ function embedRef(bundle, id, revisionId) {
61
61
  return embedParagraphRef(bundle, id, revisionId);
62
62
  }
63
63
 
@@ -1,19 +0,0 @@
1
- /**
2
- * Compatibility facade for the Sentinel HTTP client.
3
- * New call sites should import from `./sentinel-draft.js`.
4
- */
5
-
6
- export {
7
- assertDraftLangcode,
8
- assertInventoryDraftLanguage,
9
- createTranslationDraft,
10
- isMissingDraftEndpoint,
11
- isMissingTranslationEndpoint,
12
- readDraftTranslation,
13
- readNodeDraftInventory,
14
- readTranslationInventory,
15
- resolveNodeTranslationPair,
16
- rewriteTranslationWorkingRevisionError,
17
- supportsSentinelDraft,
18
- writeDraft,
19
- } from "./sentinel-draft.js";
@@ -1,5 +0,0 @@
1
- /** Sentinel discovery for translation-only node revisions (#297). */
2
- export {
3
- assertInventoryDraftLanguage,
4
- readNodeDraftInventory,
5
- } from "./sentinel-draft.js";