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 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.1",
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",
@@ -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; with the default global fetch the behavior is unchanged.
612
- const jwks = createRemoteJWKSet(new URL(asMeta.jwks_uri), { [customFetch]: fetchFn });
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 res = await backend.rawQuery({ path: `/jsonapi/${entityType}/${type}/${id}` });
101
- const data = res?.data;
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 = data.attributes?.langcode ?? null;
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: data.id,
108
+ id: entity.id,
110
109
  entityType,
111
110
  bundle: type,
112
111
  defaultLangcode,