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.
@@ -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 all named sites from config.json. No backend call and no credentials.
47
- * @returns {Promise<{sites: string[]}>}
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
- return { sites: listSiteNames() };
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 names = siteName ? [siteName] : listSiteNames();
63
- return { sites: await governanceStatus(names.map((n) => getSiteConfig(n))) };
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 all named Drupal sites configured in config.json. Useful for multi-site setups.",
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,