@productbrain/mcp 0.0.1-beta.8702 → 0.0.1-beta.8719

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.
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  KernelCallError,
3
+ PASS_SESSION_ID,
3
4
  cacheScope,
5
+ classifyError,
4
6
  closeAgentSession,
5
7
  formatGatewaySeamSummary,
6
8
  getAgentSessionId,
@@ -15,10 +17,13 @@ import {
15
17
  kernelCallEnvelope,
16
18
  kernelMutation,
17
19
  kernelQuery,
20
+ orientationRefusal,
18
21
  recordSessionActivity,
19
22
  requireActiveSession,
20
23
  requireVendorTriageAccess,
21
24
  requireWriteAccess,
25
+ retryIsFutile,
26
+ runWithSeatedSession,
22
27
  runWithToolContext,
23
28
  setSessionOriented,
24
29
  startAgentSession,
@@ -37,223 +42,16 @@ import {
37
42
  trackQualityCheck,
38
43
  trackQualityVerdict,
39
44
  trackSessionCaptureRate,
40
- trackZeroCaptureAuditFired
41
- } from "./chunk-FZWP45AU.js";
45
+ trackZeroCaptureAuditFired,
46
+ validateCallerSession
47
+ } from "./chunk-2UJDIWEX.js";
42
48
 
43
49
  // src/server.ts
44
50
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
45
-
46
- // src/lib/createEntryPreviewResult.ts
47
- async function resolvePreviewPreflight(result2, links) {
48
- if (!links?.length) return result2.preflight;
49
- try {
50
- return await kernelQuery("chain.preflightEntry", {
51
- collectionSlug: result2.collection,
52
- canonicalKey: result2.preflight.kind ?? void 0,
53
- links: links.map((link) => ({ entryId: link.to, type: link.type }))
54
- });
55
- } catch {
56
- return {
57
- ...result2.preflight,
58
- checks: [
59
- ...result2.preflight.checks,
60
- {
61
- id: "link-recheck",
62
- status: "not_checked",
63
- blocking: false,
64
- message: "Selected links could not be rechecked now; target existence and relation policy are checked at capture."
65
- }
66
- ]
67
- };
68
- }
69
- }
70
- function formatPreflightCheck(check) {
71
- if (check.status === "missing" && check.blocking) return `Blocked: ${check.message}`;
72
- if (check.status === "incomplete") return `Unverified: ${check.message}`;
73
- if (check.status === "not_checked") return `Checked later: ${check.message}`;
74
- return check.message;
75
- }
76
- function renderEntryPreflight(preflight) {
77
- return [
78
- ...preflight.checks.map(formatPreflightCheck),
79
- ...preflight.requiredFields.length ? [`Required information: ${preflight.requiredFields.join(", ")}.`] : [],
80
- preflight.notice
81
- ];
82
- }
83
- function isPreviewCreateEntryResult(result2) {
84
- return "preview" in result2 && result2.preview === true;
85
- }
86
- function resolveEffectiveCollection(result2, preview, fallbackCollection) {
87
- return preview && result2 && isPreviewCreateEntryResult(result2) ? result2.collection ?? fallbackCollection : fallbackCollection;
88
- }
89
-
90
- // src/tools/entries.ts
91
- import { z as z11 } from "zod/v3";
51
+ import { z as z34 } from "zod/v3";
92
52
 
93
53
  // src/envelope.ts
94
54
  import { z } from "zod/v3";
95
-
96
- // src/errors.ts
97
- var MCP_CALL_ERROR_RE = /^MCP call "([^"]+)" failed \(\d+\): (.+)$/s;
98
- function asKernelCallError(err) {
99
- if (!(err instanceof Error)) return void 0;
100
- const e = err;
101
- if (typeof e.code !== "string") return void 0;
102
- const match = MCP_CALL_ERROR_RE.exec(e.message);
103
- if (!match) return void 0;
104
- return {
105
- code: e.code,
106
- fn: match[1],
107
- cleanMessage: match[2],
108
- // NOTE: match[2] not match[1] — group 1 is now fn scope (DEC-798)
109
- missingRequiredFields: Array.isArray(e.missingRequiredFields) ? e.missingRequiredFields : void 0,
110
- fieldErrors: Array.isArray(e.fieldErrors) ? e.fieldErrors : void 0
111
- };
112
- }
113
- var GateError = class extends Error {
114
- constructor(message) {
115
- super(message);
116
- this.name = "GateError";
117
- }
118
- };
119
- var BackendError = class extends Error {
120
- constructor(message) {
121
- super(message);
122
- this.name = "BackendError";
123
- }
124
- };
125
- var ValidationError = class extends Error {
126
- constructor(message) {
127
- super(message);
128
- this.name = "ValidationError";
129
- }
130
- };
131
- var GATE_PATTERNS = [
132
- {
133
- pattern: /session required|no active.*session|call.*session.*start/i,
134
- code: "SESSION_REQUIRED",
135
- recovery: "Start an agent session first.",
136
- action: { tool: "session", description: "Start session", parameters: { action: "start" } }
137
- },
138
- {
139
- pattern: /session.*closed/i,
140
- code: "SESSION_CLOSED",
141
- recovery: "Start a new session.",
142
- action: { tool: "session", description: "Start new session", parameters: { action: "start" } }
143
- },
144
- {
145
- pattern: /orientation required|call.*orient/i,
146
- code: "ORIENTATION_REQUIRED",
147
- recovery: "Run orient with a task, then retry.",
148
- action: { tool: "orient", description: "Orient with a task", parameters: { action: "task" } }
149
- },
150
- {
151
- pattern: /read.?only.*scope/i,
152
- code: "READONLY_SCOPE",
153
- recovery: "This API key cannot write. Use a readwrite key.",
154
- action: { tool: "workspace", description: "Check key scope", parameters: { action: "whoami" } }
155
- }
156
- ];
157
- function orientationRefusal(err) {
158
- const gate = GATE_PATTERNS.find((g) => g.code === "ORIENTATION_REQUIRED");
159
- const mcpErr = asKernelCallError(err);
160
- const message = mcpErr?.cleanMessage ?? (err instanceof Error ? err.message : String(err));
161
- if (!gate || !gate.pattern.test(message)) return null;
162
- return { code: gate.code, message, recovery: gate.recovery, availableActions: [gate.action] };
163
- }
164
- function classifyError(err) {
165
- const message = err instanceof Error ? err.message : String(err);
166
- if (err instanceof GateError) {
167
- return {
168
- code: "PERMISSION_DENIED",
169
- message,
170
- recovery: "Check permissions or use a readwrite API key.",
171
- availableActions: [{ tool: "workspace", description: "Check key scope", parameters: { action: "whoami" } }]
172
- };
173
- }
174
- if (err instanceof BackendError) {
175
- return { code: "BACKEND_ERROR", message, recovery: "Retry the operation or check backend health." };
176
- }
177
- if (err instanceof ValidationError) {
178
- return { code: "VALIDATION_ERROR", message };
179
- }
180
- const mcpErr = asKernelCallError(err);
181
- if (mcpErr) {
182
- const { code: convexCode, fn, cleanMessage, missingRequiredFields, fieldErrors } = mcpErr;
183
- if (convexCode === "VALIDATION_FAILED") {
184
- const hasDiagnostics = missingRequiredFields?.length || fieldErrors?.length;
185
- return {
186
- code: "VALIDATION_ERROR",
187
- fn,
188
- message: cleanMessage,
189
- ...hasDiagnostics ? {
190
- diagnostics: {
191
- ...missingRequiredFields?.length ? { missingRequiredFields } : {},
192
- ...fieldErrors?.length ? { fieldErrors } : {}
193
- }
194
- } : {}
195
- };
196
- }
197
- if (convexCode === "NOT_FOUND") {
198
- return {
199
- code: "NOT_FOUND",
200
- fn,
201
- message: cleanMessage,
202
- recovery: "Use entries action=search to find the correct ID.",
203
- availableActions: [{ tool: "entries", description: "Search entries", parameters: { action: "search" } }]
204
- };
205
- }
206
- if (convexCode?.startsWith("AUTHZ_")) {
207
- return {
208
- code: "PERMISSION_DENIED",
209
- fn,
210
- message: cleanMessage,
211
- recovery: "Check permissions or use a readwrite API key.",
212
- availableActions: [{ tool: "workspace", description: "Check key scope", parameters: { action: "whoami" } }]
213
- };
214
- }
215
- if (convexCode === "WORKSPACE_SLUG_TAKEN" || convexCode === "PERSON_ALREADY_EXISTS") {
216
- return {
217
- code: "DUPLICATE",
218
- fn,
219
- message: cleanMessage,
220
- recovery: "Use entries action=get to inspect the existing entry.",
221
- availableActions: [{ tool: "entries", description: "Get entry", parameters: { action: "get" } }]
222
- };
223
- }
224
- if (convexCode === "rate-limited") {
225
- return {
226
- code: "rate-limited",
227
- fn,
228
- message: cleanMessage,
229
- recovery: "Rate limit hit for this API key. Wait the number of seconds in the message (Retry-After), then retry the same call unchanged \u2014 it is safe to retry."
230
- };
231
- }
232
- return { code: convexCode, fn, message: cleanMessage };
233
- }
234
- for (const { pattern, code, recovery, action } of GATE_PATTERNS) {
235
- if (pattern.test(message)) {
236
- return { code, message, recovery, availableActions: [action] };
237
- }
238
- }
239
- if (err?.name === "GatewayTimeoutError") {
240
- const mayHaveLanded = err.mayHaveLanded === true;
241
- return {
242
- code: "GATEWAY_TIMEOUT",
243
- message,
244
- recovery: mayHaveLanded ? "The server was reachable and may have completed this write \u2014 verify before retrying, since a blind retry can duplicate it." : "The server was reachable but did not answer in time. Nothing was written \u2014 safe to retry."
245
- };
246
- }
247
- if (/network error|fetch failed|ECONNREFUSED|ETIMEDOUT/i.test(message)) {
248
- return { code: "BACKEND_UNAVAILABLE", message, recovery: "Retry in a few seconds." };
249
- }
250
- return { code: "INTERNAL_ERROR", message: message || "An unexpected error occurred." };
251
- }
252
- function retryIsFutile(code) {
253
- return code === "VALIDATION_ERROR";
254
- }
255
-
256
- // src/envelope.ts
257
55
  function success(summary, data, next) {
258
56
  return {
259
57
  ok: true,
@@ -376,6 +174,53 @@ function orientationRefusalResult(err, lines) {
376
174
  }
377
175
  var ORIENT_REQUIRED_NOTE = "workflow checkpoints and organisation-structure writes (membership, ownership, parent) still require an oriented session.";
378
176
 
177
+ // src/lib/createEntryPreviewResult.ts
178
+ async function resolvePreviewPreflight(result2, links) {
179
+ if (!links?.length) return result2.preflight;
180
+ try {
181
+ return await kernelQuery("chain.preflightEntry", {
182
+ collectionSlug: result2.collection,
183
+ canonicalKey: result2.preflight.kind ?? void 0,
184
+ links: links.map((link) => ({ entryId: link.to, type: link.type }))
185
+ });
186
+ } catch {
187
+ return {
188
+ ...result2.preflight,
189
+ checks: [
190
+ ...result2.preflight.checks,
191
+ {
192
+ id: "link-recheck",
193
+ status: "not_checked",
194
+ blocking: false,
195
+ message: "Selected links could not be rechecked now; target existence and relation policy are checked at capture."
196
+ }
197
+ ]
198
+ };
199
+ }
200
+ }
201
+ function formatPreflightCheck(check) {
202
+ if (check.status === "missing" && check.blocking) return `Blocked: ${check.message}`;
203
+ if (check.status === "incomplete") return `Unverified: ${check.message}`;
204
+ if (check.status === "not_checked") return `Checked later: ${check.message}`;
205
+ return check.message;
206
+ }
207
+ function renderEntryPreflight(preflight) {
208
+ return [
209
+ ...preflight.checks.map(formatPreflightCheck),
210
+ ...preflight.requiredFields.length ? [`Required information: ${preflight.requiredFields.join(", ")}.`] : [],
211
+ preflight.notice
212
+ ];
213
+ }
214
+ function isPreviewCreateEntryResult(result2) {
215
+ return "preview" in result2 && result2.preview === true;
216
+ }
217
+ function resolveEffectiveCollection(result2, preview, fallbackCollection) {
218
+ return preview && result2 && isPreviewCreateEntryResult(result2) ? result2.collection ?? fallbackCollection : fallbackCollection;
219
+ }
220
+
221
+ // src/tools/entries.ts
222
+ import { z as z11 } from "zod/v3";
223
+
379
224
  // src/tools/knowledge-helpers.ts
380
225
  import { z as z2 } from "zod/v3";
381
226
 
@@ -11738,14 +11583,16 @@ var sessionSchema = z24.object({
11738
11583
  ),
11739
11584
  // DEC-1715 (WP-753 O1a)
11740
11585
  role: z24.string().max(200).optional().describe(
11741
- "For 'start': role to act as this session \u2014 only needed if you hold more than one; the error lists your roles if omitted."
11586
+ "For 'start': role to act as this session, by the '<PREFIX>-<n>' id a start reply prints beside the role name \u2014 only needed if you hold more than one; the error lists your roles if omitted."
11742
11587
  )
11743
11588
  });
11744
11589
  var sessionStartVariant = z24.object({
11745
11590
  action: z24.literal("start"),
11746
11591
  // DEC-1715 (WP-753 O1a): the role to act as this session — only needed when the person holds
11747
11592
  // more than one role; the server refuses and lists the choices when it's required and omitted.
11748
- role: z24.string().max(200).optional().describe("Role to act as this session \u2014 only needed if you hold more than one; the error lists your roles if omitted.")
11593
+ role: z24.string().max(200).optional().describe(
11594
+ "Role to act as this session, by the '<PREFIX>-<n>' id a start reply prints beside the role name \u2014 only needed if you hold more than one; the error lists your roles if omitted."
11595
+ )
11749
11596
  });
11750
11597
  var sessionCloseVariant = z24.object({ action: z24.literal("close") });
11751
11598
  var sessionStatusVariant = z24.object({ action: z24.literal("status") });
@@ -11810,10 +11657,10 @@ async function handleStart2(data) {
11810
11657
  const result2 = await startAgentSession(data?.role);
11811
11658
  const lines = [];
11812
11659
  lines.push(
11813
- `Session ${result2.sessionId} active. Initiated by ${result2.initiatedBy}. Workspace ${result2.workspaceName}. Scope: ${result2.toolsScope}. Orient with a task before substantive work: it is advice for captures, relations, accepts, ticks and questions; workflow checkpoints and organisation-structure writes still require it.`
11660
+ `Session ${result2.sessionId} active. Pass sessionId="${result2.sessionId}" on every write: the connection does not remember your session between calls. Initiated by ${result2.initiatedBy}. Workspace ${result2.workspaceName}. Scope: ${result2.toolsScope}. Orient with a task before substantive work: it is advice for captures, relations, accepts, ticks and questions; workflow checkpoints and organisation-structure writes still require it.`
11814
11661
  );
11815
11662
  if (result2.roleEntryId) {
11816
- lines.push(`Acting as role: ${result2.roleName ?? result2.roleEntryId}.`);
11663
+ lines.push(`Acting as role: ${result2.roleName ?? result2.roleEntryId}${result2.roleRef ? ` (${result2.roleRef})` : ""}.`);
11817
11664
  }
11818
11665
  lines.push(FEEDBACK_SUBMIT_HINT);
11819
11666
  const questionNotice = result2.notices?.find((n) => n.source === QUESTION_ACK_GATE_SOURCE_ID);
@@ -11829,7 +11676,7 @@ async function handleStart2(data) {
11829
11676
  openQuestionsTruncated: questionNotice?.payload?.truncated === true,
11830
11677
  surface: "mcp"
11831
11678
  }));
11832
- const summary = `Session ${result2.sessionId} active. Workspace ${result2.workspaceName}. Orient with a task first; workflow checkpoints and organisation-structure writes still require it.`;
11679
+ const summary = `Session ${result2.sessionId} active. Workspace ${result2.workspaceName}. Pass sessionId="${result2.sessionId}" on every write. Orient with a task first; workflow checkpoints and organisation-structure writes still require it.`;
11833
11680
  return {
11834
11681
  content: [{ type: "text", text: lines.join("\n") }],
11835
11682
  structuredContent: success(summary, {
@@ -11839,6 +11686,8 @@ async function handleStart2(data) {
11839
11686
  toolsScope: result2.toolsScope,
11840
11687
  roleEntryId: result2.roleEntryId,
11841
11688
  roleName: result2.roleName,
11689
+ roleRef: result2.roleRef,
11690
+ // TASK-583
11842
11691
  feedbackQueueNew: result2.feedbackQueueNew,
11843
11692
  feedbackOldestNewAt: result2.feedbackOldestNewAt,
11844
11693
  notices: result2.notices ?? [],
@@ -11852,17 +11701,18 @@ async function handleStart2(data) {
11852
11701
  // the guided-setup entrypoint this session just opened write access for.
11853
11702
  // FEAT-1409 spine core: notice next-actions are appended through this SAME existing
11854
11703
  // `next` field (HARD CONSTRAINT 2) — never a parallel action list.
11855
- }, [{ tool: "orient", description: "Orient session", parameters: { action: "start" } }, ...noticesToNextActions(result2.notices)])
11704
+ // TASK-583 (DEC-1907): every next action carries the sessionId, since the next call may arrive on a new connection.
11705
+ }, [{ tool: "orient", description: "Orient session", parameters: { action: "start" } }, ...noticesToNextActions(result2.notices)].map((next) => ({ ...next, parameters: { ...next.parameters, sessionId: result2.sessionId } })))
11856
11706
  };
11857
11707
  }
11858
11708
  async function handleClose() {
11859
11709
  const sessionId = getAgentSessionId();
11860
11710
  if (!sessionId) {
11861
11711
  return failureResult(
11862
- "No active agent session to close.",
11712
+ `No active agent session to close. Pass the sessionId to close as \`sessionId\`. ${PASS_SESSION_ID}`,
11863
11713
  "SESSION_REQUIRED",
11864
11714
  "No active agent session to close.",
11865
- "Start a session first.",
11715
+ PASS_SESSION_ID,
11866
11716
  [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
11867
11717
  );
11868
11718
  }
@@ -11953,10 +11803,10 @@ async function handleStatus() {
11953
11803
  const sessionId = getAgentSessionId();
11954
11804
  if (!sessionId) {
11955
11805
  return failureResult(
11956
- "No active agent session. Call session action=start to begin.",
11806
+ `No active agent session on this connection. Pass your sessionId as \`sessionId\` to read it. ${PASS_SESSION_ID}`,
11957
11807
  "SESSION_REQUIRED",
11958
11808
  "No active agent session.",
11959
- "Call session action=start to begin.",
11809
+ PASS_SESSION_ID,
11960
11810
  [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
11961
11811
  );
11962
11812
  }
@@ -11977,6 +11827,7 @@ async function handleStatus() {
11977
11827
  const modified = session.entriesModified?.length ?? 0;
11978
11828
  const lines = [
11979
11829
  `Session ${sessionId} ${session.status}.`,
11830
+ `Pass sessionId="${sessionId}" on every write: the connection does not remember your session between calls.`,
11980
11831
  `Initiated by ${session.initiatedBy}.`,
11981
11832
  `Scope: ${session.toolsScope}.`,
11982
11833
  `Oriented: ${oriented ? "yes" : "no"}.`,
@@ -14216,6 +14067,7 @@ var INSTRUCTIONS = [
14216
14067
  "## Writing",
14217
14068
  "",
14218
14069
  "Write actions (capture, entries update and commit, relations create, batch-create, dismiss and delete) need an active session and a readwrite key.",
14070
+ "Pass the `sessionId` that `session action=start` returned on every write: the connection does not remember it.",
14219
14071
  "Use `capture` to create an entry and read its reply: an entry that came back accepted needs no `entries action=commit`.",
14220
14072
  "Link with `relations action=suggest`, then `relations action=create`.",
14221
14073
  "Finish with `session action=wrapup-review`, then `session action=close`.",
@@ -14225,11 +14077,46 @@ var INSTRUCTIONS = [
14225
14077
  "",
14226
14078
  "Every tool and action: `productbrain://agent-cheatsheet`."
14227
14079
  ].join("\n");
14080
+ var sessionIdArg = z34.string().max(200).optional().describe(
14081
+ "The sessionId `session action=start` returned. Pass it on every write: the connection does not remember your session between calls."
14082
+ );
14083
+ function acceptSessionIdOnEveryTool(server) {
14084
+ const register = server.registerTool.bind(server);
14085
+ server.registerTool = (name, config, cb) => register(
14086
+ name,
14087
+ { ...config, inputSchema: config.inputSchema.extend({ sessionId: sessionIdArg }) },
14088
+ async ({ sessionId, ...args }, extra) => {
14089
+ if (typeof sessionId === "string" && sessionId.length > 0 && !(name === "session" && args.action === "start")) {
14090
+ const seated = await validateOrRefuse(sessionId);
14091
+ if ("refused" in seated) return seated.refused;
14092
+ return runWithSeatedSession(seated.lc, () => cb(args, extra));
14093
+ }
14094
+ return cb(args, extra);
14095
+ }
14096
+ );
14097
+ }
14098
+ async function validateOrRefuse(sessionId) {
14099
+ try {
14100
+ return { lc: await validateCallerSession(sessionId) };
14101
+ } catch (err) {
14102
+ const { code, message, recovery } = classifyError(err);
14103
+ const reason = message.split("\n")[0].replace(/\.?\s*$/, ".");
14104
+ const result2 = err instanceof KernelCallError && err.status >= 400 && err.status < 500 && err.status !== 429 ? failureResult(
14105
+ `[SESSION_INVALID] The sessionId you passed (${sessionId}) was refused: ${reason} ${PASS_SESSION_ID}`,
14106
+ "SESSION_INVALID",
14107
+ `sessionId refused: ${reason}`,
14108
+ PASS_SESSION_ID,
14109
+ [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
14110
+ ) : failureResult(`[${code}] ${message}`, code, message, recovery);
14111
+ return { refused: { ...result2, isError: true } };
14112
+ }
14113
+ }
14228
14114
  function createProductBrainServer() {
14229
14115
  const server = new McpServer2(
14230
14116
  { name: "Product Brain", version: SERVER_VERSION },
14231
14117
  { capabilities: { logging: {} }, instructions: INSTRUCTIONS }
14232
14118
  );
14119
+ acceptSessionIdOnEveryTool(server);
14233
14120
  registerSessionTools(server);
14234
14121
  registerEntriesTools(server);
14235
14122
  registerRelationsTools(server);
@@ -14280,4 +14167,4 @@ export {
14280
14167
  createProductBrainServer,
14281
14168
  initFeatureFlags
14282
14169
  };
14283
- //# sourceMappingURL=chunk-RPKS4G3K.js.map
14170
+ //# sourceMappingURL=chunk-ZOYUYCKV.js.map