drupal-mcp-connector 2.18.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.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: "Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff."
2
+ description: "Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff."
3
3
  argument-hint: "[site] [bundles] [skipFields]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_codegen_diff`.
7
7
 
8
- Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff.
8
+ Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff.
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: "Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate."
2
+ description: "Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate."
3
3
  argument-hint: "[site] [bundles] [skipFields]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_codegen_generate`.
7
7
 
8
- Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate.
8
+ Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate.
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 node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files."
2
+ description: "List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files."
3
3
  argument-hint: "[site] [bundles] [skipFields]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_codegen_inspect`.
7
7
 
8
- List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files.
8
+ List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files.
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: "Run a single read-only SELECT through mcp_sentinel's governed command (`drush mcp-sentinel:sql-query`). Requires the site to set drushSsh.rawSql=\"governed\" AND the site's policy profile to set allow_raw_sql; both are off by default. The server refuses statements touching a denied entity type, a non-entity table, or a redacted field, and records every attempt in the tamper-evident audit log. Use the site-context or entity-schema tools for schema introspection."
2
+ description: "Run a single read-only SELECT through the configured sqlQuery module binding, or the legacy governed Drush command on unbound sites. Requires the site to set drushSsh.rawSql=\"governed\" AND the site's policy profile to set allow_raw_sql; both are off by default. The server refuses statements touching a denied entity type, a non-entity table, or a redacted field, and records every attempt in the tamper-evident audit log. Use the site-context or entity-schema tools for schema introspection."
3
3
  argument-hint: "<query> [site]"
4
4
  ---
5
5
 
6
6
  Call the MCP tool `drupal_drush_sql_query`.
7
7
 
8
- Run a single read-only SELECT through mcp_sentinel's governed command (`drush mcp-sentinel:sql-query`). Requires the site to set drushSsh.rawSql="governed" AND the site's policy profile to set allow_raw_sql; both are off by default. The server refuses statements touching a denied entity type, a non-entity table, or a redacted field, and records every attempt in the tamper-evident audit log. Use the site-context or entity-schema tools for schema introspection.
8
+ Run a single read-only SELECT through the configured sqlQuery module binding, or the legacy governed Drush command on unbound sites. Requires the site to set drushSsh.rawSql="governed" AND the site's policy profile to set allow_raw_sql; both are off by default. The server refuses statements touching a denied entity type, a non-entity table, or a redacted field, and records every attempt in the tamper-evident audit log. Use the site-context or entity-schema tools for schema introspection.
9
9
 
10
10
  Parse the arguments supplied with this command into this tool's parameters:
11
11
 
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ 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
+
21
+ ## [2.19.0] - 2026-09-16
22
+
23
+ ### Added
24
+ - Codegen and governed SQL compatibility commands can use module-owned Tool API
25
+ actions through explicit bindings (#317). Configured calls validate fresh
26
+ schemas and never fall back to SSH on failure. Core and third-party commands
27
+ remain available; unbound sites retain their existing migration path.
28
+
10
29
  ## [2.18.0] - 2026-09-16
11
30
 
12
31
  ### Fixed
@@ -1671,6 +1690,9 @@ The connector is now **dual-protocol**: every tool runs against an abstract back
1671
1690
  - User tools gained explicit PII-access assertions.
1672
1691
  - Whole tree lint-clean (`npm run lint`) with object-injection sinks rewritten to safe lookups.
1673
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
1674
1696
  [2.17.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.17.0
1675
1697
  [2.16.0]: https://github.com/Wilkes-Liberty/drupal-mcp-connector/releases/tag/v2.16.0
1676
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.18.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. */
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Tool group: GraphQL Compose Codegen (TypeScript / Next.js scaffolds).
3
3
  *
4
- * Thin Drush wrappers around `drupal/graphql_compose_codegen`. The generator
4
+ * Compatibility adapters through configured module bindings, or legacy Drush.
5
+ * Thin wrappers around `drupal/graphql_compose_codegen`. The generator
5
6
  * lives on the Drupal site; this connector does not reimplement it and does
6
7
  * not run GraphQL Code Generator against the SDL.
7
8
  *
@@ -16,6 +17,7 @@
16
17
 
17
18
  import { getSiteConfig } from "../lib/config.js";
18
19
  import { sshDrush } from "./drush.js";
20
+ import { callBoundModuleTool, toolResultData } from "../lib/server-tools.js";
19
21
  import { SecurityError } from "../lib/security.js";
20
22
  import { validateMachineName } from "../lib/validate.js";
21
23
 
@@ -75,6 +77,18 @@ async function runGqcc(
75
77
  const site = getSiteConfig(siteName);
76
78
  const names = parseMachineNameList(bundles, "bundles");
77
79
  const skip = parseMachineNameList(skipFields, "skipFields");
80
+ if (site.serverTools?.bindings !== undefined) {
81
+ const binding = new Map([
82
+ [INSPECT, "codegenInspect"], [DIFF, "codegenDiff"], [GENERATE, "codegenPreview"],
83
+ ]).get(subcommand);
84
+ const result = toolResultData(await callBoundModuleTool(site, binding, {
85
+ bundles: names, skip_fields: skip,
86
+ }, { operation: "read", scope: "mcp_config", capabilities: ["configRead"] }));
87
+ if (result?.success !== true || !result.data || typeof result.data !== "object" || Array.isArray(result.data)) {
88
+ throw new SecurityError("Codegen module returned an invalid result. No SSH fallback was attempted.");
89
+ }
90
+ return { output: JSON.stringify(result.data), command: `module:${binding}`, wroteFiles: false };
91
+ }
78
92
  const args = [subcommand];
79
93
  if (names.length) args.push(`--bundles=${names.join(",")}`);
80
94
  if (skip.length) args.push(`--skip-fields=${skip.join(",")}`);
@@ -126,19 +140,19 @@ export const definitions = [
126
140
  {
127
141
  name: "drupal_codegen_inspect",
128
142
  description:
129
- "List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files.",
143
+ "List node and paragraph bundles and extra fields from graphql_compose_codegen (`drush graphql-compose-codegen:inspect`). Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:inspect. Does not write files.",
130
144
  inputSchema: { type: "object", properties: LIST_PROPS },
131
145
  },
132
146
  {
133
147
  name: "drupal_codegen_diff",
134
148
  description:
135
- "Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff.",
149
+ "Compare the live graphql_compose schema to the last gqcc:generate snapshot (`drush graphql-compose-codegen:diff`). Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:diff.",
136
150
  inputSchema: { type: "object", properties: LIST_PROPS },
137
151
  },
138
152
  {
139
153
  name: "drupal_codegen_generate",
140
154
  description:
141
- "Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Requires the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate.",
155
+ "Return TypeScript/GraphQL scaffold artefacts from graphql_compose_codegen as text (`drush graphql-compose-codegen:generate --dry-run`). Never writes on the Drupal host (no --output-dir). Copy artefacts locally. Uses the configured module binding; unbound sites require the module and drushSsh. Missing command fails loud. If allowedCommands is set, include graphql-compose-codegen:generate.",
142
156
  inputSchema: { type: "object", properties: LIST_PROPS },
143
157
  },
144
158
  ];
@@ -20,6 +20,7 @@
20
20
  * Requires per-site "drushSsh" config block — tools fail gracefully if absent.
21
21
  */
22
22
 
23
+ import { callBoundModuleTool, toolResultData } from "../lib/server-tools.js";
23
24
  import { Client } from "ssh2";
24
25
  import { readFileSync } from "fs";
25
26
  import { homedir } from "os";
@@ -456,6 +457,15 @@ async function sqlQuery({ site: siteName, query }) {
456
457
  assertGovernedRawSql(sshCfg, site._name);
457
458
  // Fast local reject only; mcp_sentinel's guard is the authority.
458
459
  validateSqlQuery(query);
460
+ if (site.serverTools?.bindings !== undefined) {
461
+ const result = toolResultData(await callBoundModuleTool(site, "sqlQuery", { query }, {
462
+ operation: "read", scope: "mcp_admin", capabilities: ["rawSql"],
463
+ }));
464
+ if (result?.success !== true || !result.data || !Array.isArray(result.data.rows)) {
465
+ throw new SecurityError("SQL module returned an invalid governed result. No SSH fallback was attempted.");
466
+ }
467
+ return result.data;
468
+ }
459
469
  const out = await sshDrush(site, ["mcp-sentinel:sql-query", query]);
460
470
  // The command emits a JSON object; a non-JSON reply means the command was
461
471
  // not found (mcp_sentinel absent or too old), which must not be reported as
@@ -534,7 +544,7 @@ export const definitions = [
534
544
  },
535
545
  {
536
546
  name: "drupal_drush_sql_query",
537
- description: "Run a single read-only SELECT through mcp_sentinel's governed command (`drush mcp-sentinel:sql-query`). Requires the site to set drushSsh.rawSql=\"governed\" AND the site's policy profile to set allow_raw_sql; both are off by default. The server refuses statements touching a denied entity type, a non-entity table, or a redacted field, and records every attempt in the tamper-evident audit log. Use the site-context or entity-schema tools for schema introspection.",
547
+ description: "Run a single read-only SELECT through the configured sqlQuery module binding, or the legacy governed Drush command on unbound sites. Requires the site to set drushSsh.rawSql=\"governed\" AND the site's policy profile to set allow_raw_sql; both are off by default. The server refuses statements touching a denied entity type, a non-entity table, or a redacted field, and records every attempt in the tamper-evident audit log. Use the site-context or entity-schema tools for schema introspection.",
538
548
  inputSchema: { type: "object", required: ["query"], properties: { site: { type: "string" }, query: { type: "string" } } },
539
549
  },
540
550
  {
@@ -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";