@productbrain/mcp 0.0.1-beta.34 → 0.0.1-beta.35
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 +2 -2
- package/dist/http.js.map +1 -1
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -30,8 +30,8 @@ app.use(express.json());
|
|
|
30
30
|
var ALLOWED_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean);
|
|
31
31
|
app.use((_req, res, next) => {
|
|
32
32
|
const origin = _req.headers.origin;
|
|
33
|
-
if (
|
|
34
|
-
res.setHeader("Access-Control-Allow-Origin", origin
|
|
33
|
+
if (ALLOWED_ORIGINS && origin && ALLOWED_ORIGINS.includes(origin)) {
|
|
34
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
35
35
|
}
|
|
36
36
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
37
37
|
res.setHeader(
|
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\", \"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 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 } 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 — 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 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":[]}
|