@productbrain/mcp 0.0.1-beta.18 → 0.0.1-beta.181

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 } from \"./analytics.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3000\", 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();\napp.use(express.json());\n\n// CORS — allow any origin by default; auth is via Bearer token.\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\"],\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\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}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"<>]/g, (c) =>\n ({ \"&\": \"&amp;\", '\"': \"&quot;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[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\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 } = req.body;\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({\n access_token: pending.apiKey,\n token_type: \"Bearer\",\n expires_in: 3600,\n });\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 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 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): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}\\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\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);\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 logRequest(\"POST\", \"ok\", sid);\n },\n });\n\n transport.onclose = () => {\n const sid = transport.sessionId;\n if (sid) sessions.delete(sid);\n };\n\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n } else {\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);\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\napp.listen(PORT, \"0.0.0.0\", () => {\n console.log(`Product Brain MCP HTTP server v${SERVER_VERSION} listening on port ${PORT}`);\n});\n\nasync function gracefulShutdown() {\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n await shutdownAnalytics();\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n"],"mappings":"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","names":[]}
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, 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\";\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: 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 token store — declared here so the cleanup interval can reference it.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1000;\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>();\nconst MAX_REFRESH_TOKENS = 2000;\n// Per-key cap mirrors MAX_SESSIONS_PER_KEY (see ~line 524): prevents any single\n// API key from monopolising the global refresh-token budget. PR #34 review Finding 3.\nconst MAX_REFRESH_TOKENS_PER_KEY = 20;\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 for (const [token, entry] of refreshTokens) {\n if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);\n }\n if (refreshTokens.size > MAX_REFRESH_TOKENS) {\n const sorted = [...refreshTokens.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);\n for (let i = 0; i < sorted.length - MAX_REFRESH_TOKENS; i++) {\n refreshTokens.delete(sorted[i][0]);\n }\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 ({ \"&\": \"&amp;\", '\"': \"&quot;\", \"'\": \"&#39;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[c]!,\n );\n}\n\n// ── Authorize Page Templates ─────────────────────────────────────────────\n// Zen-themed OAuth pages: warm dark palette, dot-grid background, subtle animations.\n\nfunction authPageShell(title: string, bodyContent: string, headExtra = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${title} — Product Brain</title>\n${headExtra}\n<style>\n:root{--bg:#0d0c10;--surface:#18161e;--border:#2d2840;--border-focus:#7c3aed;--text:#e8e4f0;--muted:#7b7590;--accent:#7c3aed;--accent-h:#6d28d9;--err:#f87171;--ok:#34d399}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:-apple-system,BlinkMacSystemFont,'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1.5rem}\nbody::before{content:'';position:fixed;inset:0;background-image:radial-gradient(circle,#3d3560 1px,transparent 1px);background-size:28px 28px;opacity:.22;pointer-events:none;z-index:0}\n.card{position:relative;z-index:1;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:2rem 2rem 1.75rem;max-width:380px;width:100%}\n</style>\n</head><body>\n<div class=\"card\">${bodyContent}</div>\n</body></html>`;\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}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const body = `\n<style>\n.brand{display:flex;align-items:center;gap:.55rem;margin-bottom:1.5rem}\n.brand-icon{width:30px;height:30px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0}\n.brand-name{font-size:.88rem;font-weight:600;letter-spacing:-.01em}\nh1{font-size:1.3rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.35rem;line-height:1.3}\n.sub{font-size:.8rem;color:var(--muted);margin-bottom:1.75rem;line-height:1.55}\nlabel{display:block;font-size:.72rem;font-weight:600;color:#a89fc4;margin-bottom:.4rem;letter-spacing:.04em;text-transform:uppercase}\n.inp-wrap{position:relative}\ninput[type=password]{width:100%;padding:.65rem .85rem;background:var(--bg);border:1px solid var(--border);border-radius:10px;color:var(--text);font:.875rem/1.4 'JetBrains Mono','Fira Code',ui-monospace,monospace;outline:none;transition:border-color .15s,box-shadow .15s}\ninput:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,58,237,.15)}\ninput.err{border-color:var(--err);box-shadow:0 0 0 3px rgba(248,113,113,.12)}\n.field-row{display:flex;align-items:center;justify-content:space-between;margin-top:.4rem}\n.hint{font-size:.73rem;color:var(--muted)}\n.hint code{font-size:.72rem;background:rgba(255,255,255,.06);padding:.1rem .3rem;border-radius:4px}\n.get-key{font-size:.73rem;color:var(--accent);text-decoration:none;opacity:.85;transition:opacity .1s}\n.get-key:hover{opacity:1;text-decoration:underline}\n.err-msg{font-size:.73rem;color:var(--err);margin-top:.35rem;display:none}\n.err-msg.show{display:block}\n.btn{width:100%;padding:.7rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.88rem;font-weight:600;cursor:pointer;margin-top:1.25rem;letter-spacing:-.01em;transition:background .15s,transform .1s;display:flex;align-items:center;justify-content:center;gap:.5rem}\n.btn:hover{background:var(--accent-h)}\n.btn:active{transform:scale(.98)}\n.btn:disabled{opacity:.6;cursor:not-allowed;transform:none}\n.spinner{width:15px;height:15px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;display:none}\n@keyframes spin{to{transform:rotate(360deg)}}\n.divider{height:1px;background:var(--border);margin:1.5rem -2rem}\n.footer{font-size:.7rem;color:var(--muted);text-align:center;padding-top:.5rem;line-height:1.6}\n.footer a{color:var(--muted);text-decoration:underline}\n</style>\n<div class=\"brand\"><div class=\"brand-icon\">&#11041;</div><span class=\"brand-name\">Product Brain</span></div>\n<h1>Connect to Claude</h1>\n<p class=\"sub\">Enter your API key to give Claude access to your workspace.</p>\n<form method=\"POST\" action=\"/authorize\" id=\"f\">\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 <label for=\"k\">API Key</label>\n <div class=\"inp-wrap\">\n <input type=\"password\" id=\"k\" name=\"api_key\" placeholder=\"pb_sk_…\" required autofocus autocomplete=\"off\">\n </div>\n <div class=\"field-row\">\n <span class=\"hint\">Starts with <code>pb_sk_</code></span>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"get-key\">No key? Get one &rarr;</a>\n </div>\n <p class=\"err-msg\" id=\"em\">Key must start with <code>pb_sk_</code></p>\n <button type=\"submit\" class=\"btn\" id=\"sb\">\n <span id=\"bt\">Authorize</span>\n <div class=\"spinner\" id=\"sp\"></div>\n </button>\n</form>\n<div class=\"divider\"></div>\n<p class=\"footer\">Product Brain gives Claude access to your knowledge graph.<br><a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\">Learn more</a></p>\n<script>\nvar f=document.getElementById('f'),k=document.getElementById('k'),em=document.getElementById('em'),sb=document.getElementById('sb'),bt=document.getElementById('bt'),sp=document.getElementById('sp');\nk.addEventListener('input',function(){var v=k.value.trim();if(v&&!v.startsWith('pb_sk_')){k.classList.add('err');em.classList.add('show')}else{k.classList.remove('err');em.classList.remove('show')}});\nf.addEventListener('submit',function(e){var v=k.value.trim();if(!v.startsWith('pb_sk_')){e.preventDefault();k.classList.add('err');em.classList.add('show');k.focus();return}sb.disabled=true;bt.textContent='Verifying…';sp.style.display='block'});\n</script>`;\n return authPageShell(\"Connect to Claude\", body);\n}\n\nfunction authorizeSuccessPage(workspaceName: string, redirectUrl: string): string {\n // Escape </script> sequences so a crafted redirect_uri can't break script context.\n const safeUrl = JSON.stringify(redirectUrl).replace(/<\\/script>/gi, \"<\\\\/script>\");\n const body = `\n<style>\n.card{text-align:center;padding:2.5rem 2rem}\n.ring{width:60px;height:60px;background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.25);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.4rem;animation:pop .5s cubic-bezier(.175,.885,.32,1.275) .15s both}\n@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}\nsvg{animation:draw .6s ease .5s both}\n@keyframes draw{from{stroke-dashoffset:30}to{stroke-dashoffset:0}}\nh1{font-size:1.3rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.3rem}\n.ws{font-size:.8rem;color:var(--ok);font-weight:500;margin-bottom:1.5rem}\n.redirect-msg{font-size:.78rem;color:var(--muted);margin-bottom:1.4rem}\n.dots::after{content:'';animation:dots 1.4s steps(4,end) infinite}\n@keyframes dots{0%{content:''}25%{content:'.'}50%{content:'..'}75%{content:'...'}}\n.continue{display:inline-flex;align-items:center;gap:.4rem;padding:.6rem 1.2rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.84rem;font-weight:600;cursor:pointer;text-decoration:none;transition:background .15s}\n.continue:hover{background:var(--accent-h)}\n@keyframes fadein{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}\n.card{animation:fadein .35s ease}\n</style>\n<div class=\"ring\">\n <svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#34d399\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-dasharray=\"30\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n</div>\n<h1>Claude is connected</h1>\n<p class=\"ws\">${esc(workspaceName)}</p>\n<p class=\"redirect-msg\">Returning to Claude<span class=\"dots\"></span></p>\n<a href=\"${esc(redirectUrl)}\" class=\"continue\">Continue to Claude &rarr;</a>\n<script>setTimeout(function(){window.location.href=${safeUrl}},1800)</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<style>\n.card{text-align:center;padding:2.25rem 2rem}\n.x-ring{width:52px;height:52px;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.25rem;font-size:1.4rem;animation:shake .4s ease .1s}\n@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}\nh1{font-size:1.2rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem;color:var(--err)}\n.detail{font-size:.8rem;color:var(--muted);line-height:1.6;margin-bottom:1.5rem}\n.detail code{font-size:.75rem;background:rgba(255,255,255,.06);padding:.1rem .3rem;border-radius:4px}\n.actions{display:flex;gap:.75rem;justify-content:center;flex-wrap:wrap}\n.btn-retry{display:inline-flex;align-items:center;padding:.55rem 1.1rem;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:.82rem;font-weight:600;cursor:pointer;text-decoration:none;transition:background .15s}\n.btn-retry:hover{background:var(--accent-h)}\n.btn-key{display:inline-flex;align-items:center;padding:.55rem 1rem;background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:10px;font-size:.82rem;text-decoration:none;transition:border-color .15s,color .15s}\n.btn-key:hover{border-color:var(--accent);color:var(--text)}\n</style>\n<div class=\"x-ring\">&#10005;</div>\n<h1>${esc(title)}</h1>\n<p class=\"detail\">${trustedDetailHtml}</p>\n<div class=\"actions\">\n <a href=\"${esc(retryUrl)}\" class=\"btn-retry\">&larr; Try again</a>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-key\">Get an API key &rarr;</a>\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 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: String(client_id ?? \"\"),\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 // 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 if (!api_key?.startsWith(\"pb_sk_\")) {\n res.type(\"html\").send(authorizeErrorPage(\n \"Invalid key format\",\n \"API keys start with <code>pb_sk_</code>. Check your key and try again.\",\n retryUrl,\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 if (!client_id || !registeredClients.has(client_id)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"Unknown or missing client_id\",\n });\n return;\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 res.type(\"html\").send(authorizeErrorPage(\n \"Key not recognized\",\n \"This API key wasn't found in Product Brain. Check your API Keys in Studio and try again.\",\n retryUrl,\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\n // Return a success page that auto-redirects to Claude after 1.8s.\n res.type(\"html\").send(authorizeSuccessPage(workspaceName, redirectUrl));\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 Studio); actual validity is enforced by Convex per tool call.\n // extractBearerKey() already handles pb_sk_* with zero Map lookup.\n //\n // NOTE: pb_at_* tokens issued before this change are still resolved by\n // the accessTokens Map for backward compat (until clients re-auth).\n const now = Date.now();\n\n const refreshToken = `pb_rt_${randomUUID()}`;\n\n // Per-key cap (Finding 3, PR #34): before the global backstop kicks in, ensure\n // no single apiKey holds more than MAX_REFRESH_TOKENS_PER_KEY entries. Mirrors\n // MAX_SESSIONS_PER_KEY. Prevents one abusive/leaked key from evicting every\n // other tenant's refresh tokens via the global FIFO backstop below.\n let perKeyCount = 0;\n let oldestKeyForApiKey: string | null = null;\n let oldestAtForApiKey = Infinity;\n for (const [k, v] of refreshTokens) {\n if (v.apiKey === apiKey) {\n perKeyCount++;\n if (v.createdAt < oldestAtForApiKey) {\n oldestAtForApiKey = v.createdAt;\n oldestKeyForApiKey = k;\n }\n }\n }\n if (perKeyCount >= MAX_REFRESH_TOKENS_PER_KEY && oldestKeyForApiKey) {\n refreshTokens.delete(oldestKeyForApiKey);\n }\n\n // Global backstop — unchanged behaviour.\n if (refreshTokens.size >= MAX_REFRESH_TOKENS) {\n let oldestKey: string | null = null;\n let oldestAt = Infinity;\n for (const [k, v] of refreshTokens) {\n if (v.createdAt < oldestAt) {\n oldestAt = v.createdAt;\n oldestKey = k;\n }\n }\n if (oldestKey) refreshTokens.delete(oldestKey);\n }\n refreshTokens.set(refreshToken, { apiKey, createdAt: now });\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: refreshToken,\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 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// ── 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 {\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 || !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 // 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 || !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 // 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"],"mappings":"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","names":["url"]}
package/dist/index.js CHANGED
@@ -1,71 +1,92 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  SERVER_VERSION,
4
- createProductBrainServer
5
- } from "./chunk-TUNNDDD7.js";
6
- import {
7
4
  bootstrap,
5
+ createProductBrainServer,
8
6
  getAgentSessionId,
9
7
  getWorkspaceId,
8
+ initFeatureFlags,
10
9
  orphanAgentSession,
11
- recoverSessionState,
12
- startAgentSession
13
- } from "./chunk-47LO6K2R.js";
10
+ recoverSessionState
11
+ } from "./chunk-XPFSARXP.js";
14
12
  import {
13
+ getPostHogClient,
15
14
  initAnalytics,
16
15
  shutdownAnalytics,
17
16
  trackSessionStarted
18
- } from "./chunk-XBMI6QHR.js";
17
+ } from "./chunk-YMF3IQ5E.js";
19
18
 
20
19
  // src/index.ts
21
20
  import { readFileSync } from "fs";
22
21
  import { resolve } from "path";
23
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
- if (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {
23
+ try {
24
+ const envPath = resolve(process.cwd(), ".env.mcp");
25
+ for (const line of readFileSync(envPath, "utf-8").split("\n")) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith("#")) continue;
28
+ const eqIdx = trimmed.indexOf("=");
29
+ if (eqIdx === -1) continue;
30
+ process.env[trimmed.slice(0, eqIdx)] ??= trimmed.slice(eqIdx + 1);
31
+ }
32
+ } catch {
33
+ }
34
+ var withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(r, ms))]);
35
+ var shuttingDown = false;
36
+ async function gracefulShutdown() {
37
+ if (shuttingDown) return;
38
+ shuttingDown = true;
39
+ process.stderr.write("[MCP] Graceful shutdown initiated.\n");
40
+ const hardExit = setTimeout(() => {
41
+ process.stderr.write("[MCP] Hard exit after shutdown timeout.\n");
42
+ process.exit(0);
43
+ }, 3e3);
44
+ try {
45
+ if (getAgentSessionId()) await withTimeout(orphanAgentSession(), 1500);
46
+ } catch {
47
+ }
25
48
  try {
26
- const envPath = resolve(process.cwd(), ".env.mcp");
27
- for (const line of readFileSync(envPath, "utf-8").split("\n")) {
28
- const trimmed = line.trim();
29
- if (!trimmed || trimmed.startsWith("#")) continue;
30
- const eqIdx = trimmed.indexOf("=");
31
- if (eqIdx === -1) continue;
32
- process.env[trimmed.slice(0, eqIdx)] ??= trimmed.slice(eqIdx + 1);
33
- }
49
+ await withTimeout(shutdownAnalytics(), 1e3);
34
50
  } catch {
35
51
  }
52
+ clearTimeout(hardExit);
53
+ process.exit(0);
36
54
  }
55
+ process.on("SIGINT", gracefulShutdown);
56
+ process.on("SIGTERM", gracefulShutdown);
57
+ process.on("unhandledRejection", (reason) => {
58
+ const msg = reason instanceof Error ? reason.message : String(reason);
59
+ process.stderr.write(`[MCP] Unhandled rejection: ${msg}
60
+ `);
61
+ });
62
+ process.on("uncaughtException", (err) => {
63
+ process.stderr.write(`[MCP] Uncaught exception: ${err.stack ?? err.message}
64
+ `);
65
+ getPostHogClient()?.capture({ distinctId: "mcp-server", event: "mcp_process_crashed", properties: { message: err.message, stack: err.stack } });
66
+ gracefulShutdown();
67
+ });
37
68
  bootstrap();
38
69
  initAnalytics();
70
+ initFeatureFlags(getPostHogClient());
39
71
  var server = createProductBrainServer();
40
72
  var transport = new StdioServerTransport();
73
+ transport.onerror = (error) => {
74
+ process.stderr.write(`[MCP] Transport error: ${error.message}
75
+ `);
76
+ };
77
+ process.stderr.write("[MCP] Starting server.\n");
41
78
  await server.connect(transport);
79
+ process.stderr.write("[MCP] Server connected.\n");
80
+ process.stdin.on("end", () => {
81
+ gracefulShutdown();
82
+ });
42
83
  getWorkspaceId().then(async (wsId) => {
43
84
  trackSessionStarted(wsId, SERVER_VERSION);
44
85
  try {
45
- await startAgentSession();
46
- process.stderr.write("[MCP] Agent session started automatically.\n");
47
- } catch (err) {
48
- process.stderr.write(`[MCP] Auto session start failed: ${err.message}. Call agent-start manually.
49
- `);
50
86
  await recoverSessionState();
87
+ } catch {
51
88
  }
52
89
  }).catch(() => {
53
90
  process.stderr.write("[MCP] Workspace resolution deferred \u2014 will retry on first tool call.\n");
54
91
  });
55
- async function gracefulShutdown() {
56
- if (getAgentSessionId()) {
57
- await orphanAgentSession();
58
- }
59
- await shutdownAnalytics();
60
- process.exit(0);
61
- }
62
- process.on("SIGINT", gracefulShutdown);
63
- process.on("SIGTERM", gracefulShutdown);
64
- process.stdin.on("end", async () => {
65
- if (getAgentSessionId()) {
66
- await orphanAgentSession();
67
- }
68
- await shutdownAnalytics();
69
- process.exit(0);
70
- });
71
92
  //# sourceMappingURL=index.js.map
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 startAgentSession,\n getAgentSessionId,\n recoverSessionState,\n} from \"./client.js\";\nimport { initAnalytics, trackSessionStarted, shutdownAnalytics } from \"./analytics.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\n\nif (!process.env.CONVEX_SITE_URL && !process.env.PRODUCTBRAIN_API_KEY) {\n try {\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}\n\nbootstrap();\ninitAnalytics();\n\nconst server = createProductBrainServer();\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n\ngetWorkspaceId()\n .then(async (wsId) => {\n trackSessionStarted(wsId, SERVER_VERSION);\n try {\n await startAgentSession();\n process.stderr.write(\"[MCP] Agent session started automatically.\\n\");\n } catch (err: any) {\n process.stderr.write(`[MCP] Auto session start failed: ${err.message}. Call agent-start manually.\\n`);\n await recoverSessionState();\n }\n })\n .catch(() => {\n process.stderr.write(\"[MCP] Workspace resolution deferred will retry on first tool call.\\n\");\n });\n\nasync function gracefulShutdown() {\n if (getAgentSessionId()) {\n await orphanAgentSession();\n }\n await shutdownAnalytics();\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n\nprocess.stdin.on(\"end\", async () => {\n if (getAgentSessionId()) {\n await orphanAgentSession();\n }\n await shutdownAnalytics();\n process.exit(0);\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,4BAA4B;AAarC,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAC,QAAQ,IAAI,sBAAsB;AACrE,MAAI;AACF,UAAM,UAAU,QAAQ,QAAQ,IAAI,GAAG,UAAU;AACjD,eAAW,QAAQ,aAAa,SAAS,OAAO,EAAE,MAAM,IAAI,GAAG;AAC7D,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AACzC,YAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,UAAI,UAAU,GAAI;AAClB,cAAQ,IAAI,QAAQ,MAAM,GAAG,KAAK,CAAC,MAAM,QAAQ,MAAM,QAAQ,CAAC;AAAA,IAClE;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,UAAU;AACV,cAAc;AAEd,IAAM,SAAS,yBAAyB;AACxC,IAAM,YAAY,IAAI,qBAAqB;AAC3C,MAAM,OAAO,QAAQ,SAAS;AAE9B,eAAe,EACZ,KAAK,OAAO,SAAS;AACpB,sBAAoB,MAAM,cAAc;AACxC,MAAI;AACF,UAAM,kBAAkB;AACxB,YAAQ,OAAO,MAAM,8CAA8C;AAAA,EACrE,SAAS,KAAU;AACjB,YAAQ,OAAO,MAAM,oCAAoC,IAAI,OAAO;AAAA,CAAgC;AACpG,UAAM,oBAAoB;AAAA,EAC5B;AACF,CAAC,EACA,MAAM,MAAM;AACX,UAAQ,OAAO,MAAM,6EAAwE;AAC/F,CAAC;AAEH,eAAe,mBAAmB;AAChC,MAAI,kBAAkB,GAAG;AACvB,UAAM,mBAAmB;AAAA,EAC3B;AACA,QAAM,kBAAkB;AACxB,UAAQ,KAAK,CAAC;AAChB;AAEA,QAAQ,GAAG,UAAU,gBAAgB;AACrC,QAAQ,GAAG,WAAW,gBAAgB;AAEtC,QAAQ,MAAM,GAAG,OAAO,YAAY;AAClC,MAAI,kBAAkB,GAAG;AACvB,UAAM,mBAAmB;AAAA,EAC3B;AACA,QAAM,kBAAkB;AACxB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
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":[]}
@@ -1,90 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ MCP_NPX_PACKAGE,
3
4
  initAnalytics,
5
+ resolveClient,
4
6
  shutdownAnalytics,
5
7
  trackSetupCompleted,
6
- trackSetupStarted
7
- } from "./chunk-XBMI6QHR.js";
8
+ trackSetupStarted,
9
+ writeClientConfig
10
+ } from "./chunk-YMF3IQ5E.js";
8
11
 
9
12
  // src/cli/setup.ts
10
13
  import { execSync } from "child_process";
11
14
  import { createInterface } from "readline";
12
- import { existsSync as existsSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
13
- import { join as join2 } from "path";
14
-
15
- // src/cli/config-writer.ts
16
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
17
- import { join, dirname } from "path";
18
- import { homedir, platform } from "os";
19
- var SERVER_ENTRY_KEY = "Product Brain";
20
- var LEGACY_ENTRY_KEY = "productbrain";
21
- function buildServerEntry(apiKey) {
22
- return {
23
- command: "npx",
24
- args: ["-y", "@productbrain/mcp@beta"],
25
- env: { PRODUCTBRAIN_API_KEY: apiKey }
26
- };
27
- }
28
- function getCursorConfigPath() {
29
- return join(process.cwd(), ".cursor", "mcp.json");
30
- }
31
- function getClaudeDesktopConfigPath() {
32
- const os = platform();
33
- if (os === "darwin") {
34
- return join(
35
- homedir(),
36
- "Library",
37
- "Application Support",
38
- "Claude",
39
- "claude_desktop_config.json"
40
- );
41
- }
42
- if (os === "win32") {
43
- const appData = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
44
- return join(appData, "Claude", "claude_desktop_config.json");
45
- }
46
- return null;
47
- }
48
- function resolveClient(name) {
49
- if (name === "Cursor") {
50
- return { name, configPath: getCursorConfigPath() };
51
- }
52
- const configPath = getClaudeDesktopConfigPath();
53
- return configPath ? { name, configPath } : null;
54
- }
55
- function readJsonSafe(path) {
56
- if (!existsSync(path)) return {};
57
- try {
58
- return JSON.parse(readFileSync(path, "utf-8"));
59
- } catch {
60
- return {};
61
- }
62
- }
63
- async function writeClientConfig(client, apiKey) {
64
- const config = readJsonSafe(client.configPath);
65
- const serversKey = "mcpServers";
66
- if (!config[serversKey]) config[serversKey] = {};
67
- if (config[serversKey][LEGACY_ENTRY_KEY]) {
68
- const legacy = config[serversKey][LEGACY_ENTRY_KEY];
69
- config[serversKey][SERVER_ENTRY_KEY] = {
70
- ...buildServerEntry(apiKey),
71
- env: { ...legacy.env, PRODUCTBRAIN_API_KEY: legacy.env?.PRODUCTBRAIN_API_KEY ?? apiKey }
72
- };
73
- delete config[serversKey][LEGACY_ENTRY_KEY];
74
- } else {
75
- const existing = config[serversKey][SERVER_ENTRY_KEY];
76
- config[serversKey][SERVER_ENTRY_KEY] = existing ? { ...existing, env: { ...existing.env, PRODUCTBRAIN_API_KEY: apiKey } } : buildServerEntry(apiKey);
77
- }
78
- const dir = dirname(client.configPath);
79
- if (!existsSync(dir)) {
80
- mkdirSync(dir, { recursive: true });
81
- }
82
- writeFileSync(client.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
83
- return true;
84
- }
85
-
86
- // src/cli/setup.ts
87
- var APP_URL = process.env.PRODUCTBRAIN_APP_URL ?? "https://productbrain.io";
15
+ import { existsSync, writeFileSync, mkdirSync } from "fs";
16
+ import { join } from "path";
17
+ var APP_URL = process.env.PRODUCTBRAIN_APP_URL ?? "https://work.productbrain.io";
88
18
  function bold(s) {
89
19
  return `\x1B[1m${s}\x1B[0m`;
90
20
  }
@@ -102,10 +32,10 @@ function log(msg) {
102
32
  `);
103
33
  }
104
34
  function openBrowser(url) {
105
- const platform2 = process.platform;
35
+ const platform = process.platform;
106
36
  try {
107
- if (platform2 === "darwin") execSync(`open "${url}"`);
108
- else if (platform2 === "win32") execSync(`start "" "${url}"`);
37
+ if (platform === "darwin") execSync(`open "${url}"`);
38
+ else if (platform === "win32") execSync(`start "" "${url}"`);
109
39
  else execSync(`xdg-open "${url}"`);
110
40
  } catch {
111
41
  log(dim(` Could not open browser automatically.`));
@@ -139,11 +69,11 @@ function promptChoice(question, choices) {
139
69
  });
140
70
  });
141
71
  }
142
- var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
72
+ var DEFAULT_CLOUD_URL = "https://gateway.productbrain.io";
143
73
  async function verifyWorkspace(apiKey) {
144
74
  const siteUrl = process.env.CONVEX_SITE_URL ?? process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
145
75
  try {
146
- const res = await fetch(`${siteUrl.replace(/\/$/, "")}/api/mcp`, {
76
+ const res = await fetch(`${siteUrl.replace(/\/$/, "")}/api/aki`, {
147
77
  method: "POST",
148
78
  headers: {
149
79
  "Content-Type": "application/json",
@@ -249,7 +179,7 @@ function printConfigSnippet(apiKey) {
249
179
  mcpServers: {
250
180
  "Product Brain": {
251
181
  command: "npx",
252
- args: ["-y", "@productbrain/mcp@beta"],
182
+ args: ["-y", MCP_NPX_PACKAGE],
253
183
  env: { PRODUCTBRAIN_API_KEY: apiKey }
254
184
  }
255
185
  }
@@ -285,34 +215,35 @@ Say **"Start PB"** or **"Start Product Brain"** to begin. This single call:
285
215
 
286
216
  ## Tool Workflow
287
217
 
288
- 1. **Start here**: \`orient\` or \`start\` \u2014 workspace context + next action
289
- 2. **Search**: \`search\` \u2014 find entries across all collections
290
- 3. **Drill in**: \`get-entry\` \u2014 full record with data, labels, relations
291
- 4. **Context**: \`gather-context\` \u2014 related knowledge around an entry or task
292
- 5. **Capture**: \`capture\` \u2014 create a draft with auto-linking + quality score
293
- 6. **Commit**: \`commit-entry\` \u2014 promote draft to SSOT (only when user confirms)
294
- 7. **Connect**: \`suggest-links\` then \`relate-entries\` to build the graph
218
+ 1. **Start here**: \`start\` \u2014 workspace context + next action
219
+ 2. **Search**: \`entries action=search\` \u2014 find entries across all collections
220
+ 3. **Drill in**: \`entries action=get\` \u2014 full record with data, labels, relations
221
+ 4. **Context**: \`context action=gather\` \u2014 related knowledge around an entry or task
222
+ 5. **Capture**: \`capture\` \u2014 create knowledge with auto-linking + quality score
223
+ 6. **Commit**: \`commit-entry\` \u2014 promote drafts to SSOT when confirmation is still required
224
+ 7. **Connect**: \`graph action=suggest\` then \`relations action=create\` to build the graph
295
225
 
296
226
  ## Bulk Knowledge Input
297
227
 
298
228
  When given a document or batch of knowledge to capture:
299
229
  1. Scan the input \u2014 identify all collections needed
300
- 2. Call \`list-collections\` \u2014 compare against what exists
230
+ 2. Call \`collections action=list\` \u2014 compare against what exists
301
231
  3. Propose missing collections to the user for confirmation
302
- 4. Call \`create-collection\` for each confirmed collection
232
+ 4. Call \`collections action=create\` for each confirmed collection
303
233
  5. Then capture entries into the correct collections
304
234
 
305
235
  Never stuff entries into the wrong collection. Never silently skip knowledge.
306
236
 
307
237
  ## Rules
308
238
 
309
- - Always capture as draft first. Only call \`commit-entry\` when the user confirms.
310
- - Use \`suggest-links\` after capturing to discover and create relations.
311
- - Collections are dynamic \u2014 use \`create-collection\` when the workspace needs new ones.
239
+ - In Open mode, user-authored captures can commit immediately unless the user asks to keep them as drafts.
240
+ - In consensus/role modes, only call \`commit-entry\` when the user confirms.
241
+ - Use \`graph action=suggest\` after capturing to discover and create relations.
242
+ - Collections are dynamic \u2014 use \`collections action=create\` when the workspace needs new ones.
312
243
  - When lost, fetch \`productbrain://orientation\` for the full system map.
313
244
  `;
314
245
  function isCursorProject() {
315
- return existsSync2(join2(process.cwd(), ".cursor")) || existsSync2(join2(process.cwd(), ".cursorignore"));
246
+ return existsSync(join(process.cwd(), ".cursor")) || existsSync(join(process.cwd(), ".cursorignore"));
316
247
  }
317
248
  async function offerCursorRulesInstall() {
318
249
  if (!isCursorProject()) return;
@@ -322,22 +253,22 @@ async function offerCursorRulesInstall() {
322
253
  log(dim(" Skipped rule install."));
323
254
  return;
324
255
  }
325
- const rulesDir = join2(process.cwd(), ".cursor", "rules");
326
- const rulePath = join2(rulesDir, CURSOR_RULE_FILENAME);
327
- if (existsSync2(rulePath)) {
256
+ const rulesDir = join(process.cwd(), ".cursor", "rules");
257
+ const rulePath = join(rulesDir, CURSOR_RULE_FILENAME);
258
+ if (existsSync(rulePath)) {
328
259
  log(` ${dim("\u2139")} Rule already exists at ${dim(rulePath)} \u2014 skipped`);
329
260
  return;
330
261
  }
331
- if (!existsSync2(rulesDir)) {
332
- mkdirSync2(rulesDir, { recursive: true });
262
+ if (!existsSync(rulesDir)) {
263
+ mkdirSync(rulesDir, { recursive: true });
333
264
  }
334
- writeFileSync2(rulePath, CURSOR_RULE_CONTENT, "utf-8");
265
+ writeFileSync(rulePath, CURSOR_RULE_CONTENT, "utf-8");
335
266
  log(` ${green("\u2713")} Installed rule at ${dim(rulePath)}`);
336
267
  }
337
268
  function buildDeeplink(apiKey) {
338
269
  const config = JSON.stringify({
339
270
  command: "npx",
340
- args: ["-y", "@productbrain/mcp@beta"],
271
+ args: ["-y", MCP_NPX_PACKAGE],
341
272
  env: { PRODUCTBRAIN_API_KEY: apiKey }
342
273
  });
343
274
  const encoded = Buffer.from(config).toString("base64url");
@@ -356,10 +287,11 @@ function printClaudeSnippet() {
356
287
  log("");
357
288
  log(` When Product Brain MCP is available, say "Start PB" at the beginning`);
358
289
  log(` of each session to orient to the workspace and unlock write tools.`);
359
- log(` Always capture as draft first; only commit when the user confirms.`);
290
+ log(` In Open mode, user-authored captures can commit immediately unless the user asks to keep drafts.`);
291
+ log(` In consensus/role modes, only commit when the user confirms.`);
360
292
  log("");
361
293
  }
362
294
  export {
363
295
  runSetup
364
296
  };
365
- //# sourceMappingURL=setup-GZ5OZ5OP.js.map
297
+ //# sourceMappingURL=setup-RYYXRDPB.js.map