drupal-mcp-connector 2.23.1 → 2.23.2
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 +13 -0
- package/package.json +1 -1
- package/src/lib/http-auth.js +25 -6
- package/src/tools/translations.js +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.23.2] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Adapter-neutral `drupal_list_translations` fallback.** When Sentinel's
|
|
14
|
+
translation inventory is unavailable, the tool reads `langcode` from
|
|
15
|
+
`backend.getEntity` instead of a JSON:API `rawQuery` path. GraphQL
|
|
16
|
+
backends can report the one observable language; `FALLBACK_NOTE` is
|
|
17
|
+
unchanged.
|
|
18
|
+
- **Inbound IdP HTTP timeouts.** RFC 8414 / OIDC discovery, JWKS retrieval,
|
|
19
|
+
and RFC 7662 introspection abort after 30s (`AbortSignal.timeout`), matching
|
|
20
|
+
outbound Drupal HTTP. A hung issuer fails closed (discovery throw on HTTPS
|
|
21
|
+
bring-up; 401 on `/mcp` auth) instead of stalling the process.
|
|
22
|
+
|
|
10
23
|
## [2.23.1] - 2026-09-23
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.2",
|
|
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",
|
package/src/lib/http-auth.js
CHANGED
|
@@ -6,13 +6,29 @@
|
|
|
6
6
|
* - OAuth resource server (`createResourceAuthenticator`) — network-facing
|
|
7
7
|
* HTTPS. JWT via issuer discovery + JWKS, optional RFC 7662 introspection,
|
|
8
8
|
* and a hot-reloaded revocation file. Caller-supplied identity headers
|
|
9
|
-
* never become the principal.
|
|
9
|
+
* never become the principal. IdP HTTP (discovery, JWKS, introspection)
|
|
10
|
+
* aborts after `INBOUND_IDP_TIMEOUT_MS` (30s).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { timingSafeEqual } from "crypto";
|
|
13
14
|
import { readFileSync, statSync, writeFileSync } from "fs";
|
|
14
15
|
import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
|
|
15
16
|
|
|
17
|
+
/** Inbound IdP HTTP timeout. Matches Drupal/Drush outbound HTTP. */
|
|
18
|
+
export const INBOUND_IDP_TIMEOUT_MS = 30_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Attach the inbound IdP abort timeout. A caller `signal` wins.
|
|
22
|
+
* @param {RequestInit} [options]
|
|
23
|
+
* @returns {RequestInit}
|
|
24
|
+
*/
|
|
25
|
+
function withInboundTimeout(options = {}) {
|
|
26
|
+
return {
|
|
27
|
+
...options,
|
|
28
|
+
signal: options.signal ?? AbortSignal.timeout(INBOUND_IDP_TIMEOUT_MS),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
/** Header names that must never become identity. */
|
|
17
33
|
export const SPOOFABLE_IDENTITY_HEADERS = Object.freeze([
|
|
18
34
|
"x-mcp-subject",
|
|
@@ -361,7 +377,7 @@ export async function discoverAuthorizationServer(issuer, fetchFn = fetch) {
|
|
|
361
377
|
for (const url of authorizationServerDiscoveryUrls(issuer)) {
|
|
362
378
|
let res;
|
|
363
379
|
try {
|
|
364
|
-
res = await fetchFn(url, { headers: { accept: "application/json" } });
|
|
380
|
+
res = await fetchFn(url, withInboundTimeout({ headers: { accept: "application/json" } }));
|
|
365
381
|
} catch {
|
|
366
382
|
continue;
|
|
367
383
|
}
|
|
@@ -393,7 +409,7 @@ export async function introspectToken(token, {
|
|
|
393
409
|
fetchFn = fetch,
|
|
394
410
|
}) {
|
|
395
411
|
const creds = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
396
|
-
const res = await fetchFn(url, {
|
|
412
|
+
const res = await fetchFn(url, withInboundTimeout({
|
|
397
413
|
method: "POST",
|
|
398
414
|
headers: {
|
|
399
415
|
authorization: `Basic ${creds}`,
|
|
@@ -401,7 +417,7 @@ export async function introspectToken(token, {
|
|
|
401
417
|
accept: "application/json",
|
|
402
418
|
},
|
|
403
419
|
body: new URLSearchParams({ token }).toString(),
|
|
404
|
-
});
|
|
420
|
+
}));
|
|
405
421
|
if (!res.ok) return null;
|
|
406
422
|
const body = await res.json();
|
|
407
423
|
if (body.active !== true) return null;
|
|
@@ -608,8 +624,11 @@ export async function createInboundHttpsAuth({ inboundCfg, fetchFn = fetch }) {
|
|
|
608
624
|
const asMeta = await discoverAuthorizationServer(issuer, fetchFn);
|
|
609
625
|
const advertisedIssuer = asMeta.issuer || issuer;
|
|
610
626
|
// JWKS retrieval goes through the same injectable fetch as issuer
|
|
611
|
-
// discovery
|
|
612
|
-
const jwks = createRemoteJWKSet(new URL(asMeta.jwks_uri), {
|
|
627
|
+
// discovery, with jose's timeoutDuration aligned to the inbound budget.
|
|
628
|
+
const jwks = createRemoteJWKSet(new URL(asMeta.jwks_uri), {
|
|
629
|
+
timeoutDuration: INBOUND_IDP_TIMEOUT_MS,
|
|
630
|
+
[customFetch]: (url, options) => fetchFn(url, withInboundTimeout(options)),
|
|
631
|
+
});
|
|
613
632
|
const resourceMetadataUrl = resourceMetadataUrlFor(resource);
|
|
614
633
|
const revocationStore = inboundCfg.revocationFile
|
|
615
634
|
? createRevocationStore({ filePath: inboundCfg.revocationFile })
|
|
@@ -97,16 +97,15 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
if (!data) return null;
|
|
100
|
+
const entity = await backend.getEntity({ entityType, bundle: type, id });
|
|
101
|
+
if (!entity) return null;
|
|
103
102
|
|
|
104
|
-
const defaultLangcode =
|
|
103
|
+
const defaultLangcode = entity.langcode ?? null;
|
|
105
104
|
const langcodes = defaultLangcode ? [defaultLangcode] : [];
|
|
106
105
|
const translations = langcodes.map((lc) => ({ langcode: lc, default: lc === defaultLangcode }));
|
|
107
106
|
|
|
108
107
|
return {
|
|
109
|
-
id:
|
|
108
|
+
id: entity.id,
|
|
110
109
|
entityType,
|
|
111
110
|
bundle: type,
|
|
112
111
|
defaultLangcode,
|