drupal-mcp-connector 2.7.2 → 2.7.3

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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.7.3] - 2026-08-18
11
+
12
+ ### Fixed
13
+ - **Launcher and startup name a secret-table / config mismatch (#211).**
14
+ The shipped Keychain table matches `config/config.example.json`. A
15
+ `config.json` that uses different `clientSecretEnv` names without a
16
+ `config/secrets.map` left the table and the config each valid and
17
+ jointly inert. Per-item Keychain misses stay silent (break-glass).
18
+ When the table matches **no** named secret, `bin/drupal-mcp-launch.sh`
19
+ and `src/lib/load-secrets.js` now print one stderr line that names
20
+ the unmapped variables and says `secrets.map` is absent (or that the
21
+ map does not name them). Fail-closed start when every named secret is
22
+ unset is unchanged.
23
+
10
24
  ## [2.7.2] - 2026-08-18
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  Built by **Jeremy Michael Cerda** (opensource@wilkesliberty.com). Maintained by [Wilkes & Liberty, LLC](https://github.com/Wilkes-Liberty).
11
11
 
12
- **If the client only shows `drupal_list_sites` and `drupal_governance_status`**, the secret env vars named in `config.json` are unset. Upgrade to **2.7.2** (or at least 2.6.1), or stay on 2.6.0 and launch via `bin/drupal-mcp-launch.sh` with a `config/secrets.map` (`ENV_VAR=keychain-item`). Then restart the MCP server. See [#199](https://github.com/Wilkes-Liberty/drupal-mcp-connector/issues/199).
12
+ **If the client only shows `drupal_list_sites` and `drupal_governance_status`**, the secret env vars named in `config.json` are unset. Upgrade to **2.7.3** (or at least 2.6.1), or stay on 2.6.0 and launch via `bin/drupal-mcp-launch.sh` with a `config/secrets.map` (`ENV_VAR=keychain-item`). Then restart the MCP server. See [#199](https://github.com/Wilkes-Liberty/drupal-mcp-connector/issues/199).
13
13
 
14
14
  ---
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drupal-mcp-connector",
3
- "version": "2.7.2",
3
+ "version": "2.7.3",
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",
package/src/index.js CHANGED
@@ -34,7 +34,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
34
34
  import { toNodeHandler } from "@modelcontextprotocol/node";
35
35
 
36
36
  import { listSiteNames, getTlsConfig, loadConfig, CLIENT_VERSION } from "./lib/config.js";
37
- import { loadLocalSecrets, secretLoadFatalMessage } from "./lib/load-secrets.js";
37
+ import { loadLocalSecrets, secretLoadFatalMessage, secretTableMismatchMessage } from "./lib/load-secrets.js";
38
38
  import {
39
39
  makeBearerCheck,
40
40
  resolveInboundAuthConfig,
@@ -68,7 +68,10 @@ if (secretFatal) {
68
68
  console.error(`[drupal-mcp-connector] FATAL: ${secretFatal}`);
69
69
  process.exit(1);
70
70
  }
71
- if (secretLoad.unset.length) {
71
+ const secretMismatch = secretTableMismatchMessage(secretLoad);
72
+ if (secretMismatch) {
73
+ console.error(`[drupal-mcp-connector] WARNING: ${secretMismatch}`);
74
+ } else if (secretLoad.unset.length) {
72
75
  console.error(
73
76
  "[drupal-mcp-connector] WARNING: config.json names secret env vars that are unset: " +
74
77
  `${secretLoad.unset.join(", ")}. Those sites will fail closed.`
@@ -9,8 +9,10 @@
9
9
  * The default table matches config/config.example.json. A gitignored
10
10
  * config/secrets.map replaces that table for a deployment whose env-var
11
11
  * names differ. Per-item Keychain misses stay silent (inert break-glass).
12
- * If the active config.json names secret env vars and none of them are set
13
- * after this step, the caller must refuse to start.
12
+ * Zero overlap between the table and the names in config.json is a
13
+ * distinct case the two files are jointly inert — and the caller should
14
+ * say so. If every named secret is still unset after this step, the caller
15
+ * must refuse to start.
14
16
  */
15
17
 
16
18
  import { execFileSync } from "node:child_process";
@@ -101,7 +103,7 @@ export function lookupKeychainItem(item) {
101
103
  * @param {NodeJS.ProcessEnv} [options.env] Mutated when a lookup succeeds.
102
104
  * @param {typeof readFileSync} [options.readFile]
103
105
  * @param {(item: string) => string} [options.lookup]
104
- * @returns {{pairs: number, resolved: number, named: string[], unset: string[]}}
106
+ * @returns {{pairs: number, resolved: number, named: string[], unset: string[], tableVars: string[], source: "map"|"default"}}
105
107
  */
106
108
  export function loadLocalSecrets({
107
109
  cwd = process.cwd(),
@@ -109,11 +111,13 @@ export function loadLocalSecrets({
109
111
  readFile = readFileSync,
110
112
  lookup = lookupKeychainItem,
111
113
  } = {}) {
114
+ let source = "map";
112
115
  let pairs;
113
116
  try {
114
117
  pairs = parseSecretMap(readFile(join(cwd, "config", "secrets.map"), "utf8"));
115
118
  } catch {
116
119
  pairs = DEFAULT_SECRET_PAIRS;
120
+ source = "default";
117
121
  }
118
122
 
119
123
  let resolved = 0;
@@ -140,16 +144,47 @@ export function loadLocalSecrets({
140
144
 
141
145
  const envMap = new Map(Object.entries(env));
142
146
  const unset = named.filter((name) => !envMap.get(name));
143
- return { pairs: pairs.length, resolved, named, unset };
147
+ const tableVars = pairs.map(([varName]) => varName);
148
+ return { pairs: pairs.length, resolved, named, unset, tableVars, source };
149
+ }
150
+
151
+ /**
152
+ * One-line diagnosis when the secret table and config.json share no names.
153
+ * Null when there is any overlap, or when config names no secrets.
154
+ * Per-item Keychain misses are not a mismatch.
155
+ * @param {{named: string[], tableVars?: string[], source?: "map"|"default"}} loaded
156
+ * @returns {string|null}
157
+ */
158
+ export function secretTableMismatchMessage(loaded) {
159
+ if (!loaded.named.length) return null;
160
+ const table = new Set(loaded.tableVars || []);
161
+ const unmapped = loaded.named.filter((name) => !table.has(name));
162
+ if (unmapped.length !== loaded.named.length) return null;
163
+ const source = loaded.source === "map"
164
+ ? "config/secrets.map does not name them"
165
+ : "using shipped defaults; config/secrets.map is absent";
166
+ const unset = Array.isArray(loaded.unset) ? loaded.unset : [];
167
+ const closer = unset.length
168
+ ? "Unset: " + unset.join(", ") + ". Those sites will fail closed."
169
+ : "Named secrets already in the environment stay set; the table will not populate any of them.";
170
+ return (
171
+ "no secret-table entries match clientSecretEnv/apiTokenEnv names in config.json " +
172
+ "(" + unmapped.join(", ") + "); " + source + ". " +
173
+ closer
174
+ );
144
175
  }
145
176
 
146
177
  /**
147
178
  * Refuse to boot a server that can only advertise diagnostic tools.
148
- * @param {{named: string[], unset: string[]}} loaded
179
+ * @param {{named: string[], unset: string[], tableVars?: string[], source?: "map"|"default"}} loaded
149
180
  * @returns {string|null} Fatal message, or null when start is allowed.
150
181
  */
151
182
  export function secretLoadFatalMessage(loaded) {
152
183
  if (!loaded.named.length || loaded.unset.length !== loaded.named.length) return null;
184
+ const mismatch = secretTableMismatchMessage(loaded);
185
+ if (mismatch) {
186
+ return mismatch + " Refusing to start. Map those names in config/secrets.map (ENV_VAR=keychain-item) or export them before launch.";
187
+ }
153
188
  return (
154
189
  `every clientSecretEnv/apiTokenEnv named in config.json is unset (${loaded.unset.join(", ")}). ` +
155
190
  "Refusing to start. Map those names in config/secrets.map (ENV_VAR=keychain-item) " +