@productbrain/mcp 0.0.1-beta.15 → 0.0.1-beta.151
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/chunk-ML7BPLBX.js +15321 -0
- package/dist/chunk-ML7BPLBX.js.map +1 -0
- package/dist/chunk-X3S5UTTZ.js +363 -0
- package/dist/chunk-X3S5UTTZ.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +262 -29
- package/dist/http.js.map +1 -1
- package/dist/index.js +56 -31
- package/dist/index.js.map +1 -1
- package/dist/{setup-GZ5OZ5OP.js → setup-I6KRGWFC.js} +36 -104
- package/dist/setup-I6KRGWFC.js.map +1 -0
- package/dist/views/src/entry-cards/index.html +227 -0
- package/dist/views/src/graph-constellation/index.html +254 -0
- package/package.json +6 -3
- package/dist/chunk-3QNBVXRP.js +0 -4543
- package/dist/chunk-3QNBVXRP.js.map +0 -1
- package/dist/chunk-47LO6K2R.js +0 -1423
- package/dist/chunk-47LO6K2R.js.map +0 -1
- package/dist/chunk-XBMI6QHR.js +0 -100
- package/dist/chunk-XBMI6QHR.js.map +0 -1
- package/dist/setup-GZ5OZ5OP.js.map +0 -1
- package/dist/smart-capture-4DNBNMRG.js +0 -14
- package/dist/smart-capture-4DNBNMRG.js.map +0 -1
package/dist/http.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SERVER_VERSION,
|
|
3
|
-
createProductBrainServer
|
|
4
|
-
} from "./chunk-3QNBVXRP.js";
|
|
5
|
-
import {
|
|
6
3
|
bootstrapHttp,
|
|
4
|
+
createProductBrainServer,
|
|
5
|
+
hashKey,
|
|
6
|
+
initFeatureFlags,
|
|
7
7
|
runWithAuth
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ML7BPLBX.js";
|
|
9
9
|
import {
|
|
10
|
+
getPostHogClient,
|
|
10
11
|
initAnalytics,
|
|
11
12
|
shutdownAnalytics
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-X3S5UTTZ.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
|
-
|
|
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 (
|
|
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,12 @@ 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 accessTokens = /* @__PURE__ */ new Map();
|
|
127
|
+
var MAX_ACCESS_TOKENS = 1e3;
|
|
103
128
|
setInterval(() => {
|
|
104
129
|
const now = Date.now();
|
|
105
130
|
for (const [code, auth] of pendingCodes) {
|
|
@@ -108,6 +133,23 @@ setInterval(() => {
|
|
|
108
133
|
for (const [id, client] of registeredClients) {
|
|
109
134
|
if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
|
|
110
135
|
}
|
|
136
|
+
for (const [token, entry] of refreshTokens) {
|
|
137
|
+
if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);
|
|
138
|
+
}
|
|
139
|
+
for (const [token, entry] of accessTokens) {
|
|
140
|
+
if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
|
|
141
|
+
}
|
|
142
|
+
for (const [ip, rec] of authFailures) {
|
|
143
|
+
if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
|
|
144
|
+
authFailures.delete(ip);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {
|
|
148
|
+
const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);
|
|
149
|
+
for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {
|
|
150
|
+
authFailures.delete(sorted[i][0]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
111
153
|
}, 6e4);
|
|
112
154
|
function esc(s) {
|
|
113
155
|
return String(s ?? "").replace(
|
|
@@ -115,8 +157,8 @@ function esc(s) {
|
|
|
115
157
|
(c) => ({ "&": "&", '"': """, "<": "<", ">": ">" })[c]
|
|
116
158
|
);
|
|
117
159
|
}
|
|
118
|
-
app.get("/authorize", (req, res) => {
|
|
119
|
-
const { redirect_uri, code_challenge, code_challenge_method, state } = req.query;
|
|
160
|
+
app.get("/authorize", authLimiter, (req, res) => {
|
|
161
|
+
const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;
|
|
120
162
|
res.type("html").send(`<!DOCTYPE html>
|
|
121
163
|
<html lang="en"><head>
|
|
122
164
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -145,6 +187,7 @@ button:hover{background:#6d28d9}
|
|
|
145
187
|
<input type="hidden" name="code_challenge" value="${esc(code_challenge)}">
|
|
146
188
|
<input type="hidden" name="code_challenge_method" value="${esc(code_challenge_method)}">
|
|
147
189
|
<input type="hidden" name="state" value="${esc(state)}">
|
|
190
|
+
<input type="hidden" name="client_id" value="${esc(client_id)}">
|
|
148
191
|
<label for="k">API Key</label>
|
|
149
192
|
<input type="password" id="k" name="api_key" placeholder="pb_sk_\u2026" required autofocus>
|
|
150
193
|
<p class="err" id="e">Key must start with pb_sk_</p>
|
|
@@ -158,13 +201,29 @@ e.preventDefault();document.getElementById("e").style.display="block"}}</script>
|
|
|
158
201
|
});
|
|
159
202
|
app.post(
|
|
160
203
|
"/authorize",
|
|
204
|
+
authLimiter,
|
|
161
205
|
express.urlencoded({ extended: false }),
|
|
162
206
|
(req, res) => {
|
|
163
|
-
const { api_key, redirect_uri, code_challenge, state } = req.body;
|
|
207
|
+
const { api_key, redirect_uri, code_challenge, state, client_id } = req.body;
|
|
164
208
|
if (!api_key?.startsWith("pb_sk_")) {
|
|
165
209
|
res.status(400).send("Invalid API key");
|
|
166
210
|
return;
|
|
167
211
|
}
|
|
212
|
+
if (!client_id || !registeredClients.has(client_id)) {
|
|
213
|
+
res.status(400).json({
|
|
214
|
+
error: "invalid_request",
|
|
215
|
+
error_description: "Unknown or missing client_id"
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const client = registeredClients.get(client_id);
|
|
220
|
+
if (!client.redirect_uris.includes(redirect_uri)) {
|
|
221
|
+
res.status(400).json({
|
|
222
|
+
error: "invalid_request",
|
|
223
|
+
error_description: "redirect_uri does not match any registered redirect for this client"
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
168
227
|
const code = randomUUID();
|
|
169
228
|
pendingCodes.set(code, {
|
|
170
229
|
apiKey: api_key,
|
|
@@ -178,12 +237,53 @@ app.post(
|
|
|
178
237
|
res.redirect(302, url.toString());
|
|
179
238
|
}
|
|
180
239
|
);
|
|
240
|
+
function issueTokens(apiKey) {
|
|
241
|
+
const opaqueToken = `pb_at_${randomUUID()}`;
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
if (accessTokens.size >= MAX_ACCESS_TOKENS) {
|
|
244
|
+
let oldestKey = "";
|
|
245
|
+
let oldestTime = Infinity;
|
|
246
|
+
for (const [k, v] of accessTokens) {
|
|
247
|
+
if (v.createdAt < oldestTime) {
|
|
248
|
+
oldestTime = v.createdAt;
|
|
249
|
+
oldestKey = k;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (oldestKey) accessTokens.delete(oldestKey);
|
|
253
|
+
}
|
|
254
|
+
accessTokens.set(opaqueToken, { apiKey, createdAt: now });
|
|
255
|
+
const refreshToken = `pb_rt_${randomUUID()}`;
|
|
256
|
+
refreshTokens.set(refreshToken, { apiKey, createdAt: now });
|
|
257
|
+
return {
|
|
258
|
+
access_token: opaqueToken,
|
|
259
|
+
token_type: "Bearer",
|
|
260
|
+
expires_in: ACCESS_TOKEN_TTL,
|
|
261
|
+
refresh_token: refreshToken
|
|
262
|
+
};
|
|
263
|
+
}
|
|
181
264
|
app.post(
|
|
182
265
|
"/oauth/token",
|
|
266
|
+
authLimiter,
|
|
183
267
|
express.urlencoded({ extended: false }),
|
|
184
268
|
express.json(),
|
|
185
269
|
(req, res) => {
|
|
186
|
-
const { grant_type, code, code_verifier, redirect_uri } = req.body;
|
|
270
|
+
const { grant_type, code, code_verifier, redirect_uri, refresh_token } = req.body;
|
|
271
|
+
if (grant_type === "refresh_token") {
|
|
272
|
+
const entry = refreshTokens.get(refresh_token);
|
|
273
|
+
if (!entry) {
|
|
274
|
+
res.status(400).json({ error: "invalid_grant", error_description: "Invalid refresh token" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (Date.now() - entry.createdAt > REFRESH_TOKEN_TTL_MS) {
|
|
278
|
+
refreshTokens.delete(refresh_token);
|
|
279
|
+
res.status(400).json({ error: "invalid_grant", error_description: "Refresh token expired" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const apiKey = entry.apiKey;
|
|
283
|
+
refreshTokens.delete(refresh_token);
|
|
284
|
+
res.json(issueTokens(apiKey));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
187
287
|
if (grant_type !== "authorization_code") {
|
|
188
288
|
res.status(400).json({ error: "unsupported_grant_type" });
|
|
189
289
|
return;
|
|
@@ -203,11 +303,7 @@ app.post(
|
|
|
203
303
|
return;
|
|
204
304
|
}
|
|
205
305
|
pendingCodes.delete(code);
|
|
206
|
-
res.json(
|
|
207
|
-
access_token: pending.apiKey,
|
|
208
|
-
token_type: "Bearer",
|
|
209
|
-
expires_in: 3600
|
|
210
|
-
});
|
|
306
|
+
res.json(issueTokens(pending.apiKey));
|
|
211
307
|
}
|
|
212
308
|
);
|
|
213
309
|
var mcpLimiter = rateLimit({
|
|
@@ -217,16 +313,46 @@ var mcpLimiter = rateLimit({
|
|
|
217
313
|
legacyHeaders: false,
|
|
218
314
|
message: { error: "Too many requests. Try again later." }
|
|
219
315
|
});
|
|
316
|
+
var authFailures = /* @__PURE__ */ new Map();
|
|
317
|
+
var AUTH_FAILURE_MAX = 10;
|
|
318
|
+
var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
|
|
319
|
+
var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
|
|
320
|
+
var MAX_AUTH_FAILURE_ENTRIES = 1e4;
|
|
321
|
+
function checkAuthBlock(ip) {
|
|
322
|
+
const rec = authFailures.get(ip);
|
|
323
|
+
if (!rec) return false;
|
|
324
|
+
return rec.blockedUntil > Date.now();
|
|
325
|
+
}
|
|
326
|
+
function recordAuthFailure(ip) {
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
const rec = authFailures.get(ip);
|
|
329
|
+
if (!rec) {
|
|
330
|
+
authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
|
|
334
|
+
rec.count = 1;
|
|
335
|
+
rec.firstFailure = now;
|
|
336
|
+
rec.blockedUntil = 0;
|
|
337
|
+
} else {
|
|
338
|
+
rec.count++;
|
|
339
|
+
if (rec.count >= AUTH_FAILURE_MAX) {
|
|
340
|
+
rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
220
344
|
app.get("/health", (_req, res) => {
|
|
221
345
|
res.json({ status: "ok", version: SERVER_VERSION, transport: "http" });
|
|
222
346
|
});
|
|
223
347
|
var sessions = /* @__PURE__ */ new Map();
|
|
224
348
|
var SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
225
349
|
var MAX_SESSIONS = 200;
|
|
350
|
+
var MAX_SESSIONS_PER_KEY = 5;
|
|
226
351
|
function evictStaleSessions() {
|
|
227
352
|
const now = Date.now();
|
|
228
353
|
for (const [id, entry] of sessions) {
|
|
229
354
|
if (now - entry.lastAccess > SESSION_TTL_MS) {
|
|
355
|
+
logSessionLifecycle("session_deleted", id, "ttl");
|
|
230
356
|
entry.transport.close().catch(() => {
|
|
231
357
|
});
|
|
232
358
|
sessions.delete(id);
|
|
@@ -237,6 +363,7 @@ function evictStaleSessions() {
|
|
|
237
363
|
(a, b) => a[1].lastAccess - b[1].lastAccess
|
|
238
364
|
);
|
|
239
365
|
for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {
|
|
366
|
+
logSessionLifecycle("session_deleted", sorted[i][0], "eviction");
|
|
240
367
|
sorted[i][1].transport.close().catch(() => {
|
|
241
368
|
});
|
|
242
369
|
sessions.delete(sorted[i][0]);
|
|
@@ -248,7 +375,20 @@ function extractBearerKey(req) {
|
|
|
248
375
|
const header = req.headers?.authorization;
|
|
249
376
|
if (typeof header !== "string" || !header.startsWith("Bearer ")) return null;
|
|
250
377
|
const token = header.slice(7).trim();
|
|
251
|
-
|
|
378
|
+
if (token.startsWith("pb_sk_")) {
|
|
379
|
+
return token;
|
|
380
|
+
}
|
|
381
|
+
if (token.startsWith("pb_at_")) {
|
|
382
|
+
const entry = accessTokens.get(token);
|
|
383
|
+
if (!entry) return null;
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {
|
|
386
|
+
accessTokens.delete(token);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return entry.apiKey;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
252
392
|
}
|
|
253
393
|
function send401(req, res) {
|
|
254
394
|
const base = baseUrl(req);
|
|
@@ -257,43 +397,86 @@ function send401(req, res) {
|
|
|
257
397
|
`Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`
|
|
258
398
|
).json({ error: "unauthorized" });
|
|
259
399
|
}
|
|
260
|
-
function logRequest(method, outcome, sessionId) {
|
|
400
|
+
function logRequest(method, outcome, sessionId, durationMs) {
|
|
261
401
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
262
402
|
const sid = sessionId ? ` session=${sessionId}` : "";
|
|
263
|
-
|
|
403
|
+
const dur = durationMs != null ? ` duration=${durationMs}ms` : "";
|
|
404
|
+
process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}
|
|
405
|
+
`);
|
|
406
|
+
}
|
|
407
|
+
function logSessionLifecycle(event, sessionId, reason) {
|
|
408
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
409
|
+
const r = reason ? ` reason=${reason}` : "";
|
|
410
|
+
process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}
|
|
264
411
|
`);
|
|
265
412
|
}
|
|
266
413
|
app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
414
|
+
const reqIp = req.ip ?? "unknown";
|
|
415
|
+
if (checkAuthBlock(reqIp)) {
|
|
416
|
+
res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
267
419
|
const apiKey = extractBearerKey(req);
|
|
268
420
|
if (!apiKey) {
|
|
269
421
|
logRequest("POST", "auth_fail");
|
|
422
|
+
recordAuthFailure(reqIp);
|
|
270
423
|
send401(req, res);
|
|
271
424
|
return;
|
|
272
425
|
}
|
|
273
426
|
const sessionId = req.headers["mcp-session-id"];
|
|
427
|
+
const reqStart = Date.now();
|
|
274
428
|
try {
|
|
275
429
|
await runWithAuth({ apiKey }, async () => {
|
|
276
430
|
if (sessionId && sessions.has(sessionId)) {
|
|
277
431
|
const entry = sessions.get(sessionId);
|
|
432
|
+
if (entry.keyHash !== hashKey(apiKey)) {
|
|
433
|
+
res.status(403).json({
|
|
434
|
+
jsonrpc: "2.0",
|
|
435
|
+
error: { code: -32e3, message: "Session key mismatch" },
|
|
436
|
+
id: null
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
278
440
|
entry.lastAccess = Date.now();
|
|
279
441
|
await entry.transport.handleRequest(req, res, req.body);
|
|
280
|
-
logRequest("POST", "ok", sessionId);
|
|
442
|
+
logRequest("POST", "ok", sessionId, Date.now() - reqStart);
|
|
281
443
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
444
|
+
const keyH = hashKey(apiKey);
|
|
445
|
+
let keySessionCount = 0;
|
|
446
|
+
for (const entry of sessions.values()) {
|
|
447
|
+
if (entry.keyHash === keyH) keySessionCount++;
|
|
448
|
+
}
|
|
449
|
+
if (keySessionCount >= MAX_SESSIONS_PER_KEY) {
|
|
450
|
+
res.status(429).json({
|
|
451
|
+
jsonrpc: "2.0",
|
|
452
|
+
error: { code: -32e3, message: "Too many sessions for this API key" },
|
|
453
|
+
id: null
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
282
457
|
const transport = new StreamableHTTPServerTransport({
|
|
283
458
|
sessionIdGenerator: () => randomUUID(),
|
|
284
459
|
onsessioninitialized: (sid) => {
|
|
285
|
-
sessions.set(sid, { transport, lastAccess: Date.now() });
|
|
286
|
-
|
|
460
|
+
sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });
|
|
461
|
+
logSessionLifecycle("session_created", sid);
|
|
287
462
|
}
|
|
288
463
|
});
|
|
289
464
|
transport.onclose = () => {
|
|
290
465
|
const sid = transport.sessionId;
|
|
291
|
-
if (sid)
|
|
466
|
+
if (sid) {
|
|
467
|
+
logSessionLifecycle("session_deleted", sid, "onclose");
|
|
468
|
+
sessions.delete(sid);
|
|
469
|
+
}
|
|
292
470
|
};
|
|
293
471
|
const server = createProductBrainServer();
|
|
294
472
|
await server.connect(transport);
|
|
295
473
|
await transport.handleRequest(req, res, req.body);
|
|
474
|
+
logRequest("POST", "ok", transport.sessionId ?? void 0, Date.now() - reqStart);
|
|
296
475
|
} else {
|
|
476
|
+
process.stderr.write(
|
|
477
|
+
`[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)
|
|
478
|
+
`
|
|
479
|
+
);
|
|
297
480
|
res.status(400).json({
|
|
298
481
|
jsonrpc: "2.0",
|
|
299
482
|
error: { code: -32e3, message: "Bad Request: no valid session ID provided" },
|
|
@@ -302,7 +485,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
302
485
|
}
|
|
303
486
|
});
|
|
304
487
|
} catch (err) {
|
|
305
|
-
logRequest("POST", "error", sessionId);
|
|
488
|
+
logRequest("POST", "error", sessionId, Date.now() - reqStart);
|
|
306
489
|
if (!res.headersSent) {
|
|
307
490
|
res.status(500).json({
|
|
308
491
|
jsonrpc: "2.0",
|
|
@@ -313,9 +496,15 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
313
496
|
}
|
|
314
497
|
});
|
|
315
498
|
app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
499
|
+
const reqIp = req.ip ?? "unknown";
|
|
500
|
+
if (checkAuthBlock(reqIp)) {
|
|
501
|
+
res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
316
504
|
const apiKey = extractBearerKey(req);
|
|
317
505
|
if (!apiKey) {
|
|
318
506
|
logRequest("GET", "auth_fail");
|
|
507
|
+
recordAuthFailure(reqIp);
|
|
319
508
|
send401(req, res);
|
|
320
509
|
return;
|
|
321
510
|
}
|
|
@@ -327,6 +516,14 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
|
327
516
|
try {
|
|
328
517
|
await runWithAuth({ apiKey }, async () => {
|
|
329
518
|
const entry = sessions.get(sessionId);
|
|
519
|
+
if (entry.keyHash !== hashKey(apiKey)) {
|
|
520
|
+
res.status(403).json({
|
|
521
|
+
jsonrpc: "2.0",
|
|
522
|
+
error: { code: -32e3, message: "Session key mismatch" },
|
|
523
|
+
id: null
|
|
524
|
+
});
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
330
527
|
entry.lastAccess = Date.now();
|
|
331
528
|
await entry.transport.handleRequest(req, res);
|
|
332
529
|
logRequest("GET", "ok", sessionId);
|
|
@@ -336,9 +533,15 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
|
336
533
|
}
|
|
337
534
|
});
|
|
338
535
|
app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
536
|
+
const reqIp = req.ip ?? "unknown";
|
|
537
|
+
if (checkAuthBlock(reqIp)) {
|
|
538
|
+
res.status(429).json({ error: "Too many failed auth attempts. Try again later." });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
339
541
|
const apiKey = extractBearerKey(req);
|
|
340
542
|
if (!apiKey) {
|
|
341
543
|
logRequest("DELETE", "auth_fail");
|
|
544
|
+
recordAuthFailure(reqIp);
|
|
342
545
|
send401(req, res);
|
|
343
546
|
return;
|
|
344
547
|
}
|
|
@@ -350,6 +553,14 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
|
350
553
|
try {
|
|
351
554
|
await runWithAuth({ apiKey }, async () => {
|
|
352
555
|
const entry = sessions.get(sessionId);
|
|
556
|
+
if (entry.keyHash !== hashKey(apiKey)) {
|
|
557
|
+
res.status(403).json({
|
|
558
|
+
jsonrpc: "2.0",
|
|
559
|
+
error: { code: -32e3, message: "Session key mismatch" },
|
|
560
|
+
id: null
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
353
564
|
await entry.transport.handleRequest(req, res);
|
|
354
565
|
logRequest("DELETE", "ok", sessionId);
|
|
355
566
|
});
|
|
@@ -357,18 +568,40 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
|
357
568
|
logRequest("DELETE", "error", sessionId);
|
|
358
569
|
}
|
|
359
570
|
});
|
|
360
|
-
|
|
361
|
-
|
|
571
|
+
process.on("unhandledRejection", (reason) => {
|
|
572
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
573
|
+
console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);
|
|
574
|
+
});
|
|
575
|
+
process.on("uncaughtException", (err) => {
|
|
576
|
+
console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);
|
|
577
|
+
gracefulShutdown();
|
|
362
578
|
});
|
|
579
|
+
var shuttingDown = false;
|
|
363
580
|
async function gracefulShutdown() {
|
|
581
|
+
if (shuttingDown) return;
|
|
582
|
+
shuttingDown = true;
|
|
583
|
+
setTimeout(() => process.exit(1), 3e3).unref();
|
|
364
584
|
console.log("Shutting down...");
|
|
365
585
|
for (const [, entry] of sessions) {
|
|
366
586
|
await entry.transport.close().catch(() => {
|
|
367
587
|
});
|
|
368
588
|
}
|
|
369
|
-
|
|
589
|
+
try {
|
|
590
|
+
await shutdownAnalytics();
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
370
593
|
process.exit(0);
|
|
371
594
|
}
|
|
595
|
+
var LISTEN_HOST = "0.0.0.0";
|
|
596
|
+
var httpServer = app.listen(PORT, LISTEN_HOST, () => {
|
|
597
|
+
console.log(
|
|
598
|
+
`Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
httpServer.on("error", (err) => {
|
|
602
|
+
console.error(`[MCP HTTP] Server error: ${err.message}`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
});
|
|
372
605
|
process.on("SIGINT", gracefulShutdown);
|
|
373
606
|
process.on("SIGTERM", gracefulShutdown);
|
|
374
607
|
//# sourceMappingURL=http.js.map
|