@productbrain/mcp 0.0.1-beta.45 → 0.0.1-beta.47

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.
@@ -4,6 +4,8 @@ import {
4
4
  checkEntryQuality,
5
5
  closeAgentSession,
6
6
  extractPreview,
7
+ failure,
8
+ failureResult,
7
9
  formatRubricCoaching,
8
10
  formatRubricVerdictSection,
9
11
  getAgentSessionId,
@@ -16,6 +18,8 @@ import {
16
18
  mcpCall,
17
19
  mcpMutation,
18
20
  mcpQuery,
21
+ notFoundResult,
22
+ parseOrFail,
19
23
  recordSessionActivity,
20
24
  registerSmartCaptureTools,
21
25
  requireActiveSession,
@@ -23,9 +27,14 @@ import {
23
27
  runWithToolContext,
24
28
  setSessionOriented,
25
29
  startAgentSession,
30
+ success,
31
+ successResult,
26
32
  trackWriteTool,
27
- translateStaleToolNames
28
- } from "./chunk-CYECJTRI.js";
33
+ translateStaleToolNames,
34
+ unknownAction,
35
+ validationResult,
36
+ withEnvelope
37
+ } from "./chunk-FYFF4QKF.js";
29
38
  import {
30
39
  trackQualityCheck,
31
40
  trackQualityVerdict
@@ -94,7 +103,7 @@ function registerKnowledgeTools(server) {
94
103
  inputSchema: updateEntrySchema,
95
104
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
96
105
  },
97
- async ({ entryId, name, status: rawStatus, workflowStatus: rawWorkflowStatus, data, order, canonicalKey, autoPublish, changeNote }) => {
106
+ withEnvelope(async ({ entryId, name, status: rawStatus, workflowStatus: rawWorkflowStatus, data, order, canonicalKey, autoPublish, changeNote }) => {
98
107
  requireWriteAccess();
99
108
  const PROMOTED_FIELDS = ["status", "workflowStatus", "name", "order", "canonicalKey"];
100
109
  const topLevelByField = { status: rawStatus, workflowStatus: rawWorkflowStatus, name, order, canonicalKey };
@@ -164,11 +173,16 @@ function registerKnowledgeTools(server) {
164
173
  responseLines.push(`\u26A0\uFE0F \`data.${field}\` detected \u2014 did you mean the top-level \`${field}\` parameter?`);
165
174
  }
166
175
  }
176
+ const summary = `Updated ${entryId} \u2014 ${versionMode}. Fields: ${fieldsProvided.join(", ") || "none"}.`;
177
+ const next = [
178
+ { tool: "entries", description: "View updated entry", parameters: { action: "get", entryId } },
179
+ { tool: "commit-entry", description: "Commit to Chain", parameters: { entryId } }
180
+ ];
167
181
  return {
168
182
  content: [{ type: "text", text: responseLines.join("\n") }],
169
- structuredContent: { entryId: id, versionMode, fieldsProvided, confusedFields }
183
+ structuredContent: success(summary, { entryId: id, versionMode, fieldsProvided, confusedFields }, next)
170
184
  };
171
- }
185
+ })
172
186
  );
173
187
  trackWriteTool(updateTool);
174
188
  server.registerTool(
@@ -179,10 +193,14 @@ function registerKnowledgeTools(server) {
179
193
  inputSchema: getHistorySchema,
180
194
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
181
195
  },
182
- async ({ entryId }) => {
196
+ withEnvelope(async ({ entryId }) => {
183
197
  const history = await mcpQuery("chain.listEntryHistory", { entryId });
184
198
  if (history.length === 0) {
185
- return { content: [{ type: "text", text: `No history found for \`${entryId}\`.` }] };
199
+ return successResult(
200
+ `No history found for ${entryId}.`,
201
+ `No history events recorded for ${entryId}.`,
202
+ { entryId, eventCount: 0, events: [] }
203
+ );
186
204
  }
187
205
  const formatted = history.map((h) => {
188
206
  const date = new Date(h.timestamp).toISOString();
@@ -192,9 +210,13 @@ function registerKnowledgeTools(server) {
192
210
  return {
193
211
  content: [{ type: "text", text: `# History for \`${entryId}\` (${history.length} events)
194
212
 
195
- ${formatted}` }]
213
+ ${formatted}` }],
214
+ structuredContent: success(
215
+ `Found ${history.length} history events for ${entryId}.`,
216
+ { entryId, eventCount: history.length, events: history }
217
+ )
196
218
  };
197
- }
219
+ })
198
220
  );
199
221
  const commitTool = server.registerTool(
200
222
  "commit-entry",
@@ -204,14 +226,12 @@ ${formatted}` }]
204
226
  inputSchema: commitEntrySchema,
205
227
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
206
228
  },
207
- async ({ entryId }) => {
229
+ withEnvelope(async ({ entryId }) => {
208
230
  requireWriteAccess();
209
- const { runContradictionCheck } = await import("./smart-capture-XBOUPHFI.js");
231
+ const { runContradictionCheck } = await import("./smart-capture-XLBFE252.js");
210
232
  const entry = await mcpQuery("chain.getEntry", { entryId });
211
233
  if (!entry) {
212
- return {
213
- content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }]
214
- };
234
+ return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
215
235
  }
216
236
  const descField = entry.data?.description ?? entry.data?.canonical ?? entry.data?.rationale ?? "";
217
237
  const warnings = await runContradictionCheck(entry.name, descField);
@@ -234,8 +254,9 @@ ${formatted}` }]
234
254
  const docId = result?._id ?? entry._id;
235
255
  await recordSessionActivity({ entryModified: docId });
236
256
  const wsCtx = await getWorkspaceContext();
257
+ const isProposal = result?.status === "proposal_created";
237
258
  let lines;
238
- if (result?.status === "proposal_created") {
259
+ if (isProposal) {
239
260
  lines = [
240
261
  `# Proposal created: ${result.entryId ?? entryId}`,
241
262
  `**${result.name ?? entry.name}** \u2014 commit requires consent. A proposal was created instead of committing directly.`,
@@ -287,11 +308,29 @@ ${formatted}` }]
287
308
  }
288
309
  }
289
310
  }
311
+ const next = isProposal ? [
312
+ { tool: "entries", description: "View entry", parameters: { action: "get", entryId } }
313
+ ] : [
314
+ { tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId } },
315
+ { tool: "entries", description: "View committed entry", parameters: { action: "get", entryId } }
316
+ ];
317
+ const summary = isProposal ? `Proposal created for ${entryId} (${entry.name}). Awaiting consent.` : `Committed ${entryId} (${entry.name}) to the Chain.`;
290
318
  return {
291
319
  content: [{ type: "text", text: lines.join("\n") }],
292
- structuredContent: coachingResult?.verdict ? { qualityVerdict: coachingResult.verdict, source: coachingResult.source ?? "heuristic" } : void 0
320
+ structuredContent: success(
321
+ summary,
322
+ {
323
+ entryId,
324
+ name: entry.name,
325
+ outcome: isProposal ? "proposal_created" : "committed",
326
+ qualityVerdict: coachingResult?.verdict ?? void 0,
327
+ source: coachingResult?.source ?? void 0,
328
+ contradictions: warnings.length
329
+ },
330
+ next
331
+ )
293
332
  };
294
- }
333
+ })
295
334
  );
296
335
  trackWriteTool(commitTool);
297
336
  }
@@ -365,49 +404,41 @@ function registerEntriesTools(server) {
365
404
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
366
405
  _meta: { ui: { resourceUri: "ui://entries/entry-cards.html" } }
367
406
  },
368
- async (args) => {
369
- const parsed = entriesSchema.safeParse(args);
370
- if (!parsed.success) {
371
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
372
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
373
- }
407
+ withEnvelope(async (args) => {
408
+ const parsed = parseOrFail(entriesSchema, args);
409
+ if (!parsed.ok) return parsed.result;
374
410
  const { action, entryId, entryIds, collection, status, tag, label, query } = parsed.data;
375
411
  return runWithToolContext({ tool: "entries", action }, async () => {
376
412
  if (action === "get") {
377
413
  if (!entryId) {
378
- return { content: [{ type: "text", text: "`entryId` is required when action is 'get'." }] };
414
+ return validationResult("entryId is required when action is 'get'.");
379
415
  }
380
416
  return handleGet(entryId);
381
417
  }
382
418
  if (action === "batch") {
383
419
  if (!entryIds || entryIds.length === 0) {
384
- return { content: [{ type: "text", text: "`entryIds` is required when action is 'batch'." }] };
420
+ return validationResult("entryIds is required when action is 'batch'.");
385
421
  }
386
422
  return handleBatch(entryIds);
387
423
  }
388
424
  if (action === "search") {
389
425
  if (!query || query.length < 2) {
390
- return { content: [{ type: "text", text: "`query` is required for search (min 2 characters)." }] };
426
+ return validationResult("query is required for search (min 2 characters).");
391
427
  }
392
428
  return handleSearch(server, query, collection, status);
393
429
  }
394
430
  if (action === "list") {
395
431
  return handleList(collection, status, tag, label);
396
432
  }
397
- return {
398
- content: [{
399
- type: "text",
400
- text: `Unknown action '${action}'. Valid actions: ${ENTRIES_ACTIONS.join(", ")}.`
401
- }]
402
- };
433
+ return unknownAction(action, ENTRIES_ACTIONS);
403
434
  });
404
- }
435
+ })
405
436
  );
406
437
  }
407
438
  async function handleGet(entryId) {
408
439
  const entry = await mcpQuery("chain.getEntry", { entryId });
409
440
  if (!entry) {
410
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
441
+ return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
411
442
  }
412
443
  const e = entry;
413
444
  const lines = [
@@ -447,7 +478,7 @@ async function handleGet(entryId) {
447
478
  lines.push(`- ${date}: ${h.event}${h.changedBy ? ` _(${h.changedBy})_` : ""}`);
448
479
  }
449
480
  }
450
- const structuredContent = {
481
+ const entryData = {
451
482
  entryId: String(e.entryId ?? ""),
452
483
  name: String(e.name ?? ""),
453
484
  collection: String(e.canonicalKey ?? ""),
@@ -467,7 +498,14 @@ async function handleGet(entryId) {
467
498
  };
468
499
  return {
469
500
  content: [{ type: "text", text: lines.join("\n") }],
470
- structuredContent
501
+ structuredContent: success(
502
+ `Found ${entryId}: ${String(e.name)} [${String(e.canonicalKey ?? "")}].`,
503
+ entryData,
504
+ [
505
+ { tool: "graph", description: "Explore connections", parameters: { action: "suggest", entryId } },
506
+ { tool: "context", description: "Gather related context", parameters: { action: "gather", entryId } }
507
+ ]
508
+ )
471
509
  };
472
510
  }
473
511
  async function handleBatch(entryIds) {
@@ -530,9 +568,10 @@ async function handleBatch(entryIds) {
530
568
  ...entry.data && typeof entry.data === "object" ? { data: entry.data } : {}
531
569
  };
532
570
  });
571
+ const total = structuredEntries.length;
533
572
  return {
534
573
  content: [{ type: "text", text: lines.join("\n") }],
535
- structuredContent: { entries: structuredEntries, total: structuredEntries.length }
574
+ structuredContent: success(`Fetched ${total} entries in batch.`, { entries: structuredEntries, total })
536
575
  };
537
576
  }
538
577
  async function handleList(collection, status, tag, label) {
@@ -550,7 +589,14 @@ async function handleList(collection, status, tag, label) {
550
589
  });
551
590
  }
552
591
  if (entries.length === 0) {
553
- return { content: [{ type: "text", text: "No entries match the given filters." }] };
592
+ return {
593
+ content: [{ type: "text", text: "No entries match the given filters." }],
594
+ structuredContent: success(
595
+ "No entries match the given filters.",
596
+ { entries: [], total: 0 },
597
+ [{ tool: "collections", description: "List collections", parameters: { action: "list" } }]
598
+ )
599
+ };
554
600
  }
555
601
  const formatted = entries.map((e) => {
556
602
  const id = e.entryId ? `**${e.entryId}:** ` : "";
@@ -567,13 +613,14 @@ ${dataPreview}` : ""}`;
567
613
  status: String(e.status ?? ""),
568
614
  ...e.workflowStatus ? { workflowStatus: e.workflowStatus } : {}
569
615
  }));
616
+ const total = structuredEntries.length;
570
617
  return {
571
618
  content: [{ type: "text", text: `## List Result
572
619
 
573
620
  # Entries${scope} (${entries.length})
574
621
 
575
622
  ${formatted}` }],
576
- structuredContent: { entries: structuredEntries, total: structuredEntries.length }
623
+ structuredContent: success(`Found ${total} entries${scope}.`, { entries: structuredEntries, total })
577
624
  };
578
625
  }
579
626
  async function handleSearch(server, query, collection, status) {
@@ -592,7 +639,12 @@ async function handleSearch(server, query, collection, status) {
592
639
  content: [{
593
640
  type: "text",
594
641
  text: `No results for "${query}"${scope}. Try a broader search or use \`collections action=list\` to see available data.`
595
- }]
642
+ }],
643
+ structuredContent: success(
644
+ `No results for '${query}'${scope}.`,
645
+ { entries: [], total: 0, query },
646
+ [{ tool: "collections", description: "List collections", parameters: { action: "list" } }]
647
+ )
596
648
  };
597
649
  }
598
650
  const collMap = /* @__PURE__ */ new Map();
@@ -637,18 +689,19 @@ async function handleSearch(server, query, collection, status) {
637
689
  status: e.status,
638
690
  collectionName: e.collection
639
691
  }));
692
+ const total = structuredResults.length;
640
693
  return {
641
694
  content: [{ type: "text", text: `${header}
642
695
 
643
696
  ${formatted}
644
697
 
645
698
  ${footer}` }],
646
- structuredContent: {
699
+ structuredContent: success(`Found ${total} entries matching '${query}'.`, {
647
700
  results: structuredResults,
648
701
  entries: entriesForView,
649
- total: structuredResults.length,
702
+ total,
650
703
  query
651
- }
704
+ })
652
705
  };
653
706
  }
654
707
 
@@ -696,12 +749,9 @@ function registerGraphTools(server) {
696
749
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
697
750
  _meta: { ui: { resourceUri: "ui://graph/constellation.html" } }
698
751
  },
699
- async (args) => {
700
- const parsed = graphSchema.safeParse(args);
701
- if (!parsed.success) {
702
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
703
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
704
- }
752
+ withEnvelope(async (args) => {
753
+ const parsed = parseOrFail(graphSchema, args);
754
+ if (!parsed.ok) return parsed.result;
705
755
  const { action, entryId, direction, limit, depth } = parsed.data;
706
756
  return runWithToolContext({ tool: "graph", action }, async () => {
707
757
  if (action === "find") {
@@ -710,24 +760,29 @@ function registerGraphTools(server) {
710
760
  if (action === "suggest") {
711
761
  return handleSuggest(entryId, limit ?? 10, depth ?? 2);
712
762
  }
713
- return {
714
- content: [{
715
- type: "text",
716
- text: `Unknown action '${action}'. Valid actions: ${GRAPH_ACTIONS.join(", ")}.`
717
- }]
718
- };
763
+ return unknownAction(action, GRAPH_ACTIONS);
719
764
  });
720
- }
765
+ })
721
766
  );
722
767
  }
723
768
  async function handleFind(entryId, direction) {
724
769
  const relations = await mcpQuery("chain.listEntryRelations", { entryId });
725
770
  if (relations.length === 0) {
726
- return { content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relations action=create to create connections.` }] };
771
+ return {
772
+ content: [{ type: "text", text: `No relations found for \`${entryId}\`. Use relations action=create to create connections.` }],
773
+ structuredContent: success(
774
+ `No relations found for '${entryId}'.`,
775
+ { entryId, relations: [], nodes: [], edges: [], total: 0 },
776
+ [
777
+ { tool: "relations", description: "Create a relation", parameters: { action: "create", from: entryId } },
778
+ { tool: "graph", description: "Get suggestions", parameters: { action: "suggest", entryId } }
779
+ ]
780
+ )
781
+ };
727
782
  }
728
783
  const sourceEntry = await mcpQuery("chain.getEntry", { entryId });
729
784
  if (!sourceEntry) {
730
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try entries action=search to find the right ID.` }] };
785
+ return notFoundResult(entryId, `Entry '${entryId}' not found. Try entries action=search to find the right ID.`);
731
786
  }
732
787
  const sourceInternalId = sourceEntry._id;
733
788
  const MAX_RELATIONS = 25;
@@ -801,13 +856,11 @@ async function handleFind(entryId, direction) {
801
856
  }));
802
857
  return {
803
858
  content: [{ type: "text", text: lines.join("\n") }],
804
- structuredContent: {
805
- entryId,
806
- relations: structuredRelations,
807
- nodes,
808
- edges,
809
- total: relations.length
810
- }
859
+ structuredContent: success(
860
+ `Found ${relations.length} relations for ${entryId}.`,
861
+ { entryId, relations: structuredRelations, nodes, edges, total: relations.length },
862
+ [{ tool: "entries", description: "View entry details", parameters: { action: "get", entryId } }]
863
+ )
811
864
  };
812
865
  }
813
866
  async function handleSuggest(entryId, limit, depth) {
@@ -817,7 +870,13 @@ async function handleSuggest(entryId, limit, depth) {
817
870
  limit
818
871
  });
819
872
  if (!result?.suggestions || result.suggestions.length === 0) {
820
- return { content: [{ type: "text", text: `No suggestions found for \`${entryId}\` \u2014 it may already be well-connected, or no similar entries exist.` }] };
873
+ return failureResult(
874
+ `No suggestions found for '${entryId}' \u2014 it may already be well-connected, or no similar entries exist.`,
875
+ "NOT_FOUND",
876
+ `No suggestions found for '${entryId}'.`,
877
+ "Entry may already be well-connected.",
878
+ [{ tool: "graph", description: "Check existing relations", parameters: { action: "find", entryId } }]
879
+ );
821
880
  }
822
881
  const resolved = result.resolvedEntry;
823
882
  const resolvedLabel = resolved ? `\`${resolved.entryId ?? resolved.name}\` (${resolved.name})` : `\`${entryId}\``;
@@ -885,13 +944,11 @@ async function handleSuggest(entryId, limit, depth) {
885
944
  }));
886
945
  return {
887
946
  content: [{ type: "text", text: lines.join("\n") }],
888
- structuredContent: {
889
- entryId: sourceId,
890
- suggestions: structuredSuggestions,
891
- nodes,
892
- edges,
893
- total: suggestions.length
894
- }
947
+ structuredContent: success(
948
+ `Found ${suggestions.length} link suggestions for ${resolvedLabel}.`,
949
+ { entryId: sourceId, suggestions: structuredSuggestions, nodes, edges, total: suggestions.length },
950
+ [{ tool: "relations", description: "Create links", parameters: { action: "batch-create" } }]
951
+ )
895
952
  };
896
953
  }
897
954
 
@@ -921,40 +978,32 @@ function registerRelationsTools(server) {
921
978
  inputSchema: relationsSchema,
922
979
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
923
980
  },
924
- async (args) => {
925
- const parsed = relationsSchema.safeParse(args);
926
- if (!parsed.success) {
927
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
928
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
929
- }
981
+ withEnvelope(async (args) => {
982
+ const parsed = parseOrFail(relationsSchema, args);
983
+ if (!parsed.ok) return parsed.result;
930
984
  const { action, from, to, type, score, relations } = parsed.data;
931
985
  return runWithToolContext({ tool: "relations", action }, async () => {
932
986
  if (action === "create") {
933
987
  if (!from || !to || !type) {
934
- return { content: [{ type: "text", text: "`from`, `to`, and `type` are required when action is 'create'." }] };
988
+ return validationResult("from, to, and type are required when action is 'create'.");
935
989
  }
936
990
  return handleCreate(from, to, type, score);
937
991
  }
938
992
  if (action === "batch-create") {
939
993
  if (!relations || relations.length === 0) {
940
- return { content: [{ type: "text", text: "`relations` is required when action is 'batch-create'." }] };
994
+ return validationResult("relations is required when action is 'batch-create'.");
941
995
  }
942
996
  return handleBatchCreate(relations);
943
997
  }
944
998
  if (action === "dismiss") {
945
999
  if (!from || !to) {
946
- return { content: [{ type: "text", text: "`from` and `to` are required when action is 'dismiss'." }] };
1000
+ return validationResult("from and to are required when action is 'dismiss'.");
947
1001
  }
948
1002
  return handleDismiss(from, to, type ?? "related_to", score);
949
1003
  }
950
- return {
951
- content: [{
952
- type: "text",
953
- text: `Unknown action '${action}'. Valid actions: ${RELATIONS_ACTIONS.join(", ")}.`
954
- }]
955
- };
1004
+ return unknownAction(action, RELATIONS_ACTIONS);
956
1005
  });
957
- }
1006
+ })
958
1007
  );
959
1008
  trackWriteTool(tool);
960
1009
  }
@@ -983,14 +1032,24 @@ ${result.message ?? "Governs relation requires async consent. A proposal was cre
983
1032
  **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})
984
1033
 
985
1034
  The relation was **not applied** \u2014 it will be created when the proposal is approved.`
986
- }]
1035
+ }],
1036
+ structuredContent: success(
1037
+ `Proposal created for ${from} \u2192 ${to} (${type}). Awaiting consent.`,
1038
+ { status: "proposal_created", from, to, type, proposalId: result.proposalId },
1039
+ [{ tool: "entries", description: "Check proposal", parameters: { action: "get", entryId: result.proposalId } }]
1040
+ )
987
1041
  };
988
1042
  }
989
1043
  return {
990
1044
  content: [{ type: "text", text: `# Relation Created
991
1045
 
992
1046
  **${from}** \u2014[${type}]\u2192 **${to}**
993
- **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }]
1047
+ **Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})` }],
1048
+ structuredContent: success(
1049
+ `Created relation ${from} \u2192 ${to} (${type}).`,
1050
+ { status: "created", from, to, type },
1051
+ [{ tool: "graph", description: "See connections", parameters: { action: "find", entryId: from } }]
1052
+ )
994
1053
  };
995
1054
  }
996
1055
  async function handleBatchCreate(relations) {
@@ -1043,17 +1102,25 @@ async function handleBatchCreate(relations) {
1043
1102
  lines.push(`- **${r.from}** \u2192 **${r.to}** (${r.type}): _${r.error}_`);
1044
1103
  }
1045
1104
  }
1046
- return { content: [{ type: "text", text: lines.join("\n") }] };
1105
+ const next = failed.length === 0 ? [{ tool: "graph", description: "Check connections", parameters: { action: "find", entryId: relations[0].from } }] : [];
1106
+ return {
1107
+ content: [{ type: "text", text: lines.join("\n") }],
1108
+ structuredContent: success(
1109
+ `Batch: ${created.length} created, ${proposals.length} proposals, ${failed.length} failed of ${relations.length}.`,
1110
+ { created: created.length, proposals: proposals.length, failed: failed.length, total: relations.length, results },
1111
+ next.length ? next : void 0
1112
+ )
1113
+ };
1047
1114
  }
1048
1115
  async function handleDismiss(from, to, type, score) {
1049
1116
  requireWriteAccess();
1050
1117
  const fromEntry = await mcpQuery("chain.getEntry", { entryId: from });
1051
1118
  if (!fromEntry) {
1052
- return { content: [{ type: "text", text: `Entry \`${from}\` not found.` }] };
1119
+ return notFoundResult(from);
1053
1120
  }
1054
1121
  const toEntry = await mcpQuery("chain.getEntry", { entryId: to });
1055
1122
  if (!toEntry) {
1056
- return { content: [{ type: "text", text: `Entry \`${to}\` not found.` }] };
1123
+ return notFoundResult(to);
1057
1124
  }
1058
1125
  await mcpMutation("chain.dismissSuggestion", {
1059
1126
  fromEntryId: fromEntry._id,
@@ -1061,12 +1128,11 @@ async function handleDismiss(from, to, type, score) {
1061
1128
  recommendedType: type,
1062
1129
  score: score ?? 0
1063
1130
  });
1064
- return {
1065
- content: [{
1066
- type: "text",
1067
- text: `Dismissed suggestion: ${from} \u2192 ${to} (${type}). Feedback recorded for scoring improvements.`
1068
- }]
1069
- };
1131
+ return successResult(
1132
+ `Dismissed suggestion: ${from} \u2192 ${to} (${type}). Feedback recorded for scoring improvements.`,
1133
+ `Dismissed suggestion ${from} \u2192 ${to} (${type}). Feedback recorded.`,
1134
+ { from, to, type }
1135
+ );
1070
1136
  }
1071
1137
 
1072
1138
  // src/tools/context.ts
@@ -1099,12 +1165,9 @@ function registerContextTools(server) {
1099
1165
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
1100
1166
  _meta: { ui: { resourceUri: "ui://graph/constellation.html" } }
1101
1167
  },
1102
- async (args) => {
1103
- const parsed = contextSchema.safeParse(args);
1104
- if (!parsed.success) {
1105
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1106
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
1107
- }
1168
+ withEnvelope(async (args) => {
1169
+ const parsed = parseOrFail(contextSchema, args);
1170
+ if (!parsed.ok) return parsed.result;
1108
1171
  const { action, entryId, mapEntryId, task, mode, maxHops, maxResults } = parsed.data;
1109
1172
  return runWithToolContext({ tool: "context", action }, async () => {
1110
1173
  if (action === "gather") {
@@ -1115,23 +1178,18 @@ function registerContextTools(server) {
1115
1178
  }
1116
1179
  if (action === "build") {
1117
1180
  if (!entryId) {
1118
- return { content: [{ type: "text", text: "`entryId` is required when action is 'build'." }] };
1181
+ return validationResult("entryId is required when action is 'build'.");
1119
1182
  }
1120
1183
  return handleBuild(server, entryId, maxHops ?? 2);
1121
1184
  }
1122
- return {
1123
- content: [{
1124
- type: "text",
1125
- text: `Unknown action '${action}'. Valid actions: ${CONTEXT_ACTIONS.join(", ")}.`
1126
- }]
1127
- };
1185
+ return unknownAction(action, CONTEXT_ACTIONS);
1128
1186
  });
1129
- }
1187
+ })
1130
1188
  );
1131
1189
  }
1132
1190
  async function handleGather(server, entryId, task, mode, maxHops, maxResults) {
1133
1191
  if (!entryId && !task) {
1134
- return { content: [{ type: "text", text: "Provide either `entryId` (graph traversal) or `task` (auto-load context for a task)." }] };
1192
+ return validationResult("Provide either entryId (graph traversal) or task (auto-load context).");
1135
1193
  }
1136
1194
  if (entryId && mode === "graph") {
1137
1195
  await server.sendLoggingMessage({
@@ -1144,7 +1202,7 @@ async function handleGather(server, entryId, task, mode, maxHops, maxResults) {
1144
1202
  maxHops
1145
1203
  });
1146
1204
  if (!result2?.root) {
1147
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
1205
+ return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
1148
1206
  }
1149
1207
  if (result2.context.length === 0) {
1150
1208
  return {
@@ -1155,7 +1213,12 @@ async function handleGather(server, entryId, task, mode, maxHops, maxResults) {
1155
1213
  _No relations found._ This entry is not yet connected to the knowledge graph.
1156
1214
 
1157
1215
  Use \`graph action=suggest\` to discover potential connections.`
1158
- }]
1216
+ }],
1217
+ structuredContent: success(
1218
+ `No relations found for ${result2.root.entryId}. Entry not yet connected.`,
1219
+ { root: result2.root, context: [], totalFound: 0 },
1220
+ [{ tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: result2.root.entryId } }]
1221
+ )
1159
1222
  };
1160
1223
  }
1161
1224
  const byCollection2 = /* @__PURE__ */ new Map();
@@ -1201,7 +1264,10 @@ Use \`graph action=suggest\` to discover potential connections.`
1201
1264
  }));
1202
1265
  return {
1203
1266
  content: [{ type: "text", text: lines2.join("\n") }],
1204
- structuredContent: { nodes: nodes2, edges: edges2 }
1267
+ structuredContent: success(
1268
+ `Gathered ${result2.totalFound} related entries for ${result2.root.entryId} (${result2.hopsTraversed} hops, graph mode).`,
1269
+ { nodes: nodes2, edges: edges2, root: result2.root, totalFound: result2.totalFound }
1270
+ )
1205
1271
  };
1206
1272
  }
1207
1273
  if (task && !entryId) {
@@ -1226,7 +1292,13 @@ Use \`graph action=suggest\` to discover potential connections.`
1226
1292
  No context found for this task. The chain may not cover this area yet.
1227
1293
 
1228
1294
  _Consider capturing domain knowledge discovered during this task via \`capture\`._`
1229
- }]
1295
+ }],
1296
+ structuredContent: failure(
1297
+ "NOT_FOUND",
1298
+ "No context found for this task.",
1299
+ "The chain may not cover this area. Capture domain knowledge via capture.",
1300
+ [{ tool: "capture", description: "Capture knowledge", parameters: {} }]
1301
+ )
1230
1302
  };
1231
1303
  }
1232
1304
  const byCollection2 = /* @__PURE__ */ new Map();
@@ -1262,7 +1334,10 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
1262
1334
  }));
1263
1335
  return {
1264
1336
  content: [{ type: "text", text: lines2.join("\n") }],
1265
- structuredContent: { entries: taskEntries }
1337
+ structuredContent: success(
1338
+ `Loaded ${result2.totalFound} entries for task (${result2.confidence} confidence).`,
1339
+ { entries: taskEntries, totalFound: result2.totalFound, confidence: result2.confidence }
1340
+ )
1266
1341
  };
1267
1342
  }
1268
1343
  await server.sendLoggingMessage({
@@ -1272,7 +1347,7 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
1272
1347
  });
1273
1348
  const result = await mcpQuery("chain.gatherContext", { entryId, maxHops });
1274
1349
  if (!result?.root) {
1275
- return { content: [{ type: "text", text: `Entry \`${entryId}\` not found. Try search to find the right ID.` }] };
1350
+ return notFoundResult(entryId, `Entry '${entryId}' not found. Try search to find the right ID.`);
1276
1351
  }
1277
1352
  if (result.related.length === 0) {
1278
1353
  return {
@@ -1283,7 +1358,12 @@ _Consider capturing domain knowledge discovered during this task via \`capture\`
1283
1358
  _No relations found._ This entry is not yet connected to the knowledge graph.
1284
1359
 
1285
1360
  Use \`graph action=suggest\` to discover potential connections, or \`relations action=create\` to link manually.`
1286
- }]
1361
+ }],
1362
+ structuredContent: success(
1363
+ `No relations found for ${result.root.entryId}. Entry not yet connected.`,
1364
+ { root: result.root, related: [], totalRelations: 0 },
1365
+ [{ tool: "graph", description: "Discover connections", parameters: { action: "suggest", entryId: result.root.entryId } }]
1366
+ )
1287
1367
  };
1288
1368
  }
1289
1369
  const byCollection = /* @__PURE__ */ new Map();
@@ -1323,7 +1403,10 @@ Use \`graph action=suggest\` to discover potential connections, or \`relations a
1323
1403
  }));
1324
1404
  return {
1325
1405
  content: [{ type: "text", text: lines.join("\n") }],
1326
- structuredContent: { nodes, edges }
1406
+ structuredContent: success(
1407
+ `Gathered ${result.totalRelations} related entries for ${result.root.entryId} (${result.hopsTraversed} hops).`,
1408
+ { nodes, edges, root: result.root, totalRelations: result.totalRelations }
1409
+ )
1327
1410
  };
1328
1411
  }
1329
1412
  async function handleJourneyGather(server, mapEntryId, maxHops, maxResults) {
@@ -1338,7 +1421,11 @@ async function handleJourneyGather(server, mapEntryId, maxHops, maxResults) {
1338
1421
  maxResults
1339
1422
  });
1340
1423
  if (!result) {
1341
- return { content: [{ type: "text", text: `Journey map \`${mapEntryId}\` not found or has no slot data. Verify the map entry ID.` }] };
1424
+ return notFoundResult(
1425
+ mapEntryId,
1426
+ `Journey map '${mapEntryId}' not found or has no slot data. Verify the map entry ID.`,
1427
+ "Verify the map entry ID is correct."
1428
+ );
1342
1429
  }
1343
1430
  if (result.stages.length === 0 || result.seedCount === 0) {
1344
1431
  return {
@@ -1349,7 +1436,11 @@ async function handleJourneyGather(server, mapEntryId, maxHops, maxResults) {
1349
1436
  _No ingredients found in this journey map._
1350
1437
 
1351
1438
  Add journey steps via MCP: \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntryId="..."\``
1352
- }]
1439
+ }],
1440
+ structuredContent: success(
1441
+ `Journey map ${result.map.name} has no ingredients.`,
1442
+ { map: result.map, stages: result.stages, context: [] }
1443
+ )
1353
1444
  };
1354
1445
  }
1355
1446
  const lines = [
@@ -1389,15 +1480,18 @@ Add journey steps via MCP: \`map-slot action=add mapEntryId="${mapEntryId}" slot
1389
1480
  lines.push(`_Use \`entries action=get\` for full details. Link governance to steps: \`relations action=create\`._`);
1390
1481
  return {
1391
1482
  content: [{ type: "text", text: lines.join("\n") }],
1392
- structuredContent: {
1393
- map: result.map,
1394
- stages: result.stages,
1395
- context: result.context.map((e) => ({
1396
- entryId: e.entryId ?? "",
1397
- name: e.name,
1398
- collectionName: e.collectionName
1399
- }))
1400
- }
1483
+ structuredContent: success(
1484
+ `Journey context for ${result.map.name}: ${result.totalFound} related entries across ${result.stages.length} stages.`,
1485
+ {
1486
+ map: result.map,
1487
+ stages: result.stages,
1488
+ context: result.context.map((e) => ({
1489
+ entryId: e.entryId ?? "",
1490
+ name: e.name,
1491
+ collectionName: e.collectionName
1492
+ }))
1493
+ }
1494
+ )
1401
1495
  };
1402
1496
  }
1403
1497
  async function handleBuild(server, entryId, maxHops) {
@@ -1467,7 +1561,13 @@ async function handleBuild(server, entryId, maxHops) {
1467
1561
  lines.push("");
1468
1562
  lines.push("_Context was truncated (limits: 50 related, 20 rules, 30 glossary). Use entries action=get for full details._");
1469
1563
  }
1470
- return { content: [{ type: "text", text: annotateStaleNames(lines.join("\n")) }] };
1564
+ return {
1565
+ content: [{ type: "text", text: annotateStaleNames(lines.join("\n")) }],
1566
+ structuredContent: success(
1567
+ `Build context for ${entry.name}: ${relatedCount} related, ${businessRules.length} rules, ${glossaryTerms.length} terms.`,
1568
+ { entry, relatedByCollection, businessRulesCount: businessRules.length, glossaryTermsCount: glossaryTerms.length, contextTruncated }
1569
+ )
1570
+ };
1471
1571
  }
1472
1572
 
1473
1573
  // src/tools/collections.ts
@@ -1502,12 +1602,9 @@ function registerCollectionsTools(server) {
1502
1602
  inputSchema: collectionsSchema,
1503
1603
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
1504
1604
  },
1505
- async (args) => {
1506
- const parsed = collectionsSchema.safeParse(args);
1507
- if (!parsed.success) {
1508
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1509
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
1510
- }
1605
+ withEnvelope(async (args) => {
1606
+ const parsed = parseOrFail(collectionsSchema, args);
1607
+ if (!parsed.ok) return parsed.result;
1511
1608
  const { action, slug, name, description, purpose, icon, navGroup, fields } = parsed.data;
1512
1609
  return runWithToolContext({ tool: "collections", action }, async () => {
1513
1610
  if (action === "list") {
@@ -1515,39 +1612,34 @@ function registerCollectionsTools(server) {
1515
1612
  }
1516
1613
  if (action === "create") {
1517
1614
  if (!slug || !name || !fields || fields.length === 0) {
1518
- return {
1519
- content: [{
1520
- type: "text",
1521
- text: "`slug`, `name`, and `fields` are required when action is 'create'."
1522
- }]
1523
- };
1615
+ return validationResult("slug, name, and fields are required when action is 'create'.");
1524
1616
  }
1525
1617
  return handleCreate2(slug, name, description, purpose, icon, navGroup ?? "collections", fields);
1526
1618
  }
1527
1619
  if (action === "update") {
1528
1620
  if (!slug) {
1529
- return { content: [{ type: "text", text: "`slug` is required when action is 'update'." }] };
1621
+ return validationResult("slug is required when action is 'update'.");
1530
1622
  }
1531
1623
  if (!name && !description && !purpose && !icon && !navGroup && !fields) {
1532
- return { content: [{ type: "text", text: "Provide at least one field to update (name, description, purpose, icon, navGroup, or fields)." }] };
1624
+ return validationResult("Provide at least one field to update (name, description, purpose, icon, navGroup, or fields).");
1533
1625
  }
1534
1626
  return handleUpdate(slug, name, description, purpose, icon, navGroup, fields);
1535
1627
  }
1536
- return {
1537
- content: [{
1538
- type: "text",
1539
- text: `Unknown action '${action}'. Valid actions: ${COLLECTIONS_ACTIONS.join(", ")}.`
1540
- }]
1541
- };
1628
+ return unknownAction(action, COLLECTIONS_ACTIONS);
1542
1629
  });
1543
- }
1630
+ })
1544
1631
  );
1545
1632
  trackWriteTool(tool);
1546
1633
  }
1547
1634
  async function handleList2() {
1548
1635
  const collections = await mcpQuery("chain.listCollections");
1549
1636
  if (collections.length === 0) {
1550
- return { content: [{ type: "text", text: "No collections found in this workspace." }] };
1637
+ return successResult(
1638
+ "No collections found in this workspace.",
1639
+ "No collections found yet.",
1640
+ { collections: [], total: 0 },
1641
+ [{ tool: "collections", description: "Create collection", parameters: { action: "create" } }]
1642
+ );
1551
1643
  }
1552
1644
  const formatted = collections.map((c) => {
1553
1645
  const fieldList = c.fields.map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
@@ -1565,7 +1657,19 @@ ${fieldList}`;
1565
1657
  return {
1566
1658
  content: [{ type: "text", text: `# Knowledge Collections (${collections.length})
1567
1659
 
1568
- ${formatted}` }]
1660
+ ${formatted}` }],
1661
+ structuredContent: success(
1662
+ `Found ${collections.length} collections.`,
1663
+ {
1664
+ collections: collections.map((c) => ({
1665
+ slug: c.slug,
1666
+ name: c.name,
1667
+ description: c.description,
1668
+ fieldCount: c.fields.length
1669
+ })),
1670
+ total: collections.length
1671
+ }
1672
+ )
1569
1673
  };
1570
1674
  }
1571
1675
  async function handleCreate2(slug, name, description, purpose, icon, navGroup, fields) {
@@ -1596,17 +1700,23 @@ async function handleCreate2(slug, name, description, purpose, icon, navGroup, f
1596
1700
  ${fieldList}
1597
1701
 
1598
1702
  You can now capture entries: \`capture collection="${slug}" name="..." description="..."\``
1599
- }]
1703
+ }],
1704
+ structuredContent: success(
1705
+ `Created collection '${name}' (${slug}) with ${fields.length} fields.`,
1706
+ { slug, name, description, purpose, navGroup, fieldCount: fields.length },
1707
+ [{ tool: "capture", description: "Capture an entry", parameters: { collection: slug } }]
1708
+ )
1600
1709
  };
1601
1710
  } catch (error) {
1602
1711
  const msg = error instanceof Error ? error.message : String(error);
1603
1712
  if (msg.includes("already exists")) {
1604
- return {
1605
- content: [{
1606
- type: "text",
1607
- text: `Collection \`${slug}\` already exists. Use \`collections action=update slug="${slug}"\` to modify it, or choose a different slug.`
1608
- }]
1609
- };
1713
+ return failureResult(
1714
+ `Collection '${slug}' already exists. Use collections action=update to modify it, or choose a different slug.`,
1715
+ "DUPLICATE",
1716
+ `Collection '${slug}' already exists.`,
1717
+ "Use collections action=update to modify it.",
1718
+ [{ tool: "collections", description: "Update collection", parameters: { action: "update", slug } }]
1719
+ );
1610
1720
  }
1611
1721
  throw error;
1612
1722
  }
@@ -1631,7 +1741,11 @@ async function handleUpdate(slug, name, description, purpose, icon, navGroup, fi
1631
1741
  Changed: ${changes || "no changes"}.
1632
1742
 
1633
1743
  Use \`collections action=list\` to verify the result.`
1634
- }]
1744
+ }],
1745
+ structuredContent: success(
1746
+ `Updated collection '${slug}'. Changed: ${changes || "no changes"}.`,
1747
+ { slug, changes }
1748
+ )
1635
1749
  };
1636
1750
  }
1637
1751
 
@@ -1657,11 +1771,16 @@ function registerLabelTools(server) {
1657
1771
  inputSchema: labelsSchema,
1658
1772
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
1659
1773
  },
1660
- async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
1774
+ withEnvelope(async ({ action, slug, name, color, description, parentSlug, isGroup, order, entryId }) => {
1661
1775
  if (action === "list") {
1662
1776
  const labels = await mcpQuery("chain.listLabels");
1663
1777
  if (labels.length === 0) {
1664
- return { content: [{ type: "text", text: "No labels defined in this workspace yet." }] };
1778
+ return successResult(
1779
+ "No labels defined in this workspace yet.",
1780
+ "No labels defined yet.",
1781
+ { labels: [], total: 0 },
1782
+ [{ tool: "labels", description: "Create a label", parameters: { action: "create" } }]
1783
+ );
1665
1784
  }
1666
1785
  const groups = labels.filter((l) => l.isGroup);
1667
1786
  const ungrouped = labels.filter((l) => !l.isGroup && !l.parentId);
@@ -1683,54 +1802,74 @@ function registerLabelTools(server) {
1683
1802
  lines.push(`- \`${label.slug}\` ${label.name}${c}${label.description ? ` \u2014 _${label.description}_` : ""}`);
1684
1803
  }
1685
1804
  }
1686
- return { content: [{ type: "text", text: lines.join("\n") }] };
1805
+ return {
1806
+ content: [{ type: "text", text: lines.join("\n") }],
1807
+ structuredContent: success(
1808
+ `Found ${labels.length} labels in ${groups.length} groups.`,
1809
+ { labels, total: labels.length }
1810
+ )
1811
+ };
1687
1812
  }
1688
1813
  if (!slug) {
1689
- return { content: [{ type: "text", text: "A `slug` is required for this action." }] };
1814
+ return validationResult("A slug is required for this action.");
1690
1815
  }
1691
1816
  if (action === "create") {
1692
1817
  if (!name) {
1693
- return { content: [{ type: "text", text: "Cannot create a label without a name." }] };
1818
+ return validationResult("Cannot create a label without a name.");
1694
1819
  }
1695
1820
  let parentId;
1696
1821
  if (parentSlug) {
1697
1822
  const labels = await mcpQuery("chain.listLabels");
1698
1823
  const parent = labels.find((l) => l.slug === parentSlug);
1699
1824
  if (!parent) {
1700
- return { content: [{ type: "text", text: `Parent label \`${parentSlug}\` not found. Use \`labels action=list\` to see available groups.` }] };
1825
+ return failureResult(
1826
+ `Parent label '${parentSlug}' not found. Use labels action=list to see available groups.`,
1827
+ "NOT_FOUND",
1828
+ `Parent label '${parentSlug}' not found.`,
1829
+ "Use labels action=list to see available groups.",
1830
+ [{ tool: "labels", description: "List labels", parameters: { action: "list" } }]
1831
+ );
1701
1832
  }
1702
1833
  parentId = parent._id;
1703
1834
  }
1704
1835
  await mcpMutation("chain.createLabel", { slug, name, color, description, parentId, isGroup, order });
1705
- return { content: [{ type: "text", text: `# Label Created
1706
-
1707
- **${name}** (\`${slug}\`)` }] };
1836
+ return successResult(
1837
+ `Label '${name}' (${slug}) created.`,
1838
+ `Created label '${name}' (${slug}).`,
1839
+ { slug, name, color, parentSlug },
1840
+ [{ tool: "labels", description: "List labels", parameters: { action: "list" } }]
1841
+ );
1708
1842
  }
1709
1843
  if (action === "update") {
1710
1844
  await mcpMutation("chain.updateLabel", { slug, name, color, description, isGroup, order });
1711
- return { content: [{ type: "text", text: `# Label Updated
1712
-
1713
- \`${slug}\` has been updated.` }] };
1845
+ return successResult(
1846
+ `Label '${slug}' updated.`,
1847
+ `Updated label '${slug}'.`,
1848
+ { slug },
1849
+ [{ tool: "labels", description: "List labels", parameters: { action: "list" } }]
1850
+ );
1714
1851
  }
1715
1852
  if (action === "delete") {
1716
1853
  await mcpMutation("chain.deleteLabel", { slug });
1717
- return { content: [{ type: "text", text: `# Label Deleted
1718
-
1719
- \`${slug}\` removed from all entries and deleted.` }] };
1854
+ return successResult(
1855
+ `Label '${slug}' removed from all entries and deleted.`,
1856
+ `Deleted label '${slug}'.`,
1857
+ { slug }
1858
+ );
1720
1859
  }
1721
1860
  if (action === "apply" || action === "remove") {
1722
1861
  if (!entryId) {
1723
- return { content: [{ type: "text", text: "An `entryId` is required for apply/remove actions." }] };
1862
+ return validationResult("An entryId is required for apply/remove actions.");
1724
1863
  }
1725
1864
  if (action === "apply") {
1726
1865
  await mcpMutation("chain.applyLabel", { entryId, labelSlug: slug });
1727
- return { content: [{ type: "text", text: `Label \`${slug}\` applied to **${entryId}**.` }] };
1866
+ return successResult(`Label '${slug}' applied to ${entryId}.`, `Applied label '${slug}' to ${entryId}.`, { slug, entryId });
1728
1867
  }
1729
1868
  await mcpMutation("chain.removeLabel", { entryId, labelSlug: slug });
1730
- return { content: [{ type: "text", text: `Label \`${slug}\` removed from **${entryId}**.` }] };
1869
+ return successResult(`Label '${slug}' removed from ${entryId}.`, `Removed label '${slug}' from ${entryId}.`, { slug, entryId });
1731
1870
  }
1732
- return { content: [{ type: "text", text: "Unknown action." }] };
1733
- }
1871
+ return validationResult("Unknown action.");
1872
+ })
1734
1873
  );
1735
1874
  trackWriteTool(tool);
1736
1875
  }
@@ -1792,17 +1931,17 @@ async function suggestLinksForEntries(entries) {
1792
1931
  async function runWrapupReview() {
1793
1932
  const sessionId = getAgentSessionId();
1794
1933
  if (!sessionId) {
1795
- return { text: "No active agent session. Call `session action=start` first.", data: null, suggestions: [] };
1934
+ return { text: "No active agent session. Call `session action=start` first.", data: null, suggestions: [], failureCode: "SESSION_REQUIRED" };
1796
1935
  }
1797
1936
  if (getApiKeyScope() === "read") {
1798
- return { text: "Read-only session \u2014 nothing to review.", data: null, suggestions: [] };
1937
+ return { text: "Read-only session \u2014 nothing to review.", data: null, suggestions: [], failureCode: "READONLY_SCOPE" };
1799
1938
  }
1800
1939
  const data = await mcpCall("agent.getSessionWrapup", { sessionId });
1801
1940
  if (data.wrapupCompletedAt) {
1802
- return { text: "Wrapup already completed this session.", data, suggestions: [] };
1941
+ return { text: "Wrapup already completed this session.", data, suggestions: [], failureCode: "ALREADY_COMPLETED" };
1803
1942
  }
1804
1943
  if (data.drafts.length === 0 && data.committed.length === 0) {
1805
- return { text: "Empty session \u2014 no entries to review.", data, suggestions: [] };
1944
+ return { text: "Empty session \u2014 no entries to review.", data, suggestions: [], failureCode: "NOT_FOUND" };
1806
1945
  }
1807
1946
  const lines = [];
1808
1947
  lines.push(
@@ -1922,50 +2061,62 @@ function registerWrapupTools(server) {
1922
2061
  let lastReviewData = null;
1923
2062
  let lastReviewSuggestions = [];
1924
2063
  const wrapupHandler = async ({ action }) => {
1925
- try {
1926
- if (action === "commit-all") {
1927
- const sessionId = getAgentSessionId();
1928
- if (!sessionId) {
1929
- return { content: [{ type: "text", text: "No active agent session." }] };
1930
- }
1931
- const data2 = lastReviewData ?? await mcpCall("agent.getSessionWrapup", { sessionId });
1932
- if (data2.drafts.length === 0) {
1933
- return { content: [{ type: "text", text: "No uncommitted drafts to commit." }] };
1934
- }
1935
- const result = await runWrapupCommitAll(data2, lastReviewSuggestions);
1936
- lastReviewData = null;
1937
- lastReviewSuggestions = [];
1938
- return { content: [{ type: "text", text: result }] };
2064
+ if (action === "commit-all") {
2065
+ const sessionId = getAgentSessionId();
2066
+ if (!sessionId) {
2067
+ return failureResult(
2068
+ "No active agent session.",
2069
+ "SESSION_REQUIRED",
2070
+ "No active agent session.",
2071
+ "Start a session first.",
2072
+ [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
2073
+ );
2074
+ }
2075
+ const data2 = lastReviewData ?? await mcpCall("agent.getSessionWrapup", { sessionId });
2076
+ if (data2.drafts.length === 0) {
2077
+ return failureResult("No uncommitted drafts to commit.", "NOT_FOUND", "No uncommitted drafts to commit.", "Nothing to commit.");
1939
2078
  }
1940
- const { text, data, suggestions } = await runWrapupReview();
1941
- lastReviewData = data;
1942
- lastReviewSuggestions = suggestions;
1943
- const fullText = data && (data.drafts.length > 0 || data.committed.length > 0) ? `## Session Wrapup
2079
+ const result = await runWrapupCommitAll(data2, lastReviewSuggestions);
2080
+ lastReviewData = null;
2081
+ lastReviewSuggestions = [];
2082
+ return successResult(result, "Wrapup complete. Committed drafts and created links.", { committed: true });
2083
+ }
2084
+ const { text, data, suggestions, failureCode } = await runWrapupReview();
2085
+ lastReviewData = data;
2086
+ lastReviewSuggestions = suggestions;
2087
+ const fullText = data && (data.drafts.length > 0 || data.committed.length > 0) ? `## Session Wrapup
1944
2088
 
1945
2089
  ${text}` : text;
1946
- return { content: [{ type: "text", text: fullText }] };
1947
- } catch (err) {
1948
- const msg = err instanceof Error ? err.message : String(err);
1949
- return { content: [{ type: "text", text: `Wrapup failed: ${msg}` }], isError: true };
1950
- }
2090
+ if (failureCode) {
2091
+ return failureResult(fullText, failureCode, text);
2092
+ }
2093
+ const next = data.drafts.length > 0 ? [{ tool: "session-wrapup", description: "Commit all drafts", parameters: { action: "commit-all" } }] : void 0;
2094
+ return successResult(
2095
+ fullText,
2096
+ `Session review: ${data.drafts.length} uncommitted, ${data.committed.length} committed, ${suggestions.length} link suggestions.`,
2097
+ {
2098
+ drafts: data.drafts.length,
2099
+ committed: data.committed.length,
2100
+ uncommitted: data.summary.uncommitted,
2101
+ suggestedLinks: suggestions.length
2102
+ },
2103
+ next
2104
+ );
1951
2105
  };
1952
2106
  const tool = server.registerTool("session-wrapup", {
1953
2107
  title: "Session Wrapup",
1954
2108
  description: WRAPUP_TOOL_DESCRIPTION + " Also known as: wrapup, finish.",
1955
2109
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
1956
2110
  inputSchema: wrapupSchema
1957
- }, async (args) => {
1958
- const parsed = wrapupSchema.safeParse(args);
1959
- if (!parsed.success) {
1960
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1961
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
1962
- }
2111
+ }, withEnvelope(async (args) => {
2112
+ const parsed = parseOrFail(wrapupSchema, args);
2113
+ if (!parsed.ok) return parsed.result;
1963
2114
  const act = parsed.data.action ?? "review";
1964
2115
  return runWithToolContext(
1965
2116
  { tool: "session-wrapup", action: act },
1966
2117
  () => wrapupHandler({ action: act })
1967
2118
  );
1968
- });
2119
+ }));
1969
2120
  trackWriteTool(tool);
1970
2121
  }
1971
2122
 
@@ -1985,12 +2136,9 @@ function registerSessionTools(server) {
1985
2136
  inputSchema: sessionSchema,
1986
2137
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
1987
2138
  },
1988
- async (args) => {
1989
- const parsed = sessionSchema.safeParse(args);
1990
- if (!parsed.success) {
1991
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1992
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
1993
- }
2139
+ withEnvelope(async (args) => {
2140
+ const parsed = parseOrFail(sessionSchema, args);
2141
+ if (!parsed.ok) return parsed.result;
1994
2142
  const { action } = parsed.data;
1995
2143
  return runWithToolContext({ tool: "session", action }, async () => {
1996
2144
  if (action === "start") {
@@ -2002,141 +2150,159 @@ function registerSessionTools(server) {
2002
2150
  if (action === "status") {
2003
2151
  return handleStatus();
2004
2152
  }
2005
- return {
2006
- content: [{
2007
- type: "text",
2008
- text: `Unknown action '${action}'. Valid actions: ${SESSION_ACTIONS.join(", ")}.`
2009
- }]
2010
- };
2153
+ return unknownAction(action, SESSION_ACTIONS);
2011
2154
  });
2012
- }
2155
+ })
2013
2156
  );
2014
2157
  }
2015
2158
  async function handleStart() {
2016
- try {
2017
- const result = await startAgentSession();
2018
- const lines = [];
2019
- if (result.superseded) {
2020
- lines.push(
2021
- `Previous session superseded. Session ${result.superseded.previousSessionId} (started ${result.superseded.startedAt}, initiated by ${result.superseded.initiatedBy}) was closed.`,
2022
- ""
2023
- );
2024
- }
2159
+ const result = await startAgentSession();
2160
+ const lines = [];
2161
+ if (result.superseded) {
2025
2162
  lines.push(
2026
- `Session ${result.sessionId} active. Initiated by ${result.initiatedBy}. Workspace ${result.workspaceName}. Scope: ${result.toolsScope}. Write tools available after orient.`
2163
+ `Previous session superseded. Session ${result.superseded.previousSessionId} (started ${result.superseded.startedAt}, initiated by ${result.superseded.initiatedBy}) was closed.`,
2164
+ ""
2027
2165
  );
2028
- return { content: [{ type: "text", text: lines.join("\n") }] };
2029
- } catch (err) {
2030
- const msg = err instanceof Error ? err.message : String(err);
2031
- return {
2032
- content: [{ type: "text", text: `Failed to start agent session: ${msg}` }],
2033
- isError: true
2034
- };
2035
2166
  }
2167
+ lines.push(
2168
+ `Session ${result.sessionId} active. Initiated by ${result.initiatedBy}. Workspace ${result.workspaceName}. Scope: ${result.toolsScope}. Write tools available after orient.`
2169
+ );
2170
+ const summary = `Session ${result.sessionId} active. Workspace ${result.workspaceName}. Write tools available after orient.`;
2171
+ return {
2172
+ content: [{ type: "text", text: lines.join("\n") }],
2173
+ structuredContent: success(summary, {
2174
+ sessionId: result.sessionId,
2175
+ initiatedBy: result.initiatedBy,
2176
+ workspaceName: result.workspaceName,
2177
+ toolsScope: result.toolsScope,
2178
+ superseded: !!result.superseded
2179
+ }, [{ tool: "orient", description: "Orient session", parameters: {} }])
2180
+ };
2036
2181
  }
2037
2182
  async function handleClose() {
2038
- try {
2039
- const sessionId = getAgentSessionId();
2040
- if (!sessionId) {
2041
- return {
2042
- content: [{ type: "text", text: "No active agent session to close." }]
2043
- };
2044
- }
2045
- const session = await mcpCall("agent.getSession", {
2046
- sessionId
2047
- });
2048
- let wrapupNudge = "";
2049
- if (session && !session.wrapupCompletedAt) {
2050
- try {
2051
- const { text, data } = await runWrapupReview();
2052
- if (data && data.drafts.length > 0) {
2053
- wrapupNudge = [
2054
- "## Session Wrapup",
2055
- "",
2056
- text,
2057
- "",
2058
- `> **${data.drafts.length} uncommitted draft(s) remain.** Run \`session-wrapup\` with action \`commit-all\` before closing next time.`,
2059
- "",
2060
- "---",
2061
- ""
2062
- ].join("\n");
2063
- }
2064
- } catch {
2183
+ const sessionId = getAgentSessionId();
2184
+ if (!sessionId) {
2185
+ return failureResult(
2186
+ "No active agent session to close.",
2187
+ "SESSION_REQUIRED",
2188
+ "No active agent session to close.",
2189
+ "Start a session first.",
2190
+ [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
2191
+ );
2192
+ }
2193
+ const session = await mcpCall("agent.getSession", {
2194
+ sessionId
2195
+ });
2196
+ let wrapupNudge = "";
2197
+ if (session && !session.wrapupCompletedAt) {
2198
+ try {
2199
+ const { text, data } = await runWrapupReview();
2200
+ if (data && data.drafts.length > 0) {
2201
+ wrapupNudge = [
2202
+ "## Session Wrapup",
2203
+ "",
2204
+ text,
2205
+ "",
2206
+ `> **${data.drafts.length} uncommitted draft(s) remain.** Run \`session-wrapup\` with action \`commit-all\` before closing next time.`,
2207
+ "",
2208
+ "---",
2209
+ ""
2210
+ ].join("\n");
2065
2211
  }
2212
+ } catch {
2066
2213
  }
2067
- await closeAgentSession();
2068
- const lines = [
2069
- `Session ${sessionId} closed.`,
2070
- ""
2071
- ];
2072
- if (session) {
2073
- const created = session.entriesCreated?.length ?? 0;
2074
- const modified = session.entriesModified?.length ?? 0;
2075
- const relations = session.relationsCreated ?? 0;
2076
- const gates = session.gateFailures ?? 0;
2077
- const warnings = session.contradictionWarnings ?? 0;
2078
- lines.push(
2079
- `| Metric | Count |`,
2080
- `|--------|-------|`,
2081
- `| Entries created | ${created} |`,
2082
- `| Entries modified | ${modified} |`,
2083
- `| Relations created | ${relations} |`,
2084
- `| Gate failures | ${gates} |`,
2085
- `| Contradiction warnings | ${warnings} |`
2086
- );
2087
- }
2088
- lines.push("", "Write tools are now blocked. Session data saved for future orientation.");
2089
- const fullResponse = wrapupNudge + lines.join("\n");
2090
- return { content: [{ type: "text", text: fullResponse }] };
2091
- } catch (err) {
2092
- const msg = err instanceof Error ? err.message : String(err);
2093
- return {
2094
- content: [{ type: "text", text: `Failed to close agent session: ${msg}` }],
2095
- isError: true
2096
- };
2097
2214
  }
2098
- }
2099
- async function handleStatus() {
2100
- try {
2101
- const sessionId = getAgentSessionId();
2102
- if (!sessionId) {
2103
- return {
2104
- content: [{ type: "text", text: "No active agent session. Call `session action=start` to begin." }]
2105
- };
2106
- }
2107
- const session = await mcpCall("agent.getSession", {
2108
- sessionId
2109
- });
2110
- if (!session) {
2111
- return {
2112
- content: [{ type: "text", text: "Session ID cached but not found in Convex. Call `session action=start` to create a new session." }]
2113
- };
2114
- }
2115
- const oriented = isSessionOriented();
2116
- const created = session.entriesCreated?.length ?? 0;
2117
- const modified = session.entriesModified?.length ?? 0;
2118
- const lines = [
2119
- `Session ${sessionId} ${session.status}.`,
2120
- `Initiated by ${session.initiatedBy}.`,
2121
- `Scope: ${session.toolsScope}.`,
2122
- `Oriented: ${oriented ? "yes" : "no"}.`,
2123
- "",
2124
- `| Metric | Value |`,
2215
+ await closeAgentSession();
2216
+ const lines = [
2217
+ `Session ${sessionId} closed.`,
2218
+ ""
2219
+ ];
2220
+ const created = session?.entriesCreated?.length ?? 0;
2221
+ const modified = session?.entriesModified?.length ?? 0;
2222
+ const relations = session?.relationsCreated ?? 0;
2223
+ const gates = session?.gateFailures ?? 0;
2224
+ const warnings = session?.contradictionWarnings ?? 0;
2225
+ if (session) {
2226
+ lines.push(
2227
+ `| Metric | Count |`,
2125
2228
  `|--------|-------|`,
2126
2229
  `| Entries created | ${created} |`,
2127
2230
  `| Entries modified | ${modified} |`,
2128
- `| Relations created | ${session.relationsCreated ?? 0} |`,
2129
- `| Started | ${new Date(session.startedAt).toISOString()} |`,
2130
- `| Expires | ${new Date(session.expiresAt).toISOString()} |`
2131
- ];
2132
- return { content: [{ type: "text", text: lines.join("\n") }] };
2133
- } catch (err) {
2134
- const msg = err instanceof Error ? err.message : String(err);
2135
- return {
2136
- content: [{ type: "text", text: `Failed to get session status: ${msg}` }],
2137
- isError: true
2138
- };
2231
+ `| Relations created | ${relations} |`,
2232
+ `| Gate failures | ${gates} |`,
2233
+ `| Contradiction warnings | ${warnings} |`
2234
+ );
2235
+ }
2236
+ lines.push("", "Write tools are now blocked. Session data saved for future orientation.");
2237
+ const fullResponse = wrapupNudge + lines.join("\n");
2238
+ const summary = `Session ${sessionId} closed. ${created} created, ${modified} modified.`;
2239
+ return {
2240
+ content: [{ type: "text", text: fullResponse }],
2241
+ structuredContent: success(summary, {
2242
+ sessionId,
2243
+ entriesCreated: created,
2244
+ entriesModified: modified,
2245
+ relationsCreated: relations,
2246
+ gateFailures: gates,
2247
+ contradictionWarnings: warnings,
2248
+ wrapupNudge: wrapupNudge.length > 0
2249
+ })
2250
+ };
2251
+ }
2252
+ async function handleStatus() {
2253
+ const sessionId = getAgentSessionId();
2254
+ if (!sessionId) {
2255
+ return failureResult(
2256
+ "No active agent session. Call session action=start to begin.",
2257
+ "SESSION_REQUIRED",
2258
+ "No active agent session.",
2259
+ "Call session action=start to begin.",
2260
+ [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
2261
+ );
2139
2262
  }
2263
+ const session = await mcpCall("agent.getSession", {
2264
+ sessionId
2265
+ });
2266
+ if (!session) {
2267
+ return failureResult(
2268
+ "Session not found in Convex. Call session action=start to create a new session.",
2269
+ "NOT_FOUND",
2270
+ "Session not found in Convex.",
2271
+ "Call session action=start to create a new session.",
2272
+ [{ tool: "session", description: "Start a session", parameters: { action: "start" } }]
2273
+ );
2274
+ }
2275
+ const oriented = isSessionOriented();
2276
+ const created = session.entriesCreated?.length ?? 0;
2277
+ const modified = session.entriesModified?.length ?? 0;
2278
+ const lines = [
2279
+ `Session ${sessionId} ${session.status}.`,
2280
+ `Initiated by ${session.initiatedBy}.`,
2281
+ `Scope: ${session.toolsScope}.`,
2282
+ `Oriented: ${oriented ? "yes" : "no"}.`,
2283
+ "",
2284
+ `| Metric | Value |`,
2285
+ `|--------|-------|`,
2286
+ `| Entries created | ${created} |`,
2287
+ `| Entries modified | ${modified} |`,
2288
+ `| Relations created | ${session.relationsCreated ?? 0} |`,
2289
+ `| Started | ${new Date(session.startedAt).toISOString()} |`,
2290
+ `| Expires | ${new Date(session.expiresAt).toISOString()} |`
2291
+ ];
2292
+ const summary = `Session ${sessionId} ${session.status}. ${created} created, ${modified} modified.`;
2293
+ return {
2294
+ content: [{ type: "text", text: lines.join("\n") }],
2295
+ structuredContent: success(summary, {
2296
+ sessionId,
2297
+ status: session.status,
2298
+ initiatedBy: session.initiatedBy,
2299
+ toolsScope: session.toolsScope,
2300
+ oriented,
2301
+ entriesCreated: created,
2302
+ entriesModified: modified,
2303
+ relationsCreated: session.relationsCreated ?? 0
2304
+ })
2305
+ };
2140
2306
  }
2141
2307
 
2142
2308
  // src/tools/quality.ts
@@ -2176,12 +2342,9 @@ function registerQualityTools(server) {
2176
2342
  inputSchema: qualitySchema,
2177
2343
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2178
2344
  },
2179
- async (args) => {
2180
- const parsed = qualitySchema.safeParse(args);
2181
- if (!parsed.success) {
2182
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2183
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
2184
- }
2345
+ withEnvelope(async (args) => {
2346
+ const parsed = parseOrFail(qualitySchema, args);
2347
+ if (!parsed.ok) return parsed.result;
2185
2348
  const { action, entryId, context } = parsed.data;
2186
2349
  return runWithToolContext({ tool: "quality", action }, async () => {
2187
2350
  if (action === "check") {
@@ -2190,14 +2353,9 @@ function registerQualityTools(server) {
2190
2353
  if (action === "re-evaluate") {
2191
2354
  return handleReEvaluate(entryId, context ?? "review");
2192
2355
  }
2193
- return {
2194
- content: [{
2195
- type: "text",
2196
- text: `Unknown action '${action}'. Valid actions: ${QUALITY_ACTIONS.join(", ")}.`
2197
- }]
2198
- };
2356
+ return unknownAction(action, QUALITY_ACTIONS);
2199
2357
  });
2200
- }
2358
+ })
2201
2359
  );
2202
2360
  trackWriteTool(qualityTool);
2203
2361
  }
@@ -2247,17 +2405,23 @@ ${linkHints}`;
2247
2405
  }
2248
2406
  return {
2249
2407
  content: [{ type: "text", text: result.text }],
2250
- structuredContent: {
2251
- entryId,
2252
- score: result.quality.score,
2253
- maxScore: result.quality.maxScore,
2254
- criteria: result.quality.checks.map((c) => ({
2255
- name: c.label,
2256
- score: c.passed ? 1 : 0,
2257
- maxScore: 1,
2258
- met: c.passed
2259
- }))
2260
- }
2408
+ structuredContent: success(
2409
+ `Quality check for ${entryId}: ${result.quality.score}/${result.quality.maxScore}.`,
2410
+ {
2411
+ entryId,
2412
+ score: result.quality.score,
2413
+ maxScore: result.quality.maxScore,
2414
+ criteria: result.quality.checks.map((c) => ({
2415
+ name: c.label,
2416
+ score: c.passed ? 1 : 0,
2417
+ maxScore: 1,
2418
+ met: c.passed
2419
+ }))
2420
+ },
2421
+ [
2422
+ { tool: "graph", description: "Suggest connections", parameters: { action: "suggest", entryId } }
2423
+ ]
2424
+ )
2261
2425
  };
2262
2426
  }
2263
2427
  async function handleReEvaluate(entryId, context) {
@@ -2268,10 +2432,13 @@ async function handleReEvaluate(entryId, context) {
2268
2432
  context
2269
2433
  });
2270
2434
  if (!result) {
2271
- return {
2272
- content: [{ type: "text", text: `Entry \`${entryId}\` not found or has no rubric.` }],
2273
- structuredContent: { entryId, context, score: 0, maxScore: 0, improved: false }
2274
- };
2435
+ return failureResult(
2436
+ `Entry '${entryId}' not found or has no rubric.`,
2437
+ "NOT_FOUND",
2438
+ `Entry '${entryId}' not found or has no rubric.`,
2439
+ "Use entries action=search to find the correct ID.",
2440
+ [{ tool: "entries", description: "Search entries", parameters: { action: "search", query: entryId } }]
2441
+ );
2275
2442
  }
2276
2443
  const llmScheduled = result.verdict.tier !== "passive";
2277
2444
  const lines = [`Re-evaluated \`${entryId}\` in \`${context}\` context.`];
@@ -2300,13 +2467,16 @@ async function handleReEvaluate(entryId, context) {
2300
2467
  const reEvalMaxScore = criteria.length;
2301
2468
  return {
2302
2469
  content: [{ type: "text", text: lines.join("\n") }],
2303
- structuredContent: {
2304
- entryId,
2305
- context,
2306
- score: reEvalScore,
2307
- maxScore: reEvalMaxScore,
2308
- improved: result.verdict.passed ?? false
2309
- }
2470
+ structuredContent: success(
2471
+ `Re-evaluated ${entryId} in ${context} context. Score: ${reEvalScore}/${reEvalMaxScore}.`,
2472
+ {
2473
+ entryId,
2474
+ context,
2475
+ score: reEvalScore,
2476
+ maxScore: reEvalMaxScore,
2477
+ improved: result.verdict.passed ?? false
2478
+ }
2479
+ )
2310
2480
  };
2311
2481
  }
2312
2482
 
@@ -2714,12 +2884,9 @@ function registerWorkflowTools(server) {
2714
2884
  inputSchema: workflowsSchema,
2715
2885
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2716
2886
  },
2717
- async (args) => {
2718
- const parsed = workflowsSchema.safeParse(args ?? {});
2719
- if (!parsed.success) {
2720
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2721
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
2722
- }
2887
+ withEnvelope(async (args) => {
2888
+ const parsed = parseOrFail(workflowsSchema, args);
2889
+ if (!parsed.ok) return parsed.result;
2723
2890
  const { action, workflowId, roundId, output, isFinal, summaryName, summaryDescription } = parsed.data;
2724
2891
  return runWithToolContext({ tool: "workflows", action }, async () => {
2725
2892
  if (action === "list") {
@@ -2727,35 +2894,26 @@ function registerWorkflowTools(server) {
2727
2894
  }
2728
2895
  if (action === "checkpoint") {
2729
2896
  if (!workflowId || !roundId || output === void 0) {
2730
- return {
2731
- content: [{
2732
- type: "text",
2733
- text: "`workflowId`, `roundId`, and `output` are required when action is 'checkpoint'."
2734
- }]
2735
- };
2897
+ return validationResult(
2898
+ "workflowId, roundId, and output are required when action is 'checkpoint'."
2899
+ );
2736
2900
  }
2737
2901
  return handleCheckpoint(workflowId, roundId, output, isFinal, summaryName, summaryDescription);
2738
2902
  }
2739
- return {
2740
- content: [{
2741
- type: "text",
2742
- text: `Unknown action '${action}'. Valid actions: ${WORKFLOWS_ACTIONS.join(", ")}.`
2743
- }]
2744
- };
2903
+ return unknownAction(action, WORKFLOWS_ACTIONS);
2745
2904
  });
2746
- }
2905
+ })
2747
2906
  );
2748
2907
  trackWriteTool(workflowsTool);
2749
2908
  }
2750
2909
  async function handleList3() {
2751
2910
  const workflows = listWorkflows();
2752
2911
  if (workflows.length === 0) {
2753
- return {
2754
- content: [{
2755
- type: "text",
2756
- text: "No workflows registered yet. Check back after the workflow definitions are configured."
2757
- }]
2758
- };
2912
+ return successResult(
2913
+ "No workflows registered yet. Check back after the workflow definitions are configured.",
2914
+ "No workflows registered yet.",
2915
+ { workflows: [], total: 0 }
2916
+ );
2759
2917
  }
2760
2918
  const cards = workflows.map(formatWorkflowCard).join("\n\n---\n\n");
2761
2919
  return {
@@ -2768,7 +2926,19 @@ ${workflows.length} workflow(s) available. Use the \`run-workflow\` prompt to la
2768
2926
  ---
2769
2927
 
2770
2928
  ${cards}`
2771
- }]
2929
+ }],
2930
+ structuredContent: success(
2931
+ `Found ${workflows.length} workflow(s).`,
2932
+ {
2933
+ workflows: workflows.map((w) => ({
2934
+ id: w.id,
2935
+ name: w.name,
2936
+ rounds: w.rounds.length,
2937
+ outputCollection: w.kbOutputCollection
2938
+ })),
2939
+ total: workflows.length
2940
+ }
2941
+ )
2772
2942
  };
2773
2943
  }
2774
2944
  async function handleCheckpoint(workflowId, roundId, output, isFinal, summaryName, summaryDescription) {
@@ -2780,18 +2950,31 @@ async function handleCheckpoint(workflowId, roundId, output, isFinal, summaryNam
2780
2950
  text: `Workflow "${workflowId}" not found. Available: ${listWorkflows().map((w) => w.id).join(", ")}.
2781
2951
 
2782
2952
  This checkpoint was NOT saved. Continue the conversation \u2014 the facilitator has the context.`
2783
- }]
2953
+ }],
2954
+ structuredContent: failure(
2955
+ "NOT_FOUND",
2956
+ `Workflow '${workflowId}' not found.`,
2957
+ "Use workflows action=list to see available workflows.",
2958
+ [{ tool: "workflows", description: "List workflows", parameters: { action: "list" } }]
2959
+ )
2784
2960
  };
2785
2961
  }
2786
2962
  const round = wf.rounds.find((r) => r.id === roundId);
2787
2963
  if (!round) {
2964
+ const availableRounds = wf.rounds.map((r) => r.id).join(", ");
2788
2965
  return {
2789
2966
  content: [{
2790
2967
  type: "text",
2791
- text: `Round "${roundId}" not found in workflow "${workflowId}". Available rounds: ${wf.rounds.map((r) => r.id).join(", ")}.
2968
+ text: `Round "${roundId}" not found in workflow "${workflowId}". Available rounds: ${availableRounds}.
2792
2969
 
2793
2970
  This checkpoint was NOT saved. The conversation context is preserved \u2014 continue facilitating.`
2794
- }]
2971
+ }],
2972
+ structuredContent: failure(
2973
+ "NOT_FOUND",
2974
+ `Round '${roundId}' not found in workflow '${workflowId}'.`,
2975
+ `Available rounds: ${availableRounds}`,
2976
+ [{ tool: "workflows", description: "List workflows", parameters: { action: "list" } }]
2977
+ )
2795
2978
  };
2796
2979
  }
2797
2980
  const lines = [
@@ -2822,6 +3005,31 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
2822
3005
  `The summary is captured as a draft. Use \`commit-entry entryId="${result.entryId}"\` to promote it to the Chain.`,
2823
3006
  `Use \`graph action=suggest entryId="${result.entryId}"\` to discover connections.`
2824
3007
  );
3008
+ return {
3009
+ content: [{ type: "text", text: lines.join("\n") }],
3010
+ structuredContent: success(
3011
+ `Draft '${summaryName}' created (${result.entryId}) for workflow ${workflowId}.`,
3012
+ {
3013
+ entryId: result.entryId,
3014
+ workflowId,
3015
+ roundId,
3016
+ collection: wf.kbOutputCollection,
3017
+ isFinal: true
3018
+ },
3019
+ [
3020
+ {
3021
+ tool: "commit-entry",
3022
+ description: "Promote draft to Chain",
3023
+ parameters: { entryId: result.entryId }
3024
+ },
3025
+ {
3026
+ tool: "graph",
3027
+ description: "Discover connections",
3028
+ parameters: { action: "suggest", entryId: result.entryId }
3029
+ }
3030
+ ]
3031
+ )
3032
+ };
2825
3033
  } catch (err) {
2826
3034
  const msg = err instanceof Error ? err.message : String(err);
2827
3035
  lines.push(
@@ -2833,6 +3041,15 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
2833
3041
  `- Name: ${summaryName}`,
2834
3042
  `- Description: (copy from the conversation summary above)`
2835
3043
  );
3044
+ return {
3045
+ content: [{ type: "text", text: lines.join("\n") }],
3046
+ structuredContent: failure(
3047
+ "BACKEND_ERROR",
3048
+ `Chain commit failed: ${msg}.`,
3049
+ "Capture manually with the capture tool.",
3050
+ [{ tool: "capture", description: "Capture manually", parameters: {} }]
3051
+ )
3052
+ };
2836
3053
  }
2837
3054
  } else {
2838
3055
  lines.push(
@@ -2840,8 +3057,10 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
2840
3057
  `Output: ${output.substring(0, 200)}${output.length > 200 ? "..." : ""}`
2841
3058
  );
2842
3059
  const currentIdx = wf.rounds.findIndex((r) => r.id === roundId);
3060
+ let nextRound;
2843
3061
  if (currentIdx < wf.rounds.length - 1) {
2844
3062
  const next = wf.rounds[currentIdx + 1];
3063
+ nextRound = { id: next.id, num: next.num, label: next.label };
2845
3064
  lines.push(
2846
3065
  "",
2847
3066
  `**Next**: Round ${next.num} \u2014 ${next.label}`,
@@ -2853,8 +3072,21 @@ This checkpoint was NOT saved. The conversation context is preserved \u2014 cont
2853
3072
  `**All rounds complete.** Call this tool again with \`isFinal: true\` to commit to the Chain.`
2854
3073
  );
2855
3074
  }
3075
+ return {
3076
+ content: [{ type: "text", text: lines.join("\n") }],
3077
+ structuredContent: success(
3078
+ `Round ${round.num} (${round.label}) output recorded for workflow ${workflowId}.`,
3079
+ {
3080
+ workflowId,
3081
+ roundId,
3082
+ roundNum: round.num,
3083
+ roundLabel: round.label,
3084
+ isFinal: false,
3085
+ nextRound
3086
+ }
3087
+ )
3088
+ };
2856
3089
  }
2857
- return { content: [{ type: "text", text: lines.join("\n") }] };
2858
3090
  }
2859
3091
 
2860
3092
  // src/tools/facilitate.ts
@@ -3810,13 +4042,11 @@ function registerFacilitateTools(server) {
3810
4042
  openWorldHint: false
3811
4043
  }
3812
4044
  },
3813
- async (args) => {
4045
+ withEnvelope(async (args) => {
3814
4046
  const parsed = facilitateSchema.safeParse(args);
3815
4047
  if (!parsed.success) {
3816
4048
  const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3817
- return {
3818
- content: [{ type: "text", text: `Invalid arguments: ${issues}` }]
3819
- };
4049
+ return validationResult(`Invalid arguments: ${issues}`);
3820
4050
  }
3821
4051
  const { action } = parsed.data;
3822
4052
  return runWithToolContext({ tool: "facilitate", action }, async () => {
@@ -3832,15 +4062,10 @@ function registerFacilitateTools(server) {
3832
4062
  case "commit-constellation":
3833
4063
  return handleCommitConstellation(parsed.data);
3834
4064
  default:
3835
- return {
3836
- content: [{
3837
- type: "text",
3838
- text: `Unknown action '${action}'. Valid: ${FACILITATE_ACTIONS.join(", ")}.`
3839
- }]
3840
- };
4065
+ return validationResult(`Unknown action '${action}'. Valid: ${FACILITATE_ACTIONS.join(", ")}.`);
3841
4066
  }
3842
4067
  });
3843
- }
4068
+ })
3844
4069
  );
3845
4070
  }
3846
4071
  function buildStudioUrl(workspaceSlug, entryId) {
@@ -4178,7 +4403,11 @@ async function handleStart2(args) {
4178
4403
  ].filter(Boolean).join("\n");
4179
4404
  return {
4180
4405
  content: [{ type: "text", text: output }],
4181
- structuredContent: response
4406
+ structuredContent: success(
4407
+ `Shaping session started for "${betName}". Phase: context.`,
4408
+ response,
4409
+ [{ tool: "facilitate", description: "Describe the problem", parameters: { action: "respond", betName, dimension: "problem_clarity" } }]
4410
+ )
4182
4411
  };
4183
4412
  }
4184
4413
  async function processCaptures(opts) {
@@ -4566,9 +4795,14 @@ function assembleResponse(opts) {
4566
4795
  sessionDrafts.length > 0 ? `**Session drafts (${sessionDrafts.length}):** ${sessionDrafts.map((d) => `\`${d.entryId}\` ${d.name}`).join(", ")}` : "",
4567
4796
  captureErrors.length > 0 ? `**Warnings:** ${captureErrors.map((e) => e.detail).join("; ")}` : ""
4568
4797
  ];
4798
+ const summaryText = `${PHASE_LABELS[phase]} \u2014 ${DIMENSION_LABELS[activeDimension]}: ${scorecard[activeDimension]}/10.${coaching.captureReady ? " Capture ready." : ""}`;
4569
4799
  return {
4570
4800
  content: [{ type: "text", text: summaryParts.filter(Boolean).join("\n") }],
4571
- structuredContent: response
4801
+ structuredContent: success(
4802
+ summaryText,
4803
+ response,
4804
+ [{ tool: "facilitate", description: "Continue shaping", parameters: { action: "respond", betEntryId, dimension: nextDimension } }]
4805
+ )
4572
4806
  };
4573
4807
  }
4574
4808
  async function handleRespond(args) {
@@ -4577,9 +4811,7 @@ async function handleRespond(args) {
4577
4811
  const source = argSource ?? "user";
4578
4812
  const captureItems = rawCapture ? Array.isArray(rawCapture) ? rawCapture : [rawCapture] : [];
4579
4813
  if (!userInput) {
4580
- return {
4581
- content: [{ type: "text", text: "`userInput` is required for respond action." }]
4582
- };
4814
+ return validationResult("`userInput` is required for respond action.");
4583
4815
  }
4584
4816
  let betId = argBetId;
4585
4817
  if (!betId) {
@@ -4614,7 +4846,7 @@ async function handleRespond(args) {
4614
4846
  const msg = err instanceof Error ? err.message : String(err);
4615
4847
  return {
4616
4848
  content: [{ type: "text", text: `Failed to create bet entry: ${msg}` }],
4617
- isError: true
4849
+ structuredContent: failure("BACKEND_ERROR", `Failed to create bet entry: ${msg}`, "Check workspace access and try again.")
4618
4850
  };
4619
4851
  }
4620
4852
  }
@@ -4625,7 +4857,13 @@ async function handleRespond(args) {
4625
4857
  ]);
4626
4858
  if (!betEntry) {
4627
4859
  return {
4628
- content: [{ type: "text", text: `Bet \`${betId}\` not found. Use start to create a session.` }]
4860
+ content: [{ type: "text", text: `Bet \`${betId}\` not found. Use start to create a session.` }],
4861
+ structuredContent: failure(
4862
+ "NOT_FOUND",
4863
+ `Bet '${betId}' not found.`,
4864
+ "Use facilitate action=start to begin a new session.",
4865
+ [{ tool: "facilitate", description: "Start session", parameters: { action: "start" } }]
4866
+ )
4629
4867
  };
4630
4868
  }
4631
4869
  const betData = betEntry.data ?? {};
@@ -4687,14 +4925,18 @@ async function handleRespond(args) {
4687
4925
  async function handleScore(args) {
4688
4926
  const betId = args.betEntryId;
4689
4927
  if (!betId) {
4690
- return {
4691
- content: [{ type: "text", text: "`betEntryId` is required for score action." }]
4692
- };
4928
+ return validationResult("`betEntryId` is required for score action.");
4693
4929
  }
4694
4930
  const betEntry = await loadBetEntry(betId);
4695
4931
  if (!betEntry) {
4696
4932
  return {
4697
- content: [{ type: "text", text: `Bet \`${betId}\` not found.` }]
4933
+ content: [{ type: "text", text: `Bet \`${betId}\` not found.` }],
4934
+ structuredContent: failure(
4935
+ "NOT_FOUND",
4936
+ `Bet '${betId}' not found.`,
4937
+ "Use facilitate action=start to begin a session.",
4938
+ [{ tool: "facilitate", description: "Start session", parameters: { action: "start" } }]
4939
+ )
4698
4940
  };
4699
4941
  }
4700
4942
  const constellation = await loadConstellationState(betId, betEntry._id);
@@ -4764,31 +5006,38 @@ async function handleScore(args) {
4764
5006
  }
4765
5007
  return {
4766
5008
  content: [{ type: "text", text: lines.join("\n") }],
4767
- structuredContent: {
4768
- scorecard,
4769
- phase,
4770
- phaseLabel: PHASE_LABELS[phase],
4771
- completedDimensions: completed,
4772
- nextDimension: active,
4773
- sessionDrafts,
4774
- captureReady,
4775
- constellationDrafts,
4776
- commitBlockers: commitBlockers.length > 0 ? commitBlockers : void 0,
4777
- scorecardCriteria
4778
- }
5009
+ structuredContent: success(
5010
+ `Scorecard for ${betEntry.name}: ${completed.length}/${dims.length} complete. Phase: ${PHASE_LABELS[phase]}.${captureReady ? " Capture ready." : ""}`,
5011
+ {
5012
+ scorecard,
5013
+ phase,
5014
+ phaseLabel: PHASE_LABELS[phase],
5015
+ completedDimensions: completed,
5016
+ nextDimension: active,
5017
+ sessionDrafts,
5018
+ captureReady,
5019
+ constellationDrafts,
5020
+ commitBlockers: commitBlockers.length > 0 ? commitBlockers : void 0,
5021
+ scorecardCriteria
5022
+ }
5023
+ )
4779
5024
  };
4780
5025
  }
4781
5026
  async function handleResume(args) {
4782
5027
  const betId = args.betEntryId;
4783
5028
  if (!betId) {
4784
- return {
4785
- content: [{ type: "text", text: "`betEntryId` is required for resume action." }]
4786
- };
5029
+ return validationResult("`betEntryId` is required for resume action.");
4787
5030
  }
4788
5031
  const betEntry = await loadBetEntry(betId);
4789
5032
  if (!betEntry) {
4790
5033
  return {
4791
- content: [{ type: "text", text: `Bet \`${betId}\` not found. Cannot resume.` }]
5034
+ content: [{ type: "text", text: `Bet \`${betId}\` not found. Cannot resume.` }],
5035
+ structuredContent: failure(
5036
+ "NOT_FOUND",
5037
+ `Bet '${betId}' not found.`,
5038
+ "Use facilitate action=start to begin a new session.",
5039
+ [{ tool: "facilitate", description: "Start session", parameters: { action: "start" } }]
5040
+ )
4792
5041
  };
4793
5042
  }
4794
5043
  const constellation = await loadConstellationState(betId, betEntry._id);
@@ -4875,21 +5124,29 @@ ${directive}` : ""
4875
5124
  ].filter(Boolean).join("\n");
4876
5125
  return {
4877
5126
  content: [{ type: "text", text: output }],
4878
- structuredContent: response
5127
+ structuredContent: success(
5128
+ `Resumed session for "${betEntry.name}". Phase: ${PHASE_LABELS[phase]}. Next: ${DIMENSION_LABELS[active]}.`,
5129
+ response,
5130
+ [{ tool: "facilitate", description: `Continue with ${DIMENSION_LABELS[active]}`, parameters: { action: "respond", betEntryId: betId, dimension: active } }]
5131
+ )
4879
5132
  };
4880
5133
  }
4881
5134
  async function handleCommitConstellation(args) {
4882
5135
  requireWriteAccess();
4883
5136
  const betId = args.betEntryId;
4884
5137
  if (!betId) {
4885
- return {
4886
- content: [{ type: "text", text: "`betEntryId` is required for commit-constellation action." }]
4887
- };
5138
+ return validationResult("`betEntryId` is required for commit-constellation action.");
4888
5139
  }
4889
5140
  const betEntry = await loadBetEntry(betId);
4890
5141
  if (!betEntry) {
4891
5142
  return {
4892
- content: [{ type: "text", text: `Bet \`${betId}\` not found.` }]
5143
+ content: [{ type: "text", text: `Bet \`${betId}\` not found.` }],
5144
+ structuredContent: failure(
5145
+ "NOT_FOUND",
5146
+ `Bet '${betId}' not found.`,
5147
+ "Use facilitate action=start to begin a session.",
5148
+ [{ tool: "facilitate", description: "Start session", parameters: { action: "start" } }]
5149
+ )
4893
5150
  };
4894
5151
  }
4895
5152
  const betData = betEntry.data ?? {};
@@ -4934,21 +5191,23 @@ async function handleCommitConstellation(args) {
4934
5191
  "No entries were committed."
4935
5192
  ].join("\n")
4936
5193
  }],
4937
- structuredContent: {
4938
- operationId,
4939
- blockers: commitBlockerItems,
4940
- committedIds: [],
4941
- alreadyCommittedIds: [],
4942
- proposedIds: [],
4943
- failedIds: [],
4944
- conflictIds: [],
4945
- totalRelations: relations.length
4946
- }
5194
+ structuredContent: failure(
5195
+ "VALIDATION_ERROR",
5196
+ `${commitBlockerItems.length} blocker(s) found. Fix before committing.`,
5197
+ commitBlockerItems.map((b) => `${b.blocker} \u2192 ${b.fix}`).join("; "),
5198
+ void 0,
5199
+ {
5200
+ operationId,
5201
+ blockers: commitBlockerItems,
5202
+ committedIds: [],
5203
+ totalRelations: relations.length
5204
+ }
5205
+ )
4947
5206
  };
4948
5207
  }
4949
5208
  let contradictionWarnings = [];
4950
5209
  try {
4951
- const { runContradictionCheck } = await import("./smart-capture-XBOUPHFI.js");
5210
+ const { runContradictionCheck } = await import("./smart-capture-XLBFE252.js");
4952
5211
  const descField = betData.problem ?? betData.description ?? "";
4953
5212
  contradictionWarnings = await runContradictionCheck(
4954
5213
  betEntry.name ?? betId,
@@ -4980,7 +5239,12 @@ async function handleCommitConstellation(args) {
4980
5239
 
4981
5240
  No constellation entries were committed.`
4982
5241
  }],
4983
- isError: true
5242
+ structuredContent: failure(
5243
+ "BACKEND_ERROR",
5244
+ `Bet commit failed: ${msg}`,
5245
+ "Retry or check for commit blockers.",
5246
+ [{ tool: "facilitate", description: "Check scorecard", parameters: { action: "score", betEntryId: betId } }]
5247
+ )
4984
5248
  };
4985
5249
  }
4986
5250
  result = {
@@ -5043,9 +5307,13 @@ No constellation entries were committed.`
5043
5307
  lines.push(`- \`${f.entryId}\` ${f.name}: ${f.error}`);
5044
5308
  }
5045
5309
  }
5310
+ const summaryText = result.proposalCreated ? `Proposal created for ${betId}. ${result.committedIds.length} entries committed.` : `Published ${result.committedIds.length} entries for ${betId}. ${result.totalRelations} connections preserved.${hasIssues ? ` ${result.failedIds.length} failed, ${result.conflictIds.length} conflicts.` : ""}`;
5046
5311
  return {
5047
5312
  content: [{ type: "text", text: lines.join("\n") }],
5048
- structuredContent: { ...result, contradictionWarnings }
5313
+ structuredContent: success(
5314
+ summaryText,
5315
+ { ...result, contradictionWarnings }
5316
+ )
5049
5317
  };
5050
5318
  }
5051
5319
 
@@ -5209,15 +5477,14 @@ function registerVerifyTools(server) {
5209
5477
  inputSchema: verifySchema,
5210
5478
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
5211
5479
  },
5212
- async ({ collection, mode }) => {
5480
+ withEnvelope(async ({ collection, mode }) => {
5213
5481
  const projectRoot = resolveProjectRoot();
5214
5482
  if (!projectRoot) {
5215
- return {
5216
- content: [{
5217
- type: "text",
5218
- text: "# Verification Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent directory).\n\nSet `WORKSPACE_PATH` in `.env.mcp` to the absolute path of the Product Brain project root."
5219
- }]
5220
- };
5483
+ return failureResult(
5484
+ "# Verification Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent directory).\n\nSet `WORKSPACE_PATH` in `.env.mcp` to the absolute path of the Product Brain project root.",
5485
+ "VALIDATION_ERROR",
5486
+ "Cannot find project root. Set WORKSPACE_PATH in .env.mcp."
5487
+ );
5221
5488
  }
5222
5489
  const schema = parseConvexSchema(resolve(projectRoot, "convex/schema.ts"));
5223
5490
  await server.sendLoggingMessage({
@@ -5227,9 +5494,13 @@ function registerVerifyTools(server) {
5227
5494
  });
5228
5495
  const scopedEntries = await mcpQuery("chain.listEntries", { collectionSlug: collection });
5229
5496
  if (scopedEntries.length === 0) {
5230
- return {
5231
- content: [{ type: "text", text: `No entries found in \`${collection}\`. Nothing to verify.` }]
5232
- };
5497
+ return failureResult(
5498
+ `No entries found in '${collection}'. Nothing to verify.`,
5499
+ "NOT_FOUND",
5500
+ `No entries found in '${collection}'. Nothing to verify.`,
5501
+ "Capture entries first, then verify.",
5502
+ [{ tool: "capture", description: "Capture an entry", parameters: {} }]
5503
+ );
5233
5504
  }
5234
5505
  await server.sendLoggingMessage({
5235
5506
  level: "info",
@@ -5323,6 +5594,14 @@ function registerVerifyTools(server) {
5323
5594
  fixes.push(entry.entryId);
5324
5595
  }
5325
5596
  }
5597
+ const verified = mappingChecks.filter((c) => c.result === "verified").length;
5598
+ const drifted = mappingChecks.filter((c) => c.result === "drifted").length;
5599
+ const unverifiable = mappingChecks.filter((c) => c.result === "unverifiable").length;
5600
+ const refsValid = refChecks.filter((c) => c.found).length;
5601
+ const refsBroken = refChecks.filter((c) => !c.found).length;
5602
+ const totalChecks = mappingChecks.length + refChecks.length;
5603
+ const totalPassed = verified + refsValid;
5604
+ const trustScore = totalChecks > 0 ? Math.round(totalPassed / totalChecks * 100) : 100;
5326
5605
  const report = formatTrustReport(
5327
5606
  collection,
5328
5607
  scopedEntries.length,
@@ -5333,8 +5612,27 @@ function registerVerifyTools(server) {
5333
5612
  schema.size,
5334
5613
  projectRoot
5335
5614
  );
5336
- return { content: [{ type: "text", text: report }] };
5337
- }
5615
+ return {
5616
+ content: [{ type: "text", text: report }],
5617
+ structuredContent: success(
5618
+ `Trust report for ${collection}: ${totalPassed}/${totalChecks} checks passed (${trustScore}%).`,
5619
+ {
5620
+ collection,
5621
+ entryCount: scopedEntries.length,
5622
+ trustScore,
5623
+ totalChecks,
5624
+ totalPassed,
5625
+ verified,
5626
+ drifted,
5627
+ unverifiable,
5628
+ refsValid,
5629
+ refsBroken,
5630
+ fixesApplied: fixes.length,
5631
+ mode
5632
+ }
5633
+ )
5634
+ };
5635
+ })
5338
5636
  );
5339
5637
  trackWriteTool(verifyTool);
5340
5638
  }
@@ -5815,7 +6113,7 @@ function registerStartTools(server) {
5815
6113
  inputSchema: startSchema,
5816
6114
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5817
6115
  },
5818
- async ({ preset }) => {
6116
+ withEnvelope(async ({ preset }) => {
5819
6117
  const errors = [];
5820
6118
  const agentSessionId = getAgentSessionId();
5821
6119
  let wsCtx = null;
@@ -5825,13 +6123,14 @@ function registerStartTools(server) {
5825
6123
  errors.push(`Workspace: ${e.message}`);
5826
6124
  }
5827
6125
  if (!wsCtx) {
6126
+ const text = "# Could not connect to Product Brain\n\n" + (errors.length > 0 ? errors.map((e) => `- ${e}`).join("\n") : "Check your API key and CONVEX_SITE_URL.");
5828
6127
  return {
5829
- content: [
5830
- {
5831
- type: "text",
5832
- text: "# Could not connect to Product Brain\n\n" + (errors.length > 0 ? errors.map((e) => `- ${e}`).join("\n") : "Check your API key and CONVEX_SITE_URL.")
5833
- }
5834
- ]
6128
+ content: [{ type: "text", text }],
6129
+ structuredContent: failure(
6130
+ "BACKEND_UNAVAILABLE",
6131
+ "Could not connect to Product Brain.",
6132
+ "Check your API key and CONVEX_SITE_URL."
6133
+ )
5835
6134
  };
5836
6135
  }
5837
6136
  let stage = null;
@@ -5851,15 +6150,14 @@ function registerStartTools(server) {
5851
6150
  }
5852
6151
  if (stage === "blank") {
5853
6152
  const { oriented, orientationStatus } = await tryMarkOriented(agentSessionId);
5854
- const blankResult = buildBlankResponse(wsCtx);
6153
+ const blankResult = buildBlankResponse(wsCtx, {
6154
+ oriented,
6155
+ orientationStatus,
6156
+ ...agentSessionId ? { sessionId: agentSessionId } : {}
6157
+ });
5855
6158
  return {
5856
6159
  content: [{ type: "text", text: blankResult.text }],
5857
- structuredContent: {
5858
- ...blankResult.structuredContent,
5859
- oriented,
5860
- orientationStatus,
5861
- ...agentSessionId ? { sessionId: agentSessionId } : {}
5862
- }
6160
+ structuredContent: blankResult.structuredContent
5863
6161
  };
5864
6162
  }
5865
6163
  if (stage === "seeded") {
@@ -5880,10 +6178,10 @@ function registerStartTools(server) {
5880
6178
  content: [{ type: "text", text: orientResult.text }],
5881
6179
  structuredContent: orientResult.structuredContent
5882
6180
  };
5883
- }
6181
+ })
5884
6182
  );
5885
6183
  }
5886
- function buildBlankResponse(wsCtx) {
6184
+ function buildBlankResponse(wsCtx, sessionCtx) {
5887
6185
  const instructions = getInterviewInstructions(wsCtx.workspaceName);
5888
6186
  const text = [
5889
6187
  `# Welcome to ${wsCtx.workspaceName}`,
@@ -5947,7 +6245,14 @@ function buildBlankResponse(wsCtx) {
5947
6245
  ].join("\n");
5948
6246
  return {
5949
6247
  text,
5950
- structuredContent: { stage: "blank" }
6248
+ structuredContent: success(
6249
+ `Workspace is blank. Choose: tell me about your product, scan codebase, or use a preset.`,
6250
+ { stage: "blank", ...sessionCtx },
6251
+ [
6252
+ { tool: "capture", description: "Capture product knowledge", parameters: {} },
6253
+ { tool: "start", description: "Seed with a preset", parameters: { preset: "software-product" } }
6254
+ ]
6255
+ )
5951
6256
  };
5952
6257
  }
5953
6258
  async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
@@ -5974,17 +6279,22 @@ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
5974
6279
  lines.push("No gaps detected \u2014 your workspace is filling up nicely. What would you like to work on?");
5975
6280
  }
5976
6281
  const { oriented, orientationStatus } = await tryMarkOriented(agentSessionId);
6282
+ const next = gaps.length > 0 ? [{ tool: "capture", description: `Fill gap: ${gaps[0].label}`, parameters: {} }] : [];
5977
6283
  return {
5978
6284
  text: lines.join("\n"),
5979
- structuredContent: {
5980
- stage,
5981
- readinessScore: score ?? null,
5982
- totalGaps: gaps.length,
5983
- topGapLabels: gaps.slice(0, 3).map((g) => g.label),
5984
- oriented,
5985
- orientationStatus,
5986
- ...agentSessionId ? { sessionId: agentSessionId } : {}
5987
- }
6285
+ structuredContent: success(
6286
+ gaps.length > 0 ? `Workspace seeded. ${gaps.length} gap${gaps.length === 1 ? "" : "s"} to fill. Top: ${gaps[0].label}.` : `Workspace seeded. No gaps detected.`,
6287
+ {
6288
+ stage,
6289
+ readinessScore: score ?? null,
6290
+ totalGaps: gaps.length,
6291
+ topGapLabels: gaps.slice(0, 3).map((g) => g.label),
6292
+ oriented,
6293
+ orientationStatus,
6294
+ ...agentSessionId ? { sessionId: agentSessionId } : {}
6295
+ },
6296
+ next.length > 0 ? next : void 0
6297
+ )
5988
6298
  };
5989
6299
  }
5990
6300
  async function seedPreset(wsCtx, presetId, agentSessionId) {
@@ -5994,7 +6304,12 @@ async function seedPreset(wsCtx, presetId, agentSessionId) {
5994
6304
  text: `Preset "${presetId}" not found.
5995
6305
 
5996
6306
  Available presets: ${listPresets().map((p) => `\`${p.id}\``).join(", ")}`,
5997
- structuredContent: { stage: "blank", error: "preset_not_found", preset: presetId }
6307
+ structuredContent: failure(
6308
+ "NOT_FOUND",
6309
+ `Preset '${presetId}' not found.`,
6310
+ `Available presets: ${listPresets().map((p) => p.id).join(", ")}.`,
6311
+ [{ tool: "start", description: "Start with a preset", parameters: { preset: "software-product" } }]
6312
+ )
5998
6313
  };
5999
6314
  }
6000
6315
  const seeded = [];
@@ -6039,15 +6354,19 @@ Available presets: ${listPresets().map((p) => `\`${p.id}\``).join(", ")}`,
6039
6354
  );
6040
6355
  return {
6041
6356
  text: lines.join("\n"),
6042
- structuredContent: {
6043
- stage: "blank",
6044
- preset: presetId,
6045
- seededCollections: seeded,
6046
- skippedCollections: skipped,
6047
- oriented,
6048
- orientationStatus,
6049
- ...agentSessionId ? { sessionId: agentSessionId } : {}
6050
- }
6357
+ structuredContent: success(
6358
+ `Seeded '${preset.name}' preset with ${seeded.length} collection${seeded.length === 1 ? "" : "s"}.`,
6359
+ {
6360
+ stage: "blank",
6361
+ preset: presetId,
6362
+ seededCollections: seeded,
6363
+ skippedCollections: skipped,
6364
+ oriented,
6365
+ orientationStatus,
6366
+ ...agentSessionId ? { sessionId: agentSessionId } : {}
6367
+ },
6368
+ [{ tool: "capture", description: "Capture your first entry", parameters: {} }]
6369
+ )
6051
6370
  };
6052
6371
  }
6053
6372
  function computeWorkspaceAge(createdAt) {
@@ -6255,17 +6574,33 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
6255
6574
  lines.push("");
6256
6575
  }
6257
6576
  const { oriented, orientationStatus } = await tryMarkOriented(agentSessionId);
6577
+ const stageLabel = stage ?? "active";
6578
+ const gapCount = readiness?.gaps?.length ?? 0;
6579
+ const summaryParts = [`Product Brain ready. Stage: ${stageLabel}.`];
6580
+ if (gapCount > 0) summaryParts.push(`${gapCount} gap${gapCount === 1 ? "" : "s"}.`);
6581
+ if (openTensions.length > 0) summaryParts.push(`${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}.`);
6582
+ const next = [];
6583
+ if (gapCount > 0) {
6584
+ next.push({ tool: "capture", description: "Fill a gap", parameters: {} });
6585
+ }
6586
+ if (openTensions.length > 0) {
6587
+ next.push({ tool: "entries", description: "View open tensions", parameters: { action: "list", collection: "tensions" } });
6588
+ }
6258
6589
  return {
6259
6590
  text: lines.join("\n"),
6260
- structuredContent: {
6261
- stage,
6262
- readinessScore: readiness?.score ?? null,
6263
- totalGaps: readiness?.gaps?.length ?? 0,
6264
- openTensions: openTensions.length,
6265
- oriented,
6266
- orientationStatus,
6267
- ...agentSessionId ? { sessionId: agentSessionId } : {}
6268
- }
6591
+ structuredContent: success(
6592
+ summaryParts.join(" "),
6593
+ {
6594
+ stage,
6595
+ readinessScore: readiness?.score ?? null,
6596
+ totalGaps: gapCount,
6597
+ openTensions: openTensions.length,
6598
+ oriented,
6599
+ orientationStatus,
6600
+ ...agentSessionId ? { sessionId: agentSessionId } : {}
6601
+ },
6602
+ next.length > 0 ? next : void 0
6603
+ )
6269
6604
  };
6270
6605
  }
6271
6606
 
@@ -6283,21 +6618,19 @@ function registerUsageTools(server) {
6283
6618
  inputSchema: usageSummarySchema,
6284
6619
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
6285
6620
  },
6286
- async ({ periodDays }) => {
6621
+ withEnvelope(async ({ periodDays }) => {
6287
6622
  const ws = await getWorkspaceContext();
6288
6623
  const sessionId = getAgentSessionId();
6289
6624
  const summary = await mcpQuery("usage.getWorkspaceSummary", {
6290
6625
  periodDays: periodDays ?? 30
6291
6626
  });
6292
6627
  if (!summary) {
6293
- return {
6294
- content: [
6295
- {
6296
- type: "text",
6297
- text: "No usage data available yet. LLM tracking may not be active or no calls have been made in this period."
6298
- }
6299
- ]
6300
- };
6628
+ return failureResult(
6629
+ "No usage data available yet. LLM tracking may not be active or no calls have been made in this period.",
6630
+ "NOT_FOUND",
6631
+ "No usage data available yet.",
6632
+ "LLM tracking may not be active or no calls have been made in this period."
6633
+ );
6301
6634
  }
6302
6635
  const lines = [
6303
6636
  `## Usage Summary \u2014 ${ws.workspaceName}`,
@@ -6328,9 +6661,21 @@ function registerUsageTools(server) {
6328
6661
  type: "text",
6329
6662
  text: lines.join("\n")
6330
6663
  }
6331
- ]
6664
+ ],
6665
+ structuredContent: success(
6666
+ `Usage for ${ws.workspaceName}: $${summary.totalCostUsd.toFixed(4)} over ${summary.periodDays} days, ${summary.totalRequests} requests.`,
6667
+ {
6668
+ periodDays: summary.periodDays,
6669
+ totalTokens: summary.totalTokens,
6670
+ totalCostUsd: summary.totalCostUsd,
6671
+ totalRequests: summary.totalRequests,
6672
+ cacheHitRate: summary.cacheHitRate,
6673
+ modelBreakdown: summary.modelBreakdown,
6674
+ featureBreakdown: summary.featureBreakdown
6675
+ }
6676
+ )
6332
6677
  };
6333
- }
6678
+ })
6334
6679
  );
6335
6680
  }
6336
6681
 
@@ -6389,9 +6734,11 @@ function registerGitChainTools(server) {
6389
6734
  inputSchema: chainSchema,
6390
6735
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
6391
6736
  },
6392
- async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
6737
+ withEnvelope(async ({ action, chainEntryId, title, chainTypeId, description, linkId, content, status, author }) => {
6393
6738
  if (action === "create") {
6394
- if (!title) return { content: [{ type: "text", text: "A `title` is required to create a process." }] };
6739
+ if (!title) {
6740
+ return validationResult("A `title` is required to create a process.");
6741
+ }
6395
6742
  const result = await mcpMutation(
6396
6743
  "gitchain.createChain",
6397
6744
  { title, chainTypeId, description, author }
@@ -6407,14 +6754,29 @@ function registerGitChainTools(server) {
6407
6754
  - **Status:** draft
6408
6755
 
6409
6756
  Use \`chain action=edit chainEntryId="${result.entryId}" linkId="problem" content="..."\` to start filling in links.`
6410
- }]
6757
+ }],
6758
+ structuredContent: success(
6759
+ `Created process '${title}' (${result.entryId}), type: ${chainTypeId}.`,
6760
+ { entryId: result.entryId, title, chainTypeId, status: "draft" },
6761
+ [{ tool: "chain", description: "Edit a link", parameters: { action: "edit", chainEntryId: result.entryId, linkId: "problem" } }]
6762
+ )
6411
6763
  };
6412
6764
  }
6413
6765
  if (action === "get") {
6414
- if (!chainEntryId) return { content: [{ type: "text", text: "A `chainEntryId` is required." }] };
6766
+ if (!chainEntryId) {
6767
+ return validationResult("A `chainEntryId` is required.");
6768
+ }
6415
6769
  const chain = await mcpQuery("gitchain.getChain", { chainEntryId });
6416
6770
  if (!chain) {
6417
- return { content: [{ type: "text", text: `Process "${chainEntryId}" not found.` }] };
6771
+ return {
6772
+ content: [{ type: "text", text: `Process "${chainEntryId}" not found.` }],
6773
+ structuredContent: failure(
6774
+ "NOT_FOUND",
6775
+ `Process "${chainEntryId}" not found.`,
6776
+ "Use chain action=list to find available processes.",
6777
+ [{ tool: "chain", description: "List processes", parameters: { action: "list" } }]
6778
+ )
6779
+ };
6418
6780
  }
6419
6781
  const scoreSection = chain.scores ? `
6420
6782
  ## Coherence: ${chain.coherenceScore}%
@@ -6437,28 +6799,48 @@ Use \`chain action=edit chainEntryId="${result.entryId}" linkId="problem" conten
6437
6799
  ## Links
6438
6800
 
6439
6801
  ` + linkSummary(chain.links)
6440
- }]
6802
+ }],
6803
+ structuredContent: success(
6804
+ `${chain.name}: ${chain.filledCount}/${chain.totalCount} links, coherence ${chain.coherenceScore}%, v${chain.currentVersion}.`,
6805
+ { entryId: chain.entryId, name: chain.name, status: chain.status, filledCount: chain.filledCount, totalCount: chain.totalCount, coherenceScore: chain.coherenceScore, currentVersion: chain.currentVersion }
6806
+ )
6441
6807
  };
6442
6808
  }
6443
6809
  if (action === "list") {
6444
- const chains = await mcpQuery("gitchain.listChains", {
6445
- chainTypeId,
6446
- status
6447
- });
6810
+ const chains = await mcpQuery("gitchain.listChains", { chainTypeId, status });
6448
6811
  if (chains.length === 0) {
6449
- return { content: [{ type: "text", text: "No processes found. Use `chain action=create` to create one." }] };
6812
+ return {
6813
+ content: [{ type: "text", text: "No processes found. Use `chain action=create` to create one." }],
6814
+ structuredContent: success(
6815
+ "No processes found.",
6816
+ { chains: [], total: 0 },
6817
+ [{ tool: "chain", description: "Create a process", parameters: { action: "create" } }]
6818
+ )
6819
+ };
6450
6820
  }
6451
6821
  const formatted = chains.map(
6452
6822
  (c) => `- **\`${c.entryId}\`** ${c.name} \u2014 ${c.chainTypeId} \xB7 ${c.filledCount}/${c.totalCount} links \xB7 coherence: ${c.coherenceScore}% \xB7 status: ${c.status}`
6453
6823
  ).join("\n");
6454
- return { content: [{ type: "text", text: `# Chains (${chains.length})
6824
+ return {
6825
+ content: [{ type: "text", text: `# Chains (${chains.length})
6455
6826
 
6456
- ${formatted}` }] };
6827
+ ${formatted}` }],
6828
+ structuredContent: success(
6829
+ `Found ${chains.length} process(es).`,
6830
+ { total: chains.length }
6831
+ )
6832
+ };
6457
6833
  }
6458
6834
  if (action === "edit") {
6459
- if (!chainEntryId) return { content: [{ type: "text", text: "A `chainEntryId` is required." }] };
6460
- if (!linkId) return { content: [{ type: "text", text: "A `linkId` is required (e.g. problem, insight, choice, action, outcome)." }] };
6461
- if (!content) return { content: [{ type: "text", text: "The `content` for the link is required." }] };
6835
+ if (!chainEntryId) {
6836
+ return validationResult("A `chainEntryId` is required.");
6837
+ }
6838
+ if (!linkId) {
6839
+ return validationResult("A `linkId` is required (e.g. problem, insight, choice, action, outcome).");
6840
+ }
6841
+ if (!content) {
6842
+ return validationResult("The `content` for the link is required.");
6843
+ }
6462
6844
  const result = await mcpMutation("gitchain.editLink", { chainEntryId, linkId, content, author });
6463
6845
  return {
6464
6846
  content: [{
@@ -6471,11 +6853,16 @@ ${formatted}` }] };
6471
6853
  - **Content length:** ${content.length} chars
6472
6854
 
6473
6855
  Use \`chain action=get\` to see the full chain with updated scores.`
6474
- }]
6856
+ }],
6857
+ structuredContent: success(
6858
+ `Updated link "${result.linkId}" on ${result.entryId}. Status: ${result.status}.`,
6859
+ { entryId: result.entryId, linkId: result.linkId, status: result.status, contentLength: content.length },
6860
+ [{ tool: "chain", description: "View full chain", parameters: { action: "get", chainEntryId } }]
6861
+ )
6475
6862
  };
6476
6863
  }
6477
- return { content: [{ type: "text", text: "Unknown action." }] };
6478
- }
6864
+ return unknownAction(action, ["create", "get", "list", "edit"]);
6865
+ })
6479
6866
  );
6480
6867
  trackWriteTool(chainTool);
6481
6868
  const chainVersionTool = server.registerTool(
@@ -6486,9 +6873,11 @@ Use \`chain action=get\` to see the full chain with updated scores.`
6486
6873
  inputSchema: chainVersionSchema,
6487
6874
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
6488
6875
  },
6489
- async ({ action, chainEntryId, commitMessage, versionA, versionB, toVersion, author }) => {
6876
+ withEnvelope(async ({ action, chainEntryId, commitMessage, versionA, versionB, toVersion, author }) => {
6490
6877
  if (action === "commit") {
6491
- if (!commitMessage) return { content: [{ type: "text", text: "A `commitMessage` is required." }] };
6878
+ if (!commitMessage) {
6879
+ return validationResult("A `commitMessage` is required.");
6880
+ }
6492
6881
  const result = await mcpMutation("gitchain.commitChain", { chainEntryId, commitMessage, author });
6493
6882
  const warning = result.commitLintWarning ? `
6494
6883
 
@@ -6504,13 +6893,24 @@ Use \`chain action=get\` to see the full chain with updated scores.`
6504
6893
  - **Coherence:** ${result.coherenceScore}%
6505
6894
  - **Links modified:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
6506
6895
  ` + warning
6507
- }]
6896
+ }],
6897
+ structuredContent: success(
6898
+ `Committed v${result.version} on ${result.entryId}. Coherence: ${result.coherenceScore}%.`,
6899
+ { entryId: result.entryId, version: result.version, coherenceScore: result.coherenceScore, linksModified: result.linksModified, lintWarning: result.commitLintWarning }
6900
+ )
6508
6901
  };
6509
6902
  }
6510
6903
  if (action === "list") {
6511
6904
  const commits = await mcpQuery("gitchain.listCommits", { chainEntryId });
6512
6905
  if (commits.length === 0) {
6513
- return { content: [{ type: "text", text: `No commits found for chain "${chainEntryId}". Use \`chain-version action=commit\` to create the first snapshot.` }] };
6906
+ return {
6907
+ content: [{ type: "text", text: `No commits found for chain "${chainEntryId}". Use \`chain-version action=commit\` to create the first snapshot.` }],
6908
+ structuredContent: success(
6909
+ `No commits for ${chainEntryId}.`,
6910
+ { chainEntryId, commits: [], total: 0 },
6911
+ [{ tool: "chain-version", description: "Commit chain", parameters: { action: "commit", chainEntryId } }]
6912
+ )
6913
+ };
6514
6914
  }
6515
6915
  const formatted = commits.map((c) => {
6516
6916
  const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
@@ -6518,13 +6918,19 @@ Use \`chain action=get\` to see the full chain with updated scores.`
6518
6918
  const links = c.linksModified?.length > 0 ? ` [${c.linksModified.join(", ")}]` : "";
6519
6919
  return `- **v${c.version}** ${date} by ${c.author} \u2014 ${msg}${links} (${c.versionStatus})`;
6520
6920
  }).join("\n");
6521
- return { content: [{ type: "text", text: `# Commits for ${chainEntryId} (${commits.length})
6921
+ return {
6922
+ content: [{ type: "text", text: `# Commits for ${chainEntryId} (${commits.length})
6522
6923
 
6523
- ${formatted}` }] };
6924
+ ${formatted}` }],
6925
+ structuredContent: success(
6926
+ `${commits.length} commit(s) for ${chainEntryId}.`,
6927
+ { chainEntryId, total: commits.length }
6928
+ )
6929
+ };
6524
6930
  }
6525
6931
  if (action === "diff") {
6526
6932
  if (versionA == null || versionB == null) {
6527
- return { content: [{ type: "text", text: "Both `versionA` and `versionB` are required for diff." }] };
6933
+ return validationResult("Both `versionA` and `versionB` are required for diff.");
6528
6934
  }
6529
6935
  const diff = await mcpMutation("gitchain.diffVersions", { chainEntryId, versionA, versionB });
6530
6936
  let text = `# Diff: v${versionA} \u2192 v${versionB}
@@ -6549,10 +6955,18 @@ ${formatted}` }] };
6549
6955
  text += "\n";
6550
6956
  }
6551
6957
  }
6552
- return { content: [{ type: "text", text }] };
6958
+ return {
6959
+ content: [{ type: "text", text }],
6960
+ structuredContent: success(
6961
+ `Diff v${versionA} \u2192 v${versionB}: coherence ${diff.coherenceBefore}% \u2192 ${diff.coherenceAfter}%, ${diff.linksChanged.length} link(s) changed.`,
6962
+ { chainEntryId: diff.chainEntryId, versionA, versionB, coherenceBefore: diff.coherenceBefore, coherenceAfter: diff.coherenceAfter, coherenceDelta: diff.coherenceDelta, linksChanged: diff.linksChanged }
6963
+ )
6964
+ };
6553
6965
  }
6554
6966
  if (action === "revert") {
6555
- if (toVersion == null) return { content: [{ type: "text", text: "A `toVersion` is required for revert." }] };
6967
+ if (toVersion == null) {
6968
+ return validationResult("A `toVersion` is required for revert.");
6969
+ }
6556
6970
  const result = await mcpMutation("gitchain.revertChain", { chainEntryId, toVersion, author });
6557
6971
  return {
6558
6972
  content: [{
@@ -6565,24 +6979,40 @@ ${formatted}` }] };
6565
6979
  - **Links affected:** ${result.linksModified.length > 0 ? result.linksModified.join(", ") : "none"}
6566
6980
 
6567
6981
  History is preserved \u2014 this created a new version, not a destructive reset.`
6568
- }]
6982
+ }],
6983
+ structuredContent: success(
6984
+ `Reverted ${result.entryId} to v${result.revertedTo}. New version: v${result.newVersion}.`,
6985
+ { entryId: result.entryId, revertedTo: result.revertedTo, newVersion: result.newVersion, linksModified: result.linksModified }
6986
+ )
6569
6987
  };
6570
6988
  }
6571
6989
  if (action === "history") {
6572
6990
  const history = await mcpQuery("gitchain.getHistory", { chainEntryId });
6573
6991
  if (history.length === 0) {
6574
- return { content: [{ type: "text", text: `No history found for chain "${chainEntryId}".` }] };
6992
+ return {
6993
+ content: [{ type: "text", text: `No history found for chain "${chainEntryId}".` }],
6994
+ structuredContent: success(
6995
+ `No history for ${chainEntryId}.`,
6996
+ { chainEntryId, events: [], total: 0 }
6997
+ )
6998
+ };
6575
6999
  }
6576
7000
  const formatted = history.sort((a, b) => b.timestamp - a.timestamp).map((h) => {
6577
7001
  const date = new Date(h.timestamp).toISOString().replace("T", " ").substring(0, 19);
6578
7002
  return `- **${date}** [${h.event}] by ${h.changedBy ?? "unknown"} \u2014 ${h.note ?? ""}`;
6579
7003
  }).join("\n");
6580
- return { content: [{ type: "text", text: `# History for ${chainEntryId} (${history.length} events)
7004
+ return {
7005
+ content: [{ type: "text", text: `# History for ${chainEntryId} (${history.length} events)
6581
7006
 
6582
- ${formatted}` }] };
7007
+ ${formatted}` }],
7008
+ structuredContent: success(
7009
+ `${history.length} history event(s) for ${chainEntryId}.`,
7010
+ { chainEntryId, total: history.length }
7011
+ )
7012
+ };
6583
7013
  }
6584
- return { content: [{ type: "text", text: "Unknown action." }] };
6585
- }
7014
+ return unknownAction(action, ["commit", "list", "diff", "revert", "history"]);
7015
+ })
6586
7016
  );
6587
7017
  trackWriteTool(chainVersionTool);
6588
7018
  const chainBranchTool = server.registerTool(
@@ -6593,7 +7023,7 @@ ${formatted}` }] };
6593
7023
  inputSchema: chainBranchSchema,
6594
7024
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
6595
7025
  },
6596
- async ({ action, chainEntryId, branchName, strategy, author }) => {
7026
+ withEnvelope(async ({ action, chainEntryId, branchName, strategy, author }) => {
6597
7027
  if (action === "create") {
6598
7028
  const result = await mcpMutation("gitchain.createBranch", { chainEntryId, name: branchName, author });
6599
7029
  return {
@@ -6606,21 +7036,41 @@ ${formatted}` }] };
6606
7036
  - **Chain:** \`${chainEntryId}\`
6607
7037
 
6608
7038
  Edit links and commit on this branch, then use \`chain-branch action=merge\` to land changes.`
6609
- }]
7039
+ }],
7040
+ structuredContent: success(
7041
+ `Created branch "${result.name}" based on v${result.baseVersion}.`,
7042
+ { branchId: result.branchId, name: result.name, baseVersion: result.baseVersion, chainEntryId },
7043
+ [{ tool: "chain-branch", description: "Merge branch", parameters: { action: "merge", chainEntryId, branchName: result.name } }]
7044
+ )
6610
7045
  };
6611
7046
  }
6612
7047
  if (action === "list") {
6613
7048
  const branches = await mcpMutation("gitchain.listBranches", { chainEntryId });
6614
7049
  if (branches.length === 0) {
6615
- return { content: [{ type: "text", text: `No branches found for chain "${chainEntryId}".` }] };
7050
+ return {
7051
+ content: [{ type: "text", text: `No branches found for chain "${chainEntryId}".` }],
7052
+ structuredContent: success(
7053
+ `No branches for ${chainEntryId}.`,
7054
+ { chainEntryId, branches: [], total: 0 },
7055
+ [{ tool: "chain-branch", description: "Create a branch", parameters: { action: "create", chainEntryId } }]
7056
+ )
7057
+ };
6616
7058
  }
6617
7059
  const formatted = branches.map((b) => `- **${b.name}** (${b.status}) \u2014 based on v${b.baseVersion}, by ${b.createdBy}`).join("\n");
6618
- return { content: [{ type: "text", text: `# Branches for ${chainEntryId} (${branches.length})
7060
+ return {
7061
+ content: [{ type: "text", text: `# Branches for ${chainEntryId} (${branches.length})
6619
7062
 
6620
- ${formatted}` }] };
7063
+ ${formatted}` }],
7064
+ structuredContent: success(
7065
+ `${branches.length} branch(es) for ${chainEntryId}.`,
7066
+ { chainEntryId, total: branches.length }
7067
+ )
7068
+ };
6621
7069
  }
6622
7070
  if (action === "merge") {
6623
- if (!branchName) return { content: [{ type: "text", text: "A `branchName` is required for merge." }] };
7071
+ if (!branchName) {
7072
+ return validationResult("A `branchName` is required for merge.");
7073
+ }
6624
7074
  const result = await mcpMutation("gitchain.mergeBranch", { chainEntryId, branchName, strategy, author });
6625
7075
  return {
6626
7076
  content: [{
@@ -6633,11 +7083,17 @@ ${formatted}` }] };
6633
7083
  - **Strategy:** ${result.strategy}
6634
7084
 
6635
7085
  Main is now at v${result.version}. The branch has been closed.`
6636
- }]
7086
+ }],
7087
+ structuredContent: success(
7088
+ `Merged branch "${result.branchName}" into ${result.entryId} at v${result.version}.`,
7089
+ { entryId: result.entryId, branchName: result.branchName, version: result.version, strategy: result.strategy }
7090
+ )
6637
7091
  };
6638
7092
  }
6639
7093
  if (action === "conflicts") {
6640
- if (!branchName) return { content: [{ type: "text", text: "A `branchName` is required for conflict check." }] };
7094
+ if (!branchName) {
7095
+ return validationResult("A `branchName` is required for conflict check.");
7096
+ }
6641
7097
  const result = await mcpMutation("gitchain.checkConflicts", { chainEntryId, branchName });
6642
7098
  if (!result.hasConflicts) {
6643
7099
  return {
@@ -6646,7 +7102,12 @@ Main is now at v${result.version}. The branch has been closed.`
6646
7102
  text: `# No Conflicts
6647
7103
 
6648
7104
  Branch "${branchName}" on \`${chainEntryId}\` has no conflicts. Safe to merge.`
6649
- }]
7105
+ }],
7106
+ structuredContent: success(
7107
+ `No conflicts on branch "${branchName}". Safe to merge.`,
7108
+ { chainEntryId, branchName, hasConflicts: false },
7109
+ [{ tool: "chain-branch", description: "Merge branch", parameters: { action: "merge", chainEntryId, branchName } }]
7110
+ )
6650
7111
  };
6651
7112
  }
6652
7113
  const conflictLines = result.conflicts.map(
@@ -6662,11 +7123,16 @@ Branch "${branchName}" conflicts with other branches on these links:
6662
7123
  ` + conflictLines + `
6663
7124
 
6664
7125
  Resolve conflicts before merging.`
6665
- }]
7126
+ }],
7127
+ structuredContent: failure(
7128
+ "CONFLICT",
7129
+ `Branch "${branchName}" has ${result.conflicts.length} conflict(s).`,
7130
+ "Resolve conflicts on the conflicting links before merging."
7131
+ )
6666
7132
  };
6667
7133
  }
6668
- return { content: [{ type: "text", text: "Unknown action." }] };
6669
- }
7134
+ return unknownAction(action, ["create", "list", "merge", "conflicts"]);
7135
+ })
6670
7136
  );
6671
7137
  trackWriteTool(chainBranchTool);
6672
7138
  const chainReviewTool = server.registerTool(
@@ -6677,7 +7143,7 @@ Resolve conflicts before merging.`
6677
7143
  inputSchema: chainReviewSchema,
6678
7144
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
6679
7145
  },
6680
- async ({ action, chainEntryId, commitMessage, versionNumber, linkId, body, commentId, author }) => {
7146
+ withEnvelope(async ({ action, chainEntryId, commitMessage, versionNumber, linkId, body, commentId, author }) => {
6681
7147
  if (action === "gate") {
6682
7148
  const gate = await mcpQuery("gitchain.runGate", { chainEntryId, commitMessage });
6683
7149
  const checkLines = gate.checks.map((c) => `- ${c.pass ? "PASS" : "FAIL"} **${c.name}**: ${c.detail}`).join("\n");
@@ -6693,12 +7159,20 @@ Resolve conflicts before merging.`
6693
7159
  ## Checks
6694
7160
 
6695
7161
  ${checkLines}`
6696
- }]
7162
+ }],
7163
+ structuredContent: success(
7164
+ `Gate ${icon}: ${gate.score}% (threshold ${gate.threshold}%).`,
7165
+ { pass: gate.pass, score: gate.score, threshold: gate.threshold, checks: gate.checks }
7166
+ )
6697
7167
  };
6698
7168
  }
6699
7169
  if (action === "comment") {
6700
- if (!versionNumber) return { content: [{ type: "text", text: "A `versionNumber` is required." }] };
6701
- if (!body) return { content: [{ type: "text", text: "A `body` is required." }] };
7170
+ if (!versionNumber) {
7171
+ return validationResult("A `versionNumber` is required.");
7172
+ }
7173
+ if (!body) {
7174
+ return validationResult("A `body` is required.");
7175
+ }
6702
7176
  const result = await mcpMutation("gitchain.addComment", {
6703
7177
  chainEntryId,
6704
7178
  versionNumber,
@@ -6715,13 +7189,19 @@ ${checkLines}`
6715
7189
  - **Version:** v${result.versionNumber}
6716
7190
  ` + (result.linkId ? `- **Link:** ${result.linkId}
6717
7191
  ` : "") + `- **Body:** ${body.substring(0, 200)}`
6718
- }]
7192
+ }],
7193
+ structuredContent: success(
7194
+ `Comment added on ${result.chainEntryId} v${result.versionNumber}${result.linkId ? ` [${result.linkId}]` : ""}.`,
7195
+ { chainEntryId: result.chainEntryId, versionNumber: result.versionNumber, linkId: result.linkId }
7196
+ )
6719
7197
  };
6720
7198
  }
6721
7199
  if (action === "resolve-comment") {
6722
- if (!commentId) return { content: [{ type: "text", text: "A `commentId` is required." }] };
7200
+ if (!commentId) {
7201
+ return validationResult("A `commentId` is required.");
7202
+ }
6723
7203
  await mcpMutation("gitchain.resolveComment", { commentId });
6724
- return { content: [{ type: "text", text: `Comment resolved.` }] };
7204
+ return successResult("Comment resolved.", `Comment ${commentId} resolved.`, { commentId, resolved: true });
6725
7205
  }
6726
7206
  if (action === "list-comments") {
6727
7207
  const comments = await mcpMutation("gitchain.listComments", {
@@ -6729,7 +7209,13 @@ ${checkLines}`
6729
7209
  versionNumber
6730
7210
  });
6731
7211
  if (comments.length === 0) {
6732
- return { content: [{ type: "text", text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.` }] };
7212
+ return {
7213
+ content: [{ type: "text", text: `No comments found for chain "${chainEntryId}"${versionNumber ? ` v${versionNumber}` : ""}.` }],
7214
+ structuredContent: success(
7215
+ `No comments for ${chainEntryId}${versionNumber ? ` v${versionNumber}` : ""}.`,
7216
+ { chainEntryId, comments: [], total: 0, unresolved: 0 }
7217
+ )
7218
+ };
6733
7219
  }
6734
7220
  const formatted = comments.map((c) => {
6735
7221
  const date = new Date(c.createdAt).toISOString().replace("T", " ").substring(0, 19);
@@ -6744,11 +7230,15 @@ ${checkLines}`
6744
7230
  text: `# Comments for ${chainEntryId} (${comments.length}, ${unresolvedCount} unresolved)
6745
7231
 
6746
7232
  ${formatted}`
6747
- }]
7233
+ }],
7234
+ structuredContent: success(
7235
+ `${comments.length} comment(s) for ${chainEntryId}, ${unresolvedCount} unresolved.`,
7236
+ { chainEntryId, total: comments.length, unresolved: unresolvedCount }
7237
+ )
6748
7238
  };
6749
7239
  }
6750
- return { content: [{ type: "text", text: "Unknown action." }] };
6751
- }
7240
+ return unknownAction(action, ["gate", "comment", "resolve-comment", "list-comments"]);
7241
+ })
6752
7242
  );
6753
7243
  trackWriteTool(chainReviewTool);
6754
7244
  }
@@ -6808,7 +7298,7 @@ function registerMapTools(server) {
6808
7298
  inputSchema: createAudienceMapSetSchema,
6809
7299
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
6810
7300
  },
6811
- async ({ audienceEntryId }) => {
7301
+ withEnvelope(async ({ audienceEntryId }) => {
6812
7302
  const result = await mcpMutation(
6813
7303
  "maps.createAudienceMapSet",
6814
7304
  { audienceEntryId }
@@ -6832,9 +7322,18 @@ ${lines.join("\n")}
6832
7322
 
6833
7323
  Use map-slot to add ingredients. View at /empathy, /narrowing, and /jobs.`
6834
7324
  }
6835
- ]
7325
+ ],
7326
+ structuredContent: success(
7327
+ `Created ${result.maps.length} audience maps for ${result.audienceEntryId}.`,
7328
+ { audienceEntryId: result.audienceEntryId, maps: result.maps },
7329
+ result.maps.map((m) => ({
7330
+ tool: "map-slot",
7331
+ description: `Add ingredients to ${m.templateId}`,
7332
+ parameters: { action: "add", mapEntryId: m.mapEntryId }
7333
+ }))
7334
+ )
6836
7335
  };
6837
- }
7336
+ })
6838
7337
  );
6839
7338
  trackWriteTool(createAudienceMapSetTool);
6840
7339
  const mapTool = server.registerTool(
@@ -6845,14 +7344,10 @@ Use map-slot to add ingredients. View at /empathy, /narrowing, and /jobs.`
6845
7344
  inputSchema: mapSchema,
6846
7345
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
6847
7346
  },
6848
- async ({ action, mapEntryId, title, templateId, description, slotIds, status }) => {
7347
+ withEnvelope(async ({ action, mapEntryId, title, templateId, description, slotIds, status }) => {
6849
7348
  if (action === "create") {
6850
7349
  if (!title) {
6851
- return {
6852
- content: [
6853
- { type: "text", text: "A `title` is required to create a map." }
6854
- ]
6855
- };
7350
+ return validationResult("A `title` is required to create a map.");
6856
7351
  }
6857
7352
  const result = await mcpMutation(
6858
7353
  "maps.createMap",
@@ -6873,19 +7368,28 @@ Use map-slot to add ingredients. View at /empathy, /narrowing, and /jobs.`
6873
7368
 
6874
7369
  Use \`map-slot action=add mapEntryId="${result.entryId}" slotId="problem" ingredientEntryId="..."\` to start filling slots.`
6875
7370
  }
6876
- ]
7371
+ ],
7372
+ structuredContent: success(
7373
+ `Created map '${title}' (${result.entryId}) using ${templateId} template.`,
7374
+ { entryId: result.entryId, title, templateId, status: "draft" },
7375
+ [{ tool: "map-slot", description: "Add ingredient to slot", parameters: { action: "add", mapEntryId: result.entryId } }]
7376
+ )
6877
7377
  };
6878
7378
  }
6879
7379
  if (action === "get") {
6880
7380
  if (!mapEntryId) {
6881
- return {
6882
- content: [{ type: "text", text: "A `mapEntryId` is required." }]
6883
- };
7381
+ return validationResult("A `mapEntryId` is required.");
6884
7382
  }
6885
7383
  const map = await mcpQuery("maps.getMap", { mapEntryId });
6886
7384
  if (!map) {
6887
7385
  return {
6888
- content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
7386
+ content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }],
7387
+ structuredContent: failure(
7388
+ "NOT_FOUND",
7389
+ `Map "${mapEntryId}" not found.`,
7390
+ "Use map action=list to find available maps.",
7391
+ [{ tool: "map", description: "List maps", parameters: { action: "list" } }]
7392
+ )
6889
7393
  };
6890
7394
  }
6891
7395
  return {
@@ -6904,7 +7408,12 @@ Use \`map-slot action=add mapEntryId="${result.entryId}" slotId="problem" ingred
6904
7408
 
6905
7409
  ` + slotSummary(map.slots)
6906
7410
  }
6907
- ]
7411
+ ],
7412
+ structuredContent: success(
7413
+ `${map.name}: ${map.completionScore}% complete (${map.filledSlots}/${map.totalSlots} slots), v${map.currentVersion}.`,
7414
+ { entryId: map.entryId, name: map.name, templateName: map.templateName, status: map.status, completionScore: map.completionScore, filledSlots: map.filledSlots, totalSlots: map.totalSlots, currentVersion: map.currentVersion },
7415
+ [{ tool: "map-slot", description: "Manage slot ingredients", parameters: { action: "list", mapEntryId } }]
7416
+ )
6908
7417
  };
6909
7418
  }
6910
7419
  if (action === "list") {
@@ -6914,32 +7423,29 @@ Use \`map-slot action=add mapEntryId="${result.entryId}" slotId="problem" ingred
6914
7423
  });
6915
7424
  if (!maps || maps.length === 0) {
6916
7425
  return {
6917
- content: [
6918
- {
6919
- type: "text",
6920
- text: 'No maps found. Create one with `map action=create title="My Canvas"`.'
6921
- }
6922
- ]
7426
+ content: [{ type: "text", text: 'No maps found. Create one with `map action=create title="My Canvas"`.' }],
7427
+ structuredContent: success(
7428
+ "No maps found.",
7429
+ { maps: [], total: 0 },
7430
+ [{ tool: "map", description: "Create a map", parameters: { action: "create" } }]
7431
+ )
6923
7432
  };
6924
7433
  }
6925
7434
  const lines = maps.map(
6926
7435
  (m) => `- **${m.name}** (\`${m.entryId}\`) \u2014 ${m.templateId}, ${m.completionScore}% complete, v${m.currentVersion}`
6927
7436
  );
6928
7437
  return {
6929
- content: [
6930
- {
6931
- type: "text",
6932
- text: `# Maps (${maps.length})
7438
+ content: [{ type: "text", text: `# Maps (${maps.length})
6933
7439
 
6934
- ${lines.join("\n")}`
6935
- }
6936
- ]
7440
+ ${lines.join("\n")}` }],
7441
+ structuredContent: success(
7442
+ `Found ${maps.length} map(s).`,
7443
+ { maps: maps.map((m) => ({ entryId: m.entryId, name: m.name, templateId: m.templateId, completionScore: m.completionScore })), total: maps.length }
7444
+ )
6937
7445
  };
6938
7446
  }
6939
- return {
6940
- content: [{ type: "text", text: `Unknown action: ${action}` }]
6941
- };
6942
- }
7447
+ return unknownAction(action, ["create", "get", "list"]);
7448
+ })
6943
7449
  );
6944
7450
  trackWriteTool(mapTool);
6945
7451
  const mapSlotTool = server.registerTool(
@@ -6950,7 +7456,7 @@ ${lines.join("\n")}`
6950
7456
  inputSchema: mapSlotSchema,
6951
7457
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
6952
7458
  },
6953
- async ({
7459
+ withEnvelope(async ({
6954
7460
  action,
6955
7461
  mapEntryId,
6956
7462
  slotId,
@@ -6963,116 +7469,88 @@ ${lines.join("\n")}`
6963
7469
  const map = await mcpQuery("maps.getMap", { mapEntryId });
6964
7470
  if (!map) {
6965
7471
  return {
6966
- content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
7472
+ content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }],
7473
+ structuredContent: failure(
7474
+ "NOT_FOUND",
7475
+ `Map "${mapEntryId}" not found.`,
7476
+ "Use map action=list to find available maps.",
7477
+ [{ tool: "map", description: "List maps", parameters: { action: "list" } }]
7478
+ )
6967
7479
  };
6968
7480
  }
6969
7481
  if (slotId && map.slots[slotId]) {
7482
+ const refs = map.slots[slotId];
6970
7483
  return {
6971
7484
  content: [
6972
7485
  {
6973
7486
  type: "text",
6974
7487
  text: `# Slot: ${slotId}
6975
7488
 
6976
- ` + (map.slots[slotId].length === 0 ? "(empty)" : map.slots[slotId].map(
7489
+ ` + (refs.length === 0 ? "(empty)" : refs.map(
6977
7490
  (r) => `- **${r.ingredientName ?? r.entryId}** (\`${r.entryId}\`)${r.pinnedVersion ? ` v${r.pinnedVersion}` : ""}`
6978
7491
  ).join("\n"))
6979
7492
  }
6980
- ]
7493
+ ],
7494
+ structuredContent: success(
7495
+ refs.length === 0 ? `Slot "${slotId}" is empty.` : `Slot "${slotId}": ${refs.length} ingredient(s).`,
7496
+ { mapEntryId, slotId, ingredients: refs, count: refs.length }
7497
+ )
6981
7498
  };
6982
7499
  }
6983
7500
  return {
6984
- content: [
6985
- { type: "text", text: `## All Slots
7501
+ content: [{ type: "text", text: `## All Slots
6986
7502
 
6987
- ${slotSummary(map.slots)}` }
6988
- ]
7503
+ ${slotSummary(map.slots)}` }],
7504
+ structuredContent: success(
7505
+ `${map.name}: ${Object.keys(map.slots).length} slots.`,
7506
+ { mapEntryId, slotCount: Object.keys(map.slots).length }
7507
+ )
6989
7508
  };
6990
7509
  }
6991
7510
  if (action === "add") {
6992
7511
  if (!slotId || !ingredientEntryId) {
6993
- return {
6994
- content: [
6995
- {
6996
- type: "text",
6997
- text: "Both `slotId` and `ingredientEntryId` are required for add."
6998
- }
6999
- ]
7000
- };
7512
+ return validationResult("Both `slotId` and `ingredientEntryId` are required for add.");
7001
7513
  }
7002
- await mcpMutation("maps.addToSlot", {
7003
- mapEntryId,
7004
- slotId,
7005
- ingredientEntryId,
7006
- label,
7007
- author
7008
- });
7514
+ await mcpMutation("maps.addToSlot", { mapEntryId, slotId, ingredientEntryId, label, author });
7009
7515
  return {
7010
- content: [
7011
- {
7012
- type: "text",
7013
- text: `Added \`${ingredientEntryId}\` to slot "${slotId}" on map \`${mapEntryId}\`.`
7014
- }
7015
- ]
7516
+ content: [{ type: "text", text: `Added \`${ingredientEntryId}\` to slot "${slotId}" on map \`${mapEntryId}\`.` }],
7517
+ structuredContent: success(
7518
+ `Added ${ingredientEntryId} to slot "${slotId}".`,
7519
+ { mapEntryId, slotId, ingredientEntryId },
7520
+ [{ tool: "map", description: "View updated map", parameters: { action: "get", mapEntryId } }]
7521
+ )
7016
7522
  };
7017
7523
  }
7018
7524
  if (action === "remove") {
7019
7525
  if (!slotId || !ingredientEntryId) {
7020
- return {
7021
- content: [
7022
- {
7023
- type: "text",
7024
- text: "Both `slotId` and `ingredientEntryId` are required for remove."
7025
- }
7026
- ]
7027
- };
7526
+ return validationResult("Both `slotId` and `ingredientEntryId` are required for remove.");
7028
7527
  }
7029
- await mcpMutation("maps.removeFromSlot", {
7030
- mapEntryId,
7031
- slotId,
7032
- ingredientEntryId,
7033
- author
7034
- });
7528
+ await mcpMutation("maps.removeFromSlot", { mapEntryId, slotId, ingredientEntryId, author });
7035
7529
  return {
7036
- content: [
7037
- {
7038
- type: "text",
7039
- text: `Removed \`${ingredientEntryId}\` from slot "${slotId}" on map \`${mapEntryId}\`.`
7040
- }
7041
- ]
7530
+ content: [{ type: "text", text: `Removed \`${ingredientEntryId}\` from slot "${slotId}" on map \`${mapEntryId}\`.` }],
7531
+ structuredContent: success(
7532
+ `Removed ${ingredientEntryId} from slot "${slotId}".`,
7533
+ { mapEntryId, slotId, ingredientEntryId },
7534
+ [{ tool: "map", description: "View updated map", parameters: { action: "get", mapEntryId } }]
7535
+ )
7042
7536
  };
7043
7537
  }
7044
7538
  if (action === "replace") {
7045
7539
  if (!slotId || !ingredientEntryId || !newIngredientEntryId) {
7046
- return {
7047
- content: [
7048
- {
7049
- type: "text",
7050
- text: "`slotId`, `ingredientEntryId`, and `newIngredientEntryId` are required for replace."
7051
- }
7052
- ]
7053
- };
7540
+ return validationResult("`slotId`, `ingredientEntryId`, and `newIngredientEntryId` are required for replace.");
7054
7541
  }
7055
- await mcpMutation("maps.replaceInSlot", {
7056
- mapEntryId,
7057
- slotId,
7058
- oldIngredientEntryId: ingredientEntryId,
7059
- newIngredientEntryId,
7060
- label,
7061
- author
7062
- });
7542
+ await mcpMutation("maps.replaceInSlot", { mapEntryId, slotId, oldIngredientEntryId: ingredientEntryId, newIngredientEntryId, label, author });
7063
7543
  return {
7064
- content: [
7065
- {
7066
- type: "text",
7067
- text: `Replaced \`${ingredientEntryId}\` with \`${newIngredientEntryId}\` in slot "${slotId}".`
7068
- }
7069
- ]
7544
+ content: [{ type: "text", text: `Replaced \`${ingredientEntryId}\` with \`${newIngredientEntryId}\` in slot "${slotId}".` }],
7545
+ structuredContent: success(
7546
+ `Replaced ${ingredientEntryId} with ${newIngredientEntryId} in slot "${slotId}".`,
7547
+ { mapEntryId, slotId, removed: ingredientEntryId, added: newIngredientEntryId },
7548
+ [{ tool: "map", description: "View updated map", parameters: { action: "get", mapEntryId } }]
7549
+ )
7070
7550
  };
7071
7551
  }
7072
- return {
7073
- content: [{ type: "text", text: `Unknown action: ${action}` }]
7074
- };
7075
- }
7552
+ return unknownAction(action, ["add", "remove", "replace", "list"]);
7553
+ })
7076
7554
  );
7077
7555
  trackWriteTool(mapSlotTool);
7078
7556
  const mapVersionTool = server.registerTool(
@@ -7083,7 +7561,7 @@ ${slotSummary(map.slots)}` }
7083
7561
  inputSchema: mapVersionSchema,
7084
7562
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
7085
7563
  },
7086
- async ({ action, mapEntryId, commitMessage, author }) => {
7564
+ withEnvelope(async ({ action, mapEntryId, commitMessage, author }) => {
7087
7565
  if (action === "commit") {
7088
7566
  const result = await mcpMutation("maps.commitMap", {
7089
7567
  mapEntryId,
@@ -7102,21 +7580,23 @@ ${slotSummary(map.slots)}` }
7102
7580
 
7103
7581
  All ingredient references have been pinned at their current versions.`
7104
7582
  }
7105
- ]
7583
+ ],
7584
+ structuredContent: success(
7585
+ `Committed map ${mapEntryId} at v${result.version} (${result.completionScore}% complete).`,
7586
+ { mapEntryId, version: result.version, completionScore: result.completionScore, slotsModified: result.slotsModified }
7587
+ )
7106
7588
  };
7107
7589
  }
7108
7590
  if (action === "list" || action === "history") {
7109
- const commits = await mcpQuery("maps.listMapCommits", {
7110
- mapEntryId
7111
- });
7591
+ const commits = await mcpQuery("maps.listMapCommits", { mapEntryId });
7112
7592
  if (!commits || commits.length === 0) {
7113
7593
  return {
7114
- content: [
7115
- {
7116
- type: "text",
7117
- text: `No commits yet for map \`${mapEntryId}\`. Use \`map-version action=commit\` to create the first snapshot.`
7118
- }
7119
- ]
7594
+ content: [{ type: "text", text: `No commits yet for map \`${mapEntryId}\`. Use \`map-version action=commit\` to create the first snapshot.` }],
7595
+ structuredContent: success(
7596
+ `No commits yet for ${mapEntryId}.`,
7597
+ { mapEntryId, commits: [], total: 0 },
7598
+ [{ tool: "map-version", description: "Commit the map", parameters: { action: "commit", mapEntryId } }]
7599
+ )
7120
7600
  };
7121
7601
  }
7122
7602
  const lines = commits.map(
@@ -7124,20 +7604,17 @@ All ingredient references have been pinned at their current versions.`
7124
7604
  Slots changed: ${c.slotsModified.join(", ")}` : "")
7125
7605
  );
7126
7606
  return {
7127
- content: [
7128
- {
7129
- type: "text",
7130
- text: `# Map History: \`${mapEntryId}\` (${commits.length} commits)
7607
+ content: [{ type: "text", text: `# Map History: \`${mapEntryId}\` (${commits.length} commits)
7131
7608
 
7132
- ${lines.join("\n")}`
7133
- }
7134
- ]
7609
+ ${lines.join("\n")}` }],
7610
+ structuredContent: success(
7611
+ `${commits.length} commit(s) for ${mapEntryId}.`,
7612
+ { mapEntryId, total: commits.length }
7613
+ )
7135
7614
  };
7136
7615
  }
7137
- return {
7138
- content: [{ type: "text", text: `Unknown action: ${action}` }]
7139
- };
7140
- }
7616
+ return unknownAction(action, ["commit", "list", "history"]);
7617
+ })
7141
7618
  );
7142
7619
  trackWriteTool(mapVersionTool);
7143
7620
  server.registerTool(
@@ -7148,22 +7625,25 @@ ${lines.join("\n")}`
7148
7625
  inputSchema: mapSuggestSchema,
7149
7626
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
7150
7627
  },
7151
- async ({ mapEntryId, slotId, query }) => {
7628
+ withEnvelope(async ({ mapEntryId, slotId, query }) => {
7152
7629
  const map = await mcpQuery("maps.getMap", { mapEntryId });
7153
7630
  if (!map) {
7154
7631
  return {
7155
- content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }]
7632
+ content: [{ type: "text", text: `Map "${mapEntryId}" not found.` }],
7633
+ structuredContent: failure(
7634
+ "NOT_FOUND",
7635
+ `Map "${mapEntryId}" not found.`,
7636
+ "Use map action=list to find available maps.",
7637
+ [{ tool: "map", description: "List maps", parameters: { action: "list" } }]
7638
+ )
7156
7639
  };
7157
7640
  }
7158
7641
  const emptySlots = slotId ? [slotId].filter((id) => (map.slots[id]?.length ?? 0) === 0) : Object.entries(map.slots).filter(([, refs]) => refs.length === 0).map(([id]) => id);
7159
7642
  if (emptySlots.length === 0) {
7643
+ const msg = slotId ? `Slot "${slotId}" already has ingredients.` : "All slots have ingredients. Nothing to suggest.";
7160
7644
  return {
7161
- content: [
7162
- {
7163
- type: "text",
7164
- text: slotId ? `Slot "${slotId}" already has ingredients.` : "All slots have ingredients. Nothing to suggest."
7165
- }
7166
- ]
7645
+ content: [{ type: "text", text: msg }],
7646
+ structuredContent: success(msg, { mapEntryId, emptySlots: 0 })
7167
7647
  };
7168
7648
  }
7169
7649
  const slotDefs = map.slotDefs ?? [];
@@ -7202,9 +7682,14 @@ _No matching entries found. Create ingredients first._`
7202
7682
 
7203
7683
  Use \`map-slot action=add mapEntryId="${mapEntryId}" slotId="..." ingredientEntryId="..."\` to add ingredients.`
7204
7684
  }
7205
- ]
7685
+ ],
7686
+ structuredContent: success(
7687
+ `Suggestions for ${emptySlots.length} empty slot(s) on "${map.name}".`,
7688
+ { mapEntryId, emptySlots: emptySlots.length },
7689
+ [{ tool: "map-slot", description: "Add ingredient", parameters: { action: "add", mapEntryId } }]
7690
+ )
7206
7691
  };
7207
- }
7692
+ })
7208
7693
  );
7209
7694
  }
7210
7695
 
@@ -7386,7 +7871,7 @@ function registerArchitectureTools(server) {
7386
7871
  inputSchema: architectureSchema,
7387
7872
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
7388
7873
  },
7389
- async ({ action, template, layer, flow }) => {
7874
+ withEnvelope(async ({ action, template, layer, flow }) => {
7390
7875
  await ensureCollection();
7391
7876
  const all = await listArchEntries();
7392
7877
  if (action === "show") {
@@ -7402,7 +7887,13 @@ function registerArchitectureTools(server) {
7402
7887
  content: [{
7403
7888
  type: "text",
7404
7889
  text: "# Architecture Explorer\n\nNo architecture data found. Use `architecture-admin action=seed` to populate the default architecture."
7405
- }]
7890
+ }],
7891
+ structuredContent: failure(
7892
+ "NOT_FOUND",
7893
+ "No architecture data found.",
7894
+ "Seed the default architecture first.",
7895
+ [{ tool: "architecture-admin", description: "Seed architecture", parameters: { action: "seed" } }]
7896
+ )
7406
7897
  };
7407
7898
  }
7408
7899
  const textLayers = layers.map((l) => formatLayerText(l, nodes)).join("\n\n");
@@ -7417,18 +7908,31 @@ ${textLayers}${textFlows}`;
7417
7908
  content: [
7418
7909
  { type: "text", text },
7419
7910
  { type: "resource", resource: { uri: `ui://product-os/architecture`, mimeType: "text/html", text: html } }
7420
- ]
7911
+ ],
7912
+ structuredContent: success(
7913
+ `Showing ${templateName}: ${layers.length} layers, ${nodes.length} nodes, ${flows.length} flows.`,
7914
+ { templateName, layerCount: layers.length, nodeCount: nodes.length, flowCount: flows.length }
7915
+ )
7421
7916
  };
7422
7917
  }
7423
7918
  if (action === "explore") {
7424
- if (!layer) return { content: [{ type: "text", text: "A `layer` is required for explore." }] };
7919
+ if (!layer) {
7920
+ return validationResult("A `layer` is required for explore.");
7921
+ }
7425
7922
  const layers = byTag(all, "layer");
7426
7923
  const target = layers.find(
7427
7924
  (l) => l.name.toLowerCase() === layer.toLowerCase() || l.entryId === layer
7428
7925
  );
7429
7926
  if (!target) {
7430
7927
  const available = layers.map((l) => `\`${l.name}\``).join(", ");
7431
- return { content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }] };
7928
+ return {
7929
+ content: [{ type: "text", text: `Layer "${layer}" not found. Available layers: ${available}` }],
7930
+ structuredContent: failure(
7931
+ "NOT_FOUND",
7932
+ `Layer "${layer}" not found.`,
7933
+ `Available layers: ${available}`
7934
+ )
7935
+ };
7432
7936
  }
7433
7937
  const nodes = byTag(all, "node").filter((n) => n.data?.layerRef === target.entryId);
7434
7938
  const flows = byTag(all, "flow").filter(
@@ -7459,18 +7963,31 @@ ${target.data?.description ?? ""}${depRule}${layerRationale}
7459
7963
  **${nodes.length} components**
7460
7964
 
7461
7965
  ${nodeDetail}${flowLines}`
7462
- }]
7966
+ }],
7967
+ structuredContent: success(
7968
+ `${target.name} layer: ${nodes.length} components, ${flows.length} connected flows.`,
7969
+ { layerName: target.name, entryId: target.entryId, nodeCount: nodes.length, flowCount: flows.length }
7970
+ )
7463
7971
  };
7464
7972
  }
7465
7973
  if (action === "flow") {
7466
- if (!flow) return { content: [{ type: "text", text: "A `flow` name or entry ID is required." }] };
7974
+ if (!flow) {
7975
+ return validationResult("A `flow` name or entry ID is required.");
7976
+ }
7467
7977
  const flows = byTag(all, "flow");
7468
7978
  const target = flows.find(
7469
7979
  (f) => f.name.toLowerCase() === flow.toLowerCase() || f.entryId === flow
7470
7980
  );
7471
7981
  if (!target) {
7472
7982
  const available = flows.map((f) => `\`${f.name}\``).join(", ");
7473
- return { content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }] };
7983
+ return {
7984
+ content: [{ type: "text", text: `Flow "${flow}" not found. Available flows: ${available}` }],
7985
+ structuredContent: failure(
7986
+ "NOT_FOUND",
7987
+ `Flow "${flow}" not found.`,
7988
+ `Available flows: ${available}`
7989
+ )
7990
+ };
7474
7991
  }
7475
7992
  const nodes = byTag(all, "node");
7476
7993
  const source = nodes.find((n) => n.entryId === target.data?.sourceNode);
@@ -7490,10 +8007,16 @@ ${nodeDetail}${flowLines}`
7490
8007
  lines.push("", `### Target: ${dest.name}`, String(dest.data?.description ?? ""));
7491
8008
  if (dest.data?.filePaths) lines.push(`Files: \`${dest.data.filePaths}\``);
7492
8009
  }
7493
- return { content: [{ type: "text", text: lines.join("\n") }] };
8010
+ return {
8011
+ content: [{ type: "text", text: lines.join("\n") }],
8012
+ structuredContent: success(
8013
+ `${target.name}: ${source?.name ?? "?"} \u2192 ${dest?.name ?? "?"}.`,
8014
+ { flowName: target.name, entryId: target.entryId, source: source?.name, target: dest?.name }
8015
+ )
8016
+ };
7494
8017
  }
7495
- return { content: [{ type: "text", text: "Unknown action." }] };
7496
- }
8018
+ return unknownAction(action, ["show", "explore", "flow"]);
8019
+ })
7497
8020
  );
7498
8021
  const archAdminTool = server.registerTool(
7499
8022
  "architecture-admin",
@@ -7503,7 +8026,7 @@ ${nodeDetail}${flowLines}`
7503
8026
  inputSchema: architectureAdminSchema,
7504
8027
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
7505
8028
  },
7506
- async ({ action }) => {
8029
+ withEnvelope(async ({ action }) => {
7507
8030
  if (action === "seed") {
7508
8031
  await ensureCollection();
7509
8032
  const existing = await listArchEntries();
@@ -7554,7 +8077,12 @@ ${nodeDetail}${flowLines}`
7554
8077
  **Unchanged:** ${unchanged}
7555
8078
 
7556
8079
  Use \`architecture action=show\` to view the map.`
7557
- }]
8080
+ }],
8081
+ structuredContent: success(
8082
+ `Architecture seeded: ${created} created, ${updated} updated, ${unchanged} unchanged.`,
8083
+ { created, updated, unchanged },
8084
+ [{ tool: "architecture", description: "View the map", parameters: { action: "show" } }]
8085
+ )
7558
8086
  };
7559
8087
  }
7560
8088
  if (action === "check") {
@@ -7564,7 +8092,12 @@ Use \`architecture action=show\` to view the map.`
7564
8092
  content: [{
7565
8093
  type: "text",
7566
8094
  text: "# Scan Failed\n\nCannot find project root (looked for `convex/schema.ts` in cwd and parent). Set `WORKSPACE_PATH` env var to the absolute path of the Product OS project root."
7567
- }]
8095
+ }],
8096
+ structuredContent: failure(
8097
+ "VALIDATION_ERROR",
8098
+ "Cannot find project root.",
8099
+ "Set WORKSPACE_PATH env var to the absolute path of the Product OS project root."
8100
+ )
7568
8101
  };
7569
8102
  }
7570
8103
  await ensureCollection();
@@ -7572,10 +8105,21 @@ Use \`architecture action=show\` to view the map.`
7572
8105
  const layers = byTag(all, "layer");
7573
8106
  const nodes = byTag(all, "node");
7574
8107
  const result = scanDependencies(projectRoot, layers, nodes);
7575
- return { content: [{ type: "text", text: formatScanReport(result) }] };
8108
+ return {
8109
+ content: [{ type: "text", text: formatScanReport(result) }],
8110
+ structuredContent: success(
8111
+ result.violations.length === 0 ? `Health check passed: 0 violations across ${result.filesScanned} files.` : `Health check found ${result.violations.length} violation(s) across ${result.filesScanned} files.`,
8112
+ {
8113
+ violations: result.violations.length,
8114
+ filesScanned: result.filesScanned,
8115
+ importsChecked: result.importsChecked,
8116
+ unmappedImports: result.unmappedImports
8117
+ }
8118
+ )
8119
+ };
7576
8120
  }
7577
- return { content: [{ type: "text", text: "Unknown action." }] };
7578
- }
8121
+ return unknownAction(action, ["seed", "check"]);
8122
+ })
7579
8123
  );
7580
8124
  trackWriteTool(archAdminTool);
7581
8125
  }
@@ -7937,15 +8481,19 @@ async function handleHealthCheck() {
7937
8481
  lines.push(`- ${err}`);
7938
8482
  }
7939
8483
  }
8484
+ const healthData = {
8485
+ healthy,
8486
+ collections: collections.length,
8487
+ entries: totalEntries,
8488
+ latencyMs: durationMs,
8489
+ workspace: workspaceId ?? "unresolved"
8490
+ };
7940
8491
  return {
7941
8492
  content: [{ type: "text", text: lines.join("\n") }],
7942
- structuredContent: {
7943
- healthy,
7944
- collections: collections.length,
7945
- entries: totalEntries,
7946
- latencyMs: durationMs,
7947
- workspace: workspaceId ?? "unresolved"
7948
- }
8493
+ structuredContent: success(
8494
+ healthy ? `Healthy. ${collections.length} collections, ${totalEntries} entries, ${durationMs}ms.` : `Degraded. ${errors.length} error(s).`,
8495
+ healthData
8496
+ )
7949
8497
  };
7950
8498
  }
7951
8499
  async function handleWhoami() {
@@ -7962,13 +8510,10 @@ async function handleWhoami() {
7962
8510
  ];
7963
8511
  return {
7964
8512
  content: [{ type: "text", text: lines.join("\n") }],
7965
- structuredContent: {
7966
- workspaceId: ctx.workspaceId,
7967
- workspaceName: ctx.workspaceName,
7968
- scope,
7969
- sessionId,
7970
- oriented
7971
- }
8513
+ structuredContent: success(
8514
+ `Session: ${ctx.workspaceName} (${scope}). ${oriented ? "Oriented." : "Not oriented."}`,
8515
+ { workspaceId: ctx.workspaceId, workspaceName: ctx.workspaceName, scope, sessionId, oriented }
8516
+ )
7972
8517
  };
7973
8518
  }
7974
8519
  var STAGE_LABELS = {
@@ -8021,31 +8566,32 @@ async function handleWorkspaceStatus() {
8021
8566
  lines.push(`- [x] ${check.label} (${check.current}/${check.required})`);
8022
8567
  }
8023
8568
  }
8569
+ const statusData = {
8570
+ stage,
8571
+ scoringVersion,
8572
+ readinessScore: score,
8573
+ activeEntries: stats.activeCount,
8574
+ totalRelations: stats.totalRelations,
8575
+ orphanedEntries: stats.orphanedCount,
8576
+ gaps: gaps.map((g) => ({
8577
+ id: g.id,
8578
+ label: g.label,
8579
+ guidance: g.capabilityGuidance ?? g.guidance
8580
+ }))
8581
+ };
8024
8582
  return {
8025
8583
  content: [{ type: "text", text: lines.join("\n") }],
8026
- structuredContent: {
8027
- stage,
8028
- scoringVersion,
8029
- readinessScore: score,
8030
- activeEntries: stats.activeCount,
8031
- totalRelations: stats.totalRelations,
8032
- orphanedEntries: stats.orphanedCount,
8033
- gaps: gaps.map((g) => ({
8034
- id: g.id,
8035
- label: g.label,
8036
- guidance: g.capabilityGuidance ?? g.guidance
8037
- }))
8038
- }
8584
+ structuredContent: success(
8585
+ `Brain: ${stageLabel} (${score}%). ${stats.activeCount} active entries, ${gaps.length} gap(s).`,
8586
+ statusData
8587
+ )
8039
8588
  };
8040
8589
  }
8041
8590
  async function handleAudit(limit) {
8042
8591
  const log = getAuditLog();
8043
8592
  const recent = log.slice(-limit);
8044
8593
  if (recent.length === 0) {
8045
- return {
8046
- content: [{ type: "text", text: "No calls recorded yet this session." }],
8047
- structuredContent: { totalCalls: 0, calls: [] }
8048
- };
8594
+ return successResult("No calls recorded yet this session.", "No calls recorded yet this session.", { totalCalls: 0, calls: [] });
8049
8595
  }
8050
8596
  const summary = buildSessionSummary(log);
8051
8597
  const logLines = [`# Audit Log (last ${recent.length} of ${log.length} total)
@@ -8056,21 +8602,25 @@ async function handleAudit(limit) {
8056
8602
  const toolPart = entry.toolContext ? ` [${entry.toolContext.tool}${entry.toolContext.action ? ` action=${entry.toolContext.action}` : ""}]` : "";
8057
8603
  logLines.push(`${icon} \`${entry.fn}\`${toolPart} ${entry.durationMs}ms ${entry.status}${errPart}`);
8058
8604
  }
8605
+ const auditData = {
8606
+ totalCalls: log.length,
8607
+ calls: recent.map((entry) => ({
8608
+ tool: entry.fn,
8609
+ ...entry.toolContext?.action && { action: entry.toolContext.action },
8610
+ timestamp: entry.ts,
8611
+ ...entry.durationMs != null && { durationMs: entry.durationMs }
8612
+ }))
8613
+ };
8059
8614
  return {
8060
8615
  content: [{ type: "text", text: `${summary}
8061
8616
 
8062
8617
  ---
8063
8618
 
8064
8619
  ${logLines.join("\n")}` }],
8065
- structuredContent: {
8066
- totalCalls: log.length,
8067
- calls: recent.map((entry) => ({
8068
- tool: entry.fn,
8069
- ...entry.toolContext?.action && { action: entry.toolContext.action },
8070
- timestamp: entry.ts,
8071
- ...entry.durationMs != null && { durationMs: entry.durationMs }
8072
- }))
8073
- }
8620
+ structuredContent: success(
8621
+ `Audit: ${log.length} total calls, showing last ${recent.length}.`,
8622
+ auditData
8623
+ )
8074
8624
  };
8075
8625
  }
8076
8626
  var HEALTH_ACTIONS = ["check", "whoami", "status", "audit", "self-test"];
@@ -8199,7 +8749,10 @@ function handleSelfTest() {
8199
8749
  }
8200
8750
  return {
8201
8751
  content: [{ type: "text", text: lines.join("\n") }],
8202
- structuredContent: { passed, failed, total, results }
8752
+ structuredContent: success(
8753
+ failed === 0 ? `Self-test: all ${total} schemas valid.` : `Self-test: ${failed}/${total} schemas failed.`,
8754
+ { passed, failed, total, results }
8755
+ )
8203
8756
  };
8204
8757
  }
8205
8758
  function registerHealthTools(server) {
@@ -8211,12 +8764,9 @@ function registerHealthTools(server) {
8211
8764
  inputSchema: healthSchema,
8212
8765
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
8213
8766
  },
8214
- async (args) => {
8215
- const parsed = healthSchema.safeParse(args ?? {});
8216
- if (!parsed.success) {
8217
- const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
8218
- return { content: [{ type: "text", text: `Invalid arguments: ${issues}` }] };
8219
- }
8767
+ withEnvelope(async (args) => {
8768
+ const parsed = parseOrFail(healthSchema, args);
8769
+ if (!parsed.ok) return parsed.result;
8220
8770
  const { action, limit } = parsed.data;
8221
8771
  return runWithToolContext({ tool: "health", action }, async () => {
8222
8772
  if (action === "check") return handleHealthCheck();
@@ -8224,11 +8774,9 @@ function registerHealthTools(server) {
8224
8774
  if (action === "status") return handleWorkspaceStatus();
8225
8775
  if (action === "audit") return handleAudit(limit ?? 20);
8226
8776
  if (action === "self-test") return handleSelfTest();
8227
- return {
8228
- content: [{ type: "text", text: `Unknown action '${action}'. Valid actions: ${HEALTH_ACTIONS.join(", ")}.` }]
8229
- };
8777
+ return unknownAction(action, HEALTH_ACTIONS);
8230
8778
  });
8231
- }
8779
+ })
8232
8780
  );
8233
8781
  server.registerTool(
8234
8782
  "orient",
@@ -8238,13 +8786,16 @@ function registerHealthTools(server) {
8238
8786
  inputSchema: orientSchema,
8239
8787
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
8240
8788
  },
8241
- async ({ mode = "full", task } = {}) => {
8789
+ withEnvelope(async ({ mode = "full", task } = {}) => {
8242
8790
  const errors = [];
8243
8791
  const agentSessionId = getAgentSessionId();
8244
8792
  if (isSessionOriented() && mode === "brief" && !task) {
8245
8793
  return {
8246
8794
  content: [{ type: "text", text: "Already oriented. Session active, writes unlocked. Use `orient mode='full'` or `orient task='...'` for full context." }],
8247
- structuredContent: { alreadyOriented: true, sessionId: agentSessionId }
8795
+ structuredContent: success(
8796
+ "Already oriented. Session active, writes unlocked.",
8797
+ { alreadyOriented: true, sessionId: agentSessionId }
8798
+ )
8248
8799
  };
8249
8800
  }
8250
8801
  let wsCtx = null;
@@ -8310,13 +8861,11 @@ function registerHealthTools(server) {
8310
8861
  }
8311
8862
  return {
8312
8863
  content: [{ type: "text", text: scanLines.join("\n") }],
8313
- structuredContent: {
8314
- stage: "blank",
8315
- readinessScore: readiness?.score ?? 0,
8316
- oriented,
8317
- orientationStatus,
8318
- redirectHint: "Use the `start` tool for the full guided setup experience."
8319
- }
8864
+ structuredContent: success(
8865
+ "Workspace is blank. Use the start tool for guided setup.",
8866
+ { stage: "blank", readinessScore: readiness?.score ?? 0, oriented, orientationStatus, redirectHint: "Use the `start` tool for the full guided setup experience." },
8867
+ [{ tool: "start", description: "Guided workspace setup", parameters: {} }]
8868
+ )
8320
8869
  };
8321
8870
  }
8322
8871
  const lines = [];
@@ -8390,7 +8939,13 @@ function registerHealthTools(server) {
8390
8939
  lines.push("");
8391
8940
  for (const err of errors) lines.push(`- ${err}`);
8392
8941
  }
8393
- return { content: [{ type: "text", text: lines.join("\n") }] };
8942
+ return {
8943
+ content: [{ type: "text", text: lines.join("\n") }],
8944
+ structuredContent: success(
8945
+ `Oriented (brief). Stage: ${readiness?.stage ?? "unknown"}.`,
8946
+ { mode: "brief", stage: readiness?.stage ?? "unknown", oriented: isSessionOriented(), sessionId: agentSessionId }
8947
+ )
8948
+ };
8394
8949
  }
8395
8950
  const orientStage = readiness?.stage ?? "seeded";
8396
8951
  if (isLowReadiness && wsCtx?.createdAt) {
@@ -8628,8 +9183,14 @@ function registerHealthTools(server) {
8628
9183
  lines.push("---");
8629
9184
  lines.push("_No active agent session. Call `session action=start` to begin a tracked session._");
8630
9185
  }
8631
- return { content: [{ type: "text", text: lines.join("\n") }] };
8632
- }
9186
+ return {
9187
+ content: [{ type: "text", text: lines.join("\n") }],
9188
+ structuredContent: success(
9189
+ `Oriented (full). Stage: ${orientStage}. ${isLowReadiness ? "Low readiness \u2014 gaps remain." : "Ready."}`,
9190
+ { mode: "full", stage: orientStage, oriented: isSessionOriented(), sessionId: agentSessionId }
9191
+ )
9192
+ };
9193
+ })
8633
9194
  );
8634
9195
  }
8635
9196
 
@@ -9955,4 +10516,4 @@ export {
9955
10516
  SERVER_VERSION,
9956
10517
  createProductBrainServer
9957
10518
  };
9958
- //# sourceMappingURL=chunk-ADLZPAEI.js.map
10519
+ //# sourceMappingURL=chunk-FMEZXUP5.js.map