@productbrain/mcp 0.0.1-beta.14 → 0.0.1-beta.140

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.
@@ -0,0 +1,312 @@
1
+ // src/analytics.ts
2
+ import { userInfo } from "os";
3
+ import { PostHog } from "posthog-node";
4
+ var client = null;
5
+ var distinctId = "anonymous";
6
+ var POSTHOG_HOST = "https://eu.i.posthog.com";
7
+ function log(msg) {
8
+ if (process.env.MCP_DEBUG === "1") {
9
+ process.stderr.write(msg);
10
+ }
11
+ }
12
+ function getBuildTimeKey() {
13
+ try {
14
+ return "";
15
+ } catch {
16
+ return "";
17
+ }
18
+ }
19
+ function initAnalytics() {
20
+ const apiKey = process.env.POSTHOG_MCP_KEY || getBuildTimeKey();
21
+ if (!apiKey) {
22
+ log("[MCP-ANALYTICS] No PostHog key \u2014 tracking disabled (set SYNERGYOS_POSTHOG_KEY at build time for publish)\n");
23
+ return;
24
+ }
25
+ client = new PostHog(apiKey, {
26
+ host: POSTHOG_HOST,
27
+ flushAt: 1,
28
+ flushInterval: 5e3,
29
+ featureFlagsPollingInterval: 3e4
30
+ });
31
+ distinctId = process.env.MCP_USER_ID || fallbackDistinctId();
32
+ log(`[MCP-ANALYTICS] Initialized \u2014 host=${POSTHOG_HOST} distinctId=${distinctId}
33
+ `);
34
+ }
35
+ function fallbackDistinctId() {
36
+ try {
37
+ return userInfo().username;
38
+ } catch {
39
+ return `os-${process.pid}`;
40
+ }
41
+ }
42
+ function trackSessionStarted(workspaceId, serverVersion) {
43
+ if (!client) return;
44
+ client.capture({
45
+ distinctId,
46
+ event: "mcp_session_started",
47
+ properties: {
48
+ workspace_id: workspaceId,
49
+ server_version: serverVersion,
50
+ source: "mcp-server",
51
+ $groups: { workspace: workspaceId }
52
+ }
53
+ });
54
+ }
55
+ function trackToolCall(fn, status, durationMs, workspaceId, errorMsg) {
56
+ const properties = {
57
+ tool: fn,
58
+ status,
59
+ duration_ms: durationMs,
60
+ workspace_id: workspaceId,
61
+ source: "mcp-server",
62
+ $groups: { workspace: workspaceId }
63
+ };
64
+ if (errorMsg) properties.error = errorMsg;
65
+ if (!client) return;
66
+ client.capture({
67
+ distinctId,
68
+ event: "mcp_tool_called",
69
+ properties
70
+ });
71
+ }
72
+ function trackSetupStarted() {
73
+ if (!client) return;
74
+ client.capture({
75
+ distinctId,
76
+ event: "mcp_setup_started",
77
+ properties: {
78
+ source: "mcp-server",
79
+ platform: process.platform
80
+ }
81
+ });
82
+ }
83
+ function trackSetupCompleted(chosenClient, outcome) {
84
+ if (!client) return;
85
+ client.capture({
86
+ distinctId,
87
+ event: "mcp_setup_completed",
88
+ properties: {
89
+ client: chosenClient,
90
+ outcome,
91
+ source: "mcp-server",
92
+ platform: process.platform
93
+ }
94
+ });
95
+ }
96
+ function trackQualityVerdict(workspaceId, props) {
97
+ if (!client) return;
98
+ client.capture({
99
+ distinctId,
100
+ event: "quality_verdict_generated",
101
+ properties: {
102
+ ...props,
103
+ workspace_id: workspaceId,
104
+ source_system: "mcp-server",
105
+ $groups: { workspace: workspaceId }
106
+ }
107
+ });
108
+ }
109
+ function trackQualityCheck(workspaceId, props) {
110
+ if (!client) return;
111
+ client.capture({
112
+ distinctId,
113
+ event: "quality_verdict_checked",
114
+ properties: {
115
+ ...props,
116
+ workspace_id: workspaceId,
117
+ source_system: "mcp-server",
118
+ $groups: { workspace: workspaceId }
119
+ }
120
+ });
121
+ }
122
+ function trackCaptureClassifierEvent(event, workspaceId, props) {
123
+ if (!client) return;
124
+ try {
125
+ client.capture({
126
+ distinctId,
127
+ event,
128
+ properties: {
129
+ ...props,
130
+ workspace_id: workspaceId,
131
+ source_system: "mcp-server",
132
+ $groups: { workspace: workspaceId }
133
+ }
134
+ });
135
+ } catch {
136
+ }
137
+ }
138
+ function trackCaptureClassifierEvaluated(workspaceId, props) {
139
+ trackCaptureClassifierEvent("mcp_capture_classifier_evaluated", workspaceId, props);
140
+ }
141
+ function trackCaptureClassifierAutoRouted(workspaceId, props) {
142
+ trackCaptureClassifierEvent("mcp_capture_classifier_auto_routed", workspaceId, props);
143
+ }
144
+ function trackCaptureClassifierFallback(workspaceId, props) {
145
+ trackCaptureClassifierEvent("mcp_capture_classifier_fallback", workspaceId, props);
146
+ }
147
+ function trackChainEntryCommitted(workspaceId, props) {
148
+ if (!client) return;
149
+ try {
150
+ client.capture({
151
+ distinctId,
152
+ event: "chain_entry_committed",
153
+ properties: {
154
+ workspace_id: workspaceId,
155
+ source_system: "mcp-server",
156
+ $groups: { workspace: workspaceId },
157
+ ...props
158
+ }
159
+ });
160
+ } catch {
161
+ }
162
+ }
163
+ function trackKnowledgeGap(workspaceId, props) {
164
+ if (!client) return;
165
+ try {
166
+ client.capture({
167
+ distinctId,
168
+ event: "knowledge_gap_detected",
169
+ properties: {
170
+ ...props,
171
+ query: props.query.slice(0, 200),
172
+ workspace_id: workspaceId,
173
+ source_system: "mcp-server",
174
+ $groups: { workspace: workspaceId }
175
+ }
176
+ });
177
+ } catch {
178
+ }
179
+ }
180
+ function trackCaptureQualityHints(workspaceId, props) {
181
+ if (!client) return;
182
+ try {
183
+ client.capture({
184
+ distinctId,
185
+ event: "mcp_capture_quality_hints",
186
+ properties: {
187
+ ...props,
188
+ workspace_id: workspaceId,
189
+ source_system: "mcp-server",
190
+ $groups: { workspace: workspaceId }
191
+ }
192
+ });
193
+ } catch {
194
+ }
195
+ }
196
+ function trackCaptureRelationSuggestions(workspaceId, props) {
197
+ if (!client) return;
198
+ try {
199
+ client.capture({
200
+ distinctId,
201
+ event: "mcp_capture_relation_suggestions",
202
+ properties: {
203
+ ...props,
204
+ workspace_id: workspaceId,
205
+ source_system: "mcp-server",
206
+ $groups: { workspace: workspaceId }
207
+ }
208
+ });
209
+ } catch {
210
+ }
211
+ }
212
+ function getPostHogClient() {
213
+ return client;
214
+ }
215
+ async function shutdownAnalytics() {
216
+ await client?.shutdown();
217
+ }
218
+
219
+ // src/cli/config-writer.ts
220
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
221
+ import { join, dirname } from "path";
222
+ import { homedir, platform } from "os";
223
+ var SERVER_ENTRY_KEY = "Product Brain";
224
+ var LEGACY_ENTRY_KEY = "productbrain";
225
+ var MCP_NPX_PACKAGE = "@productbrain/mcp@beta";
226
+ function buildServerEntry(apiKey) {
227
+ return {
228
+ command: "npx",
229
+ args: ["-y", MCP_NPX_PACKAGE],
230
+ env: { PRODUCTBRAIN_API_KEY: apiKey }
231
+ };
232
+ }
233
+ function getCursorConfigPath() {
234
+ return join(process.cwd(), ".cursor", "mcp.json");
235
+ }
236
+ function getClaudeDesktopConfigPath() {
237
+ const os = platform();
238
+ if (os === "darwin") {
239
+ return join(
240
+ homedir(),
241
+ "Library",
242
+ "Application Support",
243
+ "Claude",
244
+ "claude_desktop_config.json"
245
+ );
246
+ }
247
+ if (os === "win32") {
248
+ const appData = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
249
+ return join(appData, "Claude", "claude_desktop_config.json");
250
+ }
251
+ return null;
252
+ }
253
+ function resolveClient(name) {
254
+ if (name === "Cursor") {
255
+ return { name, configPath: getCursorConfigPath() };
256
+ }
257
+ const configPath = getClaudeDesktopConfigPath();
258
+ return configPath ? { name, configPath } : null;
259
+ }
260
+ function readJsonSafe(path) {
261
+ if (!existsSync(path)) return {};
262
+ try {
263
+ return JSON.parse(readFileSync(path, "utf-8"));
264
+ } catch {
265
+ return {};
266
+ }
267
+ }
268
+ async function writeClientConfig(client2, apiKey) {
269
+ const config = readJsonSafe(client2.configPath);
270
+ const serversKey = "mcpServers";
271
+ if (!config[serversKey]) config[serversKey] = {};
272
+ if (config[serversKey][LEGACY_ENTRY_KEY]) {
273
+ const legacy = config[serversKey][LEGACY_ENTRY_KEY];
274
+ config[serversKey][SERVER_ENTRY_KEY] = {
275
+ ...buildServerEntry(apiKey),
276
+ env: { ...legacy.env, PRODUCTBRAIN_API_KEY: legacy.env?.PRODUCTBRAIN_API_KEY ?? apiKey }
277
+ };
278
+ delete config[serversKey][LEGACY_ENTRY_KEY];
279
+ } else {
280
+ const existing = config[serversKey][SERVER_ENTRY_KEY];
281
+ config[serversKey][SERVER_ENTRY_KEY] = existing ? { ...existing, env: { ...existing.env, PRODUCTBRAIN_API_KEY: apiKey } } : buildServerEntry(apiKey);
282
+ }
283
+ const dir = dirname(client2.configPath);
284
+ if (!existsSync(dir)) {
285
+ mkdirSync(dir, { recursive: true });
286
+ }
287
+ writeFileSync(client2.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
288
+ return true;
289
+ }
290
+
291
+ export {
292
+ initAnalytics,
293
+ trackSessionStarted,
294
+ trackToolCall,
295
+ trackSetupStarted,
296
+ trackSetupCompleted,
297
+ trackQualityVerdict,
298
+ trackQualityCheck,
299
+ trackCaptureClassifierEvaluated,
300
+ trackCaptureClassifierAutoRouted,
301
+ trackCaptureClassifierFallback,
302
+ trackChainEntryCommitted,
303
+ trackKnowledgeGap,
304
+ trackCaptureQualityHints,
305
+ trackCaptureRelationSuggestions,
306
+ getPostHogClient,
307
+ shutdownAnalytics,
308
+ MCP_NPX_PACKAGE,
309
+ resolveClient,
310
+ writeClientConfig
311
+ };
312
+ //# sourceMappingURL=chunk-MOPOQUJP.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\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-GZ5OZ5OP.js");
6
+ const { runSetup } = await import("../setup-YYADLH22.js");
7
7
  await runSetup();
8
8
  } else {
9
9
  await import("../index.js");
package/dist/http.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  SERVER_VERSION,
3
- createProductBrainServer
4
- } from "./chunk-MV3VHRMV.js";
5
- import {
6
3
  bootstrapHttp,
4
+ createProductBrainServer,
5
+ initFeatureFlags,
7
6
  runWithAuth
8
- } from "./chunk-HLXF3QPE.js";
7
+ } from "./chunk-CDBSOVW7.js";
9
8
  import {
9
+ getPostHogClient,
10
10
  initAnalytics,
11
11
  shutdownAnalytics
12
- } from "./chunk-XBMI6QHR.js";
12
+ } from "./chunk-MOPOQUJP.js";
13
13
 
14
14
  // src/http.ts
15
15
  import { createHash, randomUUID } from "crypto";
@@ -19,19 +19,21 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
19
19
  import rateLimit from "express-rate-limit";
20
20
  bootstrapHttp();
21
21
  initAnalytics();
22
- var PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "3000", 10);
22
+ initFeatureFlags(getPostHogClient());
23
+ var PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "3002", 10);
23
24
  function baseUrl(req) {
24
25
  const proto = req.headers["x-forwarded-proto"] ?? req.protocol ?? "http";
25
26
  const host = req.headers.host ?? `localhost:${PORT}`;
26
27
  return `${proto}://${host}`;
27
28
  }
28
29
  var app = express();
30
+ app.set("trust proxy", 1);
29
31
  app.use(express.json());
30
32
  var ALLOWED_ORIGINS = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean);
31
33
  app.use((_req, res, next) => {
32
34
  const origin = _req.headers.origin;
33
- if (!ALLOWED_ORIGINS || origin && ALLOWED_ORIGINS.includes(origin)) {
34
- res.setHeader("Access-Control-Allow-Origin", origin ?? "*");
35
+ if (ALLOWED_ORIGINS && origin && ALLOWED_ORIGINS.includes(origin)) {
36
+ res.setHeader("Access-Control-Allow-Origin", origin);
35
37
  }
36
38
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
37
39
  res.setHeader(
@@ -62,7 +64,7 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
62
64
  token_endpoint: `${base}/oauth/token`,
63
65
  registration_endpoint: `${base}/register`,
64
66
  response_types_supported: ["code"],
65
- grant_types_supported: ["authorization_code"],
67
+ grant_types_supported: ["authorization_code", "refresh_token"],
66
68
  code_challenge_methods_supported: ["S256"],
67
69
  token_endpoint_auth_methods_supported: ["none"],
68
70
  scopes_supported: ["mcp:tools", "mcp:resources"]
@@ -100,6 +102,9 @@ app.post(
100
102
  }
101
103
  );
102
104
  var pendingCodes = /* @__PURE__ */ new Map();
105
+ var ACCESS_TOKEN_TTL = 3600;
106
+ var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
107
+ var refreshTokens = /* @__PURE__ */ new Map();
103
108
  setInterval(() => {
104
109
  const now = Date.now();
105
110
  for (const [code, auth] of pendingCodes) {
@@ -108,6 +113,9 @@ setInterval(() => {
108
113
  for (const [id, client] of registeredClients) {
109
114
  if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
110
115
  }
116
+ for (const [token, entry] of refreshTokens) {
117
+ if (now - entry.createdAt > REFRESH_TOKEN_TTL_MS) refreshTokens.delete(token);
118
+ }
111
119
  }, 6e4);
112
120
  function esc(s) {
113
121
  return String(s ?? "").replace(
@@ -116,7 +124,7 @@ function esc(s) {
116
124
  );
117
125
  }
118
126
  app.get("/authorize", (req, res) => {
119
- const { redirect_uri, code_challenge, code_challenge_method, state } = req.query;
127
+ const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;
120
128
  res.type("html").send(`<!DOCTYPE html>
121
129
  <html lang="en"><head>
122
130
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -145,6 +153,7 @@ button:hover{background:#6d28d9}
145
153
  <input type="hidden" name="code_challenge" value="${esc(code_challenge)}">
146
154
  <input type="hidden" name="code_challenge_method" value="${esc(code_challenge_method)}">
147
155
  <input type="hidden" name="state" value="${esc(state)}">
156
+ <input type="hidden" name="client_id" value="${esc(client_id)}">
148
157
  <label for="k">API Key</label>
149
158
  <input type="password" id="k" name="api_key" placeholder="pb_sk_\u2026" required autofocus>
150
159
  <p class="err" id="e">Key must start with pb_sk_</p>
@@ -160,11 +169,26 @@ app.post(
160
169
  "/authorize",
161
170
  express.urlencoded({ extended: false }),
162
171
  (req, res) => {
163
- const { api_key, redirect_uri, code_challenge, state } = req.body;
172
+ const { api_key, redirect_uri, code_challenge, state, client_id } = req.body;
164
173
  if (!api_key?.startsWith("pb_sk_")) {
165
174
  res.status(400).send("Invalid API key");
166
175
  return;
167
176
  }
177
+ if (!client_id || !registeredClients.has(client_id)) {
178
+ res.status(400).json({
179
+ error: "invalid_request",
180
+ error_description: "Unknown or missing client_id"
181
+ });
182
+ return;
183
+ }
184
+ const client = registeredClients.get(client_id);
185
+ if (!client.redirect_uris.includes(redirect_uri)) {
186
+ res.status(400).json({
187
+ error: "invalid_request",
188
+ error_description: "redirect_uri does not match any registered redirect for this client"
189
+ });
190
+ return;
191
+ }
168
192
  const code = randomUUID();
169
193
  pendingCodes.set(code, {
170
194
  apiKey: api_key,
@@ -178,12 +202,38 @@ app.post(
178
202
  res.redirect(302, url.toString());
179
203
  }
180
204
  );
205
+ function issueTokens(apiKey) {
206
+ const refreshToken = `pb_rt_${randomUUID()}`;
207
+ refreshTokens.set(refreshToken, { apiKey, createdAt: Date.now() });
208
+ return {
209
+ access_token: apiKey,
210
+ token_type: "Bearer",
211
+ expires_in: ACCESS_TOKEN_TTL,
212
+ refresh_token: refreshToken
213
+ };
214
+ }
181
215
  app.post(
182
216
  "/oauth/token",
183
217
  express.urlencoded({ extended: false }),
184
218
  express.json(),
185
219
  (req, res) => {
186
- const { grant_type, code, code_verifier, redirect_uri } = req.body;
220
+ const { grant_type, code, code_verifier, redirect_uri, refresh_token } = req.body;
221
+ if (grant_type === "refresh_token") {
222
+ const entry = refreshTokens.get(refresh_token);
223
+ if (!entry) {
224
+ res.status(400).json({ error: "invalid_grant", error_description: "Invalid refresh token" });
225
+ return;
226
+ }
227
+ if (Date.now() - entry.createdAt > REFRESH_TOKEN_TTL_MS) {
228
+ refreshTokens.delete(refresh_token);
229
+ res.status(400).json({ error: "invalid_grant", error_description: "Refresh token expired" });
230
+ return;
231
+ }
232
+ const apiKey = entry.apiKey;
233
+ refreshTokens.delete(refresh_token);
234
+ res.json(issueTokens(apiKey));
235
+ return;
236
+ }
187
237
  if (grant_type !== "authorization_code") {
188
238
  res.status(400).json({ error: "unsupported_grant_type" });
189
239
  return;
@@ -203,11 +253,7 @@ app.post(
203
253
  return;
204
254
  }
205
255
  pendingCodes.delete(code);
206
- res.json({
207
- access_token: pending.apiKey,
208
- token_type: "Bearer",
209
- expires_in: 3600
210
- });
256
+ res.json(issueTokens(pending.apiKey));
211
257
  }
212
258
  );
213
259
  var mcpLimiter = rateLimit({
@@ -227,6 +273,7 @@ function evictStaleSessions() {
227
273
  const now = Date.now();
228
274
  for (const [id, entry] of sessions) {
229
275
  if (now - entry.lastAccess > SESSION_TTL_MS) {
276
+ logSessionLifecycle("session_deleted", id, "ttl");
230
277
  entry.transport.close().catch(() => {
231
278
  });
232
279
  sessions.delete(id);
@@ -237,6 +284,7 @@ function evictStaleSessions() {
237
284
  (a, b) => a[1].lastAccess - b[1].lastAccess
238
285
  );
239
286
  for (let i = 0; i < sorted.length - MAX_SESSIONS; i++) {
287
+ logSessionLifecycle("session_deleted", sorted[i][0], "eviction");
240
288
  sorted[i][1].transport.close().catch(() => {
241
289
  });
242
290
  sessions.delete(sorted[i][0]);
@@ -257,10 +305,17 @@ function send401(req, res) {
257
305
  `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`
258
306
  ).json({ error: "unauthorized" });
259
307
  }
260
- function logRequest(method, outcome, sessionId) {
308
+ function logRequest(method, outcome, sessionId, durationMs) {
261
309
  const ts = (/* @__PURE__ */ new Date()).toISOString();
262
310
  const sid = sessionId ? ` session=${sessionId}` : "";
263
- process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}
311
+ const dur = durationMs != null ? ` duration=${durationMs}ms` : "";
312
+ process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}
313
+ `);
314
+ }
315
+ function logSessionLifecycle(event, sessionId, reason) {
316
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
317
+ const r = reason ? ` reason=${reason}` : "";
318
+ process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}
264
319
  `);
265
320
  }
266
321
  app.post("/mcp", mcpLimiter, async (req, res) => {
@@ -271,29 +326,38 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
271
326
  return;
272
327
  }
273
328
  const sessionId = req.headers["mcp-session-id"];
329
+ const reqStart = Date.now();
274
330
  try {
275
331
  await runWithAuth({ apiKey }, async () => {
276
332
  if (sessionId && sessions.has(sessionId)) {
277
333
  const entry = sessions.get(sessionId);
278
334
  entry.lastAccess = Date.now();
279
335
  await entry.transport.handleRequest(req, res, req.body);
280
- logRequest("POST", "ok", sessionId);
336
+ logRequest("POST", "ok", sessionId, Date.now() - reqStart);
281
337
  } else if (!sessionId && isInitializeRequest(req.body)) {
282
338
  const transport = new StreamableHTTPServerTransport({
283
339
  sessionIdGenerator: () => randomUUID(),
284
340
  onsessioninitialized: (sid) => {
285
341
  sessions.set(sid, { transport, lastAccess: Date.now() });
286
- logRequest("POST", "ok", sid);
342
+ logSessionLifecycle("session_created", sid);
287
343
  }
288
344
  });
289
345
  transport.onclose = () => {
290
346
  const sid = transport.sessionId;
291
- if (sid) sessions.delete(sid);
347
+ if (sid) {
348
+ logSessionLifecycle("session_deleted", sid, "onclose");
349
+ sessions.delete(sid);
350
+ }
292
351
  };
293
352
  const server = createProductBrainServer();
294
353
  await server.connect(transport);
295
354
  await transport.handleRequest(req, res, req.body);
355
+ logRequest("POST", "ok", transport.sessionId ?? void 0, Date.now() - reqStart);
296
356
  } else {
357
+ process.stderr.write(
358
+ `[HTTP] ${(/* @__PURE__ */ new Date()).toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)
359
+ `
360
+ );
297
361
  res.status(400).json({
298
362
  jsonrpc: "2.0",
299
363
  error: { code: -32e3, message: "Bad Request: no valid session ID provided" },
@@ -302,7 +366,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
302
366
  }
303
367
  });
304
368
  } catch (err) {
305
- logRequest("POST", "error", sessionId);
369
+ logRequest("POST", "error", sessionId, Date.now() - reqStart);
306
370
  if (!res.headersSent) {
307
371
  res.status(500).json({
308
372
  jsonrpc: "2.0",
@@ -357,18 +421,40 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
357
421
  logRequest("DELETE", "error", sessionId);
358
422
  }
359
423
  });
360
- app.listen(PORT, "0.0.0.0", () => {
361
- console.log(`Product Brain MCP HTTP server v${SERVER_VERSION} listening on port ${PORT}`);
424
+ process.on("unhandledRejection", (reason) => {
425
+ const msg = reason instanceof Error ? reason.message : String(reason);
426
+ console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);
427
+ });
428
+ process.on("uncaughtException", (err) => {
429
+ console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);
430
+ gracefulShutdown();
362
431
  });
432
+ var shuttingDown = false;
363
433
  async function gracefulShutdown() {
434
+ if (shuttingDown) return;
435
+ shuttingDown = true;
436
+ setTimeout(() => process.exit(1), 3e3).unref();
364
437
  console.log("Shutting down...");
365
438
  for (const [, entry] of sessions) {
366
439
  await entry.transport.close().catch(() => {
367
440
  });
368
441
  }
369
- await shutdownAnalytics();
442
+ try {
443
+ await shutdownAnalytics();
444
+ } catch {
445
+ }
370
446
  process.exit(0);
371
447
  }
448
+ var LISTEN_HOST = "0.0.0.0";
449
+ var httpServer = app.listen(PORT, LISTEN_HOST, () => {
450
+ console.log(
451
+ `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`
452
+ );
453
+ });
454
+ httpServer.on("error", (err) => {
455
+ console.error(`[MCP HTTP] Server error: ${err.message}`);
456
+ process.exit(1);
457
+ });
372
458
  process.on("SIGINT", gracefulShutdown);
373
459
  process.on("SIGTERM", gracefulShutdown);
374
460
  //# sourceMappingURL=http.js.map