@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/.env.mcp.example +4 -0
- package/dist/chunk-6E6HZFTX.js +15759 -0
- package/dist/chunk-6E6HZFTX.js.map +1 -0
- package/dist/chunk-YMF3IQ5E.js +465 -0
- package/dist/chunk-YMF3IQ5E.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +296 -29
- package/dist/http.js.map +1 -1
- package/dist/index.js +57 -37
- package/dist/index.js.map +1 -1
- package/dist/{setup-GZ5OZ5OP.js → setup-BPZMFI56.js} +37 -105
- package/dist/setup-BPZMFI56.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-47LO6K2R.js +0 -1423
- package/dist/chunk-47LO6K2R.js.map +0 -1
- package/dist/chunk-5V4JXM4G.js +0 -4552
- package/dist/chunk-5V4JXM4G.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-5V4JXM4G.js";
|
|
5
|
-
import {
|
|
6
3
|
bootstrapHttp,
|
|
4
|
+
createProductBrainServer,
|
|
5
|
+
hashKey,
|
|
6
|
+
initFeatureFlags,
|
|
7
7
|
runWithAuth
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6E6HZFTX.js";
|
|
9
9
|
import {
|
|
10
|
+
getPostHogClient,
|
|
10
11
|
initAnalytics,
|
|
11
12
|
shutdownAnalytics
|
|
12
|
-
} from "./chunk-
|
|
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
|
-
|
|
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,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) => ({ "&": "&", '"': """, "<": "<", ">": ">" })[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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|