@productbrain/mcp 0.0.1-beta.155 → 0.0.1-beta.157

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.
@@ -257,6 +257,54 @@ function trackFieldQualityWarning(workspaceId, props) {
257
257
  } catch {
258
258
  }
259
259
  }
260
+ function trackSessionCaptureRate(workspaceId, props) {
261
+ if (!client) return;
262
+ try {
263
+ client.capture({
264
+ distinctId,
265
+ event: "session_capture_rate",
266
+ properties: {
267
+ ...props,
268
+ workspace_id: workspaceId,
269
+ source_system: "mcp-server",
270
+ $groups: { workspace: workspaceId }
271
+ }
272
+ });
273
+ } catch {
274
+ }
275
+ }
276
+ function trackZeroCaptureAuditFired(workspaceId, props) {
277
+ if (!client) return;
278
+ try {
279
+ client.capture({
280
+ distinctId,
281
+ event: "zero_capture_audit_fired",
282
+ properties: {
283
+ ...props,
284
+ workspace_id: workspaceId,
285
+ source_system: "mcp-server",
286
+ $groups: { workspace: workspaceId }
287
+ }
288
+ });
289
+ } catch {
290
+ }
291
+ }
292
+ function trackWriteBackHintServed(workspaceId, props) {
293
+ if (!client) return;
294
+ try {
295
+ client.capture({
296
+ distinctId,
297
+ event: "write_back_hint_served",
298
+ properties: {
299
+ ...props,
300
+ workspace_id: workspaceId,
301
+ source_system: "mcp-server",
302
+ $groups: { workspace: workspaceId }
303
+ }
304
+ });
305
+ } catch {
306
+ }
307
+ }
260
308
  function getPostHogClient() {
261
309
  return client;
262
310
  }
@@ -354,10 +402,13 @@ export {
354
402
  trackCollectionClassified,
355
403
  trackFieldGuidanceApplied,
356
404
  trackFieldQualityWarning,
405
+ trackSessionCaptureRate,
406
+ trackZeroCaptureAuditFired,
407
+ trackWriteBackHintServed,
357
408
  getPostHogClient,
358
409
  shutdownAnalytics,
359
410
  MCP_NPX_PACKAGE,
360
411
  resolveClient,
361
412
  writeClientConfig
362
413
  };
363
- //# sourceMappingURL=chunk-X3S5UTTZ.js.map
414
+ //# sourceMappingURL=chunk-WYVQARYT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analytics.ts","../src/cli/config-writer.ts"],"sourcesContent":["/**\n * PostHog analytics for SynergyOS maintainers — tracks MCP usage (sessions, tool calls).\n * Not user-facing. Key is injected at build time via SYNERGYOS_POSTHOG_KEY.\n * Override with POSTHOG_MCP_KEY for self-hosted deployments.\n */\n\nimport { userInfo } from \"node:os\";\nimport { PostHog } from \"posthog-node\";\n\nlet client: PostHog | null = null;\nlet distinctId = \"anonymous\";\n\nconst POSTHOG_HOST = \"https://eu.i.posthog.com\";\n\n/** Injected at build time: SYNERGYOS_POSTHOG_KEY env when running `npm run build`/publish. */\ndeclare const __SYNERGYOS_POSTHOG_KEY__: string;\n\n/** Only write to stderr when MCP_DEBUG=1 for quieter default DX. */\nfunction log(msg: string): void {\n if (process.env.MCP_DEBUG === \"1\") {\n process.stderr.write(msg);\n }\n}\n\nfunction getBuildTimeKey(): string {\n try {\n return __SYNERGYOS_POSTHOG_KEY__;\n } catch {\n // Not replaced by bundler (e.g. running via tsx in tests) — treat as absent.\n return \"\";\n }\n}\n\nexport function initAnalytics(): void {\n const apiKey = process.env.POSTHOG_MCP_KEY || getBuildTimeKey();\n if (!apiKey) {\n log(\"[MCP-ANALYTICS] No PostHog key — tracking disabled (set SYNERGYOS_POSTHOG_KEY at build time for publish)\\n\");\n return;\n }\n\n client = new PostHog(apiKey, {\n host: POSTHOG_HOST,\n flushAt: 1,\n flushInterval: 5000,\n featureFlagsPollingInterval: 30_000,\n });\n distinctId = process.env.MCP_USER_ID || fallbackDistinctId();\n\n log(`[MCP-ANALYTICS] Initialized — host=${POSTHOG_HOST} distinctId=${distinctId}\\n`);\n}\n\nfunction fallbackDistinctId(): string {\n try {\n return userInfo().username;\n } catch {\n return `os-${process.pid}`;\n }\n}\n\nexport function trackSessionStarted(\n workspaceId: string,\n serverVersion: string,\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_session_started\",\n properties: {\n workspace_id: workspaceId,\n server_version: serverVersion,\n source: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n}\n\nexport function trackToolCall(\n fn: string,\n status: \"ok\" | \"error\",\n durationMs: number,\n workspaceId: string,\n errorMsg?: string,\n): void {\n const properties: Record<string, unknown> = {\n tool: fn,\n status,\n duration_ms: durationMs,\n workspace_id: workspaceId,\n source: \"mcp-server\",\n $groups: { workspace: workspaceId },\n };\n if (errorMsg) properties.error = errorMsg;\n\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_tool_called\",\n properties,\n });\n}\n\nexport function trackSetupStarted(): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_setup_started\",\n properties: {\n source: \"mcp-server\",\n platform: process.platform,\n },\n });\n}\n\nexport function trackSetupCompleted(\n chosenClient: string,\n outcome: \"config_written\" | \"config_existed\" | \"snippet_shown\" | \"write_error\",\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_setup_completed\",\n properties: {\n client: chosenClient,\n outcome,\n source: \"mcp-server\",\n platform: process.platform,\n },\n });\n}\n\nexport function trackQualityVerdict(\n workspaceId: string,\n props: {\n entry_id: string;\n entry_type: string;\n tier: string;\n context: string;\n passed: boolean;\n source: string;\n criteria_total: number;\n criteria_failed: number;\n llm_scheduled: boolean;\n },\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"quality_verdict_generated\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n}\n\nexport function trackQualityCheck(\n workspaceId: string,\n props: {\n entry_id: string;\n entry_type: string;\n tier: string;\n passed: boolean;\n source: string;\n llm_status?: string;\n llm_duration_ms?: number;\n llm_error?: string;\n has_roger_martin: boolean;\n },\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"quality_verdict_checked\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n}\n\nexport type ClassifierReasonCategory =\n | \"auto-routed\"\n | \"low-confidence\"\n | \"ambiguous\"\n | \"non-provisioned\";\n\ntype CaptureClassifierTelemetryProps = {\n predicted_collection: string;\n confidence: number;\n auto_routed: boolean;\n reason_category: ClassifierReasonCategory;\n explicit_collection_provided: boolean;\n};\n\nfunction trackCaptureClassifierEvent(\n event: \"mcp_capture_classifier_evaluated\" | \"mcp_capture_classifier_auto_routed\" | \"mcp_capture_classifier_fallback\",\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event,\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics are advisory and must never break capture flow.\n }\n}\n\nexport function trackCaptureClassifierEvaluated(\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n trackCaptureClassifierEvent(\"mcp_capture_classifier_evaluated\", workspaceId, props);\n}\n\nexport function trackCaptureClassifierAutoRouted(\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n trackCaptureClassifierEvent(\"mcp_capture_classifier_auto_routed\", workspaceId, props);\n}\n\nexport function trackCaptureClassifierFallback(\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n trackCaptureClassifierEvent(\"mcp_capture_classifier_fallback\", workspaceId, props);\n}\n\n/** GLO-26 / TEN-156: every SSOT commit for PostHog funnels (split auto vs manual). */\nexport function trackChainEntryCommitted(\n workspaceId: string,\n props: {\n entry_id: string;\n collection?: string;\n commit_method: \"auto\" | \"manual\";\n surface:\n | \"mcp_commit_tool\"\n | \"mcp_capture\"\n | \"mcp_wrapup\";\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"chain_entry_committed\",\n properties: {\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n ...props,\n },\n });\n } catch {\n // Analytics must never break the tool path.\n }\n}\n\nexport function trackKnowledgeGap(\n workspaceId: string,\n props: {\n query: string;\n tool: string;\n action: string;\n gap_type: \"search_zero\" | \"context_task_empty\" | \"context_entry_isolated\" | \"context_graph_empty\";\n collection_scope?: string;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"knowledge_gap_detected\",\n properties: {\n ...props,\n query: props.query.slice(0, 200),\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the tool response path.\n }\n}\n\n// ── BET-272 S6 / STD-155: Capture intelligence observability ────────────────\n\n/** Fires when formative quality hints are returned at capture time. */\nexport function trackCaptureQualityHints(\n workspaceId: string,\n props: {\n collection: string;\n hint_count: number;\n hint_fields: string[];\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"mcp_capture_quality_hints\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break capture flow.\n }\n}\n\n/** Fires when relation suggestions are returned at capture time. */\nexport function trackCaptureRelationSuggestions(\n workspaceId: string,\n props: {\n collection: string;\n suggestion_count: number;\n relation_types: string[];\n avg_confidence: number;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"mcp_capture_relation_suggestions\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break capture flow.\n }\n}\n\n// ── BET-288 S0: Collection classification confusion matrix telemetry ────────\n\n/**\n * Fires on every collection classification (auto-routed, fallback, explicit-provided).\n * Captures the full confusion signal: predicted collection, runner-up, thinkingLayer,\n * and confidence scores. This is the baseline measurement for BET-288 algorithm changes.\n */\nexport function trackCollectionClassified(\n workspaceId: string,\n props: {\n collection_slug: string;\n thinking_layer: string | null;\n confidence: number;\n classified_by: \"llm\" | \"heuristic\";\n confidence_tier: \"high\" | \"medium\" | \"low\";\n alternative_slug: string | null;\n alternative_confidence: number | null;\n explicit_collection_provided: boolean;\n auto_routed: boolean;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"collection_classified\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the capture flow.\n }\n}\n\n// ── BET-289 S5: Field-level writing guidance telemetry ────────────────────────\n\n/** Fires when field guidance is injected into a capture prompt or response. */\nexport function trackFieldGuidanceApplied(\n workspaceId: string,\n props: {\n collection: string;\n guided_field_count: number;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"field_guidance_applied\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the capture flow.\n }\n}\n\n/** Fires when commit-time heuristic detects field guidance violations. */\nexport function trackFieldQualityWarning(\n workspaceId: string,\n props: {\n warning_count: number;\n warning_types: string[];\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"field_quality_warning\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the commit flow.\n }\n}\n\n/** Fires when skipGuidanceCheck is used to bypass guidance validation. */\nexport function trackFieldQualityOverride(\n workspaceId: string,\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"field_quality_override\",\n properties: {\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the commit flow.\n }\n}\n\n// ── WP-306 S3: Write-Back Measurement (capture rate insights) ──────────────\n\n/** Fires on session close to track session capture rate. */\nexport function trackSessionCaptureRate(\n workspaceId: string,\n props: {\n entries_created: number;\n entries_modified: number;\n relations_created: number;\n had_captures: boolean;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"session_capture_rate\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break session closure.\n }\n}\n\n/** Fires when captureAudit is triggered (activity but no captures). */\nexport function trackZeroCaptureAuditFired(\n workspaceId: string,\n props: {\n suggestion_count: number;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"zero_capture_audit_fired\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break wrapup flow.\n }\n}\n\n/** Fires when writeBackHints are included in an orient response. */\nexport function trackWriteBackHintServed(\n workspaceId: string,\n props: {\n hint_count: number;\n has_task: boolean;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"write_back_hint_served\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break orient flow.\n }\n}\n\nexport function getPostHogClient(): PostHog | null {\n return client;\n}\n\nexport async function shutdownAnalytics(): Promise<void> {\n await client?.shutdown();\n}\n","/**\n * Multi-client MCP config detection and writer.\n *\n * Supports:\n * - Cursor: .cursor/mcp.json in cwd (project-level)\n * - Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)\n * %APPDATA%/Claude/claude_desktop_config.json (Windows)\n *\n * The writer reads existing config, merges the new server entry (never\n * overwrites existing entries), and writes back. Falls back to printing\n * a snippet for unsupported OS or unknown formats.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, platform } from \"node:os\";\n\nexport interface McpClientInfo {\n name: string;\n configPath: string;\n}\n\nconst SERVER_ENTRY_KEY = \"Product Brain\";\nconst LEGACY_ENTRY_KEY = \"productbrain\";\n\n/**\n * Canonical npx package specifier. Update here when exiting beta.\n * Frontend mirror: src/lib/constants/mcp.ts\n * Business rule: BR-84 (Chain)\n */\nexport const MCP_NPX_PACKAGE = \"@productbrain/mcp@beta\";\n\nfunction buildServerEntry(apiKey: string) {\n return {\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n };\n}\n\n// ── Detection ───────────────────────────────────────────────────────────\n\nfunction getCursorConfigPath(): string {\n return join(process.cwd(), \".cursor\", \"mcp.json\");\n}\n\nfunction getClaudeDesktopConfigPath(): string | null {\n const os = platform();\n if (os === \"darwin\") {\n return join(\n homedir(),\n \"Library\",\n \"Application Support\",\n \"Claude\",\n \"claude_desktop_config.json\",\n );\n }\n if (os === \"win32\") {\n const appData = process.env.APPDATA ?? join(homedir(), \"AppData\", \"Roaming\");\n return join(appData, \"Claude\", \"claude_desktop_config.json\");\n }\n // Linux: no official Claude Desktop location yet\n return null;\n}\n\nexport function resolveClient(name: \"Cursor\" | \"Claude Desktop\"): McpClientInfo | null {\n if (name === \"Cursor\") {\n return { name, configPath: getCursorConfigPath() };\n }\n const configPath = getClaudeDesktopConfigPath();\n return configPath ? { name, configPath } : null;\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────\n\nfunction readJsonSafe(path: string): Record<string, any> {\n if (!existsSync(path)) return {};\n try {\n return JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return {};\n }\n}\n\n/**\n * Write or merge the Product Brain server entry into a client config file.\n * Migrates legacy \"productbrain\" key to \"Product Brain\" when present.\n * Returns true if the config was written, false if already present.\n */\nexport async function writeClientConfig(\n client: McpClientInfo,\n apiKey: string,\n): Promise<boolean> {\n const config = readJsonSafe(client.configPath);\n\n const serversKey = \"mcpServers\";\n if (!config[serversKey]) config[serversKey] = {};\n\n // Migrate legacy \"productbrain\" key or update existing Product Brain with new API key\n if (config[serversKey][LEGACY_ENTRY_KEY]) {\n const legacy = config[serversKey][LEGACY_ENTRY_KEY];\n config[serversKey][SERVER_ENTRY_KEY] = {\n ...buildServerEntry(apiKey),\n env: { ...legacy.env, PRODUCTBRAIN_API_KEY: legacy.env?.PRODUCTBRAIN_API_KEY ?? apiKey },\n };\n delete config[serversKey][LEGACY_ENTRY_KEY];\n } else {\n const existing = config[serversKey][SERVER_ENTRY_KEY];\n config[serversKey][SERVER_ENTRY_KEY] = existing\n ? { ...existing, env: { ...existing.env, PRODUCTBRAIN_API_KEY: apiKey } }\n : buildServerEntry(apiKey);\n }\n\n const dir = dirname(client.configPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n writeFileSync(client.configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n return true;\n}\n"],"mappings":"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","names":["client"]}
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli/index.ts
4
4
  var subcommand = process.argv[2];
5
5
  if (subcommand === "setup") {
6
- const { runSetup } = await import("../setup-GUKWCWK6.js");
6
+ const { runSetup } = await import("../setup-CTBG5GQM.js");
7
7
  await runSetup();
8
8
  } else {
9
9
  await import("../index.js");
package/dist/http.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  hashKey,
6
6
  initFeatureFlags,
7
7
  runWithAuth
8
- } from "./chunk-GMSHWOXT.js";
8
+ } from "./chunk-NRP3DF6E.js";
9
9
  import {
10
10
  getPostHogClient,
11
11
  initAnalytics,
12
12
  shutdownAnalytics
13
- } from "./chunk-X3S5UTTZ.js";
13
+ } from "./chunk-WYVQARYT.js";
14
14
 
15
15
  // src/http.ts
16
16
  import { createHash, randomUUID } from "crypto";
@@ -123,6 +123,8 @@ var ACCESS_TOKEN_TTL = 3600;
123
123
  var ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1e3;
124
124
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
125
125
  var refreshTokens = /* @__PURE__ */ new Map();
126
+ var MAX_REFRESH_TOKENS = 2e3;
127
+ var MAX_REFRESH_TOKENS_PER_KEY = 20;
126
128
  var accessTokens = /* @__PURE__ */ new Map();
127
129
  var MAX_ACCESS_TOKENS = 1e3;
128
130
  setInterval(() => {
@@ -136,6 +138,12 @@ setInterval(() => {
136
138
  for (const [token, entry] of refreshTokens) {
137
139
  if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);
138
140
  }
141
+ if (refreshTokens.size > MAX_REFRESH_TOKENS) {
142
+ const sorted = [...refreshTokens.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
143
+ for (let i = 0; i < sorted.length - MAX_REFRESH_TOKENS; i++) {
144
+ refreshTokens.delete(sorted[i][0]);
145
+ }
146
+ }
139
147
  for (const [token, entry] of accessTokens) {
140
148
  if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
141
149
  }
@@ -253,6 +261,32 @@ function issueTokens(apiKey) {
253
261
  }
254
262
  accessTokens.set(opaqueToken, { apiKey, createdAt: now });
255
263
  const refreshToken = `pb_rt_${randomUUID()}`;
264
+ let perKeyCount = 0;
265
+ let oldestKeyForApiKey = null;
266
+ let oldestAtForApiKey = Infinity;
267
+ for (const [k, v] of refreshTokens) {
268
+ if (v.apiKey === apiKey) {
269
+ perKeyCount++;
270
+ if (v.createdAt < oldestAtForApiKey) {
271
+ oldestAtForApiKey = v.createdAt;
272
+ oldestKeyForApiKey = k;
273
+ }
274
+ }
275
+ }
276
+ if (perKeyCount >= MAX_REFRESH_TOKENS_PER_KEY && oldestKeyForApiKey) {
277
+ refreshTokens.delete(oldestKeyForApiKey);
278
+ }
279
+ if (refreshTokens.size >= MAX_REFRESH_TOKENS) {
280
+ let oldestKey = null;
281
+ let oldestAt = Infinity;
282
+ for (const [k, v] of refreshTokens) {
283
+ if (v.createdAt < oldestAt) {
284
+ oldestAt = v.createdAt;
285
+ oldestKey = k;
286
+ }
287
+ }
288
+ if (oldestKey) refreshTokens.delete(oldestKey);
289
+ }
256
290
  refreshTokens.set(refreshToken, { apiKey, createdAt: now });
257
291
  return {
258
292
  access_token: opaqueToken,
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, hashKey } 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>();\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 // 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;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[c]!,\n );\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } =\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 <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\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 authLimiter,\n express.urlencoded({ extended: false }),\n (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, state, client_id } = req.body;\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n res.status(400).send(\"Invalid API key\");\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 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 // Fix 1 — Generate an opaque pb_at_* access token so the raw pb_sk_* key\n // is never returned through the OAuth flow.\n const opaqueToken = `pb_at_${randomUUID()}`;\n const now = Date.now();\n\n // LRU eviction: if at cap, remove the oldest entry before inserting.\n if (accessTokens.size >= MAX_ACCESS_TOKENS) {\n let oldestKey = \"\";\n let oldestTime = Infinity;\n for (const [k, v] of accessTokens) {\n if (v.createdAt < oldestTime) {\n oldestTime = v.createdAt;\n oldestKey = k;\n }\n }\n if (oldestKey) accessTokens.delete(oldestKey);\n }\n\n accessTokens.set(opaqueToken, { apiKey, createdAt: now });\n\n const refreshToken = `pb_rt_${randomUUID()}`;\n refreshTokens.set(refreshToken, { apiKey, createdAt: now });\n return {\n access_token: opaqueToken,\n token_type: \"Bearer\",\n expires_in: ACCESS_TOKEN_TTL,\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":[]}
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, hashKey } 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;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[c]!,\n );\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } =\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 <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\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 authLimiter,\n express.urlencoded({ extended: false }),\n (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, state, client_id } = req.body;\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n res.status(400).send(\"Invalid API key\");\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 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 // Fix 1 — Generate an opaque pb_at_* access token so the raw pb_sk_* key\n // is never returned through the OAuth flow.\n const opaqueToken = `pb_at_${randomUUID()}`;\n const now = Date.now();\n\n // LRU eviction: if at cap, remove the oldest entry before inserting.\n if (accessTokens.size >= MAX_ACCESS_TOKENS) {\n let oldestKey = \"\";\n let oldestTime = Infinity;\n for (const [k, v] of accessTokens) {\n if (v.createdAt < oldestTime) {\n oldestTime = v.createdAt;\n oldestKey = k;\n }\n }\n if (oldestKey) accessTokens.delete(oldestKey);\n }\n\n accessTokens.set(opaqueToken, { apiKey, createdAt: 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: opaqueToken,\n token_type: \"Bearer\",\n expires_in: ACCESS_TOKEN_TTL,\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":[]}
package/dist/index.js CHANGED
@@ -9,13 +9,13 @@ import {
9
9
  orphanAgentSession,
10
10
  recoverSessionState,
11
11
  startAgentSession
12
- } from "./chunk-GMSHWOXT.js";
12
+ } from "./chunk-NRP3DF6E.js";
13
13
  import {
14
14
  getPostHogClient,
15
15
  initAnalytics,
16
16
  shutdownAnalytics,
17
17
  trackSessionStarted
18
- } from "./chunk-X3S5UTTZ.js";
18
+ } from "./chunk-WYVQARYT.js";
19
19
 
20
20
  // src/index.ts
21
21
  import { readFileSync } from "fs";
@@ -7,7 +7,7 @@ import {
7
7
  trackSetupCompleted,
8
8
  trackSetupStarted,
9
9
  writeClientConfig
10
- } from "./chunk-X3S5UTTZ.js";
10
+ } from "./chunk-WYVQARYT.js";
11
11
 
12
12
  // src/cli/setup.ts
13
13
  import { execSync } from "child_process";
@@ -294,4 +294,4 @@ function printClaudeSnippet() {
294
294
  export {
295
295
  runSetup
296
296
  };
297
- //# sourceMappingURL=setup-GUKWCWK6.js.map
297
+ //# sourceMappingURL=setup-CTBG5GQM.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productbrain/mcp",
3
- "version": "0.0.1-beta.155",
3
+ "version": "0.0.1-beta.157",
4
4
  "description": "Product Brain — MCP server for AI-assisted product knowledge management",
5
5
  "type": "module",
6
6
  "engines": {