@productbrain/mcp 0.0.1-beta.11 → 0.0.1-beta.13

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
  }
@@ -961,6 +1041,162 @@ Use \`get-entry\` to inspect the existing entry, or \`update-entry\` to modify i
961
1041
  return { content: [{ type: "text", text: lines.join("\n") }] };
962
1042
  }
963
1043
  );
1044
+ server.registerTool(
1045
+ "batch-capture",
1046
+ {
1047
+ title: "Batch Capture",
1048
+ description: "Create multiple knowledge entries in one call. Ideal for workspace setup, document ingestion, or any scenario where you need to capture many entries at once.\n\nEach entry is created independently \u2014 if one fails, the others still succeed. Returns a compact summary instead of per-entry quality scorecards.\n\nAuto-linking runs per entry but contradiction checks and readiness hints are skipped for speed. Use `quality-check` on individual entries afterward if needed.",
1049
+ inputSchema: {
1050
+ entries: z.array(z.object({
1051
+ collection: z.string().describe("Collection slug"),
1052
+ name: z.string().describe("Display name"),
1053
+ description: z.string().describe("Full context / definition"),
1054
+ entryId: z.string().optional().describe("Optional custom entry ID")
1055
+ })).min(1).max(50).describe("Array of entries to capture")
1056
+ },
1057
+ annotations: { destructiveHint: false }
1058
+ },
1059
+ async ({ entries }) => {
1060
+ requireWriteAccess();
1061
+ const agentId = getAgentSessionId();
1062
+ const createdBy = agentId ? `agent:${agentId}` : "capture";
1063
+ const results = [];
1064
+ const allCollections = await mcpQuery("chain.listCollections");
1065
+ const collCache = /* @__PURE__ */ new Map();
1066
+ for (const c of allCollections) collCache.set(c.slug, c);
1067
+ const collIdToSlug = /* @__PURE__ */ new Map();
1068
+ for (const c of allCollections) collIdToSlug.set(c._id, c.slug);
1069
+ for (const entry of entries) {
1070
+ const profile = PROFILES.get(entry.collection) ?? FALLBACK_PROFILE;
1071
+ const col = collCache.get(entry.collection);
1072
+ if (!col) {
1073
+ results.push({ name: entry.name, collection: entry.collection, entryId: "", ok: false, autoLinks: 0, error: `Collection "${entry.collection}" not found` });
1074
+ continue;
1075
+ }
1076
+ const finalEntryId = entry.entryId ?? generateEntryId(profile.idPrefix);
1077
+ const data = {};
1078
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1079
+ for (const field of col.fields ?? []) {
1080
+ const key = field.key;
1081
+ if (key === profile.descriptionField) {
1082
+ data[key] = entry.description;
1083
+ } else if (field.type === "array" || field.type === "multi-select") {
1084
+ data[key] = [];
1085
+ } else {
1086
+ data[key] = "";
1087
+ }
1088
+ }
1089
+ for (const def of profile.defaults) {
1090
+ if (def.value === "today") data[def.key] = today;
1091
+ else if (def.value !== "infer") data[def.key] = def.value;
1092
+ }
1093
+ if (profile.inferField) {
1094
+ const inferred = profile.inferField({
1095
+ collection: entry.collection,
1096
+ name: entry.name,
1097
+ description: entry.description,
1098
+ data,
1099
+ entryId: "",
1100
+ linksCreated: [],
1101
+ linksSuggested: [],
1102
+ collectionFields: col.fields ?? []
1103
+ });
1104
+ for (const [key, val] of Object.entries(inferred)) {
1105
+ if (val !== void 0 && val !== "") data[key] = val;
1106
+ }
1107
+ }
1108
+ if (!data[profile.descriptionField] && !data.description && !data.canonical) {
1109
+ data[profile.descriptionField || "description"] = entry.description;
1110
+ }
1111
+ try {
1112
+ await mcpMutation("chain.createEntry", {
1113
+ collectionSlug: entry.collection,
1114
+ entryId: finalEntryId,
1115
+ name: entry.name,
1116
+ status: "draft",
1117
+ data,
1118
+ createdBy
1119
+ });
1120
+ let autoLinkCount = 0;
1121
+ const searchQuery = extractSearchTerms(entry.name, entry.description);
1122
+ if (searchQuery) {
1123
+ try {
1124
+ const searchResults = await mcpQuery("chain.searchEntries", { query: searchQuery });
1125
+ const candidates = (searchResults ?? []).filter((r) => r.entryId !== finalEntryId).map((r) => ({
1126
+ ...r,
1127
+ collSlug: collIdToSlug.get(r.collectionId) ?? "unknown",
1128
+ confidence: computeLinkConfidence(r, entry.name, entry.description, entry.collection, collIdToSlug.get(r.collectionId) ?? "unknown")
1129
+ })).sort((a, b) => b.confidence - a.confidence);
1130
+ for (const c of candidates) {
1131
+ if (autoLinkCount >= MAX_AUTO_LINKS) break;
1132
+ if (c.confidence < AUTO_LINK_CONFIDENCE_THRESHOLD) break;
1133
+ if (!c.entryId) continue;
1134
+ const relationType = inferRelationType(entry.collection, c.collSlug, profile);
1135
+ try {
1136
+ await mcpMutation("chain.createEntryRelation", {
1137
+ fromEntryId: finalEntryId,
1138
+ toEntryId: c.entryId,
1139
+ type: relationType
1140
+ });
1141
+ autoLinkCount++;
1142
+ } catch {
1143
+ }
1144
+ }
1145
+ } catch {
1146
+ }
1147
+ }
1148
+ results.push({ name: entry.name, collection: entry.collection, entryId: finalEntryId, ok: true, autoLinks: autoLinkCount });
1149
+ await recordSessionActivity({ entryCreated: finalEntryId });
1150
+ } catch (error) {
1151
+ const msg = error instanceof Error ? error.message : String(error);
1152
+ results.push({ name: entry.name, collection: entry.collection, entryId: finalEntryId, ok: false, autoLinks: 0, error: msg });
1153
+ }
1154
+ }
1155
+ const created = results.filter((r) => r.ok);
1156
+ const failed = results.filter((r) => !r.ok);
1157
+ const totalAutoLinks = created.reduce((sum, r) => sum + r.autoLinks, 0);
1158
+ const byCollection = /* @__PURE__ */ new Map();
1159
+ for (const r of created) {
1160
+ byCollection.set(r.collection, (byCollection.get(r.collection) ?? 0) + 1);
1161
+ }
1162
+ const lines = [
1163
+ `# Batch Capture Complete`,
1164
+ `**${created.length}** created, **${failed.length}** failed out of ${entries.length} total.`,
1165
+ `**Auto-links created:** ${totalAutoLinks}`,
1166
+ ""
1167
+ ];
1168
+ if (byCollection.size > 0) {
1169
+ lines.push("## By Collection");
1170
+ for (const [col, count] of byCollection) {
1171
+ lines.push(`- \`${col}\`: ${count} entries`);
1172
+ }
1173
+ lines.push("");
1174
+ }
1175
+ if (created.length > 0) {
1176
+ lines.push("## Created");
1177
+ for (const r of created) {
1178
+ const linkNote = r.autoLinks > 0 ? ` (${r.autoLinks} auto-links)` : "";
1179
+ lines.push(`- **${r.entryId}**: ${r.name} [${r.collection}]${linkNote}`);
1180
+ }
1181
+ }
1182
+ if (failed.length > 0) {
1183
+ lines.push("");
1184
+ lines.push("## Failed");
1185
+ for (const r of failed) {
1186
+ lines.push(`- ${r.name} [${r.collection}]: _${r.error}_`);
1187
+ }
1188
+ }
1189
+ const entryIds = created.map((r) => r.entryId);
1190
+ if (entryIds.length > 0) {
1191
+ lines.push("");
1192
+ lines.push("## Next Steps");
1193
+ lines.push(`- **Connect:** Run \`suggest-links\` on key entries to build the knowledge graph`);
1194
+ lines.push(`- **Commit:** Use \`commit-entry\` to promote drafts to SSOT`);
1195
+ lines.push(`- **Quality:** Run \`quality-check\` on individual entries to assess completeness`);
1196
+ }
1197
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1198
+ }
1199
+ );
964
1200
  server.registerTool(
965
1201
  "quality-check",
966
1202
  {
@@ -1123,6 +1359,7 @@ async function runContradictionCheck(name, description) {
1123
1359
  }
1124
1360
 
1125
1361
  export {
1362
+ runWithAuth,
1126
1363
  getAgentSessionId,
1127
1364
  isSessionOriented,
1128
1365
  setSessionOriented,
@@ -1131,6 +1368,7 @@ export {
1131
1368
  orphanAgentSession,
1132
1369
  recordSessionActivity,
1133
1370
  bootstrap,
1371
+ bootstrapHttp,
1134
1372
  getAuditLog,
1135
1373
  mcpCall,
1136
1374
  getWorkspaceId,
@@ -1144,4 +1382,4 @@ export {
1144
1382
  registerSmartCaptureTools,
1145
1383
  runContradictionCheck
1146
1384
  };
1147
- //# sourceMappingURL=chunk-B56TXWX2.js.map
1385
+ //# sourceMappingURL=chunk-4ETXQ24K.js.map