@productbrain/mcp 0.0.1-beta.16 → 0.0.1-beta.161

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/http.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import {
2
2
  SERVER_VERSION,
3
- createProductBrainServer
4
- } from "./chunk-5V4JXM4G.js";
5
- import {
6
3
  bootstrapHttp,
4
+ createProductBrainServer,
5
+ hashKey,
6
+ initFeatureFlags,
7
7
  runWithAuth
8
- } from "./chunk-47LO6K2R.js";
8
+ } from "./chunk-6E6HZFTX.js";
9
9
  import {
10
+ getPostHogClient,
10
11
  initAnalytics,
11
12
  shutdownAnalytics
12
- } from "./chunk-XBMI6QHR.js";
13
+ } from "./chunk-YMF3IQ5E.js";
13
14
 
14
15
  // src/http.ts
15
16
  import { createHash, randomUUID } from "crypto";
@@ -19,19 +20,21 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
19
20
  import rateLimit from "express-rate-limit";
20
21
  bootstrapHttp();
21
22
  initAnalytics();
22
- var PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "3000", 10);
23
+ initFeatureFlags(getPostHogClient());
24
+ var PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "3002", 10);
23
25
  function baseUrl(req) {
24
26
  const proto = req.headers["x-forwarded-proto"] ?? req.protocol ?? "http";
25
27
  const host = req.headers.host ?? `localhost:${PORT}`;
26
28
  return `${proto}://${host}`;
27
29
  }
28
30
  var app = express();
31
+ app.set("trust proxy", 1);
29
32
  app.use(express.json());
30
33
  var ALLOWED_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean);
31
34
  app.use((_req, res, next) => {
32
35
  const origin = _req.headers.origin;
33
- if (!ALLOWED_ORIGINS || origin && ALLOWED_ORIGINS.includes(origin)) {
34
- res.setHeader("Access-Control-Allow-Origin", origin ?? "*");
36
+ if (ALLOWED_ORIGINS && origin && ALLOWED_ORIGINS.includes(origin)) {
37
+ res.setHeader("Access-Control-Allow-Origin", origin);
35
38
  }
36
39
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
37
40
  res.setHeader(
@@ -62,17 +65,33 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
62
65
  token_endpoint: `${base}/oauth/token`,
63
66
  registration_endpoint: `${base}/register`,
64
67
  response_types_supported: ["code"],
65
- grant_types_supported: ["authorization_code"],
68
+ grant_types_supported: ["authorization_code", "refresh_token"],
66
69
  code_challenge_methods_supported: ["S256"],
67
70
  token_endpoint_auth_methods_supported: ["none"],
68
71
  scopes_supported: ["mcp:tools", "mcp:resources"]
69
72
  });
70
73
  });
74
+ var authLimiter = rateLimit({
75
+ windowMs: 6e4,
76
+ max: 20,
77
+ standardHeaders: true,
78
+ legacyHeaders: false,
79
+ message: { error: "Too many auth requests. Try again later." }
80
+ });
71
81
  var registeredClients = /* @__PURE__ */ new Map();
82
+ var MAX_REGISTERED_CLIENTS = 500;
72
83
  app.post(
73
84
  "/register",
85
+ authLimiter,
74
86
  express.json(),
75
87
  (req, res) => {
88
+ if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {
89
+ res.status(503).json({
90
+ error: "server_error",
91
+ error_description: "Registration limit reached. Try again later."
92
+ });
93
+ return;
94
+ }
76
95
  const { redirect_uris, client_name } = req.body;
77
96
  if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {
78
97
  res.status(400).json({
@@ -100,6 +119,14 @@ app.post(
100
119
  }
101
120
  );
102
121
  var pendingCodes = /* @__PURE__ */ new Map();
122
+ var ACCESS_TOKEN_TTL = 3600;
123
+ var ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1e3;
124
+ var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
125
+ var refreshTokens = /* @__PURE__ */ new Map();
126
+ var MAX_REFRESH_TOKENS = 2e3;
127
+ var MAX_REFRESH_TOKENS_PER_KEY = 20;
128
+ var accessTokens = /* @__PURE__ */ new Map();
129
+ var MAX_ACCESS_TOKENS = 1e3;
103
130
  setInterval(() => {
104
131
  const now = Date.now();
105
132
  for (const [code, auth] of pendingCodes) {
@@ -108,6 +135,29 @@ setInterval(() => {
108
135
  for (const [id, client] of registeredClients) {
109
136
  if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
110
137
  }
138
+ for (const [token, entry] of refreshTokens) {
139
+ if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);
140
+ }
141
+ if (refreshTokens.size > MAX_REFRESH_TOKENS) {
142
+ const sorted = [...refreshTokens.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
143
+ for (let i = 0; i < sorted.length - MAX_REFRESH_TOKENS; i++) {
144
+ refreshTokens.delete(sorted[i][0]);
145
+ }
146
+ }
147
+ for (const [token, entry] of accessTokens) {
148
+ if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
149
+ }
150
+ for (const [ip, rec] of authFailures) {
151
+ if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
152
+ authFailures.delete(ip);
153
+ }
154
+ }
155
+ if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {
156
+ const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);
157
+ for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {
158
+ authFailures.delete(sorted[i][0]);
159
+ }
160
+ }
111
161
  }, 6e4);
112
162
  function esc(s) {
113
163
  return String(s ?? "").replace(
@@ -115,8 +165,8 @@ function esc(s) {
115
165
  (c) => ({ "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" })[c]
116
166
  );
117
167
  }
118
- app.get("/authorize", (req, res) => {
119
- const { redirect_uri, code_challenge, code_challenge_method, state } = req.query;
168
+ app.get("/authorize", authLimiter, (req, res) => {
169
+ const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;
120
170
  res.type("html").send(`<!DOCTYPE html>
121
171
  <html lang="en"><head>
122
172
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -145,6 +195,7 @@ button:hover{background:#6d28d9}
145
195
  <input type="hidden" name="code_challenge" value="${esc(code_challenge)}">
146
196
  <input type="hidden" name="code_challenge_method" value="${esc(code_challenge_method)}">
147
197
  <input type="hidden" name="state" value="${esc(state)}">
198
+ <input type="hidden" name="client_id" value="${esc(client_id)}">
148
199
  <label for="k">API Key</label>
149
200
  <input type="password" id="k" name="api_key" placeholder="pb_sk_\u2026" required autofocus>
150
201
  <p class="err" id="e">Key must start with pb_sk_</p>
@@ -158,13 +209,29 @@ e.preventDefault();document.getElementById("e").style.display="block"}}</script>
158
209
  });
159
210
  app.post(
160
211
  "/authorize",
212
+ authLimiter,
161
213
  express.urlencoded({ extended: false }),
162
214
  (req, res) => {
163
- const { api_key, redirect_uri, code_challenge, state } = req.body;
215
+ const { api_key, redirect_uri, code_challenge, state, client_id } = req.body;
164
216
  if (!api_key?.startsWith("pb_sk_")) {
165
217
  res.status(400).send("Invalid API key");
166
218
  return;
167
219
  }
220
+ if (!client_id || !registeredClients.has(client_id)) {
221
+ res.status(400).json({
222
+ error: "invalid_request",
223
+ error_description: "Unknown or missing client_id"
224
+ });
225
+ return;
226
+ }
227
+ const client = registeredClients.get(client_id);
228
+ if (!client.redirect_uris.includes(redirect_uri)) {
229
+ res.status(400).json({
230
+ error: "invalid_request",
231
+ error_description: "redirect_uri does not match any registered redirect for this client"
232
+ });
233
+ return;
234
+ }
168
235
  const code = randomUUID();
169
236
  pendingCodes.set(code, {
170
237
  apiKey: api_key,
@@ -178,12 +245,79 @@ app.post(
178
245
  res.redirect(302, url.toString());
179
246
  }
180
247
  );
248
+ function issueTokens(apiKey) {
249
+ const opaqueToken = `pb_at_${randomUUID()}`;
250
+ const now = Date.now();
251
+ if (accessTokens.size >= MAX_ACCESS_TOKENS) {
252
+ let oldestKey = "";
253
+ let oldestTime = Infinity;
254
+ for (const [k, v] of accessTokens) {
255
+ if (v.createdAt < oldestTime) {
256
+ oldestTime = v.createdAt;
257
+ oldestKey = k;
258
+ }
259
+ }
260
+ if (oldestKey) accessTokens.delete(oldestKey);
261
+ }
262
+ accessTokens.set(opaqueToken, { apiKey, createdAt: now });
263
+ const refreshToken = `pb_rt_${randomUUID()}`;
264
+ let perKeyCount = 0;
265
+ let oldestKeyForApiKey = null;
266
+ let oldestAtForApiKey = Infinity;
267
+ for (const [k, v] of refreshTokens) {
268
+ if (v.apiKey === apiKey) {
269
+ perKeyCount++;
270
+ if (v.createdAt < oldestAtForApiKey) {
271
+ oldestAtForApiKey = v.createdAt;
272
+ oldestKeyForApiKey = k;
273
+ }
274
+ }
275
+ }
276
+ if (perKeyCount >= MAX_REFRESH_TOKENS_PER_KEY && oldestKeyForApiKey) {
277
+ refreshTokens.delete(oldestKeyForApiKey);
278
+ }
279
+ if (refreshTokens.size >= MAX_REFRESH_TOKENS) {
280
+ let oldestKey = null;
281
+ let oldestAt = Infinity;
282
+ for (const [k, v] of refreshTokens) {
283
+ if (v.createdAt < oldestAt) {
284
+ oldestAt = v.createdAt;
285
+ oldestKey = k;
286
+ }
287
+ }
288
+ if (oldestKey) refreshTokens.delete(oldestKey);
289
+ }
290
+ refreshTokens.set(refreshToken, { apiKey, createdAt: now });
291
+ return {
292
+ access_token: opaqueToken,
293
+ token_type: "Bearer",
294
+ expires_in: ACCESS_TOKEN_TTL,
295
+ refresh_token: refreshToken
296
+ };
297
+ }
181
298
  app.post(
182
299
  "/oauth/token",
300
+ authLimiter,
183
301
  express.urlencoded({ extended: false }),
184
302
  express.json(),
185
303
  (req, res) => {
186
- const { grant_type, code, code_verifier, redirect_uri } = req.body;
304
+ const { grant_type, code, code_verifier, redirect_uri, refresh_token } = req.body;
305
+ if (grant_type === "refresh_token") {
306
+ const entry = refreshTokens.get(refresh_token);
307
+ if (!entry) {
308
+ res.status(400).json({ error: "invalid_grant", error_description: "Invalid refresh token" });
309
+ return;
310
+ }
311
+ if (Date.now() - entry.createdAt > REFRESH_TOKEN_TTL_MS) {
312
+ refreshTokens.delete(refresh_token);
313
+ res.status(400).json({ error: "invalid_grant", error_description: "Refresh token expired" });
314
+ return;
315
+ }
316
+ const apiKey = entry.apiKey;
317
+ refreshTokens.delete(refresh_token);
318
+ res.json(issueTokens(apiKey));
319
+ return;
320
+ }
187
321
  if (grant_type !== "authorization_code") {
188
322
  res.status(400).json({ error: "unsupported_grant_type" });
189
323
  return;
@@ -203,11 +337,7 @@ app.post(
203
337
  return;
204
338
  }
205
339
  pendingCodes.delete(code);
206
- res.json({
207
- access_token: pending.apiKey,
208
- token_type: "Bearer",
209
- expires_in: 3600
210
- });
340
+ res.json(issueTokens(pending.apiKey));
211
341
  }
212
342
  );
213
343
  var mcpLimiter = rateLimit({
@@ -217,16 +347,46 @@ var mcpLimiter = rateLimit({
217
347
  legacyHeaders: false,
218
348
  message: { error: "Too many requests. Try again later." }
219
349
  });
350
+ var authFailures = /* @__PURE__ */ new Map();
351
+ var AUTH_FAILURE_MAX = 10;
352
+ var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
353
+ var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
354
+ var MAX_AUTH_FAILURE_ENTRIES = 1e4;
355
+ function checkAuthBlock(ip) {
356
+ const rec = authFailures.get(ip);
357
+ if (!rec) return false;
358
+ return rec.blockedUntil > Date.now();
359
+ }
360
+ function recordAuthFailure(ip) {
361
+ const now = Date.now();
362
+ const rec = authFailures.get(ip);
363
+ if (!rec) {
364
+ authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
365
+ return;
366
+ }
367
+ if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
368
+ rec.count = 1;
369
+ rec.firstFailure = now;
370
+ rec.blockedUntil = 0;
371
+ } else {
372
+ rec.count++;
373
+ if (rec.count >= AUTH_FAILURE_MAX) {
374
+ rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;
375
+ }
376
+ }
377
+ }
220
378
  app.get("/health", (_req, res) => {
221
379
  res.json({ status: "ok", version: SERVER_VERSION, transport: "http" });
222
380
  });
223
381
  var sessions = /* @__PURE__ */ new Map();
224
382
  var SESSION_TTL_MS = 30 * 60 * 1e3;
225
383
  var MAX_SESSIONS = 200;
384
+ var MAX_SESSIONS_PER_KEY = 5;
226
385
  function evictStaleSessions() {
227
386
  const now = Date.now();
228
387
  for (const [id, entry] of sessions) {
229
388
  if (now - entry.lastAccess > SESSION_TTL_MS) {
389
+ logSessionLifecycle("session_deleted", id, "ttl");
230
390
  entry.transport.close().catch(() => {
231
391
  });
232
392
  sessions.delete(id);
@@ -237,6 +397,7 @@ function evictStaleSessions() {
237
397
  (a, b) => a[1].lastAccess - b[1].lastAccess
238
398
  );
239
399
  for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {
400
+ logSessionLifecycle("session_deleted", sorted[i][0], "eviction");
240
401
  sorted[i][1].transport.close().catch(() => {
241
402
  });
242
403
  sessions.delete(sorted[i][0]);
@@ -248,7 +409,20 @@ function extractBearerKey(req) {
248
409
  const header = req.headers?.authorization;
249
410
  if (typeof header !== "string" || !header.startsWith("Bearer ")) return null;
250
411
  const token = header.slice(7).trim();
251
- return token.startsWith("pb_sk_") ? token : null;
412
+ if (token.startsWith("pb_sk_")) {
413
+ return token;
414
+ }
415
+ if (token.startsWith("pb_at_")) {
416
+ const entry = accessTokens.get(token);
417
+ if (!entry) return null;
418
+ const now = Date.now();
419
+ if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {
420
+ accessTokens.delete(token);
421
+ return null;
422
+ }
423
+ return entry.apiKey;
424
+ }
425
+ return null;
252
426
  }
253
427
  function send401(req, res) {
254
428
  const base = baseUrl(req);
@@ -257,43 +431,86 @@ function send401(req, res) {
257
431
  `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`
258
432
  ).json({ error: "unauthorized" });
259
433
  }
260
- function logRequest(method, outcome, sessionId) {
434
+ function logRequest(method, outcome, sessionId, durationMs) {
261
435
  const ts = (/* @__PURE__ */ new Date()).toISOString();
262
436
  const sid = sessionId ? ` session=${sessionId}` : "";
263
- process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}
437
+ const dur = durationMs != null ? ` duration=${durationMs}ms` : "";
438
+ process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}
439
+ `);
440
+ }
441
+ function logSessionLifecycle(event, sessionId, reason) {
442
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
443
+ const r = reason ? ` reason=${reason}` : "";
444
+ process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}
264
445
  `);
265
446
  }
266
447
  app.post("/mcp", mcpLimiter, async (req, res) => {
448
+ const reqIp = req.ip ?? "unknown";
449
+ if (checkAuthBlock(reqIp)) {
450
+ res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
451
+ return;
452
+ }
267
453
  const apiKey = extractBearerKey(req);
268
454
  if (!apiKey) {
269
455
  logRequest("POST", "auth_fail");
456
+ recordAuthFailure(reqIp);
270
457
  send401(req, res);
271
458
  return;
272
459
  }
273
460
  const sessionId = req.headers["mcp-session-id"];
461
+ const reqStart = Date.now();
274
462
  try {
275
463
  await runWithAuth({ apiKey }, async () => {
276
464
  if (sessionId && sessions.has(sessionId)) {
277
465
  const entry = sessions.get(sessionId);
466
+ if (entry.keyHash !== hashKey(apiKey)) {
467
+ res.status(403).json({
468
+ jsonrpc: "2.0",
469
+ error: { code: -32e3, message: "Session key mismatch" },
470
+ id: null
471
+ });
472
+ return;
473
+ }
278
474
  entry.lastAccess = Date.now();
279
475
  await entry.transport.handleRequest(req, res, req.body);
280
- logRequest("POST", "ok", sessionId);
476
+ logRequest("POST", "ok", sessionId, Date.now() - reqStart);
281
477
  } else if (!sessionId && isInitializeRequest(req.body)) {
478
+ const keyH = hashKey(apiKey);
479
+ let keySessionCount = 0;
480
+ for (const entry of sessions.values()) {
481
+ if (entry.keyHash === keyH) keySessionCount++;
482
+ }
483
+ if (keySessionCount >= MAX_SESSIONS_PER_KEY) {
484
+ res.status(429).json({
485
+ jsonrpc: "2.0",
486
+ error: { code: -32e3, message: "Too many sessions for this API key" },
487
+ id: null
488
+ });
489
+ return;
490
+ }
282
491
  const transport = new StreamableHTTPServerTransport({
283
492
  sessionIdGenerator: () => randomUUID(),
284
493
  onsessioninitialized: (sid) => {
285
- sessions.set(sid, { transport, lastAccess: Date.now() });
286
- logRequest("POST", "ok", sid);
494
+ sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });
495
+ logSessionLifecycle("session_created", sid);
287
496
  }
288
497
  });
289
498
  transport.onclose = () => {
290
499
  const sid = transport.sessionId;
291
- if (sid) sessions.delete(sid);
500
+ if (sid) {
501
+ logSessionLifecycle("session_deleted", sid, "onclose");
502
+ sessions.delete(sid);
503
+ }
292
504
  };
293
505
  const server = createProductBrainServer();
294
506
  await server.connect(transport);
295
507
  await transport.handleRequest(req, res, req.body);
508
+ logRequest("POST", "ok", transport.sessionId ?? void 0, Date.now() - reqStart);
296
509
  } else {
510
+ process.stderr.write(
511
+ `[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)
512
+ `
513
+ );
297
514
  res.status(400).json({
298
515
  jsonrpc: "2.0",
299
516
  error: { code: -32e3, message: "Bad Request: no valid session ID provided" },
@@ -302,7 +519,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
302
519
  }
303
520
  });
304
521
  } catch (err) {
305
- logRequest("POST", "error", sessionId);
522
+ logRequest("POST", "error", sessionId, Date.now() - reqStart);
306
523
  if (!res.headersSent) {
307
524
  res.status(500).json({
308
525
  jsonrpc: "2.0",
@@ -313,9 +530,15 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
313
530
  }
314
531
  });
315
532
  app.get("/mcp", mcpLimiter, async (req, res) => {
533
+ const reqIp = req.ip ?? "unknown";
534
+ if (checkAuthBlock(reqIp)) {
535
+ res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
536
+ return;
537
+ }
316
538
  const apiKey = extractBearerKey(req);
317
539
  if (!apiKey) {
318
540
  logRequest("GET", "auth_fail");
541
+ recordAuthFailure(reqIp);
319
542
  send401(req, res);
320
543
  return;
321
544
  }
@@ -327,6 +550,14 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
327
550
  try {
328
551
  await runWithAuth({ apiKey }, async () => {
329
552
  const entry = sessions.get(sessionId);
553
+ if (entry.keyHash !== hashKey(apiKey)) {
554
+ res.status(403).json({
555
+ jsonrpc: "2.0",
556
+ error: { code: -32e3, message: "Session key mismatch" },
557
+ id: null
558
+ });
559
+ return;
560
+ }
330
561
  entry.lastAccess = Date.now();
331
562
  await entry.transport.handleRequest(req, res);
332
563
  logRequest("GET", "ok", sessionId);
@@ -336,9 +567,15 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
336
567
  }
337
568
  });
338
569
  app.delete("/mcp", mcpLimiter, async (req, res) => {
570
+ const reqIp = req.ip ?? "unknown";
571
+ if (checkAuthBlock(reqIp)) {
572
+ res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
573
+ return;
574
+ }
339
575
  const apiKey = extractBearerKey(req);
340
576
  if (!apiKey) {
341
577
  logRequest("DELETE", "auth_fail");
578
+ recordAuthFailure(reqIp);
342
579
  send401(req, res);
343
580
  return;
344
581
  }
@@ -350,6 +587,14 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
350
587
  try {
351
588
  await runWithAuth({ apiKey }, async () => {
352
589
  const entry = sessions.get(sessionId);
590
+ if (entry.keyHash !== hashKey(apiKey)) {
591
+ res.status(403).json({
592
+ jsonrpc: "2.0",
593
+ error: { code: -32e3, message: "Session key mismatch" },
594
+ id: null
595
+ });
596
+ return;
597
+ }
353
598
  await entry.transport.handleRequest(req, res);
354
599
  logRequest("DELETE", "ok", sessionId);
355
600
  });
@@ -357,18 +602,40 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
357
602
  logRequest("DELETE", "error", sessionId);
358
603
  }
359
604
  });
360
- app.listen(PORT, "0.0.0.0", () => {
361
- console.log(`Product Brain MCP HTTP server v${SERVER_VERSION} listening on port ${PORT}`);
605
+ process.on("unhandledRejection", (reason) => {
606
+ const msg = reason instanceof Error ? reason.message : String(reason);
607
+ console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);
608
+ });
609
+ process.on("uncaughtException", (err) => {
610
+ console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);
611
+ gracefulShutdown();
362
612
  });
613
+ var shuttingDown = false;
363
614
  async function gracefulShutdown() {
615
+ if (shuttingDown) return;
616
+ shuttingDown = true;
617
+ setTimeout(() => process.exit(1), 3e3).unref();
364
618
  console.log("Shutting down...");
365
619
  for (const [, entry] of sessions) {
366
620
  await entry.transport.close().catch(() => {
367
621
  });
368
622
  }
369
- await shutdownAnalytics();
623
+ try {
624
+ await shutdownAnalytics();
625
+ } catch {
626
+ }
370
627
  process.exit(0);
371
628
  }
629
+ var LISTEN_HOST = "0.0.0.0";
630
+ var httpServer = app.listen(PORT, LISTEN_HOST, () => {
631
+ console.log(
632
+ `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`
633
+ );
634
+ });
635
+ httpServer.on("error", (err) => {
636
+ console.error(`[MCP HTTP] Server error: ${err.message}`);
637
+ process.exit(1);
638
+ });
372
639
  process.on("SIGINT", gracefulShutdown);
373
640
  process.on("SIGTERM", gracefulShutdown);
374
641
  //# sourceMappingURL=http.js.map