@productbrain/mcp 0.0.1-beta.191 → 0.0.1-beta.1913

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/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli/index.ts
4
4
  var subcommand = process.argv[2];
5
5
  if (subcommand === "setup") {
6
- const { runSetup } = await import("../setup-RYYXRDPB.js");
6
+ const { runSetup } = await import("../setup-XWKTPL7V.js");
7
7
  await runSetup();
8
8
  } else {
9
9
  await import("../index.js");
package/dist/http.js CHANGED
@@ -1,18 +1,18 @@
1
1
  import {
2
- DEFAULT_CLOUD_URL,
3
2
  SERVER_VERSION,
4
- bootstrapHttp,
5
3
  createProductBrainServer,
6
- getKeyState,
7
- hashKey,
8
- initFeatureFlags,
9
- runWithAuth
10
- } from "./chunk-GJFGULJV.js";
4
+ initFeatureFlags
5
+ } from "./chunk-ZKW7JZUF.js";
11
6
  import {
7
+ DEFAULT_CLOUD_URL,
8
+ bootstrapHttp,
9
+ getKeyState,
12
10
  getPostHogClient,
11
+ hashKey,
13
12
  initAnalytics,
13
+ runWithAuth,
14
14
  shutdownAnalytics
15
- } from "./chunk-YMF3IQ5E.js";
15
+ } from "./chunk-6JNK5DIZ.js";
16
16
 
17
17
  // src/http.ts
18
18
  import { createHash, randomUUID as randomUUID2 } from "crypto";
@@ -111,6 +111,166 @@ function verifyRefreshToken(token) {
111
111
  return { apiKey: payload.k };
112
112
  }
113
113
 
114
+ // src/sessionCap.ts
115
+ function planKeyAdmission(sessions2, keyHash, cap, now, opts) {
116
+ if (cap < 1) {
117
+ throw new RangeError(`cap must be >= 1, got ${cap}`);
118
+ }
119
+ const { ttlMs, graceMs } = opts;
120
+ const isProtected = (s) => s.inFlight > 0 || now - s.lastAccess < graceMs;
121
+ const keyEntries = [];
122
+ for (const [id, session] of sessions2) {
123
+ if (session.keyHash === keyHash) {
124
+ keyEntries.push([id, session]);
125
+ }
126
+ }
127
+ const evictSet = /* @__PURE__ */ new Set();
128
+ for (const [id, session] of keyEntries) {
129
+ if (!isProtected(session) && now - session.lastAccess >= ttlMs) {
130
+ evictSet.add(id);
131
+ }
132
+ }
133
+ let survivors = keyEntries.filter(([id]) => !evictSet.has(id));
134
+ if (survivors.length >= cap) {
135
+ const idleSurvivors = survivors.filter(([, s]) => !isProtected(s)).sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
136
+ for (const [id] of idleSurvivors) {
137
+ if (survivors.length < cap) break;
138
+ evictSet.add(id);
139
+ survivors = survivors.filter(([sid]) => sid !== id);
140
+ }
141
+ }
142
+ const evict = Array.from(evictSet);
143
+ const admit = survivors.length < cap;
144
+ let retryAfterSeconds = 0;
145
+ let retryIsPoll = false;
146
+ if (!admit) {
147
+ const graceBlockers = survivors.filter(([, s]) => s.inFlight === 0);
148
+ if (graceBlockers.length > 0) {
149
+ const minLastAccess = Math.min(...graceBlockers.map(([, s]) => s.lastAccess));
150
+ const msRemaining = graceMs - (now - minLastAccess);
151
+ retryAfterSeconds = Math.max(1, Math.ceil(msRemaining / 1e3));
152
+ } else {
153
+ retryAfterSeconds = 5;
154
+ retryIsPoll = true;
155
+ }
156
+ }
157
+ return { evict, admit, retryAfterSeconds, retryIsPoll };
158
+ }
159
+ function countKeySessions(sessions2, keyHash) {
160
+ let count = 0;
161
+ for (const session of sessions2.values()) {
162
+ if (session.keyHash === keyHash) count++;
163
+ }
164
+ return count;
165
+ }
166
+
167
+ // src/httpErrors.ts
168
+ function clampSeconds(s) {
169
+ return Math.max(0, Math.round(s));
170
+ }
171
+ function build(reason, message, p) {
172
+ const seconds = clampSeconds(p.retryAfterSeconds);
173
+ return {
174
+ status: 429,
175
+ headers: { "Retry-After": String(seconds) },
176
+ body: {
177
+ jsonrpc: "2.0",
178
+ id: p.id ?? null,
179
+ error: {
180
+ code: -32e3,
181
+ message,
182
+ data: {
183
+ reason,
184
+ retryAfterSeconds: seconds,
185
+ limit: p.limit,
186
+ current: p.current
187
+ }
188
+ }
189
+ }
190
+ };
191
+ }
192
+ function buildRateLimitError(p) {
193
+ const seconds = clampSeconds(p.retryAfterSeconds);
194
+ return build(
195
+ "rate_limited",
196
+ `Too many requests \u2014 retry after ${seconds} s.`,
197
+ p
198
+ );
199
+ }
200
+ function buildAuthLockoutError(p) {
201
+ const seconds = clampSeconds(p.retryAfterSeconds);
202
+ return build(
203
+ "auth_lockout",
204
+ `Too many failed auth attempts \u2014 locked out, retry after ${seconds} s.`,
205
+ p
206
+ );
207
+ }
208
+ function buildSessionCapError(p) {
209
+ const seconds = clampSeconds(p.retryAfterSeconds);
210
+ const message = p.poll ? "Server busy \u2014 all sessions active; retry shortly." : `Too many active sessions for this key \u2014 retry after ${seconds} s.`;
211
+ return build("session_cap", message, p);
212
+ }
213
+ function send429(res, built) {
214
+ if (res.headersSent) return;
215
+ for (const [k, v] of Object.entries(built.headers)) {
216
+ res.setHeader(k, v);
217
+ }
218
+ res.status(built.status).json(built.body);
219
+ }
220
+
221
+ // src/authLockout.ts
222
+ var AUTH_FAILURE_MAX = 10;
223
+ var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
224
+ var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
225
+ var MAX_AUTH_FAILURE_ENTRIES = 1e4;
226
+ function resolveBearerAuth(authorizationHeader, accessTokens2, now, accessTokenTtlMs) {
227
+ if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
228
+ return { status: "absent" };
229
+ }
230
+ if (!authorizationHeader.startsWith("Bearer ")) {
231
+ return { status: "rejected" };
232
+ }
233
+ const token = authorizationHeader.slice(7).trim();
234
+ if (token === "") {
235
+ return { status: "rejected" };
236
+ }
237
+ if (token.startsWith("pb_sk_")) {
238
+ return { status: "ok", apiKey: token };
239
+ }
240
+ if (token.startsWith("pb_at_")) {
241
+ const entry = accessTokens2.get(token);
242
+ if (!entry) return { status: "rejected" };
243
+ if (now - entry.createdAt > accessTokenTtlMs) {
244
+ accessTokens2.delete(token);
245
+ return { status: "rejected" };
246
+ }
247
+ return { status: "ok", apiKey: entry.apiKey };
248
+ }
249
+ return { status: "rejected" };
250
+ }
251
+ function checkAuthBlock(failures, ip, now) {
252
+ const rec = failures.get(ip);
253
+ if (!rec) return false;
254
+ return rec.blockedUntil > now;
255
+ }
256
+ function recordAuthFailure(failures, ip, now) {
257
+ const rec = failures.get(ip);
258
+ if (!rec) {
259
+ failures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
260
+ return;
261
+ }
262
+ if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
263
+ rec.count = 1;
264
+ rec.firstFailure = now;
265
+ rec.blockedUntil = 0;
266
+ } else {
267
+ rec.count++;
268
+ if (rec.count >= AUTH_FAILURE_MAX) {
269
+ rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;
270
+ }
271
+ }
272
+ }
273
+
114
274
  // src/http.ts
115
275
  bootstrapHttp();
116
276
  initAnalytics();
@@ -135,7 +295,7 @@ app.use((_req, res, next) => {
135
295
  "Access-Control-Allow-Headers",
136
296
  "Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id"
137
297
  );
138
- res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
298
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, Retry-After");
139
299
  if (_req.method === "OPTIONS") {
140
300
  res.status(204).end();
141
301
  return;
@@ -985,85 +1145,63 @@ var mcpLimiter = rateLimit({
985
1145
  max: 120,
986
1146
  standardHeaders: true,
987
1147
  legacyHeaders: false,
988
- message: { error: "Too many requests. Try again later." }
1148
+ handler: (req, res) => {
1149
+ const r = req.rateLimit;
1150
+ const reset = r?.resetTime?.getTime?.() ?? Date.now() + 6e4;
1151
+ send429(res, buildRateLimitError({
1152
+ retryAfterSeconds: Math.max(1, Math.ceil((reset - Date.now()) / 1e3)),
1153
+ limit: r?.limit ?? 120,
1154
+ current: r?.used ?? 0,
1155
+ id: req.body?.id ?? null
1156
+ // mirror the JSON-RPC id when a POST body is present
1157
+ }));
1158
+ }
989
1159
  });
990
1160
  var authFailures = /* @__PURE__ */ new Map();
991
- var AUTH_FAILURE_MAX = 10;
992
- var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
993
- var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
994
- var MAX_AUTH_FAILURE_ENTRIES = 1e4;
995
- function checkAuthBlock(ip) {
996
- const rec = authFailures.get(ip);
997
- if (!rec) return false;
998
- return rec.blockedUntil > Date.now();
999
- }
1000
- function recordAuthFailure(ip) {
1001
- const now = Date.now();
1002
- const rec = authFailures.get(ip);
1003
- if (!rec) {
1004
- authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
1005
- return;
1006
- }
1007
- if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
1008
- rec.count = 1;
1009
- rec.firstFailure = now;
1010
- rec.blockedUntil = 0;
1011
- } else {
1012
- rec.count++;
1013
- if (rec.count >= AUTH_FAILURE_MAX) {
1014
- rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;
1015
- }
1016
- }
1017
- }
1018
1161
  app.get("/health", (_req, res) => {
1019
1162
  res.json({ status: "ok", version: SERVER_VERSION, transport: "http" });
1020
1163
  });
1021
1164
  var sessions = /* @__PURE__ */ new Map();
1022
1165
  var SESSION_TTL_MS = 30 * 60 * 1e3;
1023
1166
  var MAX_SESSIONS = 200;
1024
- var MAX_SESSIONS_PER_KEY = 5;
1167
+ var EVICT_GRACE_MS = 6e4;
1168
+ var MAX_SESSIONS_PER_KEY = Math.max(
1169
+ 1,
1170
+ Math.min(MAX_SESSIONS - 1, Number(process.env.MCP_MAX_SESSIONS_PER_KEY) || 25)
1171
+ );
1025
1172
  function evictStaleSessions() {
1026
1173
  const now = Date.now();
1027
1174
  for (const [id, entry] of sessions) {
1028
1175
  if (now - entry.lastAccess > SESSION_TTL_MS) {
1029
- logSessionLifecycle("session_deleted", id, "ttl");
1176
+ logSessionLifecycle(
1177
+ "session_deleted",
1178
+ id,
1179
+ entry.inFlight > 0 ? "inflight_leak" : "ttl"
1180
+ );
1030
1181
  entry.transport.close().catch(() => {
1031
1182
  });
1032
1183
  sessions.delete(id);
1033
1184
  }
1034
1185
  }
1035
1186
  if (sessions.size > MAX_SESSIONS) {
1036
- const sorted = [...sessions.entries()].sort(
1037
- (a, b) => a[1].lastAccess - b[1].lastAccess
1038
- );
1039
- for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {
1187
+ const sorted = [...sessions.entries()].filter(([, e]) => e.inFlight === 0).sort((a, b) => a[1].lastAccess - b[1].lastAccess);
1188
+ const overflow = sessions.size - MAX_SESSIONS;
1189
+ for (let i = 0; i < Math.min(overflow, sorted.length); i++) {
1040
1190
  logSessionLifecycle("session_deleted", sorted[i][0], "eviction");
1041
1191
  sorted[i][1].transport.close().catch(() => {
1042
1192
  });
1043
1193
  sessions.delete(sorted[i][0]);
1044
1194
  }
1045
- }
1046
- }
1047
- setInterval(evictStaleSessions, 6e4);
1048
- function extractBearerKey(req) {
1049
- const header = req.headers?.authorization;
1050
- if (typeof header !== "string" || !header.startsWith("Bearer ")) return null;
1051
- const token = header.slice(7).trim();
1052
- if (token.startsWith("pb_sk_")) {
1053
- return token;
1054
- }
1055
- if (token.startsWith("pb_at_")) {
1056
- const entry = accessTokens.get(token);
1057
- if (!entry) return null;
1058
- const now = Date.now();
1059
- if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {
1060
- accessTokens.delete(token);
1061
- return null;
1195
+ if (sessions.size > MAX_SESSIONS) {
1196
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1197
+ process.stderr.write(
1198
+ `[HTTP] ${ts} session_cap_breach size=${sessions.size} max=${MAX_SESSIONS} reason=all_inflight (no idle sessions to evict; TTL backstop will reclaim)
1199
+ `
1200
+ );
1062
1201
  }
1063
- return entry.apiKey;
1064
1202
  }
1065
- return null;
1066
1203
  }
1204
+ setInterval(evictStaleSessions, 6e4);
1067
1205
  function send401(req, res) {
1068
1206
  const base = baseUrl(req);
1069
1207
  res.status(401).set(
@@ -1086,17 +1224,21 @@ function logSessionLifecycle(event, sessionId, reason) {
1086
1224
  }
1087
1225
  app.post("/mcp", mcpLimiter, async (req, res) => {
1088
1226
  const reqIp = req.ip ?? "unknown";
1089
- if (checkAuthBlock(reqIp)) {
1090
- res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
1227
+ const nowTs = Date.now();
1228
+ if (checkAuthBlock(authFailures, reqIp, nowTs)) {
1229
+ const rec = authFailures.get(reqIp);
1230
+ const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1e3)) : 0;
1231
+ send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1091
1232
  return;
1092
1233
  }
1093
- const apiKey = extractBearerKey(req);
1094
- if (!apiKey) {
1234
+ const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1235
+ if (auth.status !== "ok") {
1095
1236
  logRequest("POST", "auth_fail");
1096
- recordAuthFailure(reqIp);
1237
+ if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1097
1238
  send401(req, res);
1098
1239
  return;
1099
1240
  }
1241
+ const apiKey = auth.apiKey;
1100
1242
  const sessionId = req.headers["mcp-session-id"];
1101
1243
  const reqStart = Date.now();
1102
1244
  try {
@@ -1111,41 +1253,81 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1111
1253
  });
1112
1254
  return;
1113
1255
  }
1114
- entry.lastAccess = Date.now();
1115
- await entry.transport.handleRequest(req, res, req.body);
1116
- logRequest("POST", "ok", sessionId, Date.now() - reqStart);
1256
+ try {
1257
+ entry.inFlight++;
1258
+ entry.lastAccess = Date.now();
1259
+ await entry.transport.handleRequest(req, res, req.body);
1260
+ logRequest("POST", "ok", sessionId, Date.now() - reqStart);
1261
+ } finally {
1262
+ entry.inFlight--;
1263
+ entry.lastAccess = Date.now();
1264
+ }
1117
1265
  } else if (!sessionId && isInitializeRequest(req.body)) {
1118
1266
  const keyH = hashKey(apiKey);
1119
- let keySessionCount = 0;
1120
- for (const entry of sessions.values()) {
1121
- if (entry.keyHash === keyH) keySessionCount++;
1122
- }
1123
- if (keySessionCount >= MAX_SESSIONS_PER_KEY) {
1124
- res.status(429).json({
1125
- jsonrpc: "2.0",
1126
- error: { code: -32e3, message: "Too many sessions for this API key" },
1127
- id: null
1267
+ const plan = planKeyAdmission(
1268
+ sessions,
1269
+ keyH,
1270
+ MAX_SESSIONS_PER_KEY,
1271
+ Date.now(),
1272
+ { ttlMs: SESSION_TTL_MS, graceMs: EVICT_GRACE_MS }
1273
+ );
1274
+ for (const sid2 of plan.evict) {
1275
+ const victim = sessions.get(sid2);
1276
+ sessions.delete(sid2);
1277
+ victim?.transport.close().catch(() => {
1128
1278
  });
1279
+ logSessionLifecycle("session_deleted", sid2, "lru_evict");
1280
+ }
1281
+ if (!plan.admit) {
1282
+ send429(res, buildSessionCapError({
1283
+ retryAfterSeconds: plan.retryAfterSeconds,
1284
+ limit: MAX_SESSIONS_PER_KEY,
1285
+ current: countKeySessions(sessions, keyH),
1286
+ poll: plan.retryIsPoll,
1287
+ id: req.body?.id ?? null
1288
+ }));
1129
1289
  return;
1130
1290
  }
1291
+ const sid = randomUUID2();
1292
+ let initialized = false;
1131
1293
  const transport = new StreamableHTTPServerTransport({
1132
- sessionIdGenerator: () => randomUUID2(),
1133
- onsessioninitialized: (sid) => {
1134
- sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });
1135
- logSessionLifecycle("session_created", sid);
1294
+ sessionIdGenerator: () => sid,
1295
+ onsessioninitialized: (s) => {
1296
+ initialized = true;
1297
+ logSessionLifecycle("session_created", s);
1136
1298
  }
1299
+ // log only — no map write
1137
1300
  });
1301
+ const reserved = { transport, lastAccess: Date.now(), keyHash: keyH, inFlight: 1 };
1302
+ sessions.set(sid, reserved);
1138
1303
  transport.onclose = () => {
1139
- const sid = transport.sessionId;
1140
- if (sid) {
1141
- logSessionLifecycle("session_deleted", sid, "onclose");
1142
- sessions.delete(sid);
1304
+ const s = transport.sessionId ?? sid;
1305
+ if (sessions.has(s)) {
1306
+ sessions.delete(s);
1307
+ logSessionLifecycle("session_deleted", s, "onclose");
1143
1308
  }
1144
1309
  };
1145
- const server = createProductBrainServer();
1146
- await server.connect(transport);
1147
- await transport.handleRequest(req, res, req.body);
1148
- logRequest("POST", "ok", transport.sessionId ?? void 0, Date.now() - reqStart);
1310
+ try {
1311
+ const server = createProductBrainServer();
1312
+ await server.connect(transport);
1313
+ await transport.handleRequest(req, res, req.body);
1314
+ logRequest("POST", "ok", transport.sessionId ?? void 0, Date.now() - reqStart);
1315
+ } catch (e) {
1316
+ transport.onclose = void 0;
1317
+ sessions.delete(sid);
1318
+ throw e;
1319
+ } finally {
1320
+ if (!initialized) {
1321
+ transport.onclose = void 0;
1322
+ if (sessions.delete(sid)) logSessionLifecycle("session_deleted", sid, "init_rejected");
1323
+ } else {
1324
+ const e = sessions.get(sid);
1325
+ if (e) {
1326
+ e.inFlight--;
1327
+ e.lastAccess = Date.now();
1328
+ }
1329
+ }
1330
+ }
1149
1331
  } else if (sessionId) {
1150
1332
  process.stderr.write(
1151
1333
  `[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_stale sessionId=${sessionId} (likely server restart \u2014 instructing client to re-initialise)
@@ -1181,17 +1363,21 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1181
1363
  });
1182
1364
  app.get("/mcp", mcpLimiter, async (req, res) => {
1183
1365
  const reqIp = req.ip ?? "unknown";
1184
- if (checkAuthBlock(reqIp)) {
1185
- res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
1366
+ const nowTs = Date.now();
1367
+ if (checkAuthBlock(authFailures, reqIp, nowTs)) {
1368
+ const rec = authFailures.get(reqIp);
1369
+ const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1e3)) : 0;
1370
+ send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1186
1371
  return;
1187
1372
  }
1188
- const apiKey = extractBearerKey(req);
1189
- if (!apiKey) {
1373
+ const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1374
+ if (auth.status !== "ok") {
1190
1375
  logRequest("GET", "auth_fail");
1191
- recordAuthFailure(reqIp);
1376
+ if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1192
1377
  send401(req, res);
1193
1378
  return;
1194
1379
  }
1380
+ const apiKey = auth.apiKey;
1195
1381
  const sessionId = req.headers["mcp-session-id"];
1196
1382
  if (!sessionId) {
1197
1383
  res.status(400).send("Missing Mcp-Session-Id header");
@@ -1226,17 +1412,21 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
1226
1412
  });
1227
1413
  app.delete("/mcp", mcpLimiter, async (req, res) => {
1228
1414
  const reqIp = req.ip ?? "unknown";
1229
- if (checkAuthBlock(reqIp)) {
1230
- res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
1415
+ const nowTs = Date.now();
1416
+ if (checkAuthBlock(authFailures, reqIp, nowTs)) {
1417
+ const rec = authFailures.get(reqIp);
1418
+ const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1e3)) : 0;
1419
+ send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1231
1420
  return;
1232
1421
  }
1233
- const apiKey = extractBearerKey(req);
1234
- if (!apiKey) {
1422
+ const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1423
+ if (auth.status !== "ok") {
1235
1424
  logRequest("DELETE", "auth_fail");
1236
- recordAuthFailure(reqIp);
1425
+ if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1237
1426
  send401(req, res);
1238
1427
  return;
1239
1428
  }
1429
+ const apiKey = auth.apiKey;
1240
1430
  const sessionId = req.headers["mcp-session-id"];
1241
1431
  if (!sessionId) {
1242
1432
  res.status(400).send("Missing Mcp-Session-Id header");