drupal-mcp-connector 2.17.0 → 2.19.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/.agents/commands/drupal-codegen-diff.md +2 -2
- package/.agents/commands/drupal-codegen-generate.md +2 -2
- package/.agents/commands/drupal-codegen-inspect.md +2 -2
- package/.agents/commands/drupal-drush-sql-query.md +2 -2
- package/CHANGELOG.md +41 -0
- package/README.md +5 -0
- package/package.json +3 -1
- package/src/index.js +16 -8
- package/src/lib/backends/backend-interface.js +3 -0
- package/src/lib/backends/graphql.js +1 -0
- package/src/lib/backends/jsonapi.js +1 -0
- package/src/lib/dispatch.js +10 -0
- package/src/lib/draft-write.js +18 -429
- package/src/lib/module-tools.js +241 -0
- package/src/lib/node-draft-inventory.js +4 -47
- package/src/lib/operations.js +5 -0
- package/src/lib/patch-preflight.js +7 -15
- package/src/lib/sentinel-draft.js +530 -0
- package/src/lib/server-tools.js +50 -16
- package/src/lib/verify.js +7 -1
- package/src/tools/codegen.js +18 -4
- package/src/tools/config.js +14 -4
- package/src/tools/drush.js +11 -1
- package/src/tools/media.js +14 -18
- package/src/tools/moderation.js +21 -21
- package/src/tools/nodes.js +1 -1
- package/src/tools/paragraphs.js +1 -1
- package/src/tools/reports-config.js +11 -1
- package/src/tools/reports-content.js +7 -6
- package/src/tools/revisions.js +24 -23
- package/src/tools/structure.js +1 -1
- package/src/tools/taxonomy.js +1 -1
- package/src/tools/translations.js +5 -3
|
@@ -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`).
|
|
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`).
|
|
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.
|
|
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.
|
|
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`).
|
|
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`).
|
|
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
|
|
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
|
|
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,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.19.0] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Codegen and governed SQL compatibility commands can use module-owned Tool API
|
|
14
|
+
actions through explicit bindings (#317). Configured calls validate fresh
|
|
15
|
+
schemas and never fall back to SSH on failure. Core and third-party commands
|
|
16
|
+
remain available; unbound sites retain their existing migration path.
|
|
17
|
+
|
|
18
|
+
## [2.18.0] - 2026-09-16
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Nullable module inputs (#320).** The generic registry accepts composed nullable
|
|
22
|
+
schemas while retaining runtime type, length, required and unknown-field checks.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Module compatibility bindings (#317).** Existing config commands and reports
|
|
26
|
+
can use locally approved module tools through generic discovery and schema
|
|
27
|
+
validation. Configured bindings never fall back after a refusal. The live
|
|
28
|
+
verifier probes the configured source tool directly.
|
|
29
|
+
- **Module-owned tools (#317).** An opt-in registry discovers typed Drupal MCP
|
|
30
|
+
tools and routes them through explicit site, scope and operation policy. Module
|
|
31
|
+
actions need no dedicated JavaScript handler. Catalog revisions, schema checks,
|
|
32
|
+
bounded results and refusal without a valid source prevent silent fallbacks.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Server-tool sessions are bounded and isolated by endpoint, credential and caller.
|
|
36
|
+
Module writes do not automatically retry after an authentication/session refusal.
|
|
37
|
+
- **Remaining writes use `prepareGuardedPatch`.** `langcode` always resolves
|
|
38
|
+
Sentinel inventory and draft-preflights, including unmoderated media.
|
|
39
|
+
`drupal_update_media` + `langcode`, unscoped `drupal_set_moderation_state`,
|
|
40
|
+
and `drupal_revert_revision` share the same preflight / `writeDraft` path
|
|
41
|
+
as node updates. `revisions.js` uses the shared `changedAheadOfRevision`.
|
|
42
|
+
- **One Sentinel HTTP client and fail-closed inventory policy.** Draft,
|
|
43
|
+
translation, and inventory requests share `src/lib/sentinel-draft.js`
|
|
44
|
+
(paths, If-Match, lang/state headers, missing-endpoint classifiers,
|
|
45
|
+
validated inventory). Missing 404/405 stays unavailable / no canonical
|
|
46
|
+
langcode PATCH. Permission, 5xx, and malformed inventory now throw from
|
|
47
|
+
`resolveNodeTranslationPair` instead of falling through to
|
|
48
|
+
`rel:working-copy`. JSON:API advertises `capabilities().sentinelDraft`;
|
|
49
|
+
GraphQL `rawQuery` does not count as Sentinel-capable.
|
|
50
|
+
|
|
10
51
|
## [2.17.0] - 2026-09-11
|
|
11
52
|
|
|
12
53
|
### Added
|
package/README.md
CHANGED
|
@@ -31,6 +31,11 @@ The connector speaks **two Drupal backends interchangeably** — Drupal core's *
|
|
|
31
31
|
|
|
32
32
|
## Dual-Protocol Backends
|
|
33
33
|
|
|
34
|
+
Drupal modules can also supply their own typed MCP actions through an opt-in
|
|
35
|
+
[module-tool registry](docs/module-tools.md). The module owns business rules and
|
|
36
|
+
schemas; the connector discovers and forwards approved tools under Sentinel and
|
|
37
|
+
connector policy. Existing built-in tools remain available during migration.
|
|
38
|
+
|
|
34
39
|
Each site declares which backend(s) it exposes via the `api` key:
|
|
35
40
|
|
|
36
41
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
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",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
70
70
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
71
|
+
"ajv": "^8.20.0",
|
|
72
|
+
"ajv-formats": "^3.0.1",
|
|
71
73
|
"graphql": "^17.0.0",
|
|
72
74
|
"jose": "^6.2.9",
|
|
73
75
|
"node-fetch": "^3.3.2",
|
package/src/index.js
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
|
|
59
59
|
// Tools — aggregated (single source of truth, side-effect-free) and per-tool prompts
|
|
60
60
|
import { allDefinitions, allHandlers, definitionsByName } from "./tools/index.js";
|
|
61
|
+
import { createModuleToolRegistry, isModuleTool } from "./lib/module-tools.js";
|
|
61
62
|
import { buildToolPrompts, getToolPromptMessages } from "./lib/tool-prompts.js";
|
|
62
63
|
|
|
63
64
|
// Apply config/secrets.map (or the shipped example table) before any site
|
|
@@ -104,6 +105,18 @@ const RESOURCES = [
|
|
|
104
105
|
},
|
|
105
106
|
];
|
|
106
107
|
|
|
108
|
+
const moduleTools = createModuleToolRegistry();
|
|
109
|
+
|
|
110
|
+
async function discoverableTools() {
|
|
111
|
+
const sites = listResolvableSiteConfigs();
|
|
112
|
+
const identity = getRequestIdentity();
|
|
113
|
+
const governed = await filterDiscoverableTools(allDefinitions, sites);
|
|
114
|
+
return [
|
|
115
|
+
...filterToolsByPrincipal(governed, sites, identity),
|
|
116
|
+
...await moduleTools.list({ sites, identity }),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
/**
|
|
108
121
|
* Resolve a resource URI to its JSON payload. URIs are matched in order; the
|
|
109
122
|
* templated forms (content-types, security-policy) capture the site name and
|
|
@@ -114,13 +127,6 @@ const RESOURCES = [
|
|
|
114
127
|
* @returns {Promise<object>} The resource data (later JSON-serialized).
|
|
115
128
|
* @throws {Error} If the URI matches no known resource.
|
|
116
129
|
*/
|
|
117
|
-
async function discoverableTools() {
|
|
118
|
-
const sites = listResolvableSiteConfigs();
|
|
119
|
-
const identity = getRequestIdentity();
|
|
120
|
-
const governed = await filterDiscoverableTools(allDefinitions, sites);
|
|
121
|
-
return filterToolsByPrincipal(governed, sites, identity);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
130
|
async function readResource(uri) {
|
|
125
131
|
const identity = getRequestIdentity();
|
|
126
132
|
const sites = listResolvableSiteConfigs();
|
|
@@ -295,7 +301,9 @@ const buildConnectorServer = createConnectorServerFactory({
|
|
|
295
301
|
tools: {
|
|
296
302
|
definitions: allDefinitions,
|
|
297
303
|
list: discoverableTools,
|
|
298
|
-
call:
|
|
304
|
+
call: (name, args, context) => isModuleTool(name)
|
|
305
|
+
? moduleTools.call(name, args, context)
|
|
306
|
+
: callTool(name, args, context),
|
|
299
307
|
},
|
|
300
308
|
resources: {
|
|
301
309
|
definitions: RESOURCES,
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
* @property {"full"|"enum"|"none"} sort Server-side sort support: arbitrary
|
|
24
24
|
* fields ("full"), a fixed enum of keys ("enum"), or none ("none").
|
|
25
25
|
* @property {boolean} revisions Adapter exposes entity revisions.
|
|
26
|
+
* @property {boolean} [sentinelDraft] Adapter can issue Sentinel's JSON:API
|
|
27
|
+
* `/mcp-draft` and `/mcp-translations` routes (`rawQuery({ path, options })`
|
|
28
|
+
* plus `resourcePath`). GraphQL `rawQuery({ query })` must be false.
|
|
26
29
|
* @property {((entityType: string, bundle: string) => string[])|null} fieldAvailability
|
|
27
30
|
* Optional resolver returning the known field names for a bundle, or null
|
|
28
31
|
* when the adapter cannot cheaply enumerate fields.
|
|
@@ -65,6 +65,7 @@ export class GraphqlBackend extends Backend {
|
|
|
65
65
|
return {
|
|
66
66
|
read: true, write: false, delete: false,
|
|
67
67
|
count: false, filter: false, sort: "enum", revisions: false,
|
|
68
|
+
sentinelDraft: false,
|
|
68
69
|
fieldAvailability: (entityType, bundle) => this._fieldNames(entityType, bundle),
|
|
69
70
|
};
|
|
70
71
|
}
|
package/src/lib/dispatch.js
CHANGED
|
@@ -18,6 +18,7 @@ import { inferOperation } from "./operations.js";
|
|
|
18
18
|
import { assertSourceGovernance, GovernanceError, GOVERNANCE_DIAGNOSTIC_TOOLS } from "./governance.js";
|
|
19
19
|
import {
|
|
20
20
|
assertPrincipalEntitlement, callerTargetHints, getRequestIdentity,
|
|
21
|
+
principalHasScope, resolveAuthoritativeTarget,
|
|
21
22
|
} from "./principal.js";
|
|
22
23
|
import { assertExplicitSiteForWrite, withResolvedTarget } from "./site-target.js";
|
|
23
24
|
import { buildDataFlowContext, consumeBudgetIfEnforced, runWithDataFlow } from "./data-flow.js";
|
|
@@ -62,6 +63,15 @@ export function resolveCallTarget(toolName, rawArgs, context = {}) {
|
|
|
62
63
|
|
|
63
64
|
const identity = context.identity !== undefined ? context.identity : getRequestIdentity();
|
|
64
65
|
if (identity) {
|
|
66
|
+
if (context.moduleTool) {
|
|
67
|
+
if (!principalHasScope(identity, context.moduleTool.scope)) {
|
|
68
|
+
throw new SecurityError("Not entitled to invoke this module tool.");
|
|
69
|
+
}
|
|
70
|
+
return resolveAuthoritativeTarget(rawArgs, identity,
|
|
71
|
+
context.sites ?? listResolvableSiteConfigs(), {
|
|
72
|
+
grants: context.grants, defaultSite: context.defaultSite,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
65
75
|
return assertPrincipalEntitlement({
|
|
66
76
|
toolName,
|
|
67
77
|
args: rawArgs,
|