drupal-mcp-connector 1.2.0 → 1.3.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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/lib/security.js +35 -0
- package/src/tools/config.js +17 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-06-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `drupal_mcp_whoami` no longer over-reports configuration capabilities. Capabilities are
|
|
14
|
+
now the intersection of the connector security preset **and** the token's effective OAuth
|
|
15
|
+
scopes: `configRead` / `configWrite` require the dedicated `mcp_config` scope (config-editor
|
|
16
|
+
/ Developer tier), and `write` / `delete` require `mcp_write`. Previously a content-tier
|
|
17
|
+
token (`mcp_read` / `mcp_write`) was reported with `configRead: true` even though the server
|
|
18
|
+
denies every `config_*` tool without `mcp_config`. When a site declares no OAuth scopes,
|
|
19
|
+
behaviour is unchanged (preset-only).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- The config tools (`config_get` / `config_list` / `config_set`) now check for the
|
|
23
|
+
`mcp_config` scope up front (when OAuth scopes are configured) and fail fast with a clear
|
|
24
|
+
message instead of dispatching a call the governed server will deny — keeping connector
|
|
25
|
+
behaviour consistent with `drupal_mcp_whoami`. Aligns with mcp_sentinel isolating the config
|
|
26
|
+
tools under the dedicated `mcp_config` scope.
|
|
27
|
+
|
|
10
28
|
## [1.2.0] - 2026-06-27
|
|
11
29
|
|
|
12
30
|
### Changed
|
package/package.json
CHANGED
package/src/lib/security.js
CHANGED
|
@@ -323,6 +323,41 @@ export function assertConfigWriteAllowed(secConfig) {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Whether the site's OAuth token carries a given scope. When the site declares
|
|
328
|
+
* no OAuth scopes (no agent channel configured), scope gating is a no-op and
|
|
329
|
+
* this returns true so preset-only (non-OAuth) setups are unaffected.
|
|
330
|
+
* @param {object} site Resolved site config.
|
|
331
|
+
* @param {string} scope OAuth scope machine id (e.g. "mcp_config").
|
|
332
|
+
* @returns {boolean} True if the scope is present, or no scopes are configured.
|
|
333
|
+
*/
|
|
334
|
+
export function hasScope(site, scope) {
|
|
335
|
+
const scopes = site?.oauth?.scopes ?? [];
|
|
336
|
+
return scopes.length === 0 || scopes.includes(scope);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Gate the config tools (get/list/set) on the dedicated `mcp_config` OAuth
|
|
341
|
+
* scope, which the governed server requires for every config_* tool. When OAuth
|
|
342
|
+
* scopes are configured but `mcp_config` is absent, fail fast with a clear
|
|
343
|
+
* message instead of dispatching a call the server will deny — keeping the
|
|
344
|
+
* connector's behaviour and its drupal_mcp_whoami report consistent with what
|
|
345
|
+
* the token can actually exercise.
|
|
346
|
+
* @param {object} site Resolved site config.
|
|
347
|
+
* @param {string} operationLabel Label used in the error message.
|
|
348
|
+
* @returns {void}
|
|
349
|
+
* @throws {SecurityError} if scopes are configured and `mcp_config` is missing.
|
|
350
|
+
*/
|
|
351
|
+
export function assertConfigScope(site, operationLabel) {
|
|
352
|
+
if (!hasScope(site, "mcp_config")) {
|
|
353
|
+
throw new SecurityError(
|
|
354
|
+
"Config tools require the 'mcp_config' OAuth scope (config-editor / " +
|
|
355
|
+
"Developer tier); this token does not carry it. " +
|
|
356
|
+
`Operation blocked: ${operationLabel}.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
326
361
|
/**
|
|
327
362
|
* @param {object} secConfig Resolved security config.
|
|
328
363
|
* @param {string} entityType Entity type targeted by the delete.
|
package/src/tools/config.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
assertNotReadOnly,
|
|
19
19
|
assertConfigReadAllowed,
|
|
20
20
|
assertConfigWriteAllowed,
|
|
21
|
+
assertConfigScope,
|
|
22
|
+
hasScope,
|
|
21
23
|
} from "../lib/security.js";
|
|
22
24
|
import { callServerTool, SERVER_TOOLS } from "../lib/server-tools.js";
|
|
23
25
|
|
|
@@ -33,6 +35,7 @@ import { callServerTool, SERVER_TOOLS } from "../lib/server-tools.js";
|
|
|
33
35
|
*/
|
|
34
36
|
async function configGet({ site: siteName, name }) {
|
|
35
37
|
const site = getSiteConfig(siteName);
|
|
38
|
+
assertConfigScope(site, `config:get ${name}`);
|
|
36
39
|
assertConfigReadAllowed(resolveSecurityConfig(site));
|
|
37
40
|
return callServerTool(site, SERVER_TOOLS.configGet, { name });
|
|
38
41
|
}
|
|
@@ -45,6 +48,7 @@ async function configGet({ site: siteName, name }) {
|
|
|
45
48
|
*/
|
|
46
49
|
async function configList({ site: siteName, prefix }) {
|
|
47
50
|
const site = getSiteConfig(siteName);
|
|
51
|
+
assertConfigScope(site, "config:list");
|
|
48
52
|
assertConfigReadAllowed(resolveSecurityConfig(site));
|
|
49
53
|
const args = prefix ? { prefix } : {};
|
|
50
54
|
return callServerTool(site, SERVER_TOOLS.configList, args);
|
|
@@ -60,6 +64,7 @@ async function configList({ site: siteName, prefix }) {
|
|
|
60
64
|
async function configSet({ site: siteName, name, value }) {
|
|
61
65
|
const site = getSiteConfig(siteName);
|
|
62
66
|
const sec = resolveSecurityConfig(site);
|
|
67
|
+
assertConfigScope(site, `config:set ${name}`);
|
|
63
68
|
assertNotReadOnly(sec, `config:set ${name}`);
|
|
64
69
|
assertConfigWriteAllowed(sec);
|
|
65
70
|
return callServerTool(site, SERVER_TOOLS.configSet, { name, value });
|
|
@@ -100,6 +105,13 @@ async function whoami({ site: siteName }) {
|
|
|
100
105
|
const site = getSiteConfig(siteName);
|
|
101
106
|
const sec = resolveSecurityConfig(site);
|
|
102
107
|
const summary = getSecuritySummary(site);
|
|
108
|
+
// Effective capability = connector preset AND the scope the server demands.
|
|
109
|
+
// Reporting the preset alone over-states what the token can do — e.g. the
|
|
110
|
+
// content-editor preset allows config reads locally, but every config_* tool
|
|
111
|
+
// is gated server-side on mcp_config, which the content tier does not hold.
|
|
112
|
+
// When no OAuth scopes are configured, hasScope() is a no-op (preset-only).
|
|
113
|
+
const canWrite = !sec.readOnly && hasScope(site, "mcp_write");
|
|
114
|
+
const canConfig = hasScope(site, "mcp_config");
|
|
103
115
|
return {
|
|
104
116
|
site: site._name,
|
|
105
117
|
tier: inferTier(site, sec),
|
|
@@ -108,11 +120,11 @@ async function whoami({ site: siteName }) {
|
|
|
108
120
|
api: site.api ?? "auto",
|
|
109
121
|
serverToolsConfigured: Boolean(site.serverTools?.url),
|
|
110
122
|
capabilities: {
|
|
111
|
-
read:
|
|
112
|
-
write:
|
|
113
|
-
delete: sec.allowDestructive &&
|
|
114
|
-
configRead: sec.allowConfigRead,
|
|
115
|
-
configWrite: sec.allowConfigWrite && !sec.readOnly,
|
|
123
|
+
read: hasScope(site, "mcp_read"),
|
|
124
|
+
write: canWrite,
|
|
125
|
+
delete: sec.allowDestructive && canWrite,
|
|
126
|
+
configRead: sec.allowConfigRead && canConfig,
|
|
127
|
+
configWrite: sec.allowConfigWrite && !sec.readOnly && canConfig,
|
|
116
128
|
// Publishing is always gated server-side (editorial workflow); the agent
|
|
117
129
|
// never holds the publish transition. Surfaced here so it is explicit.
|
|
118
130
|
publish: false,
|