@productbrain/mcp 0.0.1-beta.83 → 0.0.1-beta.914
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-WXT35272.js → chunk-7TU5QHN7.js} +7413 -2385
- package/dist/chunk-7TU5QHN7.js.map +1 -0
- package/dist/{chunk-RQXM3TCI.js → chunk-YMF3IQ5E.js} +205 -1
- package/dist/chunk-YMF3IQ5E.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +945 -81
- package/dist/http.js.map +1 -1
- package/dist/index.js +6 -12
- package/dist/index.js.map +1 -1
- package/dist/{setup-GQ3LQS2L.js → setup-RYYXRDPB.js} +5 -5
- package/dist/setup-RYYXRDPB.js.map +1 -0
- package/dist/views/src/graph-constellation/index.html +1 -1
- package/package.json +2 -1
- package/dist/chunk-G4JJNINW.js +0 -3441
- package/dist/chunk-G4JJNINW.js.map +0 -1
- package/dist/chunk-RQXM3TCI.js.map +0 -1
- package/dist/chunk-WXT35272.js.map +0 -1
- package/dist/setup-GQ3LQS2L.js.map +0 -1
- package/dist/smart-capture-HRJL7SGD.js +0 -41
- package/dist/smart-capture-HRJL7SGD.js.map +0 -1
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * HTTP transport entry point for Product Brain MCP.\n *\n * Serves the MCP protocol over Streamable HTTP for web clients\n * (Claude web app, API consumers) that can't spawn local processes.\n *\n * Implements the full MCP OAuth 2.1 spec (Nov 2025):\n * 1. Protected Resource Metadata (/.well-known/oauth-protected-resource)\n * 2. Authorization Server Metadata (/.well-known/oauth-authorization-server)\n * 3. Dynamic Client Registration (POST /register)\n * 4. Authorization Code + PKCE (GET/POST /authorize)\n * 5. Token Exchange (POST /oauth/token)\n *\n * Env:\n * CONVEX_SITE_URL — Convex deployment URL (defaults to cloud)\n * PORT / MCP_PORT — Listen port (default 3000)\n * CORS_ORIGINS — Comma-separated allowed origins (default: all)\n * PB_MODULES — Comma-separated modules (default: core,gitchain,arch)\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport express from \"express\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport rateLimit from \"express-rate-limit\";\n\nimport { bootstrapHttp } from \"./client.js\";\nimport { runWithAuth } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\nfunction baseUrl(req: any): string {\n const proto = req.headers[\"x-forwarded-proto\"] ?? req.protocol ?? \"http\";\n const host = req.headers.host ?? `localhost:${PORT}`;\n return `${proto}://${host}`;\n}\n\n// ── Express App ─────────────────────────────────────────────────────────\n\nconst app = express();\n// Required when behind a reverse proxy (e.g. Railway): rate limiter uses X-Forwarded-For\n// and throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR if trust proxy is false.\napp.set(\"trust proxy\", 1);\napp.use(express.json());\n\n// CORS — fail-closed; requires CORS_ORIGINS to be explicitly configured.\nconst ALLOWED_ORIGINS = process.env.CORS_ORIGINS\n ?.split(\",\")\n .map((o) => o.trim())\n .filter(Boolean);\n\napp.use((_req: any, res: any, next: any) => {\n const origin = _req.headers.origin;\n if (ALLOWED_ORIGINS && origin && ALLOWED_ORIGINS.includes(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id\",\n );\n res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id\");\n if (_req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n next();\n});\n\n// ── OAuth: Protected Resource Metadata (RFC 9728) ────────────────────────\n// Step 1 of MCP auth: Claude fetches this to discover the authorization server.\n\napp.get(\"/.well-known/oauth-protected-resource\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n resource: base,\n authorization_servers: [base],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n bearer_methods_supported: [\"header\"],\n });\n});\n\n// ── OAuth: Authorization Server Metadata (RFC 8414) ──────────────────────\n// Step 2: Claude fetches this to discover authorize, token, and register endpoints.\n\napp.get(\"/.well-known/oauth-authorization-server\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n issuer: base,\n authorization_endpoint: `${base}/authorize`,\n token_endpoint: `${base}/oauth/token`,\n registration_endpoint: `${base}/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n });\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: Claude registers itself as a client before starting the auth flow.\n\ninterface RegisteredClient {\n client_id: string;\n redirect_uris: string[];\n client_name?: string;\n registeredAt: number;\n}\n\nconst registeredClients = new Map<string, RegisteredClient>();\n\napp.post(\n \"/register\",\n express.json(),\n (req: any, res: any) => {\n const { redirect_uris, client_name } = req.body;\n\n if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {\n res.status(400).json({\n error: \"invalid_client_metadata\",\n error_description: \"redirect_uris is required\",\n });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const client: RegisteredClient = {\n client_id: clientId,\n redirect_uris,\n client_name,\n registeredAt: Date.now(),\n };\n registeredClients.set(clientId, client);\n\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris,\n grant_types: [\"authorization_code\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n);\n\n// ── OAuth: Authorization Code + PKCE ─────────────────────────────────────\n// Step 4: User enters their pb_sk_* key, server generates a one-time code.\n\ninterface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\nconst pendingCodes = new Map<string, PendingAuth>();\n\n// Refresh token store — declared here so the cleanup interval can reference it.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60_000; // 90 days\n\ninterface RefreshEntry {\n apiKey: string;\n createdAt: number;\n}\n\nconst refreshTokens = new Map<string, RefreshEntry>();\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n for (const [id, client] of registeredClients) {\n if (now - client.registeredAt > 24 * 60 * 60_000) registeredClients.delete(id);\n }\n for (const [token, entry] of refreshTokens) {\n if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"<>]/g, (c) =>\n ({ \"&\": \"&\", '\"': \""\", \"<\": \"<\", \">\": \">\" })[c]!,\n );\n}\n\napp.get(\"/authorize\", (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state } =\n req.query;\n res.type(\"html\").send(`<!DOCTYPE html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Authorize — Product Brain</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:-apple-system,system-ui,sans-serif;background:#0a0a0a;color:#e5e5e5;\n display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1rem}\n.card{background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:2rem;max-width:400px;width:100%}\nh1{font-size:1.2rem;margin-bottom:.25rem}\n.sub{color:#999;font-size:.85rem;margin-bottom:1.5rem}\nlabel{display:block;font-size:.85rem;margin-bottom:.4rem;color:#ccc}\ninput[type=password]{width:100%;padding:.6rem .75rem;background:#111;border:1px solid #444;\n border-radius:8px;color:#e5e5e5;font:.85rem/1.4 monospace}\ninput:focus{outline:none;border-color:#7c3aed}\nbutton{width:100%;padding:.6rem;background:#7c3aed;color:#fff;border:none;\n border-radius:8px;font-size:.85rem;cursor:pointer;margin-top:1rem}\nbutton:hover{background:#6d28d9}\n.err{color:#ef4444;font-size:.8rem;margin-top:.5rem;display:none}\n</style></head><body>\n<div class=\"card\">\n<h1>Product Brain</h1>\n<p class=\"sub\">Enter your API key to connect Claude to your workspace.</p>\n<form method=\"POST\" action=\"/authorize\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${esc(redirect_uri)}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${esc(code_challenge)}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${esc(code_challenge_method)}\">\n <input type=\"hidden\" name=\"state\" value=\"${esc(state)}\">\n <label for=\"k\">API Key</label>\n <input type=\"password\" id=\"k\" name=\"api_key\" placeholder=\"pb_sk_…\" required autofocus>\n <p class=\"err\" id=\"e\">Key must start with pb_sk_</p>\n <button type=\"submit\">Authorize</button>\n</form>\n</div>\n<script>document.querySelector(\"form\").onsubmit=function(e){\nif(!document.getElementById(\"k\").value.startsWith(\"pb_sk_\")){\ne.preventDefault();document.getElementById(\"e\").style.display=\"block\"}}</script>\n</body></html>`);\n});\n\napp.post(\n \"/authorize\",\n express.urlencoded({ extended: false }),\n (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, state } = req.body;\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n res.status(400).send(\"Invalid API key\");\n return;\n }\n\n const code = randomUUID();\n pendingCodes.set(code, {\n apiKey: api_key,\n codeChallenge: code_challenge,\n redirectUri: redirect_uri,\n expiresAt: Date.now() + 5 * 60_000,\n });\n\n const url = new URL(redirect_uri);\n url.searchParams.set(\"code\", code);\n if (state) url.searchParams.set(\"state\", state);\n res.redirect(302, url.toString());\n },\n);\n\n// ── OAuth: Token Exchange ────────────────────────────────────────────────\n// Step 5: Claude exchanges the authorization code (with PKCE verifier) for a token.\n// Supports both authorization_code and refresh_token grants.\n\nfunction issueTokens(apiKey: string): object {\n const refreshToken = `pb_rt_${randomUUID()}`;\n refreshTokens.set(refreshToken, { apiKey, createdAt: Date.now() });\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n expires_in: ACCESS_TOKEN_TTL,\n refresh_token: refreshToken,\n };\n}\n\napp.post(\n \"/oauth/token\",\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri, refresh_token } =\n req.body;\n\n if (grant_type === \"refresh_token\") {\n const entry = refreshTokens.get(refresh_token);\n if (!entry) {\n res.status(400).json({ error: \"invalid_grant\", error_description: \"Invalid refresh token\" });\n return;\n }\n if (Date.now() - entry.createdAt > REFRESH_TOKEN_TTL_MS) {\n refreshTokens.delete(refresh_token);\n res.status(400).json({ error: \"invalid_grant\", error_description: \"Refresh token expired\" });\n return;\n }\n // Rotate: revoke old, issue new pair\n const apiKey = entry.apiKey;\n refreshTokens.delete(refresh_token);\n res.json(issueTokens(apiKey));\n return;\n }\n\n if (grant_type !== \"authorization_code\") {\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n const pending = pendingCodes.get(code);\n if (!pending || pending.redirectUri !== redirect_uri) {\n res.status(400).json({ error: \"invalid_grant\" });\n return;\n }\n\n // PKCE S256 validation\n const challenge = createHash(\"sha256\")\n .update(code_verifier ?? \"\")\n .digest(\"base64url\");\n if (challenge !== pending.codeChallenge) {\n pendingCodes.delete(code);\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"PKCE verification failed\",\n });\n return;\n }\n\n pendingCodes.delete(code);\n res.json(issueTokens(pending.apiKey));\n },\n);\n\n// ── Rate Limiting ────────────────────────────────────────────────────────\n\nconst mcpLimiter = rateLimit({\n windowMs: 60_000,\n max: 120,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many requests. Try again later.\" },\n});\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n logSessionLifecycle(\"session_deleted\", id, \"ttl\");\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n const sorted = [...sessions.entries()].sort(\n (a, b) => a[1].lastAccess - b[1].lastAccess,\n );\n for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n\nfunction extractBearerKey(req: any): string | null {\n const header = req.headers?.authorization;\n if (typeof header !== \"string\" || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7).trim();\n return token.startsWith(\"pb_sk_\") ? token : null;\n}\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"POST\", \"auth_fail\");\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey }, async () => {\n if (sessionId && sessions.has(sessionId)) {\n const entry = sessions.get(sessionId)!;\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", sessionId, Date.now() - reqStart);\n } else if (!sessionId && isInitializeRequest(req.body)) {\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sid: string) => {\n sessions.set(sid, { transport, lastAccess: Date.now() });\n logSessionLifecycle(\"session_created\", sid);\n },\n });\n\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) {\n logSessionLifecycle(\"session_deleted\", sid, \"onclose\");\n sessions.delete(sid);\n }\n };\n\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", transport.sessionId ?? undefined, Date.now() - reqStart);\n } else {\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)\\n`,\n );\n res.status(400).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Bad Request: no valid session ID provided\" },\n id: null,\n });\n }\n });\n } catch (err: any) {\n logRequest(\"POST\", \"error\", sessionId, Date.now() - reqStart);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n }\n});\n\napp.get(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"GET\", \"auth_fail\");\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).send(\"Invalid or missing session ID\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res);\n logRequest(\"GET\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"GET\", \"error\", sessionId);\n }\n});\n\napp.delete(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"DELETE\", \"auth_fail\");\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId || !sessions.has(sessionId)) {\n res.status(400).send(\"Invalid or missing session ID\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n await entry.transport.handleRequest(req, res);\n logRequest(\"DELETE\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"DELETE\", \"error\", sessionId);\n }\n});\n\n// ── Start ───────────────────────────────────────────────────────────────\n\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);\n});\n\nprocess.on(\"uncaughtException\", (err) => {\n console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);\n gracefulShutdown();\n});\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n setTimeout(() => process.exit(1), 3_000).unref();\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\nconst httpServer = app.listen(PORT, \"127.0.0.1\", () => {\n console.log(`Product Brain MCP HTTP server v${SERVER_VERSION} listening on port ${PORT}`);\n});\nhttpServer.on(\"error\", (err) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,SAAS,YAAY,kBAAkB;AACvC,OAAO,aAAa;AACpB,SAAS,qCAAqC;AAC9C,SAAS,2BAA2B;AACpC,OAAO,eAAe;AAUtB,cAAc;AACd,cAAc;AACd,iBAAiB,iBAAiB,CAAC;AAEnC,IAAM,OAAO,SAAS,QAAQ,IAAI,QAAQ,QAAQ,IAAI,YAAY,QAAQ,EAAE;AAE5E,SAAS,QAAQ,KAAkB;AACjC,QAAM,QAAQ,IAAI,QAAQ,mBAAmB,KAAK,IAAI,YAAY;AAClE,QAAM,OAAO,IAAI,QAAQ,QAAQ,aAAa,IAAI;AAClD,SAAO,GAAG,KAAK,MAAM,IAAI;AAC3B;AAIA,IAAM,MAAM,QAAQ;AAGpB,IAAI,IAAI,eAAe,CAAC;AACxB,IAAI,IAAI,QAAQ,KAAK,CAAC;AAGtB,IAAM,kBAAkB,QAAQ,IAAI,cAChC,MAAM,GAAG,EACV,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAEjB,IAAI,IAAI,CAAC,MAAW,KAAU,SAAc;AAC1C,QAAM,SAAS,KAAK,QAAQ;AAC5B,MAAI,mBAAmB,UAAU,gBAAgB,SAAS,MAAM,GAAG;AACjE,QAAI,UAAU,+BAA+B,MAAM;AAAA,EACrD;AACA,MAAI,UAAU,gCAAgC,4BAA4B;AAC1E,MAAI;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACA,MAAI,UAAU,iCAAiC,gBAAgB;AAC/D,MAAI,KAAK,WAAW,WAAW;AAC7B,QAAI,OAAO,GAAG,EAAE,IAAI;AACpB;AAAA,EACF;AACA,OAAK;AACP,CAAC;AAKD,IAAI,IAAI,yCAAyC,CAAC,KAAU,QAAa;AACvE,QAAM,OAAO,QAAQ,GAAG;AACxB,MAAI,KAAK;AAAA,IACP,UAAU;AAAA,IACV,uBAAuB,CAAC,IAAI;AAAA,IAC5B,kBAAkB,CAAC,aAAa,eAAe;AAAA,IAC/C,0BAA0B,CAAC,QAAQ;AAAA,EACrC,CAAC;AACH,CAAC;AAKD,IAAI,IAAI,2CAA2C,CAAC,KAAU,QAAa;AACzE,QAAM,OAAO,QAAQ,GAAG;AACxB,MAAI,KAAK;AAAA,IACP,QAAQ;AAAA,IACR,wBAAwB,GAAG,IAAI;AAAA,IAC/B,gBAAgB,GAAG,IAAI;AAAA,IACvB,uBAAuB,GAAG,IAAI;AAAA,IAC9B,0BAA0B,CAAC,MAAM;AAAA,IACjC,uBAAuB,CAAC,sBAAsB,eAAe;AAAA,IAC7D,kCAAkC,CAAC,MAAM;AAAA,IACzC,uCAAuC,CAAC,MAAM;AAAA,IAC9C,kBAAkB,CAAC,aAAa,eAAe;AAAA,EACjD,CAAC;AACH,CAAC;AAYD,IAAM,oBAAoB,oBAAI,IAA8B;AAE5D,IAAI;AAAA,EACF;AAAA,EACA,QAAQ,KAAK;AAAA,EACb,CAAC,KAAU,QAAa;AACtB,UAAM,EAAE,eAAe,YAAY,IAAI,IAAI;AAE3C,QAAI,CAAC,MAAM,QAAQ,aAAa,KAAK,cAAc,WAAW,GAAG;AAC/D,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,mBAAmB;AAAA,MACrB,CAAC;AACD;AAAA,IACF;AAEA,UAAM,WAAW,aAAa,WAAW,CAAC;AAC1C,UAAM,SAA2B;AAAA,MAC/B,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA,cAAc,KAAK,IAAI;AAAA,IACzB;AACA,sBAAkB,IAAI,UAAU,MAAM;AAEtC,QAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MACnB,WAAW;AAAA,MACX,aAAa,eAAe;AAAA,MAC5B;AAAA,MACA,aAAa,CAAC,oBAAoB;AAAA,MAClC,gBAAgB,CAAC,MAAM;AAAA,MACvB,4BAA4B;AAAA,IAC9B,CAAC;AAAA,EACH;AACF;AAYA,IAAM,eAAe,oBAAI,IAAyB;AAGlD,IAAM,mBAAmB;AACzB,IAAM,uBAAuB,KAAK,KAAK,KAAK;AAO5C,IAAM,gBAAgB,oBAAI,IAA0B;AAEpD,YAAY,MAAM;AAChB,QAAM,MAAM,KAAK,IAAI;AACrB,aAAW,CAAC,MAAM,IAAI,KAAK,cAAc;AACvC,QAAI,MAAM,KAAK,UAAW,cAAa,OAAO,IAAI;AAAA,EACpD;AACA,aAAW,CAAC,IAAI,MAAM,KAAK,mBAAmB;AAC5C,QAAI,MAAM,OAAO,eAAe,KAAK,KAAK,IAAQ,mBAAkB,OAAO,EAAE;AAAA,EAC/E;AACA,aAAW,CAAC,OAAO,KAAK,KAAK,eAAe;AAC1C,QAAI,MAAM,MAAM,YAAY,qBAAsB,eAAc,OAAO,KAAK;AAAA,EAC9E;AACF,GAAG,GAAM;AAET,SAAS,IAAI,GAAoB;AAC/B,SAAO,OAAO,KAAK,EAAE,EAAE;AAAA,IAAQ;AAAA,IAAW,CAAC,OACxC,EAAE,KAAK,SAAS,KAAK,UAAU,KAAK,QAAQ,KAAK,OAAO,GAAG,CAAC;AAAA,EAC/D;AACF;AAEA,IAAI,IAAI,cAAc,CAAC,KAAU,QAAa;AAC5C,QAAM,EAAE,cAAc,gBAAgB,uBAAuB,MAAM,IACjE,IAAI;AACN,MAAI,KAAK,MAAM,EAAE,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oDAwB4B,IAAI,YAAY,CAAC;AAAA,sDACf,IAAI,cAAc,CAAC;AAAA,6DACZ,IAAI,qBAAqB,CAAC;AAAA,6CAC1C,IAAI,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAUxC;AACf,CAAC;AAED,IAAI;AAAA,EACF;AAAA,EACA,QAAQ,WAAW,EAAE,UAAU,MAAM,CAAC;AAAA,EACtC,CAAC,KAAU,QAAa;AACtB,UAAM,EAAE,SAAS,cAAc,gBAAgB,MAAM,IAAI,IAAI;AAE7D,QAAI,CAAC,SAAS,WAAW,QAAQ,GAAG;AAClC,UAAI,OAAO,GAAG,EAAE,KAAK,iBAAiB;AACtC;AAAA,IACF;AAEA,UAAM,OAAO,WAAW;AACxB,iBAAa,IAAI,MAAM;AAAA,MACrB,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,aAAa;AAAA,MACb,WAAW,KAAK,IAAI,IAAI,IAAI;AAAA,IAC9B,CAAC;AAED,UAAM,MAAM,IAAI,IAAI,YAAY;AAChC,QAAI,aAAa,IAAI,QAAQ,IAAI;AACjC,QAAI,MAAO,KAAI,aAAa,IAAI,SAAS,KAAK;AAC9C,QAAI,SAAS,KAAK,IAAI,SAAS,CAAC;AAAA,EAClC;AACF;AAMA,SAAS,YAAY,QAAwB;AAC3C,QAAM,eAAe,SAAS,WAAW,CAAC;AAC1C,gBAAc,IAAI,cAAc,EAAE,QAAQ,WAAW,KAAK,IAAI,EAAE,CAAC;AACjE,SAAO;AAAA,IACL,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB;AACF;AAEA,IAAI;AAAA,EACF;AAAA,EACA,QAAQ,WAAW,EAAE,UAAU,MAAM,CAAC;AAAA,EACtC,QAAQ,KAAK;AAAA,EACb,CAAC,KAAU,QAAa;AACtB,UAAM,EAAE,YAAY,MAAM,eAAe,cAAc,cAAc,IACnE,IAAI;AAEN,QAAI,eAAe,iBAAiB;AAClC,YAAM,QAAQ,cAAc,IAAI,aAAa;AAC7C,UAAI,CAAC,OAAO;AACV,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,iBAAiB,mBAAmB,wBAAwB,CAAC;AAC3F;AAAA,MACF;AACA,UAAI,KAAK,IAAI,IAAI,MAAM,YAAY,sBAAsB;AACvD,sBAAc,OAAO,aAAa;AAClC,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,iBAAiB,mBAAmB,wBAAwB,CAAC;AAC3F;AAAA,MACF;AAEA,YAAM,SAAS,MAAM;AACrB,oBAAc,OAAO,aAAa;AAClC,UAAI,KAAK,YAAY,MAAM,CAAC;AAC5B;AAAA,IACF;AAEA,QAAI,eAAe,sBAAsB;AACvC,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,yBAAyB,CAAC;AACxD;AAAA,IACF;AAEA,UAAM,UAAU,aAAa,IAAI,IAAI;AACrC,QAAI,CAAC,WAAW,QAAQ,gBAAgB,cAAc;AACpD,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,gBAAgB,CAAC;AAC/C;AAAA,IACF;AAGA,UAAM,YAAY,WAAW,QAAQ,EAClC,OAAO,iBAAiB,EAAE,EAC1B,OAAO,WAAW;AACrB,QAAI,cAAc,QAAQ,eAAe;AACvC,mBAAa,OAAO,IAAI;AACxB,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,mBAAmB;AAAA,MACrB,CAAC;AACD;AAAA,IACF;AAEA,iBAAa,OAAO,IAAI;AACxB,QAAI,KAAK,YAAY,QAAQ,MAAM,CAAC;AAAA,EACtC;AACF;AAIA,IAAM,aAAa,UAAU;AAAA,EAC3B,UAAU;AAAA,EACV,KAAK;AAAA,EACL,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,SAAS,EAAE,OAAO,sCAAsC;AAC1D,CAAC;AAID,IAAI,IAAI,WAAW,CAAC,MAAW,QAAa;AAC1C,MAAI,KAAK,EAAE,QAAQ,MAAM,SAAS,gBAAgB,WAAW,OAAO,CAAC;AACvE,CAAC;AASD,IAAM,WAAW,oBAAI,IAA0B;AAC/C,IAAM,iBAAiB,KAAK,KAAK;AACjC,IAAM,eAAe;AAErB,SAAS,qBAA2B;AAClC,QAAM,MAAM,KAAK,IAAI;AACrB,aAAW,CAAC,IAAI,KAAK,KAAK,UAAU;AAClC,QAAI,MAAM,MAAM,aAAa,gBAAgB;AAC3C,0BAAoB,mBAAmB,IAAI,KAAK;AAChD,YAAM,UAAU,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACtC,eAAS,OAAO,EAAE;AAAA,IACpB;AAAA,EACF;AACA,MAAI,SAAS,OAAO,cAAc;AAChC,UAAM,SAAS,CAAC,GAAG,SAAS,QAAQ,CAAC,EAAE;AAAA,MACrC,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE;AAAA,IACnC;AACA,aAAS,IAAI,GAAG,IAAI,OAAO,SAAS,cAAc,KAAK;AACrD,0BAAoB,mBAAmB,OAAO,CAAC,EAAE,CAAC,GAAG,UAAU;AAC/D,aAAO,CAAC,EAAE,CAAC,EAAE,UAAU,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAC7C,eAAS,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;AAAA,IAC9B;AAAA,EACF;AACF;AAEA,YAAY,oBAAoB,GAAM;AAItC,SAAS,iBAAiB,KAAyB;AACjD,QAAM,SAAS,IAAI,SAAS;AAC5B,MAAI,OAAO,WAAW,YAAY,CAAC,OAAO,WAAW,SAAS,EAAG,QAAO;AACxE,QAAM,QAAQ,OAAO,MAAM,CAAC,EAAE,KAAK;AACnC,SAAO,MAAM,WAAW,QAAQ,IAAI,QAAQ;AAC9C;AAEA,SAAS,QAAQ,KAAU,KAAgB;AACzC,QAAM,OAAO,QAAQ,GAAG;AACxB,MACG,OAAO,GAAG,EACV;AAAA,IACC;AAAA,IACA,6BAA6B,IAAI;AAAA,EACnC,EACC,KAAK,EAAE,OAAO,eAAe,CAAC;AACnC;AAEA,SAAS,WACP,QACA,SACA,WACA,YACM;AACN,QAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,QAAM,MAAM,YAAY,YAAY,SAAS,KAAK;AAClD,QAAM,MAAM,cAAc,OAAO,aAAa,UAAU,OAAO;AAC/D,UAAQ,OAAO,MAAM,UAAU,EAAE,IAAI,MAAM,IAAI,OAAO,GAAG,GAAG,GAAG,GAAG;AAAA,CAAI;AACxE;AAEA,SAAS,oBACP,OACA,WACA,QACM;AACN,QAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,QAAM,IAAI,SAAS,WAAW,MAAM,KAAK;AACzC,UAAQ,OAAO,MAAM,UAAU,EAAE,IAAI,KAAK,YAAY,SAAS,GAAG,CAAC;AAAA,CAAI;AACzE;AAIA,IAAI,KAAK,QAAQ,YAAY,OAAO,KAAU,QAAa;AACzD,QAAM,SAAS,iBAAiB,GAAG;AACnC,MAAI,CAAC,QAAQ;AACX,eAAW,QAAQ,WAAW;AAC9B,YAAQ,KAAK,GAAG;AAChB;AAAA,EACF;AAEA,QAAM,YAAY,IAAI,QAAQ,gBAAgB;AAC9C,QAAM,WAAW,KAAK,IAAI;AAE1B,MAAI;AACF,UAAM,YAAY,EAAE,OAAO,GAAG,YAAY;AACxC,UAAI,aAAa,SAAS,IAAI,SAAS,GAAG;AACxC,cAAM,QAAQ,SAAS,IAAI,SAAS;AACpC,cAAM,aAAa,KAAK,IAAI;AAC5B,cAAM,MAAM,UAAU,cAAc,KAAK,KAAK,IAAI,IAAI;AACtD,mBAAW,QAAQ,MAAM,WAAW,KAAK,IAAI,IAAI,QAAQ;AAAA,MAC3D,WAAW,CAAC,aAAa,oBAAoB,IAAI,IAAI,GAAG;AACtD,cAAM,YAAY,IAAI,8BAA8B;AAAA,UAClD,oBAAoB,MAAM,WAAW;AAAA,UACrC,sBAAsB,CAAC,QAAgB;AACrC,qBAAS,IAAI,KAAK,EAAE,WAAW,YAAY,KAAK,IAAI,EAAE,CAAC;AACvD,gCAAoB,mBAAmB,GAAG;AAAA,UAC5C;AAAA,QACF,CAAC;AAED,kBAAU,UAAU,MAAM;AACxB,gBAAM,MAAM,UAAU;AACtB,cAAI,KAAK;AACP,gCAAoB,mBAAmB,KAAK,SAAS;AACrD,qBAAS,OAAO,GAAG;AAAA,UACrB;AAAA,QACF;AAEA,cAAM,SAAS,yBAAyB;AACxC,cAAM,OAAO,QAAQ,SAAS;AAC9B,cAAM,UAAU,cAAc,KAAK,KAAK,IAAI,IAAI;AAChD,mBAAW,QAAQ,MAAM,UAAU,aAAa,QAAW,KAAK,IAAI,IAAI,QAAQ;AAAA,MAClF,OAAO;AACL,gBAAQ,OAAO;AAAA,UACb,WAAU,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA;AAAA,QACpC;AACA,YAAI,OAAO,GAAG,EAAE,KAAK;AAAA,UACnB,SAAS;AAAA,UACT,OAAO,EAAE,MAAM,OAAQ,SAAS,4CAA4C;AAAA,UAC5E,IAAI;AAAA,QACN,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAU;AACjB,eAAW,QAAQ,SAAS,WAAW,KAAK,IAAI,IAAI,QAAQ;AAC5D,QAAI,CAAC,IAAI,aAAa;AACpB,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,SAAS;AAAA,QACT,OAAO,EAAE,MAAM,QAAQ,SAAS,wBAAwB;AAAA,QACxD,IAAI;AAAA,MACN,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;AAED,IAAI,IAAI,QAAQ,YAAY,OAAO,KAAU,QAAa;AACxD,QAAM,SAAS,iBAAiB,GAAG;AACnC,MAAI,CAAC,QAAQ;AACX,eAAW,OAAO,WAAW;AAC7B,YAAQ,KAAK,GAAG;AAChB;AAAA,EACF;AAEA,QAAM,YAAY,IAAI,QAAQ,gBAAgB;AAC9C,MAAI,CAAC,aAAa,CAAC,SAAS,IAAI,SAAS,GAAG;AAC1C,QAAI,OAAO,GAAG,EAAE,KAAK,+BAA+B;AACpD;AAAA,EACF;AAEA,MAAI;AACF,UAAM,YAAY,EAAE,OAAO,GAAG,YAAY;AACxC,YAAM,QAAQ,SAAS,IAAI,SAAS;AACpC,YAAM,aAAa,KAAK,IAAI;AAC5B,YAAM,MAAM,UAAU,cAAc,KAAK,GAAG;AAC5C,iBAAW,OAAO,MAAM,SAAS;AAAA,IACnC,CAAC;AAAA,EACH,QAAQ;AACN,eAAW,OAAO,SAAS,SAAS;AAAA,EACtC;AACF,CAAC;AAED,IAAI,OAAO,QAAQ,YAAY,OAAO,KAAU,QAAa;AAC3D,QAAM,SAAS,iBAAiB,GAAG;AACnC,MAAI,CAAC,QAAQ;AACX,eAAW,UAAU,WAAW;AAChC,YAAQ,KAAK,GAAG;AAChB;AAAA,EACF;AAEA,QAAM,YAAY,IAAI,QAAQ,gBAAgB;AAC9C,MAAI,CAAC,aAAa,CAAC,SAAS,IAAI,SAAS,GAAG;AAC1C,QAAI,OAAO,GAAG,EAAE,KAAK,+BAA+B;AACpD;AAAA,EACF;AAEA,MAAI;AACF,UAAM,YAAY,EAAE,OAAO,GAAG,YAAY;AACxC,YAAM,QAAQ,SAAS,IAAI,SAAS;AACpC,YAAM,MAAM,UAAU,cAAc,KAAK,GAAG;AAC5C,iBAAW,UAAU,MAAM,SAAS;AAAA,IACtC,CAAC;AAAA,EACH,QAAQ;AACN,eAAW,UAAU,SAAS,SAAS;AAAA,EACzC;AACF,CAAC;AAID,QAAQ,GAAG,sBAAsB,CAAC,WAAW;AAC3C,QAAM,MAAM,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;AACpE,UAAQ,MAAM,mCAAmC,GAAG,EAAE;AACxD,CAAC;AAED,QAAQ,GAAG,qBAAqB,CAAC,QAAQ;AACvC,UAAQ,MAAM,kCAAkC,IAAI,SAAS,IAAI,OAAO,EAAE;AAC1E,mBAAiB;AACnB,CAAC;AAED,IAAI,eAAe;AACnB,eAAe,mBAAmB;AAChC,MAAI,aAAc;AAClB,iBAAe;AACf,aAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,GAAK,EAAE,MAAM;AAC/C,UAAQ,IAAI,kBAAkB;AAC9B,aAAW,CAAC,EAAE,KAAK,KAAK,UAAU;AAChC,UAAM,MAAM,UAAU,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC9C;AACA,MAAI;AACF,UAAM,kBAAkB;AAAA,EAC1B,QAAQ;AAAA,EAER;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,aAAa,IAAI,OAAO,MAAM,aAAa,MAAM;AACrD,UAAQ,IAAI,kCAAkC,cAAc,sBAAsB,IAAI,EAAE;AAC1F,CAAC;AACD,WAAW,GAAG,SAAS,CAAC,QAAQ;AAC9B,UAAQ,MAAM,4BAA4B,IAAI,OAAO,EAAE;AACvD,UAAQ,KAAK,CAAC;AAChB,CAAC;AAED,QAAQ,GAAG,UAAU,gBAAgB;AACrC,QAAQ,GAAG,WAAW,gBAAgB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/http.ts","../src/brand/logo-markup.ts","../src/lib/refresh-token.ts"],"sourcesContent":["/**\n * HTTP transport entry point for Product Brain MCP.\n *\n * Serves the MCP protocol over Streamable HTTP for web clients\n * (Claude web app, API consumers) that can't spawn local processes.\n *\n * Implements the full MCP OAuth 2.1 spec (Nov 2025):\n * 1. Protected Resource Metadata (/.well-known/oauth-protected-resource)\n * 2. Authorization Server Metadata (/.well-known/oauth-authorization-server)\n * 3. Dynamic Client Registration (POST /register)\n * 4. Authorization Code + PKCE (GET/POST /authorize)\n * 5. Token Exchange (POST /oauth/token)\n *\n * Env:\n * CONVEX_SITE_URL — Convex deployment URL (defaults to cloud)\n * PORT / MCP_PORT — Listen port (default 3000)\n * CORS_ORIGINS — Comma-separated allowed origins (default: all)\n * PB_MODULES — Comma-separated modules (default: core,gitchain,arch)\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport express from \"express\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport rateLimit from \"express-rate-limit\";\n\nimport { bootstrapHttp, DEFAULT_CLOUD_URL } from \"./client.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\nimport { appLogoMarkup, appLogoStyles } from \"./brand/logo-markup.js\";\nimport {\n signRefreshToken,\n verifyRefreshToken,\n} from \"./lib/refresh-token.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\nfunction baseUrl(req: any): string {\n const proto = req.headers[\"x-forwarded-proto\"] ?? req.protocol ?? \"http\";\n const host = req.headers.host ?? `localhost:${PORT}`;\n return `${proto}://${host}`;\n}\n\n// ── Express App ─────────────────────────────────────────────────────────\n\nconst app = express();\n// Required when behind a reverse proxy (e.g. Railway): rate limiter uses X-Forwarded-For\n// and throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR if trust proxy is false.\napp.set(\"trust proxy\", 1);\napp.use(express.json());\n\n// CORS — defaults to https://claude.ai (per REMOTE.md). Override via CORS_ORIGINS env var.\nconst ALLOWED_ORIGINS = (process.env.CORS_ORIGINS ?? \"https://claude.ai\")\n .split(\",\")\n .map((o) => o.trim())\n .filter(Boolean);\n\napp.use((_req: any, res: any, next: any) => {\n const origin = _req.headers.origin;\n if (origin && ALLOWED_ORIGINS.includes(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id\",\n );\n res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id\");\n if (_req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n next();\n});\n\n// ── OAuth: Protected Resource Metadata (RFC 9728) ────────────────────────\n// Step 1 of MCP auth: Claude fetches this to discover the authorization server.\n\napp.get(\"/.well-known/oauth-protected-resource\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n resource: base,\n authorization_servers: [base],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n bearer_methods_supported: [\"header\"],\n });\n});\n\n// ── OAuth: Authorization Server Metadata (RFC 8414) ──────────────────────\n// Step 2: Claude fetches this to discover authorize, token, and register endpoints.\n\napp.get(\"/.well-known/oauth-authorization-server\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n issuer: base,\n authorization_endpoint: `${base}/authorize`,\n token_endpoint: `${base}/oauth/token`,\n registration_endpoint: `${base}/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n });\n});\n\n// ── OAuth: Rate Limiting (Fix 2) ─────────────────────────────────────────\n// Separate, stricter limiter for auth endpoints to prevent brute-force and\n// enumeration attacks on the OAuth flow.\n\nconst authLimiter = rateLimit({\n windowMs: 60_000,\n max: 20,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many auth requests. Try again later.\" },\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: Claude registers itself as a client before starting the auth flow.\n\ninterface RegisteredClient {\n client_id: string;\n redirect_uris: string[];\n client_name?: string;\n registeredAt: number;\n}\n\nconst registeredClients = new Map<string, RegisteredClient>();\n// Fix 4 — Cap client registrations at 500 to prevent unbounded memory growth.\nconst MAX_REGISTERED_CLIENTS = 500;\n\napp.post(\n \"/register\",\n authLimiter,\n express.json(),\n (req: any, res: any) => {\n // Fix 4 — Reject registration when cap is reached.\n if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {\n res.status(503).json({\n error: \"server_error\",\n error_description: \"Registration limit reached. Try again later.\",\n });\n return;\n }\n\n const { redirect_uris, client_name } = req.body;\n\n if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {\n res.status(400).json({\n error: \"invalid_client_metadata\",\n error_description: \"redirect_uris is required\",\n });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const client: RegisteredClient = {\n client_id: clientId,\n redirect_uris,\n client_name,\n registeredAt: Date.now(),\n };\n registeredClients.set(clientId, client);\n\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris,\n grant_types: [\"authorization_code\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n);\n\n// ── OAuth: Authorization Code + PKCE ─────────────────────────────────────\n// Step 4: User enters their pb_sk_* key, server generates a one-time code.\n\ninterface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\nconst pendingCodes = new Map<string, PendingAuth>();\n\n// Refresh tokens are now stateless HMAC-signed (see lib/refresh-token.ts).\n// No in-memory store, no cleanup loop, no per-key caps — they survive\n// Railway redeploys, which fixes TEN-1661.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1000;\n\n// Fix 1 — Opaque access token store.\n// Maps pb_at_<uuid> → { apiKey, createdAt } so the raw pb_sk_* key is never\n// exposed through the OAuth flow. Capped at 1000 entries with LRU eviction.\ninterface AccessTokenEntry {\n apiKey: string;\n createdAt: number;\n}\nconst accessTokens = new Map<string, AccessTokenEntry>();\nconst MAX_ACCESS_TOKENS = 1000;\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n for (const [id, client] of registeredClients) {\n if (now - client.registeredAt > 24 * 60 * 60_000) registeredClients.delete(id);\n }\n // Fix 1 — Evict expired opaque access tokens.\n for (const [token, entry] of accessTokens) {\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);\n }\n // Fix 5 — Clean up stale auth failure tracking entries.\n for (const [ip, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(ip);\n }\n }\n // Cap authFailures map size.\n if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {\n const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);\n for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {\n authFailures.delete(sorted[i][0]);\n }\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"'<>]/g, (c) =>\n ({ \"&\": \"&\", '\"': \""\", \"'\": \"'\", \"<\": \"<\", \">\": \">\" })[c]!,\n );\n}\n\n// ── Authorize Page Templates ─────────────────────────────────────────────\n// Parchment-dark OAuth pages — brand tokens from DEC-419 (brand-tokens.json).\n// Monochrome-first per DEC-417, warm temperature per DEC-418.\n// Provider-agnostic: copy interpolates registered client_name (RFC 7591),\n// never hardcodes \"Claude\" — any MCP client may reach this page.\n\nfunction authPageShell(title: string, bodyContent: string, headExtra = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"parchment-dark\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${esc(title)} — Product Brain</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;700&display=swap\">\n${headExtra}\n<style>\n:root{\n --bg:#1a1917;--bg-warm:#201f1c;--surface:#262521;\n --fg1:#e4e0d8;--fg2:#c4bfb4;--fg3:#9a9589;--fg4:#6a6560;--fg-bright:#ffffff;\n --border:rgba(255,255,255,0.07);--border-light:rgba(255,255,255,0.04);\n --accent:#c9b99a;\n --btn-bg:#ffffff;--btn-fg:#1a1917;--btn-hover:#e4e0d8;\n --green:#4ade80;--rose:#ef4444;\n --ghost:rgba(38,37,33,0.55);\n --radius-md:7px;--radius-lg:10px;\n --font-display:\"Source Serif 4\",ui-serif,Georgia,serif;\n --font-body:\"IBM Plex Sans\",ui-sans-serif,system-ui,sans-serif;\n --font-mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace;\n}\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%}\nbody{\n font-family:var(--font-body);font-size:13px;line-height:1.45;\n color:var(--fg1);background:var(--bg);\n min-height:100vh;display:grid;place-items:center;padding:24px;\n -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;\n position:relative;overflow:hidden;\n}\nbody::before{\n content:\"\";position:fixed;inset:0;\n background:radial-gradient(900px 600px at 50% 50%,rgba(228,224,216,0.025),transparent 60%);\n pointer-events:none;z-index:0;\n}\n.top-mark{position:fixed;top:22px;left:24px;z-index:5;opacity:0.7}\n${appLogoStyles}\n.stage{\n width:100%;max-width:460px;text-align:center;\n position:relative;z-index:1;\n display:grid;\n}\n.panel{\n grid-area:1/1;\n transition:opacity 280ms ease-out,transform 380ms cubic-bezier(.2,.6,.2,1),filter 380ms ease-out;\n}\n.panel[hidden]{\n display:block !important;\n opacity:0;transform:scale(0.96) translateY(-2px);filter:blur(6px);\n pointer-events:none;\n}\n.panel:not([hidden]){opacity:1;transform:scale(1);filter:none;pointer-events:auto}\n\n/* eyebrows */\n.eyebrow{\n font-family:var(--font-mono);font-size:10px;font-weight:700;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg4);\n margin-bottom:24px;display:inline-flex;align-items:center;gap:7px;\n}\n.eyebrow .dot{width:5px;height:5px;border-radius:50%;background:currentColor}\n.eyebrow.danger{color:var(--rose)}\n.eyebrow.success{color:var(--green)}\n.eyebrow.success .dot{box-shadow:0 0 0 3px rgba(74,222,128,0.18)}\n\n/* form input */\n.input-wrap{\n display:flex;align-items:center;background:rgba(0,0,0,0.22);\n border:1px solid var(--border);border-radius:var(--radius-md);\n transition:border-color 200ms ease-out,box-shadow 200ms ease-out;\n}\n.input-wrap:focus-within{\n border-color:rgba(228,224,216,0.45);\n box-shadow:0 0 0 3px rgba(228,224,216,0.10);\n}\n.input-wrap.has-error{\n border-color:rgba(239,68,68,0.55);\n box-shadow:0 0 0 3px rgba(239,68,68,0.12);\n animation:shake 360ms cubic-bezier(.36,.07,.19,.97);\n}\n@keyframes shake{\n 10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}\n 30%,50%,70%{transform:translateX(-4px)}40%,60%{transform:translateX(4px)}\n}\n.input{\n flex:1;min-width:0;background:transparent;border:0;outline:none;\n padding:16px;\n font-family:var(--font-mono);font-size:14px;color:var(--fg1);letter-spacing:0.02em;\n}\n.input::placeholder{color:var(--fg4)}\n.hint{\n margin-top:10px;font-family:var(--font-mono);font-size:10.5px;\n letter-spacing:0.16em;text-transform:uppercase;\n text-align:left;padding-left:4px;height:14px;color:var(--fg4);\n transition:color 160ms ease-out;\n}\n.hint.is-error{color:var(--rose)}\n\n/* primary button */\n.btn-primary{\n width:100%;height:48px;margin-top:14px;\n border:0;border-radius:var(--radius-md);\n background:var(--btn-bg);color:var(--btn-fg);\n font-family:var(--font-body);font-size:14.5px;font-weight:600;letter-spacing:-0.005em;\n cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:10px;\n transition:background 140ms ease-out,transform 80ms ease-out,opacity 200ms ease-out;\n position:relative;overflow:hidden;\n}\n.btn-primary:hover:not([disabled]){background:var(--btn-hover)}\n.btn-primary:active:not([disabled]){transform:translateY(1px)}\n.btn-primary[disabled]{opacity:0.45;cursor:default}\n.spin{\n width:14px;height:14px;border-radius:50%;\n border:1.5px solid currentColor;border-top-color:transparent;\n animation:spin 700ms linear infinite;opacity:0.85;\n}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n/* secondary link */\n.small-link{\n margin-top:18px;font-family:var(--font-mono);font-size:11px;\n letter-spacing:0.18em;text-transform:uppercase;color:var(--fg4);\n}\n.small-link a{\n color:var(--fg3);text-decoration:none;border-bottom:1px dotted currentColor;\n padding-bottom:1px;transition:color 140ms;\n}\n.small-link a:hover{color:var(--fg1)}\n\n/* orb */\n.orb-wrap{\n position:relative;width:160px;height:160px;margin:0 auto 36px;\n display:grid;place-items:center;\n}\n.orb-ring{position:absolute;border-radius:50%}\n.orb-ring.r1{inset:0;border:1px solid rgba(228,224,216,0.06);animation:drift1 40s linear infinite}\n.orb-ring.r2{inset:18px;border:1px dashed rgba(228,224,216,0.10);animation:drift2 28s linear infinite}\n.orb-ring.r3{inset:38px;border:1px solid rgba(74,222,128,0.20);animation:ringPulse 3.4s ease-in-out infinite}\n@keyframes drift1{to{transform:rotate(360deg)}}\n@keyframes drift2{to{transform:rotate(-360deg)}}\n@keyframes ringPulse{0%,100%{opacity:0.6}50%{opacity:1}}\n\n.orb-wrap.is-verifying .orb-ring.r3{\n border-color:rgba(228,224,216,0.20);\n animation:ringPulse 1.1s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-core{\n box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06);\n animation:corePulseNeutral 1.6s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-dot{background:var(--fg3);box-shadow:0 0 10px rgba(228,224,216,0.4)}\n@keyframes corePulseNeutral{\n 0%,100%{box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(228,224,216,0.07),0 0 32px rgba(228,224,216,0.18),inset 0 0 22px rgba(228,224,216,0.10)}\n}\n\n.orb-wrap.is-error .orb-ring.r3{border-color:rgba(239,68,68,0.30);animation:none;opacity:1}\n.orb-wrap.is-error .orb-ring.r1,.orb-wrap.is-error .orb-ring.r2{animation-play-state:paused}\n.orb-wrap.is-error .orb-core{\n box-shadow:0 0 0 6px rgba(239,68,68,0.05),0 0 22px rgba(239,68,68,0.20),inset 0 0 16px rgba(239,68,68,0.08);\n animation:none;\n}\n.orb-wrap.is-error .orb-dot{background:var(--rose);box-shadow:0 0 10px rgba(239,68,68,0.6)}\n\n.sat-orbit{position:absolute;inset:0;pointer-events:none}\n.sat-orbit.o1{animation:drift1 40s linear infinite}\n.sat-orbit.o2{animation:drift2 56s linear infinite}\n.sat{\n position:absolute;top:50%;left:50%;\n font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.14em;\n background:var(--bg);padding:2px 6px;border-radius:3px;\n border:1px solid var(--border-light);\n transform-origin:0 0;opacity:0;\n}\n.panel:not([hidden])[data-state=\"connected\"] .sat{animation:satIn 600ms ease-out forwards}\n@keyframes satIn{from{opacity:0}to{opacity:1}}\n.sat span{display:inline-block;animation:counter 40s linear infinite}\n.sat-orbit.o2 .sat span{animation:counter2 56s linear infinite}\n@keyframes counter{to{transform:rotate(-360deg)}}\n@keyframes counter2{to{transform:rotate(360deg)}}\n\n.orb-core{\n position:relative;width:60px;height:60px;border-radius:50%;\n background:radial-gradient(circle at 50% 45%,#1a1a1a 0%,#0c0c0c 60%,#050505 100%);\n border:1px solid rgba(255,255,255,0.06);\n display:grid;place-items:center;\n box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 28px rgba(74,222,128,0.20),inset 0 0 18px rgba(74,222,128,0.08);\n animation:corePulse 3.4s ease-in-out infinite;\n}\n@keyframes corePulse{\n 0%,100%{box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 24px rgba(74,222,128,0.18),inset 0 0 16px rgba(74,222,128,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(74,222,128,0.06),0 0 40px rgba(74,222,128,0.32),inset 0 0 22px rgba(74,222,128,0.14)}\n}\n.orb-dot{width:10px;height:10px;border-radius:50%;background:#4ade80;box-shadow:0 0 12px rgba(74,222,128,0.7)}\n\n.core-shockwave{\n position:absolute;inset:0;border-radius:50%;\n border:1px solid rgba(74,222,128,0.6);\n opacity:0;pointer-events:none;\n}\n.panel:not([hidden])[data-state=\"connected\"] .core-shockwave{animation:shock 1100ms ease-out 200ms}\n@keyframes shock{\n 0%{opacity:0.7;transform:scale(0.4);border-width:2px}\n 100%{opacity:0;transform:scale(2.4);border-width:1px}\n}\n\n/* titles */\n.ok-title{\n font-family:var(--font-display);font-weight:600;\n font-size:38px;line-height:1.05;letter-spacing:-0.025em;\n color:var(--fg-bright);opacity:0;\n}\n.panel:not([hidden]) .ok-title{animation:rise 600ms ease-out 380ms forwards}\n.ok-lead{\n margin:18px auto 0;max-width:22em;font-size:16px;line-height:1.55;color:var(--fg2);\n opacity:0;\n}\n.panel:not([hidden]) .ok-lead{animation:rise 600ms ease-out 500ms forwards}\n.ok-phrase-row{\n margin-top:14px;display:flex;align-items:center;justify-content:center;\n opacity:0;\n}\n.panel:not([hidden]) .ok-phrase-row{animation:rise 600ms ease-out 620ms forwards}\n.cmd.is-copied .cmd-quote-part{display:none}\n.success-cta-wrap{\n margin-top:48px;width:100%;opacity:0;\n}\n.panel:not([hidden]) .success-cta-wrap{animation:rise 600ms ease-out 780ms forwards}\na.btn-primary.success-cta{color:var(--btn-fg);text-decoration:none}\n\n.cmd{\n display:inline-flex;align-items:center;gap:8px;vertical-align:middle;\n font-family:var(--font-mono);font-size:15px;font-weight:500;color:var(--accent);\n background:rgba(201,185,154,0.08);border:1px solid rgba(201,185,154,0.30);\n padding:5px 10px 5px 12px;border-radius:6px;letter-spacing:0.02em;\n cursor:pointer;user-select:none;\n transition:background 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;\n}\n.cmd:hover{background:rgba(201,185,154,0.14);border-color:rgba(201,185,154,0.50);color:#dcc9a4}\n.cmd.is-copied{color:var(--green);border-color:rgba(74,222,128,0.35);background:rgba(74,222,128,0.08)}\n.cmd .cmd-icon{\n width:12px;height:12px;color:currentColor;opacity:0.75;\n display:inline-grid;place-items:center;\n transition:opacity 140ms;\n}\n.cmd:hover .cmd-icon{opacity:1}\n.cmd.is-copied .cmd-icon{color:var(--green);opacity:1}\n.cmd .cmd-icon svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}\n\n/* error specifics */\n.err-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:12px;\n}\n.err-msg{\n font-size:14.5px;line-height:1.55;color:var(--fg3);\n margin-bottom:28px;\n}\n.err-msg code{\n font-family:var(--font-mono);font-size:12px;\n background:rgba(255,255,255,0.06);padding:1px 5px;border-radius:4px;color:var(--fg2);\n}\n.err-actions{display:flex;gap:8px}\n.btn-secondary{\n flex:1;height:44px;border-radius:var(--radius-md);\n background:transparent;color:var(--fg2);\n border:1px solid var(--border);\n font-family:var(--font-body);font-size:13.5px;font-weight:500;\n cursor:pointer;text-decoration:none;\n display:inline-flex;align-items:center;justify-content:center;\n transition:background 140ms,color 140ms,border-color 140ms;\n}\n.btn-secondary:hover{background:rgba(255,255,255,0.04);color:var(--fg1);border-color:rgba(255,255,255,0.14)}\n\n/* verifying */\n.verifying-eyebrow{\n font-family:var(--font-mono);font-size:10px;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg3);\n font-weight:700;margin-bottom:14px;\n}\n.verifying-title{\n font-family:var(--font-display);font-weight:600;\n font-size:28px;line-height:1.1;color:var(--fg1);margin-bottom:8px;\n letter-spacing:-0.02em;\n}\n.verifying-sub{font-size:13px;color:var(--fg3);min-height:1.45em}\n\n/* form heading */\n.form-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:10px;\n}\n.form-sub{font-size:13.5px;color:var(--fg3);margin-bottom:28px;line-height:1.55}\n\n@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n\n@media (prefers-reduced-motion: reduce){\n *,*::before,*::after{\n animation-duration:0.01ms !important;animation-iteration-count:1 !important;\n transition-duration:0.01ms !important;\n }\n}\n</style>\n</head><body>\n<div class=\"top-mark\">${appLogoMarkup({ size: \"sm\" })}</div>\n<div class=\"stage\">${bodyContent}</div>\n</body></html>`;\n}\n\n// Provider-agnostic display name. Trims and clamps to keep page chrome stable.\nfunction providerDisplayName(clientName: string | undefined): string {\n const name = (clientName ?? \"\").trim();\n if (!name) return \"your assistant\";\n return name.length > 40 ? name.slice(0, 40) + \"…\" : name;\n}\n\n// Inner HTML for the success panel. Used both by the JSON path (cloned client-side\n// from a <template>) and by the no-JS server-rendered fallback page.\nfunction successPanelInner(workspaceName: string, redirectUrl: string, providerName: string): string {\n return `\n<div class=\"orb-wrap\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"sat-orbit o1\">\n <span class=\"sat\" style=\"transform:rotate(20deg) translate(78px) rotate(-20deg);color:#4ade80;animation-delay:600ms\"><span>DEC</span></span>\n <span class=\"sat\" style=\"transform:rotate(140deg) translate(78px) rotate(-140deg);color:#c9b99a;animation-delay:720ms\"><span>WP</span></span>\n <span class=\"sat\" style=\"transform:rotate(260deg) translate(78px) rotate(-260deg);color:#f59e0b;animation-delay:840ms\"><span>TEN</span></span>\n </div>\n <div class=\"sat-orbit o2\">\n <span class=\"sat\" style=\"transform:rotate(80deg) translate(54px) rotate(-80deg);color:#60a5fa;animation-delay:960ms\"><span>STD</span></span>\n <span class=\"sat\" style=\"transform:rotate(220deg) translate(54px) rotate(-220deg);color:#a78bfa;animation-delay:1080ms\"><span>INS</span></span>\n </div>\n <div class=\"core-shockwave\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow success\"><span class=\"dot\"></span>Connected</div>\n<h1 class=\"ok-title\">Product Brain is Live</h1>\n<p class=\"ok-lead\">Return to your assistant, then say</p>\n<div class=\"ok-phrase-row\">\n <button class=\"cmd\" type=\"button\" data-cmd-pill data-redirect=\"${esc(redirectUrl)}\" aria-label=\"Copy "Start PB" and return to ${esc(providerName)}\">\n <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>\n <span class=\"cmd-icon\" aria-hidden=\"true\">\n <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>\n </span>\n </button>\n</div>\n<div class=\"success-cta-wrap\">\n <a class=\"btn-primary success-cta\" href=\"${esc(redirectUrl)}\">Continue in ${esc(providerName)}</a>\n</div>\n<!-- workspace name retained as data hook for tests, hidden from view -->\n<span hidden data-field=\"ws-name\">${esc(workspaceName)}</span>`;\n}\n\nfunction errorPanelInner(title: string, trustedDetailHtml: string, retryUrl: string): string {\n return `\n<div class=\"orb-wrap is-error\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow danger\"><span class=\"dot\"></span>Couldn't connect</div>\n<h2 class=\"err-title\" data-field=\"err-title\">${esc(title)}</h2>\n<p class=\"err-msg\" data-field=\"err-msg\">${trustedDetailHtml}</p>\n<div class=\"err-actions\">\n <a href=\"${esc(retryUrl)}\" class=\"btn-secondary\" data-retry-link>← Try again</a>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-secondary\">Get an API key →</a>\n</div>`;\n}\n\nconst cmdScript = `\n(function(){\n function bindCmd(pill){\n if(!pill||pill.__bound)return;pill.__bound=true;\n var textEl=pill.querySelector('[data-cmd-text]');\n var svgEl=pill.querySelector('[data-cmd-svg]');\n var redirectUrl=pill.getAttribute('data-redirect')||'';\n pill.addEventListener('click',function(){\n var done=function(){\n pill.classList.add('is-copied');\n if(textEl)textEl.textContent='Copied';\n if(svgEl)svgEl.innerHTML='<polyline points=\"4 12 10 18 20 6\"/>';\n setTimeout(function(){if(redirectUrl)window.location.assign(redirectUrl)},900);\n };\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){\n navigator.clipboard.writeText('Start PB').then(done,done);\n }else{done()}\n }catch(e){done()}\n });\n }\n document.querySelectorAll('[data-cmd-pill]').forEach(bindCmd);\n})();\n`;\n\nfunction authorizeFormPage(params: {\n redirect_uri: string;\n code_challenge: string;\n code_challenge_method: string;\n state: string;\n client_id: string;\n client_name?: string;\n}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const providerName = providerDisplayName(params.client_name);\n const body = `\n<!-- ─── CONNECT ─── -->\n<div class=\"panel\" id=\"p-connect\" data-state=\"connect\">\n <div class=\"eyebrow\">Connect Product Brain</div>\n <h1 class=\"form-title\">Paste your API key</h1>\n <p class=\"form-sub\">Give <span data-provider>${esc(providerName)}</span> access to your workspace memory.</p>\n <form method=\"POST\" action=\"/authorize\" id=\"f\" autocomplete=\"off\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${esc(redirect_uri)}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${esc(code_challenge)}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${esc(code_challenge_method)}\">\n <input type=\"hidden\" name=\"state\" value=\"${esc(state)}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\n <div class=\"input-wrap\" id=\"iw\">\n <input type=\"password\" id=\"k\" name=\"api_key\" class=\"input input-full\" placeholder=\"pb_sk_…\" required autofocus spellcheck=\"false\">\n </div>\n <div class=\"hint\" id=\"hint\" hidden></div>\n <button type=\"submit\" class=\"btn-primary\" id=\"sb\" disabled><span id=\"bt\">Connect</span></button>\n </form>\n <div class=\"small-link\"><a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\">No key? Generate one →</a></div>\n</div>\n\n<!-- ─── VERIFYING ─── -->\n<div class=\"panel\" id=\"p-verifying\" data-state=\"verifying\" hidden>\n <div class=\"orb-wrap is-verifying\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n </div>\n <div class=\"verifying-eyebrow\">Handshake</div>\n <h2 class=\"verifying-title\">Verifying key…</h2>\n <p class=\"verifying-sub\" id=\"verify-sub\">Checking workspace · …</p>\n</div>\n\n<!-- ─── CONNECTED (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-connected\" data-state=\"connected\" hidden></div>\n\n<!-- ─── ERROR (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-error\" data-state=\"error\" hidden></div>\n\n<template id=\"tpl-connected\">${successPanelInner(\"__WS__\", \"__URL__\", \"__PROVIDER__\")}</template>\n<template id=\"tpl-error\">${errorPanelInner(\"__TITLE__\", \"__DETAIL__\", \"__RETRY__\")}</template>\n\n<script>\n${cmdScript}\n(function(){\n var f=document.getElementById('f'),k=document.getElementById('k'),iw=document.getElementById('iw'),hint=document.getElementById('hint'),sb=document.getElementById('sb'),bt=document.getElementById('bt');\n var pConnect=document.getElementById('p-connect'),pVerify=document.getElementById('p-verifying'),pOk=document.getElementById('p-connected'),pErr=document.getElementById('p-error');\n var verifySub=document.getElementById('verify-sub');\n\n function show(panel){\n [pConnect,pVerify,pOk,pErr].forEach(function(p){\n if(p===panel){p.removeAttribute('hidden')}else{p.setAttribute('hidden','')}\n });\n }\n\n function syncInput(){\n sb.disabled=!k.value.trim();\n iw.classList.remove('has-error');\n hint.classList.remove('is-error');\n hint.textContent='';\n hint.setAttribute('hidden','');\n }\n k.addEventListener('input',syncInput);\n k.addEventListener('keydown',function(e){\n if(e.key==='Escape'){k.value='';syncInput()}\n });\n\n function showError(title,detailHtml){\n var tpl=document.getElementById('tpl-error');\n var html=tpl.innerHTML\n .replace('__TITLE__',title.replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]}))\n .replace('__DETAIL__',detailHtml)\n .replace('__RETRY__','#');\n pErr.innerHTML=html;\n var retry=pErr.querySelector('[data-retry-link]');\n if(retry){retry.addEventListener('click',function(e){e.preventDefault();show(pConnect);k.focus();k.select()})}\n show(pErr);\n }\n\n function showSuccess(workspaceName,redirectUrl,providerName){\n var tpl=document.getElementById('tpl-connected');\n var safeWs=String(workspaceName||'').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});\n var safeProv=String(providerName||'your assistant').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});\n var safeUrl=String(redirectUrl||'').replace(/\"/g,'"').replace(/[<>]/g,function(c){return{'<':'<','>':'>'}[c]});\n var html=tpl.innerHTML.split('__WS__').join(safeWs).split('__URL__').join(safeUrl).split('__PROVIDER__').join(safeProv);\n pOk.innerHTML=html;\n pOk.querySelectorAll('[data-cmd-pill]').forEach(function(pill){\n pill.__bound=false;\n });\n // Re-run binder\n var s=document.createElement('script');s.textContent=${JSON.stringify(cmdScript)};document.body.appendChild(s);s.remove();\n show(pOk);\n }\n\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var v=k.value.trim();\n if(!v){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Paste your key first';hint.removeAttribute('hidden');return}\n if(v.indexOf('pb_sk_')!==0){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Key must start with pb_sk_';hint.removeAttribute('hidden');return}\n sb.disabled=true;bt.textContent='Verifying';\n show(pVerify);\n\n var steps=['Checking workspace · …','Loading chain · …','Establishing memory · …'];\n var i=0;verifySub.textContent=steps[0];\n var ti=setInterval(function(){i++;if(i>=steps.length){clearInterval(ti);return}verifySub.textContent=steps[i]},900);\n\n var minDelay=new Promise(function(r){setTimeout(r,2800)});\n var fd=new FormData(f);\n var body=new URLSearchParams();\n fd.forEach(function(val,key){body.append(key,String(val))});\n\n var req=fetch('/authorize',{\n method:'POST',\n headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'},\n body:body.toString(),\n credentials:'same-origin'\n }).then(function(r){return r.json().then(function(j){return{status:r.status,body:j}})});\n\n Promise.all([req,minDelay]).then(function(arr){\n clearInterval(ti);\n var res=arr[0];\n if(res.body&&res.body.ok){\n showSuccess(res.body.workspaceName,res.body.redirectUrl,res.body.providerName);\n }else{\n showError(res.body&&res.body.title||'Couldn\\\\'t connect',res.body&&res.body.detail||'Try again, or generate a new key.');\n sb.disabled=false;bt.textContent='Connect';\n }\n }).catch(function(){\n clearInterval(ti);\n showError('Network error','We couldn\\\\'t reach Product Brain. Check your connection and try again.');\n sb.disabled=false;bt.textContent='Connect';\n });\n });\n\n k.focus();\n})();\n</script>`;\n return authPageShell(\"Connect Product Brain\", body);\n}\n\nfunction authorizeSuccessPage(workspaceName: string, redirectUrl: string, providerName: string): string {\n const body = `\n<div class=\"panel\" data-state=\"connected\">\n${successPanelInner(workspaceName, redirectUrl, providerName)}\n</div>\n<script>${cmdScript}</script>`;\n return authPageShell(\"Connected\", body);\n}\n\n// security: trustedDetailHtml must be a hardcoded literal — never pass user-controlled data.\nfunction authorizeErrorPage(title: string, trustedDetailHtml: string, retryUrl: string): string {\n const body = `\n<div class=\"panel\" data-state=\"error\">\n${errorPanelInner(title, trustedDetailHtml, retryUrl)}\n</div>`;\n return authPageShell(\"Connection error\", body);\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;\n const cid = String(client_id ?? \"\");\n const clientName = cid && registeredClients.has(cid)\n ? registeredClients.get(cid)!.client_name\n : undefined;\n res.type(\"html\").send(authorizeFormPage({\n redirect_uri: String(redirect_uri ?? \"\"),\n code_challenge: String(code_challenge ?? \"\"),\n code_challenge_method: String(code_challenge_method ?? \"S256\"),\n state: String(state ?? \"\"),\n client_id: cid,\n client_name: clientName,\n }));\n});\n\napp.post(\n \"/authorize\",\n authLimiter,\n express.urlencoded({ extended: false }),\n async (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n\n // Negotiate response shape: fetch path sets Accept: application/json,\n // form-post no-JS path gets the standard rebranded HTML page.\n const wantsJson = String(req.headers[\"accept\"] ?? \"\").includes(\"application/json\");\n\n // Build \"retry\" URL so error pages can link back to the form.\n const retryParams = new URLSearchParams({\n redirect_uri: redirect_uri ?? \"\",\n code_challenge: code_challenge ?? \"\",\n code_challenge_method: code_challenge_method ?? \"S256\",\n ...(state ? { state } : {}),\n ...(client_id ? { client_id } : {}),\n }).toString();\n const retryUrl = `/authorize?${retryParams}`;\n\n function sendError(title: string, trustedDetailHtml: string, status = 400): void {\n if (wantsJson) {\n res.status(status).json({ ok: false, title, detail: trustedDetailHtml });\n } else {\n res.status(status).type(\"html\").send(authorizeErrorPage(title, trustedDetailHtml, retryUrl));\n }\n }\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n sendError(\n \"Invalid key format\",\n \"API keys start with <code>pb_sk_</code>. Check your key and try again.\",\n );\n return;\n }\n\n // Validate redirect_uri against the registered client's allowed redirects.\n // Open redirect prevention: never trust a request-supplied redirect_uri without\n // checking it was pre-registered during dynamic client registration (RFC 7591).\n //\n // When client_id is provided but unknown (e.g. server restart wiped the in-memory Map\n // after claude.ai cached its client_id from a previous session), auto-re-register using\n // the supplied redirect_uri rather than hard-rejecting. The API key validation below is\n // the primary security gate; basic HTTPS validation guards against degenerate redirect URIs.\n if (!client_id) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"client_id is required\",\n });\n return;\n }\n if (!registeredClients.has(client_id)) {\n if (typeof redirect_uri === \"string\" && redirect_uri.startsWith(\"https://\")) {\n registeredClients.set(client_id, {\n client_id,\n redirect_uris: [redirect_uri],\n registeredAt: Date.now(),\n });\n process.stderr.write(`[authorize] auto-re-registered stale client_id after restart\\n`);\n } else {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"Unknown client_id and redirect_uri is not a valid https URL\",\n });\n return;\n }\n }\n\n const client = registeredClients.get(client_id)!;\n if (!client.redirect_uris.includes(redirect_uri)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"redirect_uri does not match any registered redirect for this client\",\n });\n return;\n }\n\n // Validate key against Convex before issuing the code.\n // DEC-789 S2: probe primary then fallback URLs so DEV keys work against PROD Railway MCP.\n let workspaceName = \"Your Workspace\";\n try {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n const fallbackUrls = (process.env.CONVEX_FALLBACK_URLS ?? \"\")\n .split(\",\").map((u) => u.trim().replace(/\\/$/, \"\")).filter(Boolean);\n const candidates = [primaryUrl, ...fallbackUrls];\n\n let foundUrl: string | undefined;\n let anyDefinitiveReject = false;\n\n for (const url of candidates) {\n let checkData: { ok: boolean; workspaceName?: string; deploymentUrl?: string } | null = null;\n try {\n const checkRes = await fetch(`${url}/api/key-check`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${api_key}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(5000),\n });\n checkData = await checkRes.json() as { ok: boolean; workspaceName?: string; deploymentUrl?: string };\n } catch {\n // This candidate unreachable — try next.\n continue;\n }\n if (checkData.ok) {\n if (checkData.workspaceName) workspaceName = checkData.workspaceName;\n foundUrl = checkData.deploymentUrl ?? url;\n break;\n }\n anyDefinitiveReject = true;\n }\n\n if (!foundUrl) {\n if (anyDefinitiveReject) {\n sendError(\n \"Key not recognized\",\n \"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.\",\n 401,\n );\n return;\n }\n // All candidates unreachable (network errors only) — fail-open.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n } else {\n getKeyState(api_key).deploymentUrl = foundUrl;\n }\n } catch {\n // Outer guard: fail-open so auth works even if the entire block throws.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n }\n\n const code = randomUUID();\n pendingCodes.set(code, {\n apiKey: api_key,\n codeChallenge: code_challenge,\n redirectUri: redirect_uri,\n expiresAt: Date.now() + 5 * 60_000,\n });\n\n const url = new URL(redirect_uri);\n url.searchParams.set(\"code\", code);\n if (state) url.searchParams.set(\"state\", state);\n const redirectUrl = url.toString();\n const providerName = providerDisplayName(client.client_name);\n\n if (wantsJson) {\n res.json({ ok: true, workspaceName, redirectUrl, providerName });\n } else {\n // No-JS path: server-rendered success page. User clicks the pill or the\n // Primary CTA + copy pill both complete the OAuth redirect — no auto-bounce.\n res.type(\"html\").send(authorizeSuccessPage(workspaceName, redirectUrl, providerName));\n }\n },\n);\n\n// ── OAuth: Token Exchange ────────────────────────────────────────────────\n// Step 5: Claude exchanges the authorization code (with PKCE verifier) for a token.\n// Supports both authorization_code and refresh_token grants.\n\nfunction issueTokens(apiKey: string): object {\n // Return the pb_sk_* key directly as the access_token so connections\n // survive server restarts. Railway deploys on every git push, wiping\n // in-memory Maps. pb_sk_* keys are long-lived (valid until explicitly\n // revoked in Cortex); actual validity is enforced by Convex per tool call.\n // extractBearerKey() already handles pb_sk_* with zero Map lookup.\n //\n // The refresh token is HMAC-signed and self-contained (TEN-1661, lib/\n // refresh-token.ts) so it also survives redeploys. No server-side store.\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1-year TTL: actual validity enforced by Convex, not by expiry clock.\n // Long TTL prevents unnecessary refresh cycles after restarts.\n expires_in: 365 * 24 * 3600,\n refresh_token: signRefreshToken(apiKey),\n };\n}\n\napp.post(\n \"/oauth/token\",\n authLimiter,\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri, refresh_token } =\n req.body;\n\n if (grant_type === \"refresh_token\") {\n const verified = verifyRefreshToken(refresh_token);\n if (!verified) {\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"Invalid or expired refresh token\",\n });\n return;\n }\n // Rotation = re-issuance. With stateless tokens there is no Map entry\n // to revoke; the new pair simply takes over from the next request.\n res.json(issueTokens(verified.apiKey));\n return;\n }\n\n if (grant_type !== \"authorization_code\") {\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n const pending = pendingCodes.get(code);\n if (!pending || pending.redirectUri !== redirect_uri) {\n res.status(400).json({ error: \"invalid_grant\" });\n return;\n }\n\n // PKCE S256 validation\n const challenge = createHash(\"sha256\")\n .update(code_verifier ?? \"\")\n .digest(\"base64url\");\n if (challenge !== pending.codeChallenge) {\n pendingCodes.delete(code);\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"PKCE verification failed\",\n });\n return;\n }\n\n pendingCodes.delete(code);\n res.json(issueTokens(pending.apiKey));\n },\n);\n\n// ── Rate Limiting ────────────────────────────────────────────────────────\n\nconst mcpLimiter = rateLimit({\n windowMs: 60_000,\n max: 120,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many requests. Try again later.\" },\n});\n\n// ── Auth Failure Backoff (Fix 5) ──────────────────────────────────────────\n// Per-IP progressive lockout for failed API key auth attempts to prevent\n// brute-force attacks through the MCP endpoints.\n\ninterface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\nconst authFailures = new Map<string, AuthFailureRecord>();\nconst AUTH_FAILURE_MAX = 10;\nconst AUTH_FAILURE_WINDOW_MS = 5 * 60_000; // 5 minutes\nconst AUTH_BLOCK_DURATION_MS = 15 * 60_000; // 15 minutes\nconst MAX_AUTH_FAILURE_ENTRIES = 10_000;\n\nfunction checkAuthBlock(ip: string): boolean {\n const rec = authFailures.get(ip);\n if (!rec) return false;\n return rec.blockedUntil > Date.now();\n}\n\nfunction recordAuthFailure(ip: string): void {\n const now = Date.now();\n const rec = authFailures.get(ip);\n\n if (!rec) {\n authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });\n return;\n }\n\n // Reset window if the first failure is outside the tracking window.\n if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {\n rec.count = 1;\n rec.firstFailure = now;\n rec.blockedUntil = 0;\n } else {\n rec.count++;\n if (rec.count >= AUTH_FAILURE_MAX) {\n rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;\n }\n }\n}\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n // Fix 3 — short hash of the API key that created this session. Used to\n // detect session hijacking when subsequent requests arrive with a different key.\n keyHash: string;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n// Fix 6 — prevent a single API key from monopolising all session slots.\nconst MAX_SESSIONS_PER_KEY = 5;\n\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n logSessionLifecycle(\"session_deleted\", id, \"ttl\");\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n const sorted = [...sessions.entries()].sort(\n (a, b) => a[1].lastAccess - b[1].lastAccess,\n );\n for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n\nfunction extractBearerKey(req: any): string | null {\n const header = req.headers?.authorization;\n if (typeof header !== \"string\" || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7).trim();\n\n // Fix 1 — Support both direct API keys (stdio/backward compat) and opaque\n // OAuth access tokens issued by issueTokens().\n if (token.startsWith(\"pb_sk_\")) {\n // Direct API key — accepted for stdio and backward compatibility.\n return token;\n }\n if (token.startsWith(\"pb_at_\")) {\n // Opaque OAuth access token — resolve to the underlying API key.\n const entry = accessTokens.get(token);\n if (!entry) return null;\n const now = Date.now();\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {\n // Expired — remove and reject.\n accessTokens.delete(token);\n return null;\n }\n return entry.apiKey;\n }\n return null;\n}\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"POST\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey }, async () => {\n if (sessionId && sessions.has(sessionId)) {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", sessionId, Date.now() - reqStart);\n } else if (!sessionId && isInitializeRequest(req.body)) {\n // Fix 6 — Enforce per-key session cap before creating a new session.\n const keyH = hashKey(apiKey);\n let keySessionCount = 0;\n for (const entry of sessions.values()) {\n if (entry.keyHash === keyH) keySessionCount++;\n }\n if (keySessionCount >= MAX_SESSIONS_PER_KEY) {\n res.status(429).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Too many sessions for this API key\" },\n id: null,\n });\n return;\n }\n\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (sid: string) => {\n // Fix 3 — Store a key hash with the session entry.\n sessions.set(sid, { transport, lastAccess: Date.now(), keyHash: keyH });\n logSessionLifecycle(\"session_created\", sid);\n },\n });\n\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) {\n logSessionLifecycle(\"session_deleted\", sid, \"onclose\");\n sessions.delete(sid);\n }\n };\n\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", transport.sessionId ?? undefined, Date.now() - reqStart);\n } else if (sessionId) {\n // Stale Mcp-Session-Id (client cached one from before a Railway restart\n // or after TTL eviction). Per MCP Streamable HTTP spec § Session\n // Management clause 3, the server MUST respond with HTTP 404 Not Found\n // so the client (clause 4) re-initialises with a fresh InitializeRequest\n // without prompting OAuth re-auth. Returning 400 here was the root cause\n // of \"Claude.ai re-auths after every deploy\".\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale sessionId=${sessionId} (likely server restart — instructing client to re-initialise)\\n`,\n );\n res.status(404).json({\n jsonrpc: \"2.0\",\n error: { code: -32001, message: \"Session not found — re-initialise\" },\n id: null,\n });\n } else {\n // Non-initialise request with no Mcp-Session-Id — per spec clause 2,\n // servers that require a session ID SHOULD respond with HTTP 400.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)\\n`,\n );\n res.status(400).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Bad Request: no valid session ID provided\" },\n id: null,\n });\n }\n });\n } catch (err: any) {\n logRequest(\"POST\", \"error\", sessionId, Date.now() - reqStart);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n }\n});\n\napp.get(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"GET\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale GET sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res);\n logRequest(\"GET\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"GET\", \"error\", sessionId);\n }\n});\n\napp.delete(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n res.status(429).json({ error: \"Too many failed auth attempts. Try again later.\" });\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"DELETE\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale DELETE sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n await entry.transport.handleRequest(req, res);\n logRequest(\"DELETE\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"DELETE\", \"error\", sessionId);\n }\n});\n\n// ── Start ───────────────────────────────────────────────────────────────\n\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);\n});\n\nprocess.on(\"uncaughtException\", (err) => {\n console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);\n gracefulShutdown();\n});\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n setTimeout(() => process.exit(1), 3_000).unref();\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n","// SSOT for the Product Brain APP logo (wordmark style).\n// Used by: src/lib/brand/AppLogo.svelte (Cortex UI) AND\n// packages/mcp-server (mirrored via prebuild) for /authorize and other agent surfaces.\n//\n// NOT used by marketing pages (they have their own brand treatment).\n//\n// To change the logo, edit ONLY this file. Both surfaces re-render automatically.\n\nexport type AppLogoSize = \"sm\" | \"md\";\n\nexport interface AppLogoOptions {\n size?: AppLogoSize;\n showWordmark?: boolean;\n className?: string;\n}\n\nconst SIZE_CLASSES: Record<AppLogoSize, string> = {\n sm: \"pb-logo--sm\",\n md: \"pb-logo--md\",\n};\n\n// Self-contained CSS for environments that can't load Svelte styles\n// (the MCP authorize page, embedded HTML responses, etc.).\n// Cortex UI consumers can rely on AppLogo.svelte's scoped styles instead;\n// the inline class hooks are designed not to collide.\nexport const appLogoStyles = `\n.pb-logo{display:inline-flex;align-items:center;gap:8px;color:inherit}\n.pb-logo__mark{\n border-radius:4px;background:#1c1e24;\n border:1px solid rgba(255,255,255,0.06);\n display:inline-grid;place-items:center;flex-shrink:0;\n}\n.pb-logo__core{border-radius:50%;background:var(--accent,#c9b99a)}\n.pb-logo__name{\n font-family:var(--font-mono,\"IBM Plex Mono\",ui-monospace,monospace);\n font-weight:500;text-transform:uppercase;\n letter-spacing:0.22em;color:var(--fg4,#6a6560);\n}\n.pb-logo--sm .pb-logo__mark{width:16px;height:16px}\n.pb-logo--sm .pb-logo__core{width:5px;height:5px}\n.pb-logo--sm .pb-logo__name{font-size:10.5px}\n.pb-logo--md .pb-logo__mark{width:24px;height:24px;border-radius:6px}\n.pb-logo--md .pb-logo__core{width:8px;height:8px}\n.pb-logo--md .pb-logo__name{font-size:13px}\n`;\n\nexport function appLogoMarkup(opts: AppLogoOptions = {}): string {\n const size = opts.size ?? \"sm\";\n const showWordmark = opts.showWordmark ?? true;\n const cls = [\"pb-logo\", SIZE_CLASSES[size], opts.className].filter(Boolean).join(\" \");\n const wordmark = showWordmark ? `<span class=\"pb-logo__name\">Product Brain</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\n}\n","/**\n * Stateless HMAC-signed OAuth refresh tokens.\n *\n * Why this exists (TEN-1661, DEC-783):\n * Railway redeploys on every git push, wiping in-memory Maps. The previous\n * `refreshTokens` Map made claude.ai's proactive refresh calls fail with\n * `invalid_grant`, forcing OAuth re-auth after every deploy. DEC-783 fixed\n * the access-token side by returning the underlying pb_sk_* key directly.\n * This module is the refresh-token equivalent: a self-contained,\n * HMAC-signed token format that needs no server-side state.\n *\n * Format: pb_rt_<base64url(payload)>.<base64url(hmac-sha256(payload))>\n * Payload: { k: apiKey, i: iat-ms, j: jti }\n *\n * The jti (j) field is reserved for future revocation/rotation work and is\n * not yet checked. Signature comparison uses crypto.timingSafeEqual.\n */\n\nimport { createHmac, randomBytes, randomUUID, timingSafeEqual } from \"node:crypto\";\n\nexport const REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60_000; // 90 days\nconst PREFIX = \"pb_rt_\";\n\ninterface RefreshPayload {\n k: string; // apiKey\n i: number; // iat (ms since epoch)\n j: string; // jti\n}\n\n// Read once at module load. In production we warn rather than crash so the\n// server keeps working — degrades gracefully to current Railway-lifetime\n// behaviour if the secret is missing.\nconst secret: Buffer = (() => {\n const fromEnv = process.env.MCP_REFRESH_SECRET;\n if (fromEnv && fromEnv.length > 0) return Buffer.from(fromEnv, \"utf8\");\n if (process.env.NODE_ENV === \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[HTTP] WARNING MCP_REFRESH_SECRET not set — refresh tokens will not survive restart\",\n );\n }\n return randomBytes(32);\n})();\n\nfunction sign(payloadB64: string): Buffer {\n return createHmac(\"sha256\", secret).update(payloadB64).digest();\n}\n\nexport function signRefreshToken(apiKey: string): string {\n const payload: RefreshPayload = {\n k: apiKey,\n i: Date.now(),\n j: randomUUID(),\n };\n const payloadB64 = Buffer.from(JSON.stringify(payload), \"utf8\").toString(\"base64url\");\n const sigB64 = sign(payloadB64).toString(\"base64url\");\n return `${PREFIX}${payloadB64}.${sigB64}`;\n}\n\nexport function verifyRefreshToken(token: string): { apiKey: string } | null {\n if (typeof token !== \"string\" || !token.startsWith(PREFIX)) return null;\n const body = token.slice(PREFIX.length);\n const dot = body.indexOf(\".\");\n if (dot <= 0 || dot === body.length - 1) return null;\n const payloadB64 = body.slice(0, dot);\n const sigB64 = body.slice(dot + 1);\n\n let providedSig: Buffer;\n try {\n providedSig = Buffer.from(sigB64, \"base64url\");\n } catch {\n return null;\n }\n const expectedSig = sign(payloadB64);\n if (providedSig.length !== expectedSig.length) return null;\n if (!timingSafeEqual(providedSig, expectedSig)) return null;\n\n let payload: RefreshPayload;\n try {\n const json = Buffer.from(payloadB64, \"base64url\").toString(\"utf8\");\n payload = JSON.parse(json);\n } catch {\n return null;\n }\n if (\n !payload ||\n typeof payload.k !== \"string\" ||\n typeof payload.i !== \"number\" ||\n typeof payload.j !== \"string\"\n ) {\n return null;\n }\n if (Date.now() - payload.i > REFRESH_TOKEN_TTL_MS) return null;\n\n return { apiKey: payload.k };\n}\n"],"mappings":"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","names":["randomUUID","randomUUID","url","randomUUID"]}
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
SERVER_VERSION,
|
|
4
|
-
createProductBrainServer
|
|
5
|
-
} from "./chunk-WXT35272.js";
|
|
6
|
-
import {
|
|
7
4
|
bootstrap,
|
|
5
|
+
createProductBrainServer,
|
|
8
6
|
getAgentSessionId,
|
|
9
7
|
getWorkspaceId,
|
|
10
8
|
initFeatureFlags,
|
|
11
9
|
orphanAgentSession,
|
|
12
|
-
recoverSessionState
|
|
13
|
-
|
|
14
|
-
} from "./chunk-G4JJNINW.js";
|
|
10
|
+
recoverSessionState
|
|
11
|
+
} from "./chunk-7TU5QHN7.js";
|
|
15
12
|
import {
|
|
16
13
|
getPostHogClient,
|
|
17
14
|
initAnalytics,
|
|
18
15
|
shutdownAnalytics,
|
|
19
16
|
trackSessionStarted
|
|
20
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-YMF3IQ5E.js";
|
|
21
18
|
|
|
22
19
|
// src/index.ts
|
|
23
20
|
import { readFileSync } from "fs";
|
|
@@ -65,6 +62,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
65
62
|
process.on("uncaughtException", (err) => {
|
|
66
63
|
process.stderr.write(`[MCP] Uncaught exception: ${err.stack ?? err.message}
|
|
67
64
|
`);
|
|
65
|
+
getPostHogClient()?.capture({ distinctId: "mcp-server", event: "mcp_process_crashed", properties: { message: err.message, stack: err.stack } });
|
|
68
66
|
gracefulShutdown();
|
|
69
67
|
});
|
|
70
68
|
bootstrap();
|
|
@@ -85,12 +83,8 @@ process.stdin.on("end", () => {
|
|
|
85
83
|
getWorkspaceId().then(async (wsId) => {
|
|
86
84
|
trackSessionStarted(wsId, SERVER_VERSION);
|
|
87
85
|
try {
|
|
88
|
-
await startAgentSession();
|
|
89
|
-
process.stderr.write("[MCP] Agent session started automatically.\n");
|
|
90
|
-
} catch (err) {
|
|
91
|
-
process.stderr.write(`[MCP] Auto session start failed: ${err.message}. Call session action=start manually.
|
|
92
|
-
`);
|
|
93
86
|
await recoverSessionState();
|
|
87
|
+
} catch {
|
|
94
88
|
}
|
|
95
89
|
}).catch(() => {
|
|
96
90
|
process.stderr.write("[MCP] Workspace resolution deferred \u2014 will retry on first tool call.\n");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport {\n getWorkspaceId,\n bootstrap,\n orphanAgentSession,\n
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport {\n getWorkspaceId,\n bootstrap,\n orphanAgentSession,\n getAgentSessionId,\n recoverSessionState,\n} from \"./client.js\";\nimport { initAnalytics, trackSessionStarted, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\n\ntry {\n const envPath = resolve(process.cwd(), \".env.mcp\");\n for (const line of readFileSync(envPath, \"utf-8\").split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIdx = trimmed.indexOf(\"=\");\n if (eqIdx === -1) continue;\n process.env[trimmed.slice(0, eqIdx)] ??= trimmed.slice(eqIdx + 1);\n }\n} catch {\n // .env.mcp not found — rely on env vars being set by the launcher\n}\n\nconst withTimeout = <T>(p: Promise<T>, ms: number): Promise<T | void> =>\n Promise.race([p, new Promise<void>((r) => setTimeout(r, ms))]);\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n process.stderr.write(\"[MCP] Graceful shutdown initiated.\\n\");\n // Hard-exit guard — fires if async cleanup exceeds budget.\n // Still exits 0: SIGTERM/stdin-close is an intentional shutdown regardless.\n const hardExit = setTimeout(() => {\n process.stderr.write(\"[MCP] Hard exit after shutdown timeout.\\n\");\n process.exit(0);\n }, 3_000);\n try {\n if (getAgentSessionId()) await withTimeout(orphanAgentSession(), 1_500);\n } catch {\n /* best-effort */\n }\n try {\n await withTimeout(shutdownAnalytics(), 1_000);\n } catch {\n /* best-effort */\n }\n clearTimeout(hardExit);\n process.exit(0);\n}\n\n// Register signal handlers BEFORE server.connect so they are always active\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n process.stderr.write(`[MCP] Unhandled rejection: ${msg}\\n`);\n});\nprocess.on(\"uncaughtException\", (err) => {\n process.stderr.write(`[MCP] Uncaught exception: ${err.stack ?? err.message}\\n`);\n // Track crash via PostHog so it surfaces in the analytics dashboard even without Sentry.\n getPostHogClient()?.capture({ distinctId: \"mcp-server\", event: \"mcp_process_crashed\", properties: { message: err.message, stack: err.stack } });\n gracefulShutdown();\n});\n\nbootstrap();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst server = createProductBrainServer();\nconst transport = new StdioServerTransport();\ntransport.onerror = (error) => {\n process.stderr.write(`[MCP] Transport error: ${error.message}\\n`);\n};\nprocess.stderr.write(\"[MCP] Starting server.\\n\");\nawait server.connect(transport);\nprocess.stderr.write(\"[MCP] Server connected.\\n\");\n\nprocess.stdin.on(\"end\", () => {\n gracefulShutdown();\n});\n\n// Do not auto-start agent sessions here: a second concurrent startAgentSession (e.g. from\n// `session action=start` right after connect) can supersede the first session while client\n// state still holds the stale id — markOriented then fails with \"superseded, not active\".\n// Call `session action=start` or `start` explicitly (STD-135).\ngetWorkspaceId()\n .then(async (wsId) => {\n trackSessionStarted(wsId, SERVER_VERSION);\n try {\n await recoverSessionState();\n } catch {\n /* best-effort — fresh stdio has nothing to recover */\n }\n })\n .catch(() => {\n process.stderr.write(\"[MCP] Workspace resolution deferred — will retry on first tool call.\\n\");\n });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,4BAA4B;AAarC,IAAI;AACF,QAAM,UAAU,QAAQ,QAAQ,IAAI,GAAG,UAAU;AACjD,aAAW,QAAQ,aAAa,SAAS,OAAO,EAAE,MAAM,IAAI,GAAG;AAC7D,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AACzC,UAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,QAAI,UAAU,GAAI;AAClB,YAAQ,IAAI,QAAQ,MAAM,GAAG,KAAK,CAAC,MAAM,QAAQ,MAAM,QAAQ,CAAC;AAAA,EAClE;AACF,QAAQ;AAER;AAEA,IAAM,cAAc,CAAI,GAAe,OACrC,QAAQ,KAAK,CAAC,GAAG,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC;AAE/D,IAAI,eAAe;AACnB,eAAe,mBAAmB;AAChC,MAAI,aAAc;AAClB,iBAAe;AACf,UAAQ,OAAO,MAAM,sCAAsC;AAG3D,QAAM,WAAW,WAAW,MAAM;AAChC,YAAQ,OAAO,MAAM,2CAA2C;AAChE,YAAQ,KAAK,CAAC;AAAA,EAChB,GAAG,GAAK;AACR,MAAI;AACF,QAAI,kBAAkB,EAAG,OAAM,YAAY,mBAAmB,GAAG,IAAK;AAAA,EACxE,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,YAAY,kBAAkB,GAAG,GAAK;AAAA,EAC9C,QAAQ;AAAA,EAER;AACA,eAAa,QAAQ;AACrB,UAAQ,KAAK,CAAC;AAChB;AAGA,QAAQ,GAAG,UAAU,gBAAgB;AACrC,QAAQ,GAAG,WAAW,gBAAgB;AACtC,QAAQ,GAAG,sBAAsB,CAAC,WAAW;AAC3C,QAAM,MAAM,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;AACpE,UAAQ,OAAO,MAAM,8BAA8B,GAAG;AAAA,CAAI;AAC5D,CAAC;AACD,QAAQ,GAAG,qBAAqB,CAAC,QAAQ;AACvC,UAAQ,OAAO,MAAM,6BAA6B,IAAI,SAAS,IAAI,OAAO;AAAA,CAAI;AAE9E,mBAAiB,GAAG,QAAQ,EAAE,YAAY,cAAc,OAAO,uBAAuB,YAAY,EAAE,SAAS,IAAI,SAAS,OAAO,IAAI,MAAM,EAAE,CAAC;AAC9I,mBAAiB;AACnB,CAAC;AAED,UAAU;AACV,cAAc;AACd,iBAAiB,iBAAiB,CAAC;AAEnC,IAAM,SAAS,yBAAyB;AACxC,IAAM,YAAY,IAAI,qBAAqB;AAC3C,UAAU,UAAU,CAAC,UAAU;AAC7B,UAAQ,OAAO,MAAM,0BAA0B,MAAM,OAAO;AAAA,CAAI;AAClE;AACA,QAAQ,OAAO,MAAM,0BAA0B;AAC/C,MAAM,OAAO,QAAQ,SAAS;AAC9B,QAAQ,OAAO,MAAM,2BAA2B;AAEhD,QAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAiB;AACnB,CAAC;AAMD,eAAe,EACZ,KAAK,OAAO,SAAS;AACpB,sBAAoB,MAAM,cAAc;AACxC,MAAI;AACF,UAAM,oBAAoB;AAAA,EAC5B,QAAQ;AAAA,EAER;AACF,CAAC,EACA,MAAM,MAAM;AACX,UAAQ,OAAO,MAAM,6EAAwE;AAC/F,CAAC;","names":[]}
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
trackSetupCompleted,
|
|
8
8
|
trackSetupStarted,
|
|
9
9
|
writeClientConfig
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YMF3IQ5E.js";
|
|
11
11
|
|
|
12
12
|
// src/cli/setup.ts
|
|
13
13
|
import { execSync } from "child_process";
|
|
14
14
|
import { createInterface } from "readline";
|
|
15
15
|
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
16
16
|
import { join } from "path";
|
|
17
|
-
var APP_URL = process.env.PRODUCTBRAIN_APP_URL ?? "https://productbrain.io";
|
|
17
|
+
var APP_URL = process.env.PRODUCTBRAIN_APP_URL ?? "https://work.productbrain.io";
|
|
18
18
|
function bold(s) {
|
|
19
19
|
return `\x1B[1m${s}\x1B[0m`;
|
|
20
20
|
}
|
|
@@ -69,11 +69,11 @@ function promptChoice(question, choices) {
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
var DEFAULT_CLOUD_URL = "https://
|
|
72
|
+
var DEFAULT_CLOUD_URL = "https://gateway.productbrain.io";
|
|
73
73
|
async function verifyWorkspace(apiKey) {
|
|
74
74
|
const siteUrl = process.env.CONVEX_SITE_URL ?? process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
|
|
75
75
|
try {
|
|
76
|
-
const res = await fetch(`${siteUrl.replace(/\/$/, "")}/api/
|
|
76
|
+
const res = await fetch(`${siteUrl.replace(/\/$/, "")}/api/aki`, {
|
|
77
77
|
method: "POST",
|
|
78
78
|
headers: {
|
|
79
79
|
"Content-Type": "application/json",
|
|
@@ -294,4 +294,4 @@ function printClaudeSnippet() {
|
|
|
294
294
|
export {
|
|
295
295
|
runSetup
|
|
296
296
|
};
|
|
297
|
-
//# sourceMappingURL=setup-
|
|
297
|
+
//# sourceMappingURL=setup-RYYXRDPB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/setup.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * `npx @productbrain/mcp@beta setup`\n *\n * Guided onboarding: get API key from the app, paste it, write MCP config,\n * and optionally install Cursor rules/skills (additive-only).\n */\n\nimport { execSync } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport { existsSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { resolveClient, writeClientConfig, MCP_NPX_PACKAGE, type McpClientInfo } from \"./config-writer.js\";\nimport { initAnalytics, trackSetupStarted, trackSetupCompleted, shutdownAnalytics } from \"../analytics.js\";\n\nconst APP_URL =\n process.env.PRODUCTBRAIN_APP_URL ?? \"https://work.productbrain.io\";\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nfunction bold(s: string) {\n return `\\x1b[1m${s}\\x1b[0m`;\n}\nfunction green(s: string) {\n return `\\x1b[32m${s}\\x1b[0m`;\n}\nfunction dim(s: string) {\n return `\\x1b[2m${s}\\x1b[0m`;\n}\nfunction orange(s: string) {\n return `\\x1b[33m${s}\\x1b[0m`;\n}\n\nfunction log(msg: string) {\n process.stdout.write(`${msg}\\n`);\n}\n\nfunction openBrowser(url: string) {\n const platform = process.platform;\n try {\n if (platform === \"darwin\") execSync(`open \"${url}\"`);\n else if (platform === \"win32\") execSync(`start \"\" \"${url}\"`);\n else execSync(`xdg-open \"${url}\"`);\n } catch {\n log(dim(` Could not open browser automatically.`));\n log(` Open this URL manually: ${url}`);\n }\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\nfunction promptChoice(question: string, choices: string[]): Promise<number> {\n return new Promise((resolve) => {\n log(\"\");\n log(bold(question));\n choices.forEach((c, i) => log(` ${i + 1}) ${c}`));\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(`\\n ${dim(\"Choice [1]:\")} `, (line) => {\n rl.close();\n const n = parseInt(line.trim(), 10);\n if (isNaN(n) || n < 1 || n > choices.length) {\n resolve(0);\n } else {\n resolve(n - 1);\n }\n });\n });\n}\n\n// ── Workspace Verification ───────────────────────────────────────────────\n\nconst DEFAULT_CLOUD_URL = \"https://gateway.productbrain.io\";\n\nasync function verifyWorkspace(\n apiKey: string,\n): Promise<{ name: string; slug: string } | null> {\n const siteUrl = process.env.CONVEX_SITE_URL\n ?? process.env.PRODUCTBRAIN_URL\n ?? DEFAULT_CLOUD_URL;\n\n try {\n const res = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/aki`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ fn: \"resolveWorkspace\", args: {} }),\n });\n\n if (!res.ok) return null;\n const json = (await res.json()) as {\n data?: { name: string; slug: string } | null;\n error?: string;\n };\n return json.data ?? null;\n } catch {\n return null;\n }\n}\n\n// ── Main ────────────────────────────────────────────────────────────────\n\nexport async function runSetup() {\n initAnalytics();\n trackSetupStarted();\n\n log(\"\");\n log(bold(` Product${orange(\"Brain\")} Setup`));\n log(dim(\" Connect your AI assistant to your chain\\n\"));\n\n const apiKeysUrl = `${APP_URL}/settings/api-keys`;\n\n log(` ${dim(\"1. Get your API key from Settings → API Keys\")}`);\n log(` ${dim(apiKeysUrl)}\\n`);\n\n const openNow = await prompt(` Open this URL in your browser? [Y/n]: `);\n if (openNow.toLowerCase() !== \"n\" && openNow.toLowerCase() !== \"no\") {\n openBrowser(apiKeysUrl);\n }\n\n log(\"\");\n log(` ${dim(\"2. Generate a key (if you don't have one), then copy it.\\n\")}`);\n\n const apiKey = await prompt(` Paste your API key (pb_sk_...): `);\n\n if (!apiKey || !apiKey.startsWith(\"pb_sk_\")) {\n log(` ${orange(\"!\")} Invalid key format. Keys start with pb_sk_.`);\n log(` Get one at ${apiKeysUrl}\\n`);\n await shutdownAnalytics();\n process.exit(1);\n }\n\n log(` ${green(\"✓\")} Key received`);\n\n const workspace = await verifyWorkspace(apiKey);\n if (workspace) {\n log(` ${green(\"✓\")} Connected to workspace: ${bold(workspace.name)} ${dim(`(${workspace.slug})`)}`);\n } else {\n log(` ${orange(\"!\")} Could not verify workspace. Check your key at ${apiKeysUrl}`);\n }\n log(\"\");\n\n const CLIENT_NAMES = [\"Cursor\", \"Claude Desktop\"] as const;\n const options = [...CLIENT_NAMES, \"Other\"];\n\n const choice = await promptChoice(\"Where do you want to set up Product Brain?\", options);\n\n if (choice === 2) {\n printConfigSnippet(apiKey);\n trackSetupCompleted(\"Other\", \"snippet_shown\");\n } else {\n const client = resolveClient(CLIENT_NAMES[choice]);\n if (client) {\n const outcome = await writeConfig(client, apiKey);\n trackSetupCompleted(CLIENT_NAMES[choice], outcome);\n } else {\n log(` ${orange(\"!\")} ${CLIENT_NAMES[choice]} config path not available on this platform.`);\n printConfigSnippet(apiKey);\n trackSetupCompleted(CLIENT_NAMES[choice], \"write_error\");\n }\n }\n\n // Cursor-specific: offer to install rule (additive-only)\n if (choice === 0) {\n await offerCursorRulesInstall();\n printDeeplink(apiKey);\n }\n\n // Claude-specific: print snippet (never write to CLAUDE.md)\n if (choice === 1) {\n printClaudeSnippet();\n }\n\n log(\"\");\n log(\n ` ${green(\"✓\")} Done! Restart your AI assistant and try: ${bold('\"Start PB\"')}`,\n );\n printHelpLink();\n await shutdownAnalytics();\n}\n\nasync function writeConfig(\n client: McpClientInfo,\n apiKey: string,\n): Promise<\"config_written\" | \"config_existed\" | \"write_error\"> {\n try {\n const wrote = await writeClientConfig(client, apiKey);\n if (wrote) {\n log(` ${green(\"✓\")} Wrote config to ${dim(client.configPath)}`);\n return \"config_written\";\n } else {\n log(` ${dim(\"ℹ\")} ${client.name} already configured — skipped`);\n return \"config_existed\";\n }\n } catch (err: any) {\n log(` ${orange(\"!\")} Could not write ${client.name} config: ${err.message}`);\n printConfigSnippet(apiKey);\n return \"write_error\";\n }\n}\n\nfunction printHelpLink() {\n log(` ${dim(`Need help? See ${APP_URL}/settings/api-keys`)}`);\n log(\"\");\n}\n\nfunction printConfigSnippet(apiKey: string) {\n log(\"\");\n log(bold(\" Add this to your MCP client config:\\n\"));\n const snippet = JSON.stringify(\n {\n mcpServers: {\n \"Product Brain\": {\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n },\n },\n },\n null,\n 2,\n );\n for (const line of snippet.split(\"\\n\")) {\n log(` ${line}`);\n }\n log(\"\");\n}\n\n// ── Cursor Rules/Skills Install (additive-only) ─────────────────────────\n\nconst CURSOR_RULE_FILENAME = \"product-brain.mdc\";\n\nconst CURSOR_RULE_CONTENT = `---\ndescription: Product Brain MCP — single source of truth for product knowledge\nglobs:\nalwaysApply: true\n---\n\n# Product Brain MCP\n\nProduct Brain is your product knowledge base. The Chain is the single source of truth.\n\nEvery entry is either a **draft** (captured but not committed) or **committed** (on the Chain, SSOT).\nCommitting to the Chain is the compounding act.\n\n## Quick Start\n\nSay **\"Start PB\"** or **\"Start Product Brain\"** to begin. This single call:\n- Orients you to the workspace (readiness, gaps, planned work)\n- Unlocks write tools for the session\n- Surfaces your next recommended action\n\n## Tool Workflow\n\n1. **Start here**: \\`start\\` — workspace context + next action\n2. **Search**: \\`entries action=search\\` — find entries across all collections\n3. **Drill in**: \\`entries action=get\\` — full record with data, labels, relations\n4. **Context**: \\`context action=gather\\` — related knowledge around an entry or task\n5. **Capture**: \\`capture\\` — create knowledge with auto-linking + quality score\n6. **Commit**: \\`commit-entry\\` — promote drafts to SSOT when confirmation is still required\n7. **Connect**: \\`graph action=suggest\\` then \\`relations action=create\\` to build the graph\n\n## Bulk Knowledge Input\n\nWhen given a document or batch of knowledge to capture:\n1. Scan the input — identify all collections needed\n2. Call \\`collections action=list\\` — compare against what exists\n3. Propose missing collections to the user for confirmation\n4. Call \\`collections action=create\\` for each confirmed collection\n5. Then capture entries into the correct collections\n\nNever stuff entries into the wrong collection. Never silently skip knowledge.\n\n## Rules\n\n- In Open mode, user-authored captures can commit immediately unless the user asks to keep them as drafts.\n- In consensus/role modes, only call \\`commit-entry\\` when the user confirms.\n- Use \\`graph action=suggest\\` after capturing to discover and create relations.\n- Collections are dynamic — use \\`collections action=create\\` when the workspace needs new ones.\n- When lost, fetch \\`productbrain://orientation\\` for the full system map.\n`;\n\nfunction isCursorProject(): boolean {\n return existsSync(join(process.cwd(), \".cursor\")) || existsSync(join(process.cwd(), \".cursorignore\"));\n}\n\nasync function offerCursorRulesInstall(): Promise<void> {\n if (!isCursorProject()) return;\n\n const answer = await prompt(`\\n Install Product Brain rule for Cursor? [Y/n]: `);\n if (answer.toLowerCase() === \"n\" || answer.toLowerCase() === \"no\") {\n log(dim(\" Skipped rule install.\"));\n return;\n }\n\n const rulesDir = join(process.cwd(), \".cursor\", \"rules\");\n const rulePath = join(rulesDir, CURSOR_RULE_FILENAME);\n\n if (existsSync(rulePath)) {\n log(` ${dim(\"ℹ\")} Rule already exists at ${dim(rulePath)} — skipped`);\n return;\n }\n\n if (!existsSync(rulesDir)) {\n mkdirSync(rulesDir, { recursive: true });\n }\n\n writeFileSync(rulePath, CURSOR_RULE_CONTENT, \"utf-8\");\n log(` ${green(\"✓\")} Installed rule at ${dim(rulePath)}`);\n}\n\nfunction buildDeeplink(apiKey: string): string {\n const config = JSON.stringify({\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n });\n const encoded = Buffer.from(config).toString(\"base64url\");\n return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(\"Product Brain\")}&config=${encoded}`;\n}\n\nfunction printDeeplink(apiKey: string): void {\n const link = buildDeeplink(apiKey);\n log(\"\");\n log(` ${dim(\"One-click install for Cursor (paste in browser):\")}`);\n log(` ${link}`);\n}\n\nfunction printClaudeSnippet(): void {\n log(\"\");\n log(bold(\" For Claude Code / CLAUDE.md:\"));\n log(dim(\" Add this line to your ~/.claude/CLAUDE.md:\"));\n log(\"\");\n log(` When Product Brain MCP is available, say \"Start PB\" at the beginning`);\n log(` of each session to orient to the workspace and unlock write tools.`);\n log(` In Open mode, user-authored captures can commit immediately unless the user asks to keep drafts.`);\n log(` In consensus/role modes, only commit when the user confirms.`);\n log(\"\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -244,7 +244,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
244
244
|
- omitted: host decides border`)});y.object({method:y.literal("ui/request-display-mode"),params:y.object({mode:Or.describe("The display mode being requested.")})});y.object({mode:Or.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough();var L8=y.union([y.literal("model"),y.literal("app")]).describe("Tool visibility scope - who can access the tool.");y.object({resourceUri:y.string().optional(),visibility:y.array(L8).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
245
245
|
- "model": Tool visible to and callable by the agent
|
|
246
246
|
- "app": Tool callable by the app from this server only`)});y.object({mimeTypes:y.array(y.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});y.object({method:y.literal("ui/download-file"),params:y.object({contents:y.array(y.union([hc,vc])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});y.object({method:y.literal("ui/message"),params:y.object({role:y.literal("user").describe('Message role, currently only "user" is supported.'),content:y.array(Xn).describe("Message content blocks (text, image, etc.).")})});y.object({method:y.literal("ui/notifications/sandbox-resource-ready"),params:y.object({html:y.string().describe("HTML content to load into the inner iframe."),sandbox:y.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:cp.optional().describe("CSP configuration from resource metadata."),permissions:dp.optional().describe("Sandbox permissions from resource metadata.")})});y.object({method:y.literal("ui/notifications/tool-result"),params:Rr.describe("Standard MCP tool execution result.")});var r4=y.object({toolInfo:y.object({id:Zi.optional().describe("JSON-RPC id of the tools/call request."),tool:Xo.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Z8.optional().describe("Current color theme preference."),styles:C8.optional().describe("Style configuration for theming the app."),displayMode:Or.optional().describe("How the UI is currently displayed."),availableDisplayModes:y.array(Or).optional().describe("Display modes the host supports."),containerDimensions:y.union([y.object({height:y.number().describe("Fixed container height in pixels.")}),y.object({maxHeight:y.union([y.number(),y.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(y.union([y.object({width:y.number().describe("Fixed container width in pixels.")}),y.object({maxWidth:y.union([y.number(),y.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
247
|
-
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:y.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:y.string().optional().describe("User's timezone in IANA format."),userAgent:y.string().optional().describe("Host application identifier."),platform:y.union([y.literal("web"),y.literal("desktop"),y.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:y.object({touch:y.boolean().optional().describe("Whether the device supports touch input."),hover:y.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:y.object({top:y.number().describe("Top safe area inset in pixels."),right:y.number().describe("Right safe area inset in pixels."),bottom:y.number().describe("Bottom safe area inset in pixels."),left:y.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough();y.object({method:y.literal("ui/notifications/host-context-changed"),params:r4.describe("Partial context update containing only changed fields.")});y.object({method:y.literal("ui/update-model-context"),params:y.object({content:y.array(Xn).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:y.record(y.string(),y.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});y.object({method:y.literal("ui/initialize"),params:y.object({appInfo:Ei.describe("App identification (name and version)."),appCapabilities:M8.describe("Features and capabilities this app provides."),protocolVersion:y.string().describe("Protocol version this app supports.")})});y.object({protocolVersion:y.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ei.describe("Host application identification and version."),hostCapabilities:R8.describe("Features and capabilities provided by the host."),hostContext:r4.describe("Rich context about the host environment.")}).passthrough();class F8{constructor(n=window.parent,r){Pe(this,"eventTarget");Pe(this,"eventSource");Pe(this,"messageListener");Pe(this,"onclose");Pe(this,"onerror");Pe(this,"onmessage");Pe(this,"sessionId");Pe(this,"setProtocolVersion");this.eventTarget=n,this.eventSource=r,this.messageListener=a=>{var o,s,u;if(r&&a.source!==this.eventSource){console.debug("Ignoring message from unknown source",a);return}let i=b_.safeParse(a.data);i.success?(console.debug("Parsed message",i.data),(o=this.onmessage)==null||o.call(this,i.data)):((s=a.data)==null?void 0:s.jsonrpc)!=="2.0"?console.debug("Ignoring non-JSON-RPC message",i.error.message,a):(console.error("Failed to parse message",i.error.message,a),(u=this.onerror)==null||u.call(this,Error("Invalid JSON-RPC message received: "+i.error.message)))}}async start(){window.addEventListener("message",this.messageListener)}async send(n,r){console.debug("Sending message",n),this.eventTarget.postMessage(n,"*")}async close(){var n;window.removeEventListener("message",this.messageListener),(n=this.onclose)==null||n.call(this)}}const fp=new t9({name:"GraphConstellation",version:"1.0.0"},{});let Xi=null;const
|
|
247
|
+
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:y.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:y.string().optional().describe("User's timezone in IANA format."),userAgent:y.string().optional().describe("Host application identifier."),platform:y.union([y.literal("web"),y.literal("desktop"),y.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:y.object({touch:y.boolean().optional().describe("Whether the device supports touch input."),hover:y.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:y.object({top:y.number().describe("Top safe area inset in pixels."),right:y.number().describe("Right safe area inset in pixels."),bottom:y.number().describe("Bottom safe area inset in pixels."),left:y.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough();y.object({method:y.literal("ui/notifications/host-context-changed"),params:r4.describe("Partial context update containing only changed fields.")});y.object({method:y.literal("ui/update-model-context"),params:y.object({content:y.array(Xn).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:y.record(y.string(),y.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});y.object({method:y.literal("ui/initialize"),params:y.object({appInfo:Ei.describe("App identification (name and version)."),appCapabilities:M8.describe("Features and capabilities this app provides."),protocolVersion:y.string().describe("Protocol version this app supports.")})});y.object({protocolVersion:y.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ei.describe("Host application identification and version."),hostCapabilities:R8.describe("Features and capabilities provided by the host."),hostContext:r4.describe("Rich context about the host environment.")}).passthrough();class F8{constructor(n=window.parent,r){Pe(this,"eventTarget");Pe(this,"eventSource");Pe(this,"messageListener");Pe(this,"onclose");Pe(this,"onerror");Pe(this,"onmessage");Pe(this,"sessionId");Pe(this,"setProtocolVersion");this.eventTarget=n,this.eventSource=r,this.messageListener=a=>{var o,s,u;if(r&&a.source!==this.eventSource){console.debug("Ignoring message from unknown source",a);return}let i=b_.safeParse(a.data);i.success?(console.debug("Parsed message",i.data),(o=this.onmessage)==null||o.call(this,i.data)):((s=a.data)==null?void 0:s.jsonrpc)!=="2.0"?console.debug("Ignoring non-JSON-RPC message",i.error.message,a):(console.error("Failed to parse message",i.error.message,a),(u=this.onerror)==null||u.call(this,Error("Invalid JSON-RPC message received: "+i.error.message)))}}async start(){window.addEventListener("message",this.messageListener)}async send(n,r){console.debug("Sending message",n),this.eventTarget.postMessage(n,"*")}async close(){var n;window.removeEventListener("message",this.messageListener),(n=this.onclose)==null||n.call(this)}}const fp=new t9({name:"GraphConstellation",version:"1.0.0"},{});let Xi=null;function J8(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const q8={governs:"#6366f1",informs:"#8b5cf6",part_of:"#a855f7",constrains:"#d946ef",depends_on:"#ec4899",related_to:"#f43f5e",suggested:"#94a3b8",related:"#64748b"};function V8(e){return q8[e.type??""]??"#94a3b8"}function no(e,n){const r=document.getElementById("root");if(!r)return;if(e.length===0){r.innerHTML='<div class="empty">No graph data to display</div>',Xi&&Xi.graphData({nodes:[],links:[]});return}let a=document.getElementById("graph");a||(r.innerHTML='<div class="view-title">Constellation</div><div id="graph"></div>',a=document.getElementById("graph"));const i=new Map(e.map(u=>[u.id,{...u}])),o=n.filter(u=>i.has(u.source)&&i.has(u.target)).map(u=>({source:u.source,target:u.target,type:u.type??"related"})),s={nodes:Array.from(i.values()),links:o};Xi||(Xi=zj()(a).nodeId("id").nodeLabel(u=>J8(`${u.name??u.id}${u.collectionName?` [${u.collectionName}]`:""}`)).linkColor(u=>V8(u)).linkDirectionalArrowLength(4).linkDirectionalArrowRelPos(1).linkCurvature(.15)),Xi.graphData(s)}fp.ontoolresult=e=>{var r,a;const n=e.structuredContent;if((r=n==null?void 0:n.nodes)!=null&&r.length){const i=n.edges??[];no(n.nodes,i)}else if((a=n==null?void 0:n.entries)!=null&&a.length){const i=n.entries,o=i.map(u=>({id:u.entryId??String(Math.random()),name:u.name,collectionName:u.collectionName})),s=o.length>1?i.slice(1).map(u=>({source:i[0].entryId??"",target:u.entryId??"",type:"related"})):[];no(o,s)}else no([],[])};fp.ontoolinput=()=>{no([],[])};const W8=new F8(window.parent,window.parent);fp.connect(W8);</script>
|
|
248
248
|
</head>
|
|
249
249
|
<body>
|
|
250
250
|
<div id="root">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productbrain/mcp",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.914",
|
|
4
4
|
"description": "Product Brain — MCP server for AI-assisted product knowledge management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
".env.mcp.example"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
+
"prebuild": "cp ../../convex/lib/flags_core.ts src/flags.ts && mkdir -p src/brand && cp ../../src/lib/brand/logo-markup.ts src/brand/logo-markup.ts",
|
|
20
21
|
"build": "node ./scripts/build-package.mjs",
|
|
21
22
|
"build:code": "tsup",
|
|
22
23
|
"start": "node dist/index.js",
|