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

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.
@@ -5,79 +5,149 @@ import {
5
5
  // src/tools/smart-capture.ts
6
6
  import { z } from "zod";
7
7
 
8
+ // src/auth.ts
9
+ import { AsyncLocalStorage } from "async_hooks";
10
+ var requestStore = new AsyncLocalStorage();
11
+ function runWithAuth(auth, fn) {
12
+ return requestStore.run(auth, fn);
13
+ }
14
+ function getRequestApiKey() {
15
+ return requestStore.getStore()?.apiKey;
16
+ }
17
+ var SESSION_TTL_MS = 30 * 60 * 1e3;
18
+ var MAX_KEYS = 100;
19
+ var keyStateMap = /* @__PURE__ */ new Map();
20
+ function newKeyState() {
21
+ return {
22
+ workspaceId: null,
23
+ workspaceSlug: null,
24
+ workspaceName: null,
25
+ workspaceCreatedAt: null,
26
+ agentSessionId: null,
27
+ apiKeyId: null,
28
+ apiKeyScope: "readwrite",
29
+ sessionOriented: false,
30
+ sessionClosed: false,
31
+ lastAccess: Date.now()
32
+ };
33
+ }
34
+ function getKeyState(apiKey) {
35
+ let s = keyStateMap.get(apiKey);
36
+ if (!s) {
37
+ s = newKeyState();
38
+ keyStateMap.set(apiKey, s);
39
+ evictStale();
40
+ }
41
+ s.lastAccess = Date.now();
42
+ return s;
43
+ }
44
+ function evictStale() {
45
+ if (keyStateMap.size <= MAX_KEYS) return;
46
+ const now = Date.now();
47
+ for (const [key, s] of keyStateMap) {
48
+ if (now - s.lastAccess > SESSION_TTL_MS) keyStateMap.delete(key);
49
+ }
50
+ if (keyStateMap.size > MAX_KEYS) {
51
+ const sorted = [...keyStateMap.entries()].sort((a, b) => a[1].lastAccess - b[1].lastAccess);
52
+ for (let i = 0; i < sorted.length - MAX_KEYS; i++) {
53
+ keyStateMap.delete(sorted[i][0]);
54
+ }
55
+ }
56
+ }
57
+
8
58
  // src/client.ts
9
59
  var DEFAULT_CLOUD_URL = "https://trustworthy-kangaroo-277.convex.site";
10
- var cachedWorkspaceId = null;
11
- var cachedWorkspaceSlug = null;
12
- var cachedWorkspaceName = null;
13
- var cachedWorkspaceCreatedAt = null;
14
- var cachedAgentSessionId = null;
15
- var cachedApiKeyId = null;
16
- var cachedApiKeyScope = "readwrite";
17
- var sessionOriented = false;
18
- var sessionClosed = false;
60
+ var _stdioState = {
61
+ workspaceId: null,
62
+ workspaceSlug: null,
63
+ workspaceName: null,
64
+ workspaceCreatedAt: null,
65
+ agentSessionId: null,
66
+ apiKeyId: null,
67
+ apiKeyScope: "readwrite",
68
+ sessionOriented: false,
69
+ sessionClosed: false,
70
+ lastAccess: 0
71
+ };
72
+ function state() {
73
+ const reqKey = getRequestApiKey();
74
+ if (reqKey) return getKeyState(reqKey);
75
+ return _stdioState;
76
+ }
77
+ function getActiveApiKey() {
78
+ const fromRequest = getRequestApiKey();
79
+ if (fromRequest) return fromRequest;
80
+ const fromEnv = process.env.PRODUCTBRAIN_API_KEY;
81
+ if (!fromEnv) throw new Error("No API key available \u2014 set PRODUCTBRAIN_API_KEY or provide Bearer token");
82
+ return fromEnv;
83
+ }
19
84
  function getAgentSessionId() {
20
- return cachedAgentSessionId;
85
+ return state().agentSessionId;
21
86
  }
22
87
  function isSessionOriented() {
23
- return sessionOriented;
88
+ return state().sessionOriented;
24
89
  }
25
90
  function setSessionOriented(value) {
26
- sessionOriented = value;
91
+ state().sessionOriented = value;
27
92
  }
28
93
  async function startAgentSession() {
29
94
  const workspaceId = await getWorkspaceId();
30
- if (!cachedApiKeyId) {
95
+ const s = state();
96
+ if (!s.apiKeyId) {
31
97
  throw new Error("Cannot start session: API key ID not resolved. Ensure workspace resolution completed.");
32
98
  }
33
99
  const result = await mcpCall("agent.startSession", {
34
100
  workspaceId,
35
- apiKeyId: cachedApiKeyId
101
+ apiKeyId: s.apiKeyId
36
102
  });
37
- cachedAgentSessionId = result.sessionId;
38
- cachedApiKeyScope = result.toolsScope;
39
- sessionOriented = false;
40
- sessionClosed = false;
103
+ s.agentSessionId = result.sessionId;
104
+ s.apiKeyScope = result.toolsScope;
105
+ s.sessionOriented = false;
106
+ s.sessionClosed = false;
41
107
  return result;
42
108
  }
43
109
  async function closeAgentSession() {
44
- if (!cachedAgentSessionId) return;
110
+ const s = state();
111
+ if (!s.agentSessionId) return;
45
112
  try {
46
113
  await mcpCall("agent.closeSession", {
47
- sessionId: cachedAgentSessionId,
114
+ sessionId: s.agentSessionId,
48
115
  status: "closed"
49
116
  });
50
117
  } finally {
51
- sessionClosed = true;
52
- cachedAgentSessionId = null;
53
- sessionOriented = false;
118
+ s.sessionClosed = true;
119
+ s.agentSessionId = null;
120
+ s.sessionOriented = false;
54
121
  }
55
122
  }
56
123
  async function orphanAgentSession() {
57
- if (!cachedAgentSessionId) return;
124
+ const s = state();
125
+ if (!s.agentSessionId) return;
58
126
  try {
59
127
  await mcpCall("agent.closeSession", {
60
- sessionId: cachedAgentSessionId,
128
+ sessionId: s.agentSessionId,
61
129
  status: "orphaned"
62
130
  });
63
131
  } catch {
64
132
  } finally {
65
- cachedAgentSessionId = null;
66
- sessionOriented = false;
133
+ s.agentSessionId = null;
134
+ s.sessionOriented = false;
67
135
  }
68
136
  }
69
137
  function touchSessionActivity() {
70
- if (!cachedAgentSessionId) return;
138
+ const s = state();
139
+ if (!s.agentSessionId) return;
71
140
  mcpCall("agent.touchSession", {
72
- sessionId: cachedAgentSessionId
141
+ sessionId: s.agentSessionId
73
142
  }).catch(() => {
74
143
  });
75
144
  }
76
145
  async function recordSessionActivity(activity) {
77
- if (!cachedAgentSessionId) return;
146
+ const s = state();
147
+ if (!s.agentSessionId) return;
78
148
  try {
79
149
  await mcpCall("agent.recordActivity", {
80
- sessionId: cachedAgentSessionId,
150
+ sessionId: s.agentSessionId,
81
151
  ...activity
82
152
  });
83
153
  } catch {
@@ -94,6 +164,9 @@ function bootstrap() {
94
164
  }
95
165
  process.env.CONVEX_SITE_URL ??= process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
96
166
  }
167
+ function bootstrapHttp() {
168
+ process.env.CONVEX_SITE_URL ??= process.env.PRODUCTBRAIN_URL ?? DEFAULT_CLOUD_URL;
169
+ }
97
170
  function getEnv(key) {
98
171
  const value = process.env[key];
99
172
  if (!value) throw new Error(`${key} environment variable is required`);
@@ -104,7 +177,7 @@ function shouldLogAudit(status) {
104
177
  }
105
178
  function audit(fn, status, durationMs, errorMsg) {
106
179
  const ts = (/* @__PURE__ */ new Date()).toISOString();
107
- const workspace = cachedWorkspaceId ?? "unresolved";
180
+ const workspace = state().workspaceId ?? "unresolved";
108
181
  const entry = { ts, fn, workspace, status, durationMs };
109
182
  if (errorMsg) entry.error = errorMsg;
110
183
  auditBuffer.push(entry);
@@ -125,7 +198,7 @@ function getAuditLog() {
125
198
  }
126
199
  async function mcpCall(fn, args = {}) {
127
200
  const siteUrl = getEnv("CONVEX_SITE_URL").replace(/\/$/, "");
128
- const apiKey = getEnv("PRODUCTBRAIN_API_KEY");
201
+ const apiKey = getActiveApiKey();
129
202
  const start = Date.now();
130
203
  let res;
131
204
  try {
@@ -147,19 +220,22 @@ async function mcpCall(fn, args = {}) {
147
220
  throw new Error(`MCP call "${fn}" failed (${res.status}): ${json.error ?? "unknown error"}`);
148
221
  }
149
222
  audit(fn, "ok", Date.now() - start);
150
- if (cachedAgentSessionId && fn !== "agent.touchSession" && fn !== "agent.startSession") {
223
+ const s = state();
224
+ if (s.agentSessionId && fn !== "agent.touchSession" && fn !== "agent.startSession") {
151
225
  touchSessionActivity();
152
226
  }
153
227
  return json.data;
154
228
  }
155
- var resolveInFlight = null;
229
+ var resolveInFlightMap = /* @__PURE__ */ new Map();
156
230
  async function getWorkspaceId() {
157
- if (cachedWorkspaceId) return cachedWorkspaceId;
158
- if (resolveInFlight) return resolveInFlight;
159
- resolveInFlight = resolveWorkspaceWithRetry().finally(() => {
160
- resolveInFlight = null;
161
- });
162
- return resolveInFlight;
231
+ const s = state();
232
+ if (s.workspaceId) return s.workspaceId;
233
+ const apiKey = getActiveApiKey();
234
+ const existing = resolveInFlightMap.get(apiKey);
235
+ if (existing) return existing;
236
+ const promise = resolveWorkspaceWithRetry().finally(() => resolveInFlightMap.delete(apiKey));
237
+ resolveInFlightMap.set(apiKey, promise);
238
+ return promise;
163
239
  }
164
240
  async function resolveWorkspaceWithRetry(maxRetries = 2) {
165
241
  let lastError = null;
@@ -171,13 +247,14 @@ async function resolveWorkspaceWithRetry(maxRetries = 2) {
171
247
  "API key is valid but no workspace is associated. Run `npx productbrain setup` or regenerate your key."
172
248
  );
173
249
  }
174
- cachedWorkspaceId = workspace._id;
175
- cachedWorkspaceSlug = workspace.slug;
176
- cachedWorkspaceName = workspace.name;
177
- cachedWorkspaceCreatedAt = workspace.createdAt ?? null;
178
- if (workspace.keyScope) cachedApiKeyScope = workspace.keyScope;
179
- if (workspace.keyId) cachedApiKeyId = workspace.keyId;
180
- return cachedWorkspaceId;
250
+ const s = state();
251
+ s.workspaceId = workspace._id;
252
+ s.workspaceSlug = workspace.slug;
253
+ s.workspaceName = workspace.name;
254
+ s.workspaceCreatedAt = workspace.createdAt ?? null;
255
+ if (workspace.keyScope) s.apiKeyScope = workspace.keyScope;
256
+ if (workspace.keyId) s.apiKeyId = workspace.keyId;
257
+ return s.workspaceId;
181
258
  } catch (err) {
182
259
  lastError = err;
183
260
  const isTransient = /network error|fetch failed|ECONNREFUSED|ETIMEDOUT/i.test(err.message);
@@ -194,11 +271,12 @@ async function resolveWorkspaceWithRetry(maxRetries = 2) {
194
271
  }
195
272
  async function getWorkspaceContext() {
196
273
  const workspaceId = await getWorkspaceId();
274
+ const s = state();
197
275
  return {
198
276
  workspaceId,
199
- workspaceSlug: cachedWorkspaceSlug ?? "unknown",
200
- workspaceName: cachedWorkspaceName ?? "unknown",
201
- createdAt: cachedWorkspaceCreatedAt
277
+ workspaceSlug: s.workspaceSlug ?? "unknown",
278
+ workspaceName: s.workspaceName ?? "unknown",
279
+ createdAt: s.workspaceCreatedAt
202
280
  };
203
281
  }
204
282
  async function mcpQuery(fn, args = {}) {
@@ -210,36 +288,38 @@ async function mcpMutation(fn, args = {}) {
210
288
  return mcpCall(fn, { ...args, workspaceId });
211
289
  }
212
290
  function requireWriteAccess() {
213
- if (!cachedAgentSessionId) {
291
+ const s = state();
292
+ if (!s.agentSessionId) {
214
293
  throw new Error(
215
294
  "Agent session required for write operations. Call `agent-start` first."
216
295
  );
217
296
  }
218
- if (sessionClosed) {
297
+ if (s.sessionClosed) {
219
298
  throw new Error(
220
299
  "Agent session has been closed. Write tools are no longer available."
221
300
  );
222
301
  }
223
- if (!sessionOriented) {
302
+ if (!s.sessionOriented) {
224
303
  throw new Error(
225
304
  "Orientation required before writing to the Chain. Call 'orient' first."
226
305
  );
227
306
  }
228
- if (cachedApiKeyScope === "read") {
307
+ if (s.apiKeyScope === "read") {
229
308
  throw new Error(
230
309
  "This API key has read-only scope. Write tools are not available."
231
310
  );
232
311
  }
233
312
  }
234
313
  async function recoverSessionState() {
235
- if (!cachedWorkspaceId) return;
314
+ const s = state();
315
+ if (!s.workspaceId) return;
236
316
  try {
237
- const session = await mcpCall("agent.getActiveSession", { workspaceId: cachedWorkspaceId });
317
+ const session = await mcpCall("agent.getActiveSession", { workspaceId: s.workspaceId });
238
318
  if (session && session.status === "active") {
239
- cachedAgentSessionId = session._id;
240
- sessionOriented = session.oriented;
241
- cachedApiKeyScope = session.toolsScope;
242
- sessionClosed = false;
319
+ s.agentSessionId = session._id;
320
+ s.sessionOriented = session.oriented;
321
+ s.apiKeyScope = session.toolsScope;
322
+ s.sessionClosed = false;
243
323
  }
244
324
  } catch {
245
325
  }
@@ -298,6 +378,12 @@ var COMMON_CHECKS = {
298
378
  return colls.size >= 2;
299
379
  },
300
380
  suggestion: () => "Try linking to entries in different collections (glossary, business-rules, strategy)."
381
+ },
382
+ hasType: {
383
+ id: "has-type",
384
+ label: "Has canonical type",
385
+ check: (ctx) => !!ctx.data?.canonicalKey || !!ctx.canonicalKey,
386
+ suggestion: () => "Classify this entry with a canonical type for better context assembly. Use update-entry to set canonicalKey."
301
387
  }
302
388
  };
303
389
  var PROFILES = /* @__PURE__ */ new Map([
@@ -331,6 +417,7 @@ var PROFILES = /* @__PURE__ */ new Map([
331
417
  COMMON_CHECKS.clearName,
332
418
  COMMON_CHECKS.hasDescription,
333
419
  COMMON_CHECKS.hasRelations,
420
+ COMMON_CHECKS.hasType,
334
421
  {
335
422
  id: "has-severity",
336
423
  label: "Severity specified",
@@ -371,6 +458,7 @@ var PROFILES = /* @__PURE__ */ new Map([
371
458
  COMMON_CHECKS.clearName,
372
459
  COMMON_CHECKS.hasDescription,
373
460
  COMMON_CHECKS.hasRelations,
461
+ COMMON_CHECKS.hasType,
374
462
  {
375
463
  id: "has-rationale",
376
464
  label: "Rationale provided",
@@ -415,6 +503,7 @@ var PROFILES = /* @__PURE__ */ new Map([
415
503
  },
416
504
  qualityChecks: [
417
505
  COMMON_CHECKS.clearName,
506
+ COMMON_CHECKS.hasType,
418
507
  {
419
508
  id: "has-canonical",
420
509
  label: "Canonical definition provided (>20 chars)",
@@ -450,6 +539,7 @@ var PROFILES = /* @__PURE__ */ new Map([
450
539
  },
451
540
  qualityChecks: [
452
541
  COMMON_CHECKS.clearName,
542
+ COMMON_CHECKS.hasType,
453
543
  {
454
544
  id: "has-rationale",
455
545
  label: "Rationale provided (>30 chars)",
@@ -478,6 +568,7 @@ var PROFILES = /* @__PURE__ */ new Map([
478
568
  COMMON_CHECKS.clearName,
479
569
  COMMON_CHECKS.hasDescription,
480
570
  COMMON_CHECKS.hasRelations,
571
+ COMMON_CHECKS.hasType,
481
572
  {
482
573
  id: "has-owner",
483
574
  label: "Owner assigned",
@@ -502,6 +593,7 @@ var PROFILES = /* @__PURE__ */ new Map([
502
593
  COMMON_CHECKS.clearName,
503
594
  COMMON_CHECKS.hasDescription,
504
595
  COMMON_CHECKS.hasRelations,
596
+ COMMON_CHECKS.hasType,
505
597
  {
506
598
  id: "has-behaviors",
507
599
  label: "Behaviors described",
@@ -520,6 +612,7 @@ var PROFILES = /* @__PURE__ */ new Map([
520
612
  COMMON_CHECKS.clearName,
521
613
  COMMON_CHECKS.hasDescription,
522
614
  COMMON_CHECKS.hasRelations,
615
+ COMMON_CHECKS.hasType,
523
616
  COMMON_CHECKS.diverseRelations
524
617
  ]
525
618
  }],
@@ -554,7 +647,8 @@ var PROFILES = /* @__PURE__ */ new Map([
554
647
  qualityChecks: [
555
648
  COMMON_CHECKS.clearName,
556
649
  COMMON_CHECKS.hasDescription,
557
- COMMON_CHECKS.hasRelations
650
+ COMMON_CHECKS.hasRelations,
651
+ COMMON_CHECKS.hasType
558
652
  ]
559
653
  }],
560
654
  ["tracking-events", {
@@ -578,7 +672,8 @@ var FALLBACK_PROFILE = {
578
672
  qualityChecks: [
579
673
  COMMON_CHECKS.clearName,
580
674
  COMMON_CHECKS.hasDescription,
581
- COMMON_CHECKS.hasRelations
675
+ COMMON_CHECKS.hasRelations,
676
+ COMMON_CHECKS.hasType
582
677
  ]
583
678
  };
584
679
  function generateEntryId(prefix) {
@@ -698,6 +793,7 @@ async function checkEntryQuality(entryId) {
698
793
  description,
699
794
  data: entry.data ?? {},
700
795
  entryId: entry.entryId ?? "",
796
+ canonicalKey: entry.canonicalKey,
701
797
  linksCreated,
702
798
  linksSuggested: [],
703
799
  collectionFields: []
@@ -740,11 +836,12 @@ function registerSmartCaptureTools(server) {
740
836
  name: z.string().describe("Display name \u2014 be specific (e.g. 'Convex adjacency list won't scale for graph traversal')"),
741
837
  description: z.string().describe("Full context \u2014 what's happening, why it matters, what you observed"),
742
838
  context: z.string().optional().describe("Optional additional context (e.g. 'Observed during gather-context calls taking 700ms+')"),
743
- entryId: z.string().optional().describe("Optional custom entry ID (e.g. 'TEN-my-id'). Auto-generated if omitted.")
839
+ entryId: z.string().optional().describe("Optional custom entry ID (e.g. 'TEN-my-id'). Auto-generated if omitted."),
840
+ canonicalKey: z.string().optional().describe("Semantic type (e.g. 'decision', 'tension', 'vision'). Auto-assigned from collection if omitted.")
744
841
  },
745
842
  annotations: { destructiveHint: false }
746
843
  },
747
- async ({ collection, name, description, context, entryId }) => {
844
+ async ({ collection, name, description, context, entryId, canonicalKey }) => {
748
845
  requireWriteAccess();
749
846
  const profile = PROFILES.get(collection) ?? FALLBACK_PROFILE;
750
847
  const col = await mcpQuery("chain.getCollection", { slug: collection });
@@ -815,6 +912,7 @@ Or use \`list-collections\` to see available collections.`
815
912
  name,
816
913
  status,
817
914
  data,
915
+ canonicalKey,
818
916
  createdBy: agentId ? `agent:${agentId}` : "capture"
819
917
  });
820
918
  await recordSessionActivity({ entryCreated: finalEntryId || internalId });
@@ -892,11 +990,25 @@ Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify i
892
990
  context,
893
991
  data,
894
992
  entryId: finalEntryId,
993
+ canonicalKey,
895
994
  linksCreated,
896
995
  linksSuggested,
897
996
  collectionFields: col.fields ?? []
898
997
  };
899
998
  const quality = scoreQuality(captureCtx, profile);
999
+ let cardinalityWarning = null;
1000
+ const resolvedCK = canonicalKey ?? captureCtx.canonicalKey;
1001
+ if (resolvedCK) {
1002
+ try {
1003
+ const check = await mcpQuery("chain.checkCardinalityWarning", {
1004
+ canonicalKey: resolvedCK
1005
+ });
1006
+ if (check?.warning) {
1007
+ cardinalityWarning = check.warning;
1008
+ }
1009
+ } catch {
1010
+ }
1011
+ }
900
1012
  const contradictionWarnings = await runContradictionCheck(name, description);
901
1013
  if (contradictionWarnings.length > 0) {
902
1014
  await recordSessionActivity({ contradictionWarning: true });
@@ -930,6 +1042,10 @@ Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify i
930
1042
  lines.push("");
931
1043
  lines.push(`_To improve: \`update-entry entryId="${finalEntryId}"\` to fill missing fields._`);
932
1044
  }
1045
+ if (cardinalityWarning) {
1046
+ lines.push("");
1047
+ lines.push(`**Cardinality warning:** ${cardinalityWarning}`);
1048
+ }
933
1049
  if (contradictionWarnings.length > 0) {
934
1050
  lines.push("");
935
1051
  lines.push("\u26A0 Contradiction check: proposed entry matched existing governance entries:");
@@ -1279,6 +1395,7 @@ async function runContradictionCheck(name, description) {
1279
1395
  }
1280
1396
 
1281
1397
  export {
1398
+ runWithAuth,
1282
1399
  getAgentSessionId,
1283
1400
  isSessionOriented,
1284
1401
  setSessionOriented,
@@ -1287,6 +1404,7 @@ export {
1287
1404
  orphanAgentSession,
1288
1405
  recordSessionActivity,
1289
1406
  bootstrap,
1407
+ bootstrapHttp,
1290
1408
  getAuditLog,
1291
1409
  mcpCall,
1292
1410
  getWorkspaceId,
@@ -1300,4 +1418,4 @@ export {
1300
1418
  registerSmartCaptureTools,
1301
1419
  runContradictionCheck
1302
1420
  };
1303
- //# sourceMappingURL=chunk-KISCDOJ2.js.map
1421
+ //# sourceMappingURL=chunk-HLXF3QPE.js.map