@productbrain/mcp 0.0.1-beta.190 → 0.0.1-beta.192
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-ZF6N62QQ.js → chunk-26IS4THT.js} +213 -14
- package/dist/chunk-26IS4THT.js.map +1 -0
- package/dist/http.js +126 -88
- package/dist/http.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZF6N62QQ.js.map +0 -1
package/dist/http.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
hashKey,
|
|
8
8
|
initFeatureFlags,
|
|
9
9
|
runWithAuth
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-26IS4THT.js";
|
|
11
11
|
import {
|
|
12
12
|
getPostHogClient,
|
|
13
13
|
initAnalytics,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./chunk-YMF3IQ5E.js";
|
|
16
16
|
|
|
17
17
|
// src/http.ts
|
|
18
|
-
import { createHash, randomUUID } from "crypto";
|
|
18
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
19
19
|
import express from "express";
|
|
20
20
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21
21
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -54,6 +54,63 @@ function appLogoMarkup(opts = {}) {
|
|
|
54
54
|
return `<span class="${cls}"><span class="pb-logo__mark"><span class="pb-logo__core"></span></span>${wordmark}</span>`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// src/lib/refresh-token.ts
|
|
58
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
|
|
59
|
+
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
|
|
60
|
+
var PREFIX = "pb_rt_";
|
|
61
|
+
var secret = (() => {
|
|
62
|
+
const fromEnv = process.env.MCP_REFRESH_SECRET;
|
|
63
|
+
if (fromEnv && fromEnv.length > 0) return Buffer.from(fromEnv, "utf8");
|
|
64
|
+
if (process.env.NODE_ENV === "production") {
|
|
65
|
+
console.warn(
|
|
66
|
+
"[HTTP] WARNING MCP_REFRESH_SECRET not set \u2014 refresh tokens will not survive restart"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return randomBytes(32);
|
|
70
|
+
})();
|
|
71
|
+
function sign(payloadB64) {
|
|
72
|
+
return createHmac("sha256", secret).update(payloadB64).digest();
|
|
73
|
+
}
|
|
74
|
+
function signRefreshToken(apiKey) {
|
|
75
|
+
const payload = {
|
|
76
|
+
k: apiKey,
|
|
77
|
+
i: Date.now(),
|
|
78
|
+
j: randomUUID()
|
|
79
|
+
};
|
|
80
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
81
|
+
const sigB64 = sign(payloadB64).toString("base64url");
|
|
82
|
+
return `${PREFIX}${payloadB64}.${sigB64}`;
|
|
83
|
+
}
|
|
84
|
+
function verifyRefreshToken(token) {
|
|
85
|
+
if (typeof token !== "string" || !token.startsWith(PREFIX)) return null;
|
|
86
|
+
const body = token.slice(PREFIX.length);
|
|
87
|
+
const dot = body.indexOf(".");
|
|
88
|
+
if (dot <= 0 || dot === body.length - 1) return null;
|
|
89
|
+
const payloadB64 = body.slice(0, dot);
|
|
90
|
+
const sigB64 = body.slice(dot + 1);
|
|
91
|
+
let providedSig;
|
|
92
|
+
try {
|
|
93
|
+
providedSig = Buffer.from(sigB64, "base64url");
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const expectedSig = sign(payloadB64);
|
|
98
|
+
if (providedSig.length !== expectedSig.length) return null;
|
|
99
|
+
if (!timingSafeEqual(providedSig, expectedSig)) return null;
|
|
100
|
+
let payload;
|
|
101
|
+
try {
|
|
102
|
+
const json = Buffer.from(payloadB64, "base64url").toString("utf8");
|
|
103
|
+
payload = JSON.parse(json);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (!payload || typeof payload.k !== "string" || typeof payload.i !== "number" || typeof payload.j !== "string") {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (Date.now() - payload.i > REFRESH_TOKEN_TTL_MS) return null;
|
|
111
|
+
return { apiKey: payload.k };
|
|
112
|
+
}
|
|
113
|
+
|
|
57
114
|
// src/http.ts
|
|
58
115
|
bootstrapHttp();
|
|
59
116
|
initAnalytics();
|
|
@@ -137,7 +194,7 @@ app.post(
|
|
|
137
194
|
});
|
|
138
195
|
return;
|
|
139
196
|
}
|
|
140
|
-
const clientId = `pb_client_${
|
|
197
|
+
const clientId = `pb_client_${randomUUID2()}`;
|
|
141
198
|
const client = {
|
|
142
199
|
client_id: clientId,
|
|
143
200
|
redirect_uris,
|
|
@@ -158,10 +215,6 @@ app.post(
|
|
|
158
215
|
var pendingCodes = /* @__PURE__ */ new Map();
|
|
159
216
|
var ACCESS_TOKEN_TTL = 3600;
|
|
160
217
|
var ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1e3;
|
|
161
|
-
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
|
|
162
|
-
var refreshTokens = /* @__PURE__ */ new Map();
|
|
163
|
-
var MAX_REFRESH_TOKENS = 2e3;
|
|
164
|
-
var MAX_REFRESH_TOKENS_PER_KEY = 20;
|
|
165
218
|
var accessTokens = /* @__PURE__ */ new Map();
|
|
166
219
|
setInterval(() => {
|
|
167
220
|
const now = Date.now();
|
|
@@ -171,15 +224,6 @@ setInterval(() => {
|
|
|
171
224
|
for (const [id, client] of registeredClients) {
|
|
172
225
|
if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
|
|
173
226
|
}
|
|
174
|
-
for (const [token, entry] of refreshTokens) {
|
|
175
|
-
if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);
|
|
176
|
-
}
|
|
177
|
-
if (refreshTokens.size > MAX_REFRESH_TOKENS) {
|
|
178
|
-
const sorted = [...refreshTokens.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
179
|
-
for (let i = 0; i < sorted.length - MAX_REFRESH_TOKENS; i++) {
|
|
180
|
-
refreshTokens.delete(sorted[i][0]);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
227
|
for (const [token, entry] of accessTokens) {
|
|
184
228
|
if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
|
|
185
229
|
}
|
|
@@ -415,12 +459,22 @@ ${appLogoStyles}
|
|
|
415
459
|
color:var(--fg-bright);opacity:0;
|
|
416
460
|
}
|
|
417
461
|
.panel:not([hidden]) .ok-title{animation:rise 600ms ease-out 380ms forwards}
|
|
418
|
-
.ok-
|
|
419
|
-
margin:
|
|
462
|
+
.ok-lead{
|
|
463
|
+
margin:18px auto 0;max-width:22em;font-size:16px;line-height:1.55;color:var(--fg2);
|
|
420
464
|
opacity:0;
|
|
421
|
-
display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;
|
|
422
465
|
}
|
|
423
|
-
.panel:not([hidden]) .ok-
|
|
466
|
+
.panel:not([hidden]) .ok-lead{animation:rise 600ms ease-out 500ms forwards}
|
|
467
|
+
.ok-phrase-row{
|
|
468
|
+
margin-top:14px;display:flex;align-items:center;justify-content:center;
|
|
469
|
+
opacity:0;
|
|
470
|
+
}
|
|
471
|
+
.panel:not([hidden]) .ok-phrase-row{animation:rise 600ms ease-out 620ms forwards}
|
|
472
|
+
.cmd.is-copied .cmd-quote-part{display:none}
|
|
473
|
+
.success-cta-wrap{
|
|
474
|
+
margin-top:48px;width:100%;opacity:0;
|
|
475
|
+
}
|
|
476
|
+
.panel:not([hidden]) .success-cta-wrap{animation:rise 600ms ease-out 780ms forwards}
|
|
477
|
+
a.btn-primary.success-cta{color:var(--btn-fg);text-decoration:none}
|
|
424
478
|
|
|
425
479
|
.cmd{
|
|
426
480
|
display:inline-flex;align-items:center;gap:8px;vertical-align:middle;
|
|
@@ -441,16 +495,6 @@ ${appLogoStyles}
|
|
|
441
495
|
.cmd.is-copied .cmd-icon{color:var(--green);opacity:1}
|
|
442
496
|
.cmd .cmd-icon svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
443
497
|
|
|
444
|
-
.return-link{
|
|
445
|
-
display:block;margin:36px auto 0;width:fit-content;
|
|
446
|
-
font-family:var(--font-mono);font-size:11px;letter-spacing:0.18em;
|
|
447
|
-
text-transform:uppercase;color:var(--fg4);text-decoration:none;
|
|
448
|
-
border-bottom:1px dotted currentColor;padding-bottom:2px;
|
|
449
|
-
opacity:0;transition:color 140ms;
|
|
450
|
-
}
|
|
451
|
-
.panel:not([hidden]) .return-link{animation:rise 600ms ease-out 760ms forwards}
|
|
452
|
-
.return-link:hover{color:var(--fg2)}
|
|
453
|
-
|
|
454
498
|
/* error specifics */
|
|
455
499
|
.err-title{
|
|
456
500
|
font-family:var(--font-display);font-weight:600;
|
|
@@ -514,7 +558,7 @@ ${appLogoStyles}
|
|
|
514
558
|
}
|
|
515
559
|
function providerDisplayName(clientName) {
|
|
516
560
|
const name = (clientName ?? "").trim();
|
|
517
|
-
if (!name) return "your
|
|
561
|
+
if (!name) return "your assistant";
|
|
518
562
|
return name.length > 40 ? name.slice(0, 40) + "\u2026" : name;
|
|
519
563
|
}
|
|
520
564
|
function successPanelInner(workspaceName, redirectUrl, providerName) {
|
|
@@ -536,18 +580,19 @@ function successPanelInner(workspaceName, redirectUrl, providerName) {
|
|
|
536
580
|
<div class="orb-core"><div class="orb-dot"></div></div>
|
|
537
581
|
</div>
|
|
538
582
|
<div class="eyebrow success"><span class="dot"></span>Connected</div>
|
|
539
|
-
<h1 class="ok-title">Product Brain is
|
|
540
|
-
<p class="ok-
|
|
541
|
-
|
|
542
|
-
<button class="cmd" type="button" data-cmd-pill data-redirect="${esc(redirectUrl)}" aria-label="Copy
|
|
543
|
-
<span data-cmd-text>Start PB</span>
|
|
583
|
+
<h1 class="ok-title">Product Brain is Live</h1>
|
|
584
|
+
<p class="ok-lead">Return to your assistant, then say</p>
|
|
585
|
+
<div class="ok-phrase-row">
|
|
586
|
+
<button class="cmd" type="button" data-cmd-pill data-redirect="${esc(redirectUrl)}" aria-label="Copy "Start PB" and return to ${esc(providerName)}">
|
|
587
|
+
<span class="cmd-quote-part" aria-hidden="true">“</span><span data-cmd-text>Start PB</span><span class="cmd-quote-part" aria-hidden="true">”</span>
|
|
544
588
|
<span class="cmd-icon" aria-hidden="true">
|
|
545
589
|
<svg data-cmd-svg viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>
|
|
546
590
|
</span>
|
|
547
591
|
</button>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
<a class="
|
|
592
|
+
</div>
|
|
593
|
+
<div class="success-cta-wrap">
|
|
594
|
+
<a class="btn-primary success-cta" href="${esc(redirectUrl)}">Continue in ${esc(providerName)}</a>
|
|
595
|
+
</div>
|
|
551
596
|
<!-- workspace name retained as data hook for tests, hidden from view -->
|
|
552
597
|
<span hidden data-field="ws-name">${esc(workspaceName)}</span>`;
|
|
553
598
|
}
|
|
@@ -579,7 +624,7 @@ var cmdScript = `
|
|
|
579
624
|
pill.classList.add('is-copied');
|
|
580
625
|
if(textEl)textEl.textContent='Copied';
|
|
581
626
|
if(svgEl)svgEl.innerHTML='<polyline points="4 12 10 18 20 6"/>';
|
|
582
|
-
setTimeout(function(){if(redirectUrl)window.location.
|
|
627
|
+
setTimeout(function(){if(redirectUrl)window.location.assign(redirectUrl)},900);
|
|
583
628
|
};
|
|
584
629
|
try{
|
|
585
630
|
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
@@ -677,7 +722,7 @@ ${cmdScript}
|
|
|
677
722
|
function showSuccess(workspaceName,redirectUrl,providerName){
|
|
678
723
|
var tpl=document.getElementById('tpl-connected');
|
|
679
724
|
var safeWs=String(workspaceName||'').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});
|
|
680
|
-
var safeProv=String(providerName||'your
|
|
725
|
+
var safeProv=String(providerName||'your assistant').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});
|
|
681
726
|
var safeUrl=String(redirectUrl||'').replace(/"/g,'"').replace(/[<>]/g,function(c){return{'<':'<','>':'>'}[c]});
|
|
682
727
|
var html=tpl.innerHTML.split('__WS__').join(safeWs).split('__URL__').join(safeUrl).split('__PROVIDER__').join(safeProv);
|
|
683
728
|
pOk.innerHTML=html;
|
|
@@ -865,7 +910,7 @@ app.post(
|
|
|
865
910
|
} catch {
|
|
866
911
|
process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
|
|
867
912
|
}
|
|
868
|
-
const code =
|
|
913
|
+
const code = randomUUID2();
|
|
869
914
|
pendingCodes.set(code, {
|
|
870
915
|
apiKey: api_key,
|
|
871
916
|
codeChallenge: code_challenge,
|
|
@@ -885,42 +930,13 @@ app.post(
|
|
|
885
930
|
}
|
|
886
931
|
);
|
|
887
932
|
function issueTokens(apiKey) {
|
|
888
|
-
const now = Date.now();
|
|
889
|
-
const refreshToken = `pb_rt_${randomUUID()}`;
|
|
890
|
-
let perKeyCount = 0;
|
|
891
|
-
let oldestKeyForApiKey = null;
|
|
892
|
-
let oldestAtForApiKey = Infinity;
|
|
893
|
-
for (const [k, v] of refreshTokens) {
|
|
894
|
-
if (v.apiKey === apiKey) {
|
|
895
|
-
perKeyCount++;
|
|
896
|
-
if (v.createdAt < oldestAtForApiKey) {
|
|
897
|
-
oldestAtForApiKey = v.createdAt;
|
|
898
|
-
oldestKeyForApiKey = k;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
if (perKeyCount >= MAX_REFRESH_TOKENS_PER_KEY && oldestKeyForApiKey) {
|
|
903
|
-
refreshTokens.delete(oldestKeyForApiKey);
|
|
904
|
-
}
|
|
905
|
-
if (refreshTokens.size >= MAX_REFRESH_TOKENS) {
|
|
906
|
-
let oldestKey = null;
|
|
907
|
-
let oldestAt = Infinity;
|
|
908
|
-
for (const [k, v] of refreshTokens) {
|
|
909
|
-
if (v.createdAt < oldestAt) {
|
|
910
|
-
oldestAt = v.createdAt;
|
|
911
|
-
oldestKey = k;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (oldestKey) refreshTokens.delete(oldestKey);
|
|
915
|
-
}
|
|
916
|
-
refreshTokens.set(refreshToken, { apiKey, createdAt: now });
|
|
917
933
|
return {
|
|
918
934
|
access_token: apiKey,
|
|
919
935
|
token_type: "Bearer",
|
|
920
936
|
// 1-year TTL: actual validity enforced by Convex, not by expiry clock.
|
|
921
937
|
// Long TTL prevents unnecessary refresh cycles after restarts.
|
|
922
938
|
expires_in: 365 * 24 * 3600,
|
|
923
|
-
refresh_token:
|
|
939
|
+
refresh_token: signRefreshToken(apiKey)
|
|
924
940
|
};
|
|
925
941
|
}
|
|
926
942
|
app.post(
|
|
@@ -931,19 +947,15 @@ app.post(
|
|
|
931
947
|
(req, res) => {
|
|
932
948
|
const { grant_type, code, code_verifier, redirect_uri, refresh_token } = req.body;
|
|
933
949
|
if (grant_type === "refresh_token") {
|
|
934
|
-
const
|
|
935
|
-
if (!
|
|
936
|
-
res.status(400).json({
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
refreshTokens.delete(refresh_token);
|
|
941
|
-
res.status(400).json({ error: "invalid_grant", error_description: "Refresh token expired" });
|
|
950
|
+
const verified = verifyRefreshToken(refresh_token);
|
|
951
|
+
if (!verified) {
|
|
952
|
+
res.status(400).json({
|
|
953
|
+
error: "invalid_grant",
|
|
954
|
+
error_description: "Invalid or expired refresh token"
|
|
955
|
+
});
|
|
942
956
|
return;
|
|
943
957
|
}
|
|
944
|
-
|
|
945
|
-
refreshTokens.delete(refresh_token);
|
|
946
|
-
res.json(issueTokens(apiKey));
|
|
958
|
+
res.json(issueTokens(verified.apiKey));
|
|
947
959
|
return;
|
|
948
960
|
}
|
|
949
961
|
if (grant_type !== "authorization_code") {
|
|
@@ -1117,7 +1129,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1117
1129
|
return;
|
|
1118
1130
|
}
|
|
1119
1131
|
const transport = new StreamableHTTPServerTransport({
|
|
1120
|
-
sessionIdGenerator: () =>
|
|
1132
|
+
sessionIdGenerator: () => randomUUID2(),
|
|
1121
1133
|
onsessioninitialized: (sid) => {
|
|
1122
1134
|
sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });
|
|
1123
1135
|
logSessionLifecycle("session_created", sid);
|
|
@@ -1134,6 +1146,16 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1134
1146
|
await server.connect(transport);
|
|
1135
1147
|
await transport.handleRequest(req, res, req.body);
|
|
1136
1148
|
logRequest("POST", "ok", transport.sessionId ?? void 0, Date.now() - reqStart);
|
|
1149
|
+
} else if (sessionId) {
|
|
1150
|
+
process.stderr.write(
|
|
1151
|
+
`[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_stale sessionId=${sessionId} (likely server restart \u2014 instructing client to re-initialise)
|
|
1152
|
+
`
|
|
1153
|
+
);
|
|
1154
|
+
res.status(404).json({
|
|
1155
|
+
jsonrpc: "2.0",
|
|
1156
|
+
error: { code: -32001, message: "Session not found \u2014 re-initialise" },
|
|
1157
|
+
id: null
|
|
1158
|
+
});
|
|
1137
1159
|
} else {
|
|
1138
1160
|
process.stderr.write(
|
|
1139
1161
|
`[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)
|
|
@@ -1171,8 +1193,16 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1171
1193
|
return;
|
|
1172
1194
|
}
|
|
1173
1195
|
const sessionId = req.headers["mcp-session-id"];
|
|
1174
|
-
if (!sessionId
|
|
1175
|
-
res.status(400).send("
|
|
1196
|
+
if (!sessionId) {
|
|
1197
|
+
res.status(400).send("Missing Mcp-Session-Id header");
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (!sessions.has(sessionId)) {
|
|
1201
|
+
process.stderr.write(
|
|
1202
|
+
`[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_stale GET sessionId=${sessionId}
|
|
1203
|
+
`
|
|
1204
|
+
);
|
|
1205
|
+
res.status(404).send("Session not found \u2014 re-initialise");
|
|
1176
1206
|
return;
|
|
1177
1207
|
}
|
|
1178
1208
|
try {
|
|
@@ -1208,8 +1238,16 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1208
1238
|
return;
|
|
1209
1239
|
}
|
|
1210
1240
|
const sessionId = req.headers["mcp-session-id"];
|
|
1211
|
-
if (!sessionId
|
|
1212
|
-
res.status(400).send("
|
|
1241
|
+
if (!sessionId) {
|
|
1242
|
+
res.status(400).send("Missing Mcp-Session-Id header");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (!sessions.has(sessionId)) {
|
|
1246
|
+
process.stderr.write(
|
|
1247
|
+
`[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_stale DELETE sessionId=${sessionId}
|
|
1248
|
+
`
|
|
1249
|
+
);
|
|
1250
|
+
res.status(404).send("Session not found \u2014 re-initialise");
|
|
1213
1251
|
return;
|
|
1214
1252
|
}
|
|
1215
1253
|
try {
|