drupal-mcp-connector 2.10.1 → 2.12.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 +66 -0
- package/bin/drupal-mcp-edge.js +17 -3
- package/config/config.example.json +12 -0
- package/package.json +1 -1
- package/src/lib/backends/jsonapi.js +32 -3
- package/src/lib/config.js +64 -0
- package/src/lib/http-auth.js +5 -1
- package/src/lib/policy-enforcement.js +268 -0
- package/src/lib/policy-promotion.js +130 -0
- package/src/lib/principal.js +159 -4
- package/src/lib/relay/agent.js +44 -0
- package/src/lib/relay/edge.js +426 -37
- package/src/lib/relay/frames.js +37 -9
- package/src/tools/site.js +1 -1
package/src/lib/relay/frames.js
CHANGED
|
@@ -21,6 +21,8 @@ export const FRAME_TYPES = Object.freeze([
|
|
|
21
21
|
"denied",
|
|
22
22
|
"mcp-request",
|
|
23
23
|
"mcp-response",
|
|
24
|
+
"policy-bundle",
|
|
25
|
+
"policy-bundle-ack",
|
|
24
26
|
]);
|
|
25
27
|
|
|
26
28
|
const FRAME_TYPE_SET = new Set(FRAME_TYPES);
|
|
@@ -118,13 +120,25 @@ export function writeFrame(socket, frame) {
|
|
|
118
120
|
/**
|
|
119
121
|
* Correlate mcp-request ids with their mcp-response frames.
|
|
120
122
|
*
|
|
123
|
+
* Optional `owner` binds a tracked id to one tenant agent. A response from a
|
|
124
|
+
* different owner is ignored (cross-tenant injection cannot complete the
|
|
125
|
+
* wait). Disconnecting one owner rejects only that owner's pending ids.
|
|
126
|
+
*
|
|
121
127
|
* @param {object} [options]
|
|
122
128
|
* @param {number} [options.timeoutMs] Per-request wait bound.
|
|
123
|
-
* @returns {
|
|
129
|
+
* @returns {object}
|
|
124
130
|
*/
|
|
125
131
|
export function createRequestBroker({ timeoutMs = 10_000 } = {}) {
|
|
126
132
|
const pending = new Map();
|
|
127
133
|
|
|
134
|
+
function drop(id, apply) {
|
|
135
|
+
const waiter = pending.get(id);
|
|
136
|
+
if (!waiter) return false;
|
|
137
|
+
pending.delete(id);
|
|
138
|
+
apply(waiter);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
return {
|
|
129
143
|
get size() {
|
|
130
144
|
return pending.size;
|
|
@@ -132,9 +146,10 @@ export function createRequestBroker({ timeoutMs = 10_000 } = {}) {
|
|
|
132
146
|
|
|
133
147
|
/**
|
|
134
148
|
* @param {string} id
|
|
149
|
+
* @param {{owner?: string|null}} [options]
|
|
135
150
|
* @returns {Promise<object>} Resolves with the matching response frame.
|
|
136
151
|
*/
|
|
137
|
-
track(id) {
|
|
152
|
+
track(id, { owner = null } = {}) {
|
|
138
153
|
return new Promise((resolve, reject) => {
|
|
139
154
|
const timer = setTimeout(() => {
|
|
140
155
|
pending.delete(id);
|
|
@@ -142,6 +157,7 @@ export function createRequestBroker({ timeoutMs = 10_000 } = {}) {
|
|
|
142
157
|
}, timeoutMs);
|
|
143
158
|
timer.unref?.();
|
|
144
159
|
pending.set(id, {
|
|
160
|
+
owner,
|
|
145
161
|
resolve(frame) {
|
|
146
162
|
clearTimeout(timer);
|
|
147
163
|
resolve(frame);
|
|
@@ -156,24 +172,36 @@ export function createRequestBroker({ timeoutMs = 10_000 } = {}) {
|
|
|
156
172
|
|
|
157
173
|
/**
|
|
158
174
|
* @param {{id?: string}} frame
|
|
175
|
+
* @param {{owner?: string|null}} [options]
|
|
159
176
|
* @returns {boolean} True when a tracked request was resolved.
|
|
160
177
|
*/
|
|
161
|
-
settle(frame) {
|
|
178
|
+
settle(frame, { owner = null } = {}) {
|
|
162
179
|
if (typeof frame?.id !== "string") return false;
|
|
163
180
|
const waiter = pending.get(frame.id);
|
|
164
181
|
if (!waiter) return false;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
182
|
+
if (typeof waiter.owner === "string" && waiter.owner !== owner) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return drop(frame.id, (entry) => entry.resolve(frame));
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {string} owner
|
|
190
|
+
* @param {Error} error
|
|
191
|
+
*/
|
|
192
|
+
rejectByOwner(owner, error) {
|
|
193
|
+
for (const [id, waiter] of [...pending]) {
|
|
194
|
+
if (waiter.owner !== owner) continue;
|
|
195
|
+
drop(id, (entry) => entry.reject(error));
|
|
196
|
+
}
|
|
168
197
|
},
|
|
169
198
|
|
|
170
199
|
/**
|
|
171
200
|
* @param {Error} error
|
|
172
201
|
*/
|
|
173
202
|
rejectAll(error) {
|
|
174
|
-
for (const [id
|
|
175
|
-
|
|
176
|
-
waiter.reject(error);
|
|
203
|
+
for (const [id] of [...pending]) {
|
|
204
|
+
drop(id, (entry) => entry.reject(error));
|
|
177
205
|
}
|
|
178
206
|
},
|
|
179
207
|
};
|
package/src/tools/site.js
CHANGED
|
@@ -47,7 +47,7 @@ async function listContentTypes({ site: siteName }) {
|
|
|
47
47
|
* List named sites this principal may address. No backend call and no credentials.
|
|
48
48
|
* `sites` stays a name list for compatibility; `targets` is the authoritative
|
|
49
49
|
* resolved-target record.
|
|
50
|
-
* @returns {Promise<{sites: string[], targets: Array<object>}>}
|
|
50
|
+
* @returns {Promise<{sites: string[], targets: Array<object>, tenants: Array<object>}>}
|
|
51
51
|
*/
|
|
52
52
|
async function listConfiguredSites() {
|
|
53
53
|
const names = listSiteNames();
|