@productbrain/mcp 0.0.1-beta.9450 → 0.0.1-beta.9464
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-BCYOMOD2.js → chunk-BCQ7KBLE.js} +2 -2
- package/dist/{chunk-DKJ4X2D7.js → chunk-FVH3B7MF.js} +24 -10
- package/dist/chunk-FVH3B7MF.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +71 -159
- package/dist/http.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{setup-ZT5VSESE.js → setup-YETABTTN.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-DKJ4X2D7.js.map +0 -1
- /package/dist/{chunk-BCYOMOD2.js.map → chunk-BCQ7KBLE.js.map} +0 -0
- /package/dist/{setup-ZT5VSESE.js.map → setup-YETABTTN.js.map} +0 -0
package/dist/http.js
CHANGED
|
@@ -2,22 +2,24 @@ import {
|
|
|
2
2
|
SERVER_VERSION,
|
|
3
3
|
createProductBrainServer,
|
|
4
4
|
initFeatureFlags
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-BCQ7KBLE.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CLOUD_URL,
|
|
8
8
|
apiKeysUrl,
|
|
9
9
|
bootstrapHttp,
|
|
10
|
+
checkKeyAgainstCandidates,
|
|
10
11
|
forgetStreamLifecycle,
|
|
11
12
|
getKeyState,
|
|
12
13
|
getPostHogClient,
|
|
13
14
|
hashKey,
|
|
14
15
|
initAnalytics,
|
|
16
|
+
parseFallbackUrls,
|
|
15
17
|
runWithAuth,
|
|
16
18
|
shutdownAnalytics
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-FVH3B7MF.js";
|
|
18
20
|
|
|
19
21
|
// src/http.ts
|
|
20
|
-
import { createHash, randomUUID
|
|
22
|
+
import { createHash, randomUUID } from "crypto";
|
|
21
23
|
import express from "express";
|
|
22
24
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
25
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -56,63 +58,6 @@ function appLogoMarkup(opts = {}) {
|
|
|
56
58
|
return `<span class="${cls}"><span class="pb-logo__mark"><span class="pb-logo__core"></span></span>${wordmark}</span>`;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
// src/lib/refresh-token.ts
|
|
60
|
-
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
|
|
61
|
-
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
|
|
62
|
-
var PREFIX = "pb_rt_";
|
|
63
|
-
var secret = (() => {
|
|
64
|
-
const fromEnv = process.env.MCP_REFRESH_SECRET;
|
|
65
|
-
if (fromEnv && fromEnv.length > 0) return Buffer.from(fromEnv, "utf8");
|
|
66
|
-
if (process.env.NODE_ENV === "production") {
|
|
67
|
-
console.warn(
|
|
68
|
-
"[HTTP] WARNING MCP_REFRESH_SECRET not set \u2014 refresh tokens will not survive restart"
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
return randomBytes(32);
|
|
72
|
-
})();
|
|
73
|
-
function sign(payloadB64) {
|
|
74
|
-
return createHmac("sha256", secret).update(payloadB64).digest();
|
|
75
|
-
}
|
|
76
|
-
function signRefreshToken(apiKey) {
|
|
77
|
-
const payload = {
|
|
78
|
-
k: apiKey,
|
|
79
|
-
i: Date.now(),
|
|
80
|
-
j: randomUUID()
|
|
81
|
-
};
|
|
82
|
-
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
83
|
-
const sigB64 = sign(payloadB64).toString("base64url");
|
|
84
|
-
return `${PREFIX}${payloadB64}.${sigB64}`;
|
|
85
|
-
}
|
|
86
|
-
function verifyRefreshToken(token) {
|
|
87
|
-
if (typeof token !== "string" || !token.startsWith(PREFIX)) return null;
|
|
88
|
-
const body = token.slice(PREFIX.length);
|
|
89
|
-
const dot = body.indexOf(".");
|
|
90
|
-
if (dot <= 0 || dot === body.length - 1) return null;
|
|
91
|
-
const payloadB64 = body.slice(0, dot);
|
|
92
|
-
const sigB64 = body.slice(dot + 1);
|
|
93
|
-
let providedSig;
|
|
94
|
-
try {
|
|
95
|
-
providedSig = Buffer.from(sigB64, "base64url");
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
const expectedSig = sign(payloadB64);
|
|
100
|
-
if (providedSig.length !== expectedSig.length) return null;
|
|
101
|
-
if (!timingSafeEqual(providedSig, expectedSig)) return null;
|
|
102
|
-
let payload;
|
|
103
|
-
try {
|
|
104
|
-
const json = Buffer.from(payloadB64, "base64url").toString("utf8");
|
|
105
|
-
payload = JSON.parse(json);
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
if (!payload || typeof payload.k !== "string" || typeof payload.i !== "number" || typeof payload.j !== "string") {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
if (Date.now() - payload.i > REFRESH_TOKEN_TTL_MS) return null;
|
|
113
|
-
return { apiKey: payload.k };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
61
|
// src/sessionCap.ts
|
|
117
62
|
function planKeyAdmission(sessions2, keyHash, cap, now, opts) {
|
|
118
63
|
if (cap < 1) {
|
|
@@ -225,7 +170,7 @@ var AUTH_FAILURE_MAX = 10;
|
|
|
225
170
|
var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
|
|
226
171
|
var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
|
|
227
172
|
var MAX_AUTH_FAILURE_ENTRIES = 1e4;
|
|
228
|
-
function resolveBearerAuth(authorizationHeader
|
|
173
|
+
function resolveBearerAuth(authorizationHeader) {
|
|
229
174
|
if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
|
|
230
175
|
return { status: "absent" };
|
|
231
176
|
}
|
|
@@ -239,15 +184,6 @@ function resolveBearerAuth(authorizationHeader, accessTokens2, now, accessTokenT
|
|
|
239
184
|
if (token.startsWith("pb_sk_")) {
|
|
240
185
|
return { status: "ok", apiKey: token };
|
|
241
186
|
}
|
|
242
|
-
if (token.startsWith("pb_at_")) {
|
|
243
|
-
const entry = accessTokens2.get(token);
|
|
244
|
-
if (!entry) return { status: "rejected" };
|
|
245
|
-
if (now - entry.createdAt > accessTokenTtlMs) {
|
|
246
|
-
accessTokens2.delete(token);
|
|
247
|
-
return { status: "rejected" };
|
|
248
|
-
}
|
|
249
|
-
return { status: "ok", apiKey: entry.apiKey };
|
|
250
|
-
}
|
|
251
187
|
return { status: "rejected" };
|
|
252
188
|
}
|
|
253
189
|
function checkAuthBlock(failures, ip, now) {
|
|
@@ -272,6 +208,14 @@ function recordAuthFailure(failures, ip, now) {
|
|
|
272
208
|
}
|
|
273
209
|
}
|
|
274
210
|
}
|
|
211
|
+
function initializeKeyGate(verdict, failures, ip, now) {
|
|
212
|
+
if (verdict === "ok" || verdict === "rate_limited") return "admit";
|
|
213
|
+
if (verdict === "rejected") {
|
|
214
|
+
recordAuthFailure(failures, ip, now);
|
|
215
|
+
return "unauthorized";
|
|
216
|
+
}
|
|
217
|
+
return "unavailable";
|
|
218
|
+
}
|
|
275
219
|
|
|
276
220
|
// src/lib/oauthRedirectAllowlist.ts
|
|
277
221
|
var DEFAULT_OAUTH_REDIRECT_URIS = [
|
|
@@ -361,12 +305,24 @@ function pendingCodeRefusal(pending, redirectUri, now) {
|
|
|
361
305
|
if (now > pending.expiresAt) return "expired";
|
|
362
306
|
return null;
|
|
363
307
|
}
|
|
308
|
+
function tokenResponse(apiKey) {
|
|
309
|
+
return {
|
|
310
|
+
access_token: apiKey,
|
|
311
|
+
token_type: "Bearer",
|
|
312
|
+
// 1 year: validity is enforced by Convex on every call, not by this clock.
|
|
313
|
+
expires_in: 365 * 24 * 3600
|
|
314
|
+
};
|
|
315
|
+
}
|
|
364
316
|
|
|
365
317
|
// src/http.ts
|
|
366
318
|
bootstrapHttp();
|
|
367
319
|
initAnalytics();
|
|
368
320
|
initFeatureFlags(getPostHogClient());
|
|
369
321
|
var PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "3002", 10);
|
|
322
|
+
function keyCheckCandidates() {
|
|
323
|
+
const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\/$/, "");
|
|
324
|
+
return [primaryUrl, ...parseFallbackUrls(process.env.CONVEX_FALLBACK_URLS).map((u) => u.replace(/\/$/, ""))];
|
|
325
|
+
}
|
|
370
326
|
function baseUrl(req) {
|
|
371
327
|
const proto = req.headers["x-forwarded-proto"] ?? req.protocol ?? "http";
|
|
372
328
|
const host = req.headers.host ?? `localhost:${PORT}`;
|
|
@@ -411,7 +367,8 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
|
|
411
367
|
token_endpoint: `${base}/oauth/token`,
|
|
412
368
|
registration_endpoint: `${base}/register`,
|
|
413
369
|
response_types_supported: ["code"],
|
|
414
|
-
grant_types_supported: ["authorization_code"
|
|
370
|
+
grant_types_supported: ["authorization_code"],
|
|
371
|
+
// TEN-4548: no refresh grant (lib/oauthAuthCode.ts tokenResponse)
|
|
415
372
|
code_challenge_methods_supported: ["S256"],
|
|
416
373
|
token_endpoint_auth_methods_supported: ["none"],
|
|
417
374
|
scopes_supported: ["mcp:tools", "mcp:resources"]
|
|
@@ -444,7 +401,7 @@ app.post(
|
|
|
444
401
|
res.status(400).json({ error: "invalid_redirect_uri", error_description: refusal });
|
|
445
402
|
return;
|
|
446
403
|
}
|
|
447
|
-
const clientId = `pb_client_${
|
|
404
|
+
const clientId = `pb_client_${randomUUID()}`;
|
|
448
405
|
const client = {
|
|
449
406
|
client_id: clientId,
|
|
450
407
|
redirect_uris,
|
|
@@ -463,9 +420,6 @@ app.post(
|
|
|
463
420
|
}
|
|
464
421
|
);
|
|
465
422
|
var pendingCodes = /* @__PURE__ */ new Map();
|
|
466
|
-
var ACCESS_TOKEN_TTL = 3600;
|
|
467
|
-
var ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1e3;
|
|
468
|
-
var accessTokens = /* @__PURE__ */ new Map();
|
|
469
423
|
setInterval(() => {
|
|
470
424
|
const now = Date.now();
|
|
471
425
|
for (const [code, auth] of pendingCodes) {
|
|
@@ -474,9 +428,6 @@ setInterval(() => {
|
|
|
474
428
|
for (const [id, client] of registeredClients) {
|
|
475
429
|
if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
|
|
476
430
|
}
|
|
477
|
-
for (const [token, entry] of accessTokens) {
|
|
478
|
-
if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
|
|
479
|
-
}
|
|
480
431
|
for (const [ip, rec] of authFailures) {
|
|
481
432
|
if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
|
|
482
433
|
authFailures.delete(ip);
|
|
@@ -1118,61 +1069,28 @@ app.post(
|
|
|
1118
1069
|
return;
|
|
1119
1070
|
}
|
|
1120
1071
|
let workspaceName = "Your Workspace";
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (checkData.ok) {
|
|
1141
|
-
if (checkData.workspaceName) workspaceName = checkData.workspaceName;
|
|
1142
|
-
foundUrl = checkData.deploymentUrl ?? url2;
|
|
1143
|
-
break;
|
|
1144
|
-
}
|
|
1145
|
-
if (checkData.error === "rate_limited") {
|
|
1146
|
-
rateLimitedRetryAfter = checkData.retryAfterSeconds;
|
|
1147
|
-
continue;
|
|
1148
|
-
}
|
|
1149
|
-
anyDefinitiveReject = true;
|
|
1150
|
-
}
|
|
1151
|
-
if (!foundUrl) {
|
|
1152
|
-
if (rateLimitedRetryAfter != null && !anyDefinitiveReject) {
|
|
1153
|
-
sendError(
|
|
1154
|
-
"Rate limited",
|
|
1155
|
-
`This API key is valid but temporarily rate limited. Try again in ${rateLimitedRetryAfter}s.`,
|
|
1156
|
-
429
|
|
1157
|
-
);
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
if (anyDefinitiveReject) {
|
|
1161
|
-
sendError(
|
|
1162
|
-
"Key not recognized",
|
|
1163
|
-
"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.",
|
|
1164
|
-
401
|
|
1165
|
-
);
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
|
|
1169
|
-
} else {
|
|
1170
|
-
getKeyState(api_key).deploymentUrl = foundUrl;
|
|
1171
|
-
}
|
|
1172
|
-
} catch {
|
|
1072
|
+
const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5e3);
|
|
1073
|
+
if (check.verdict === "ok") {
|
|
1074
|
+
if (check.workspaceName) workspaceName = check.workspaceName;
|
|
1075
|
+
getKeyState(api_key).deploymentUrl = check.deploymentUrl;
|
|
1076
|
+
} else if (check.verdict === "rate_limited") {
|
|
1077
|
+
sendError(
|
|
1078
|
+
"Rate limited",
|
|
1079
|
+
`This API key is valid but temporarily rate limited. Try again in ${check.retryAfterSeconds ?? 60}s.`,
|
|
1080
|
+
429
|
|
1081
|
+
);
|
|
1082
|
+
return;
|
|
1083
|
+
} else if (check.verdict === "rejected") {
|
|
1084
|
+
sendError(
|
|
1085
|
+
"Key not recognized",
|
|
1086
|
+
"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.",
|
|
1087
|
+
401
|
|
1088
|
+
);
|
|
1089
|
+
return;
|
|
1090
|
+
} else {
|
|
1173
1091
|
process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
|
|
1174
1092
|
}
|
|
1175
|
-
const code =
|
|
1093
|
+
const code = randomUUID();
|
|
1176
1094
|
pendingCodes.set(code, {
|
|
1177
1095
|
apiKey: api_key,
|
|
1178
1096
|
codeChallenge: code_challenge,
|
|
@@ -1191,35 +1109,13 @@ app.post(
|
|
|
1191
1109
|
}
|
|
1192
1110
|
}
|
|
1193
1111
|
);
|
|
1194
|
-
function issueTokens(apiKey) {
|
|
1195
|
-
return {
|
|
1196
|
-
access_token: apiKey,
|
|
1197
|
-
token_type: "Bearer",
|
|
1198
|
-
// 1-year TTL: actual validity enforced by Convex, not by expiry clock.
|
|
1199
|
-
// Long TTL prevents unnecessary refresh cycles after restarts.
|
|
1200
|
-
expires_in: 365 * 24 * 3600,
|
|
1201
|
-
refresh_token: signRefreshToken(apiKey)
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
1112
|
app.post(
|
|
1205
1113
|
"/oauth/token",
|
|
1206
1114
|
authLimiter,
|
|
1207
1115
|
express.urlencoded({ extended: false }),
|
|
1208
1116
|
express.json(),
|
|
1209
1117
|
(req, res) => {
|
|
1210
|
-
const { grant_type, code, code_verifier, redirect_uri
|
|
1211
|
-
if (grant_type === "refresh_token") {
|
|
1212
|
-
const verified = verifyRefreshToken(refresh_token);
|
|
1213
|
-
if (!verified) {
|
|
1214
|
-
res.status(400).json({
|
|
1215
|
-
error: "invalid_grant",
|
|
1216
|
-
error_description: "Invalid or expired refresh token"
|
|
1217
|
-
});
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
res.json(issueTokens(verified.apiKey));
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1118
|
+
const { grant_type, code, code_verifier, redirect_uri } = req.body;
|
|
1223
1119
|
if (grant_type !== "authorization_code") {
|
|
1224
1120
|
res.status(400).json({ error: "unsupported_grant_type" });
|
|
1225
1121
|
return;
|
|
@@ -1245,7 +1141,7 @@ app.post(
|
|
|
1245
1141
|
return;
|
|
1246
1142
|
}
|
|
1247
1143
|
pendingCodes.delete(code);
|
|
1248
|
-
res.json(
|
|
1144
|
+
res.json(tokenResponse(pending.apiKey));
|
|
1249
1145
|
}
|
|
1250
1146
|
);
|
|
1251
1147
|
var mcpLimiter = rateLimit({
|
|
@@ -1339,7 +1235,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1339
1235
|
send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
|
|
1340
1236
|
return;
|
|
1341
1237
|
}
|
|
1342
|
-
const auth = resolveBearerAuth(req.headers?.authorization
|
|
1238
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1343
1239
|
if (auth.status !== "ok") {
|
|
1344
1240
|
logRequest("POST", "auth_fail");
|
|
1345
1241
|
if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
|
|
@@ -1371,6 +1267,22 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1371
1267
|
entry.lastAccess = Date.now();
|
|
1372
1268
|
}
|
|
1373
1269
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
1270
|
+
const check = await checkKeyAgainstCandidates(keyCheckCandidates(), apiKey, 5e3);
|
|
1271
|
+
const gate = initializeKeyGate(check.verdict, authFailures, reqIp, Date.now());
|
|
1272
|
+
if (gate === "unauthorized") {
|
|
1273
|
+
logRequest("POST", "auth_fail");
|
|
1274
|
+
send401(req, res);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (gate === "unavailable") {
|
|
1278
|
+
res.status(503).json({
|
|
1279
|
+
jsonrpc: "2.0",
|
|
1280
|
+
error: { code: -32e3, message: "Product Brain could not verify this API key right now. Retry shortly." },
|
|
1281
|
+
id: req.body?.id ?? null
|
|
1282
|
+
});
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (check.verdict === "ok" || check.verdict === "rate_limited") getKeyState(apiKey).deploymentUrl = check.deploymentUrl;
|
|
1374
1286
|
const keyH = hashKey(apiKey);
|
|
1375
1287
|
const plan = planKeyAdmission(
|
|
1376
1288
|
sessions,
|
|
@@ -1396,7 +1308,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1396
1308
|
}));
|
|
1397
1309
|
return;
|
|
1398
1310
|
}
|
|
1399
|
-
const sid =
|
|
1311
|
+
const sid = randomUUID();
|
|
1400
1312
|
let initialized = false;
|
|
1401
1313
|
const transport = new StreamableHTTPServerTransport({
|
|
1402
1314
|
sessionIdGenerator: () => sid,
|
|
@@ -1479,7 +1391,7 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1479
1391
|
send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
|
|
1480
1392
|
return;
|
|
1481
1393
|
}
|
|
1482
|
-
const auth = resolveBearerAuth(req.headers?.authorization
|
|
1394
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1483
1395
|
if (auth.status !== "ok") {
|
|
1484
1396
|
logRequest("GET", "auth_fail");
|
|
1485
1397
|
if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
|
|
@@ -1528,7 +1440,7 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1528
1440
|
send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
|
|
1529
1441
|
return;
|
|
1530
1442
|
}
|
|
1531
|
-
const auth = resolveBearerAuth(req.headers?.authorization
|
|
1443
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1532
1444
|
if (auth.status !== "ok") {
|
|
1533
1445
|
logRequest("DELETE", "auth_fail");
|
|
1534
1446
|
if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
|