@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.
- package/dist/chunk-CDBSOVW7.js +15115 -0
- package/dist/chunk-CDBSOVW7.js.map +1 -0
- package/dist/chunk-MOPOQUJP.js +312 -0
- package/dist/chunk-MOPOQUJP.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +112 -26
- package/dist/http.js.map +1 -1
- package/dist/index.js +56 -31
- package/dist/index.js.map +1 -1
- package/dist/{setup-GZ5OZ5OP.js → setup-YYADLH22.js} +36 -104
- package/dist/setup-YYADLH22.js.map +1 -0
- package/dist/views/src/entry-cards/index.html +227 -0
- package/dist/views/src/graph-constellation/index.html +254 -0
- package/package.json +6 -3
- package/dist/chunk-HLXF3QPE.js +0 -1421
- package/dist/chunk-HLXF3QPE.js.map +0 -1
- package/dist/chunk-MV3VHRMV.js +0 -4538
- package/dist/chunk-MV3VHRMV.js.map +0 -1
- package/dist/chunk-XBMI6QHR.js +0 -100
- package/dist/chunk-XBMI6QHR.js.map +0 -1
- package/dist/setup-GZ5OZ5OP.js.map +0 -1
- package/dist/smart-capture-YMTYXB46.js +0 -14
- package/dist/smart-capture-YMTYXB46.js.map +0 -1
|
@@ -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-
|
|
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-
|
|
7
|
+
} from "./chunk-CDBSOVW7.js";
|
|
9
8
|
import {
|
|
9
|
+
getPostHogClient,
|
|
10
10
|
initAnalytics,
|
|
11
11
|
shutdownAnalytics
|
|
12
|
-
} from "./chunk-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
342
|
+
logSessionLifecycle("session_created", sid);
|
|
287
343
|
}
|
|
288
344
|
});
|
|
289
345
|
transport.onclose = () => {
|
|
290
346
|
const sid = transport.sessionId;
|
|
291
|
-
if (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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|