drupal-mcp-connector 2.6.0 → 2.7.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/.claude/commands/drupal-list-sites.md +2 -2
- package/CHANGELOG.md +63 -0
- package/README.md +3 -1
- package/config/config.example.json +16 -1
- package/package.json +4 -3
- package/src/index.js +108 -21
- package/src/lib/config.js +42 -1
- package/src/lib/dispatch.js +36 -10
- package/src/lib/http-auth.js +541 -3
- package/src/lib/http-handler.js +62 -9
- package/src/lib/load-secrets.js +158 -0
- package/src/lib/mcp-server.js +32 -6
- package/src/lib/principal.js +372 -0
- package/src/lib/verify.js +40 -4
- package/src/tools/config.js +6 -0
- package/src/tools/site.js +51 -6
package/src/tools/config.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getSiteConfig } from "../lib/config.js";
|
|
15
|
+
import { describeTarget, getRequestIdentity } from "../lib/principal.js";
|
|
15
16
|
import {
|
|
16
17
|
resolveSecurityConfig,
|
|
17
18
|
getSecuritySummary,
|
|
@@ -117,8 +118,13 @@ async function whoami({ site: siteName }) {
|
|
|
117
118
|
// When no OAuth scopes are configured, hasScope() is a no-op (preset-only).
|
|
118
119
|
const canWrite = !sec.readOnly && hasScope(site, "mcp_write");
|
|
119
120
|
const canConfig = hasScope(site, "mcp_config");
|
|
121
|
+
const identity = getRequestIdentity();
|
|
120
122
|
return {
|
|
121
123
|
site: site._name,
|
|
124
|
+
target: describeTarget(site, siteName ? "hint" : "default"),
|
|
125
|
+
principal: identity
|
|
126
|
+
? { sub: identity.sub, clientId: identity.clientId, scopes: [...(identity.scopes ?? [])] }
|
|
127
|
+
: null,
|
|
122
128
|
tier: inferTier(site, sec),
|
|
123
129
|
preset: summary.preset,
|
|
124
130
|
scopes: site.oauth?.scopes ?? [],
|
package/src/tools/site.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { getSiteConfig, listSiteNames } from "../lib/config.js";
|
|
10
10
|
import { governanceStatus } from "../lib/governance.js";
|
|
11
11
|
import { resolveBackend } from "../lib/backends/index.js";
|
|
12
|
+
import { getRequestIdentity, resolveGrantedSiteNames, visibleSiteTargets } from "../lib/principal.js";
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Implementations
|
|
@@ -43,11 +44,21 @@ async function listContentTypes({ site: siteName }) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* List
|
|
47
|
-
*
|
|
47
|
+
* List named sites this principal may address. No backend call and no credentials.
|
|
48
|
+
* `sites` stays a name list for compatibility; `targets` is the authoritative
|
|
49
|
+
* resolved-target record.
|
|
50
|
+
* @returns {Promise<{sites: string[], targets: Array<object>}>}
|
|
48
51
|
*/
|
|
49
52
|
async function listConfiguredSites() {
|
|
50
|
-
|
|
53
|
+
const names = listSiteNames();
|
|
54
|
+
const resolvable = names.flatMap((name) => {
|
|
55
|
+
try {
|
|
56
|
+
return [getSiteConfig(name)];
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return visibleSiteTargets(getRequestIdentity(), resolvable, names);
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
/**
|
|
@@ -58,9 +69,41 @@ async function listConfiguredSites() {
|
|
|
58
69
|
* @param {object} args - { site? } (a named site narrows the report).
|
|
59
70
|
* @returns {Promise<{sites: object[]}>} required/ok/reason per site.
|
|
60
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* Classify a getSiteConfig failure so the diagnostic reason matches the cause.
|
|
74
|
+
* @param {string} message
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function classifySiteResolutionFailure(message) {
|
|
78
|
+
if (message.includes("Unknown site:")) return "unknown_site";
|
|
79
|
+
if (message.includes("baseUrl is not HTTPS")) return "insecure_base_url";
|
|
80
|
+
if (message.includes("is not set in the environment")) return "credential_unresolved";
|
|
81
|
+
return "site_unresolved";
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
async function getGovernanceStatus({ site: siteName } = {}) {
|
|
62
|
-
const
|
|
63
|
-
|
|
85
|
+
const identity = getRequestIdentity();
|
|
86
|
+
const configured = listSiteNames();
|
|
87
|
+
const allowed = identity ? resolveGrantedSiteNames(identity, configured) : configured;
|
|
88
|
+
const names = siteName ? [siteName] : allowed;
|
|
89
|
+
const resolved = [];
|
|
90
|
+
const unresolved = [];
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
try {
|
|
93
|
+
resolved.push(getSiteConfig(name));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const detail = error instanceof Error ? error.message : "site could not be resolved";
|
|
96
|
+
unresolved.push({
|
|
97
|
+
site: name,
|
|
98
|
+
required: null,
|
|
99
|
+
ok: false,
|
|
100
|
+
reason: classifySiteResolutionFailure(detail),
|
|
101
|
+
detail,
|
|
102
|
+
checkedAt: null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { sites: [...unresolved, ...(await governanceStatus(resolved))] };
|
|
64
107
|
}
|
|
65
108
|
|
|
66
109
|
// ---------------------------------------------------------------------------
|
|
@@ -94,7 +137,7 @@ export const definitions = [
|
|
|
94
137
|
},
|
|
95
138
|
{
|
|
96
139
|
name: "drupal_list_sites",
|
|
97
|
-
description: "List
|
|
140
|
+
description: "List the Drupal sites this principal may address. Each target includes the authoritative site name and base URL.",
|
|
98
141
|
inputSchema: {
|
|
99
142
|
type: "object",
|
|
100
143
|
properties: {},
|
|
@@ -102,6 +145,8 @@ export const definitions = [
|
|
|
102
145
|
},
|
|
103
146
|
];
|
|
104
147
|
|
|
148
|
+
export { getGovernanceStatus };
|
|
149
|
+
|
|
105
150
|
export const handlers = {
|
|
106
151
|
drupal_site_info: getSiteInfo,
|
|
107
152
|
drupal_list_content_types: listContentTypes,
|