@productbrain/mcp 0.0.1-beta.25 → 0.0.1-beta.27

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.
@@ -10,10 +10,11 @@ import {
10
10
  mcpQuery,
11
11
  recordSessionActivity,
12
12
  registerSmartCaptureTools,
13
+ requireActiveSession,
13
14
  requireWriteAccess,
14
15
  setSessionOriented,
15
16
  startAgentSession
16
- } from "./chunk-EVF3CYZN.js";
17
+ } from "./chunk-YCHKN75N.js";
17
18
 
18
19
  // src/server.ts
19
20
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -429,6 +430,9 @@ The relation was **not applied** \u2014 it will be created when the proposal is
429
430
  const created = results.filter((r) => r.ok && !r.proposalCreated);
430
431
  const proposals = results.filter((r) => r.ok && r.proposalCreated);
431
432
  const failed = results.filter((r) => !r.ok);
433
+ for (let i = 0; i < created.length; i++) {
434
+ await recordSessionActivity({ relationCreated: true });
435
+ }
432
436
  const lines = [`# Batch Link Results
433
437
  `];
434
438
  lines.push(`**${created.length}** created, **${proposals.length}** proposals, **${failed.length}** failed out of ${relations.length} total.
@@ -690,6 +694,82 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
690
694
  return { content: [{ type: "text", text: lines.join("\n") }] };
691
695
  }
692
696
  );
697
+ server.registerTool(
698
+ "get-build-context",
699
+ {
700
+ title: "Get Build Context",
701
+ description: "Assemble a structured build spec for any entry. Collection-agnostic: works with bets, features, epics, or any collection type. Returns the entry data, related entries by collection, applicable business rules, glossary terms, and chain refs to consult. Use this when starting a build to get full context in one call \u2014 no need to call orient, gather-context, and search separately.\n\nPass an entry ID (e.g. 'ENT-wc7u71', 'BET-001') or name. The output adapts to the entry's collection schema.",
702
+ inputSchema: {
703
+ entryId: z.string().describe("Entry ID or name (e.g. 'ENT-wc7u71', 'PB as Shared Memory')"),
704
+ maxHops: z.number().min(1).max(3).default(2).optional().describe("Relation traversal depth (default 2)")
705
+ },
706
+ annotations: { readOnlyHint: true }
707
+ },
708
+ async ({ entryId, maxHops }) => {
709
+ requireActiveSession();
710
+ const result = await mcpQuery("chain.assembleBuildContext", {
711
+ entryId,
712
+ maxHops: maxHops ?? 2
713
+ });
714
+ const { entry, relatedByCollection, businessRules, glossaryTerms, rulesToHonor, chainRefs, contextTruncated } = result;
715
+ const lines = [
716
+ `# Build Context: ${entry.name}`,
717
+ `**Entry:** ${entry.entryId ?? entry.name} [${entry.collectionSlug}]`,
718
+ `**Status:** ${entry.status}`,
719
+ ""
720
+ ];
721
+ if (Object.keys(entry.data).length > 0) {
722
+ lines.push("## Entry Data");
723
+ for (const [key, val] of Object.entries(entry.data)) {
724
+ const str = typeof val === "string" ? val : JSON.stringify(val);
725
+ if (str.length > 200) {
726
+ lines.push(`- **${key}:** ${str.slice(0, 200)}...`);
727
+ } else {
728
+ lines.push(`- **${key}:** ${str}`);
729
+ }
730
+ }
731
+ lines.push("");
732
+ }
733
+ if (businessRules.length > 0) {
734
+ lines.push("## Business Rules to Honor");
735
+ for (const r of businessRules) {
736
+ const id = r.entryId ?? r.name;
737
+ lines.push(`- **${id}:** ${r.name}`);
738
+ if (r.description) lines.push(` ${r.description.slice(0, 150)}${r.description.length > 150 ? "..." : ""}`);
739
+ }
740
+ lines.push("");
741
+ }
742
+ if (glossaryTerms.length > 0) {
743
+ lines.push("## Glossary Terms");
744
+ for (const t of glossaryTerms.slice(0, 15)) {
745
+ const id = t.entryId ?? t.name;
746
+ lines.push(`- **${id}:** ${t.name}`);
747
+ if (t.definition) lines.push(` ${t.definition.slice(0, 120)}${t.definition.length > 120 ? "..." : ""}`);
748
+ }
749
+ if (glossaryTerms.length > 15) lines.push(` _...and ${glossaryTerms.length - 15} more_`);
750
+ lines.push("");
751
+ }
752
+ const relatedCount = Object.values(relatedByCollection).reduce((sum, arr) => sum + arr.length, 0);
753
+ if (relatedCount > 0) {
754
+ lines.push("## Related Entries");
755
+ for (const [coll, entries] of Object.entries(relatedByCollection)) {
756
+ lines.push(`### ${coll} (${entries.length})`);
757
+ for (const e of entries.slice(0, 5)) {
758
+ lines.push(`- ${e.entryId ?? e.name}: ${e.name} [${e.relationType}]`);
759
+ }
760
+ if (entries.length > 5) lines.push(` _...and ${entries.length - 5} more_`);
761
+ }
762
+ lines.push("");
763
+ }
764
+ lines.push("## Chain Refs to Consult");
765
+ lines.push(chainRefs.slice(0, 15).map((r) => `- ${r}`).join("\n"));
766
+ if (contextTruncated) {
767
+ lines.push("");
768
+ lines.push("_Context was truncated (limits: 50 related, 20 rules, 30 glossary). Use get-entry for full details._");
769
+ }
770
+ return { content: [{ type: "text", text: lines.join("\n") }] };
771
+ }
772
+ );
693
773
  server.registerTool(
694
774
  "suggest-links",
695
775
  {
@@ -920,7 +1000,7 @@ Use \`list-collections\` to verify the result.`
920
1000
  },
921
1001
  async ({ entryId }) => {
922
1002
  requireWriteAccess();
923
- const { runContradictionCheck } = await import("./smart-capture-NX4QDLLY.js");
1003
+ const { runContradictionCheck } = await import("./smart-capture-WWGVXMQ2.js");
924
1004
  const entry = await mcpQuery("chain.getEntry", { entryId });
925
1005
  if (!entry) {
926
1006
  return {
@@ -1103,7 +1183,21 @@ async function queryPlannedWork() {
1103
1183
  function hasPlannedWork(work) {
1104
1184
  return work.uncommittedDrafts.length > 0 || work.inProgressEntries.length > 0 || work.openTensions.length > 0;
1105
1185
  }
1106
- function buildPlannedWorkSection(work, priorSessions) {
1186
+ function formatRecoveryBlock(block) {
1187
+ const fmt = (items, overflow) => items.map((e) => `${e.entryId} [${e.collection}] ${e.name}`).join(", ") + (overflow && overflow > 0 ? ` (+${overflow} more)` : "");
1188
+ const lines = [
1189
+ "## Previous Session Recovery",
1190
+ "",
1191
+ `Session ${block.sessionId} \u2014 ${block.date}, ${block.duration}, status: ${block.status}`,
1192
+ `Created (${block.created.length + (block.createdOverflow ?? 0)}): ${fmt(block.created, block.createdOverflow)}`,
1193
+ `Modified (${block.modified.length + (block.modifiedOverflow ?? 0)}): ${fmt(block.modified, block.modifiedOverflow)}`,
1194
+ `Drafts needing attention (${block.draftsNeedingAttention.length + (block.draftsOverflow ?? 0)}): ${fmt(block.draftsNeedingAttention, block.draftsOverflow)}`,
1195
+ `Relations created: ${block.relationsCreated}`,
1196
+ ""
1197
+ ];
1198
+ return lines;
1199
+ }
1200
+ function buildPlannedWorkSection(work, priorSessions, recoveryBlock) {
1107
1201
  if (!hasPlannedWork(work) && priorSessions.length === 0) return [];
1108
1202
  const lines = [];
1109
1203
  lines.push("## Continue from where you left off");
@@ -1133,7 +1227,7 @@ function buildPlannedWorkSection(work, priorSessions) {
1133
1227
  lines.push(`- **${count} open tensions** \u2014 top: ${top.name}`);
1134
1228
  }
1135
1229
  }
1136
- if (priorSessions.length > 0 && !hasPlannedWork(work)) {
1230
+ if (priorSessions.length > 0 && !hasPlannedWork(work) && !recoveryBlock) {
1137
1231
  const last = priorSessions[0];
1138
1232
  const date = new Date(last.startedAt).toISOString().split("T")[0];
1139
1233
  const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
@@ -1142,6 +1236,10 @@ function buildPlannedWorkSection(work, priorSessions) {
1142
1236
  lines.push(`- **Last session** (${date}): ${created} created, ${modified} modified`);
1143
1237
  }
1144
1238
  }
1239
+ if (recoveryBlock) {
1240
+ lines.push("");
1241
+ lines.push(...formatRecoveryBlock(recoveryBlock));
1242
+ }
1145
1243
  lines.push("");
1146
1244
  return lines;
1147
1245
  }
@@ -1222,6 +1320,27 @@ function buildSessionSummary(log) {
1222
1320
  }
1223
1321
  return lines.join("\n");
1224
1322
  }
1323
+ function extractSessionEntryIds(priorSessions) {
1324
+ const allSeen = /* @__PURE__ */ new Set();
1325
+ const all = [];
1326
+ let lastSessionOnly = [];
1327
+ for (let i = 0; i < priorSessions.length; i++) {
1328
+ const s = priorSessions[i];
1329
+ const ids = [...s.entriesCreated ?? [], ...s.entriesModified ?? []].filter((id) => id);
1330
+ if (i === 0) {
1331
+ lastSessionOnly = [...new Set(ids)].slice(0, 5);
1332
+ }
1333
+ for (const id of ids) {
1334
+ if (!allSeen.has(id)) {
1335
+ allSeen.add(id);
1336
+ all.push(id);
1337
+ if (all.length >= 10) break;
1338
+ }
1339
+ }
1340
+ if (all.length >= 10) break;
1341
+ }
1342
+ return { all, lastSessionOnly };
1343
+ }
1225
1344
  function registerHealthTools(server) {
1226
1345
  server.registerTool(
1227
1346
  "health",
@@ -1362,15 +1481,23 @@ function registerHealthTools(server) {
1362
1481
  errors.push(`Workspace: ${e.message}`);
1363
1482
  }
1364
1483
  let priorSessions = [];
1484
+ let recoveryBlock = null;
1365
1485
  if (wsCtx) {
1366
1486
  try {
1367
- priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
1487
+ const sessionsResult = await mcpQuery("agent.recentSessions", { limit: 3 });
1488
+ priorSessions = sessionsResult?.sessions ?? [];
1489
+ recoveryBlock = sessionsResult?.recoveryBlock ?? null;
1368
1490
  } catch {
1369
1491
  }
1370
1492
  }
1493
+ const { all: sessionEntryIds, lastSessionOnly } = extractSessionEntryIds(priorSessions);
1371
1494
  let orientEntries = null;
1372
1495
  try {
1373
- orientEntries = await mcpQuery("chain.getOrientEntries", task ? { task } : {});
1496
+ const orientArgs = {};
1497
+ if (task) orientArgs.task = task;
1498
+ if (sessionEntryIds.length > 0) orientArgs.sessionEntryIds = sessionEntryIds;
1499
+ if (lastSessionOnly.length > 0) orientArgs.lastSessionEntryIds = lastSessionOnly;
1500
+ orientEntries = await mcpQuery("chain.getOrientEntries", orientArgs);
1374
1501
  } catch {
1375
1502
  }
1376
1503
  let openTensions = [];
@@ -1415,7 +1542,10 @@ function registerHealthTools(server) {
1415
1542
  lines.push(`- \`${e.entryId ?? e._id}\` ${e.name}${tensionPart}`);
1416
1543
  }
1417
1544
  }
1418
- if (priorSessions.length > 0) {
1545
+ if (recoveryBlock) {
1546
+ lines.push("");
1547
+ lines.push(...formatRecoveryBlock(recoveryBlock));
1548
+ } else if (priorSessions.length > 0) {
1419
1549
  const last = priorSessions[0];
1420
1550
  const date = new Date(last.startedAt).toISOString().split("T")[0];
1421
1551
  const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
@@ -1492,6 +1622,31 @@ function registerHealthTools(server) {
1492
1622
  lines.push(`${betLine} ${sc.activeTensionCount} open tension(s).`);
1493
1623
  lines.push("");
1494
1624
  }
1625
+ if (orientEntries?.continuingFrom && orientEntries.continuingFrom.length > 0) {
1626
+ lines.push("## Continuing from");
1627
+ lines.push("_Prior-session entries most relevant to your task._");
1628
+ lines.push("");
1629
+ for (const e of orientEntries.continuingFrom) {
1630
+ const id = e.entryId ?? e.name;
1631
+ const type = e.canonicalKey ?? "generic";
1632
+ const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
1633
+ lines.push(`- \`${id}\` (score ${e.score}) [${type}]${coll} \u2014 ${e.name}`);
1634
+ if (e.reasoning) lines.push(` _${e.reasoning}_`);
1635
+ }
1636
+ lines.push("");
1637
+ }
1638
+ if (orientEntries?.lastSessionTouched && orientEntries.lastSessionTouched.length > 0) {
1639
+ lines.push("## Last session touched");
1640
+ lines.push("_Entries created or modified in your most recent session._");
1641
+ lines.push("");
1642
+ for (const e of orientEntries.lastSessionTouched) {
1643
+ const id = e.entryId ?? e.name;
1644
+ const type = e.canonicalKey ?? "generic";
1645
+ const coll = e.collectionSlug ? ` [${e.collectionSlug}]` : "";
1646
+ lines.push(`- \`${id}\` [${type}]${coll} \u2014 ${e.name}`);
1647
+ }
1648
+ lines.push("");
1649
+ }
1495
1650
  if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
1496
1651
  const tc = orientEntries.taskContext;
1497
1652
  lines.push("## Task Context");
@@ -1561,10 +1716,10 @@ function registerHealthTools(server) {
1561
1716
  }
1562
1717
  const plannedWork = await queryPlannedWork();
1563
1718
  if (hasPlannedWork(plannedWork)) {
1564
- lines.push(...buildPlannedWorkSection(plannedWork, priorSessions));
1719
+ lines.push(...buildPlannedWorkSection(plannedWork, priorSessions, recoveryBlock));
1565
1720
  } else {
1566
1721
  const briefingItems = [];
1567
- if (priorSessions.length > 0) {
1722
+ if (priorSessions.length > 0 && !recoveryBlock) {
1568
1723
  const last = priorSessions[0];
1569
1724
  const date = new Date(last.startedAt).toISOString().split("T")[0];
1570
1725
  const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
@@ -1581,6 +1736,10 @@ function registerHealthTools(server) {
1581
1736
  }
1582
1737
  lines.push("");
1583
1738
  }
1739
+ if (recoveryBlock) {
1740
+ lines.push("");
1741
+ lines.push(...formatRecoveryBlock(recoveryBlock));
1742
+ }
1584
1743
  }
1585
1744
  lines.push("What would you like to work on?");
1586
1745
  lines.push("");
@@ -4030,8 +4189,11 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
4030
4189
  const wsFullCtx = await getWorkspaceContext();
4031
4190
  const { ageDays, isNeglected } = computeWorkspaceAge(wsFullCtx.createdAt);
4032
4191
  let priorSessions = [];
4192
+ let recoveryBlock = null;
4033
4193
  try {
4034
- priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
4194
+ const sessionsResult = await mcpQuery("agent.recentSessions", { limit: 3 });
4195
+ priorSessions = sessionsResult?.sessions ?? [];
4196
+ recoveryBlock = sessionsResult?.recoveryBlock ?? null;
4035
4197
  } catch {
4036
4198
  }
4037
4199
  let openTensions = [];
@@ -4086,10 +4248,10 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
4086
4248
  const plannedWork = await queryPlannedWork();
4087
4249
  if (hasPlannedWork(plannedWork)) {
4088
4250
  lines.push("");
4089
- lines.push(...buildPlannedWorkSection(plannedWork, priorSessions));
4251
+ lines.push(...buildPlannedWorkSection(plannedWork, priorSessions, recoveryBlock));
4090
4252
  } else {
4091
4253
  const briefingItems = [];
4092
- if (priorSessions.length > 0) {
4254
+ if (priorSessions.length > 0 && !recoveryBlock) {
4093
4255
  const last = priorSessions[0];
4094
4256
  const date = new Date(last.startedAt).toISOString().split("T")[0];
4095
4257
  const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
@@ -4107,6 +4269,10 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
4107
4269
  }
4108
4270
  lines.push("");
4109
4271
  }
4272
+ if (recoveryBlock) {
4273
+ lines.push("");
4274
+ lines.push(...formatRecoveryBlock(recoveryBlock));
4275
+ }
4110
4276
  }
4111
4277
  lines.push("What would you like to work on?");
4112
4278
  lines.push("");
@@ -4692,4 +4858,4 @@ export {
4692
4858
  SERVER_VERSION,
4693
4859
  createProductBrainServer
4694
4860
  };
4695
- //# sourceMappingURL=chunk-E75NDP6K.js.map
4861
+ //# sourceMappingURL=chunk-R4LVIGFQ.js.map