borgmcp 4.1.0 → 4.2.1

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.
@@ -63,10 +63,26 @@ async function requireActiveCube(deps: DroneManagementDeps): Promise<ActiveCube>
63
63
  return active;
64
64
  }
65
65
 
66
+ // gh#492: tool helpers return the mutation receipt data alongside the rendered
67
+ // text so the MCP handler can emit structuredContent from the same source.
68
+ export interface ReassignDroneToolResult {
69
+ text: string;
70
+ drone: { id: string; cube_id: string; role_id: string; label: string };
71
+ roleName: string;
72
+ cubeName: string;
73
+ }
74
+
75
+ export interface EvictDroneToolResult {
76
+ text: string;
77
+ droneId: string;
78
+ label: string;
79
+ cubeName: string;
80
+ }
81
+
66
82
  export async function runReassignDroneTool(
67
83
  input: { droneId: unknown; roleId: unknown },
68
84
  deps: DroneManagementDeps = defaultDeps,
69
- ): Promise<string> {
85
+ ): Promise<ReassignDroneToolResult> {
70
86
  if (typeof input.droneId !== 'string' || input.droneId.length === 0) {
71
87
  throw new Error('drone_id is required');
72
88
  }
@@ -113,19 +129,24 @@ export async function runReassignDroneTool(
113
129
  }
114
130
  }
115
131
 
116
- return formatReassignDroneSuccess({
117
- droneLabel: drone.label,
118
- cubeName: cube.name,
132
+ return {
133
+ text: formatReassignDroneSuccess({
134
+ droneLabel: drone.label,
135
+ cubeName: cube.name,
136
+ roleName: role.name,
137
+ droneId: drone.id,
138
+ roleId: drone.role_id,
139
+ }) + warning,
140
+ drone: { id: drone.id, cube_id: drone.cube_id, role_id: drone.role_id, label: drone.label },
119
141
  roleName: role.name,
120
- droneId: drone.id,
121
- roleId: drone.role_id,
122
- }) + warning;
142
+ cubeName: cube.name,
143
+ };
123
144
  }
124
145
 
125
146
  export async function runEvictDroneTool(
126
147
  args: Record<string, unknown> | undefined,
127
148
  deps: DroneManagementDeps = defaultDeps,
128
- ): Promise<string> {
149
+ ): Promise<EvictDroneToolResult> {
129
150
  const droneIdInput = typeof args?.drone_id === 'string' ? args.drone_id : undefined;
130
151
  const labelInput = typeof args?.label === 'string' ? args.label.trim() : undefined;
131
152
  const cubeIdInput = typeof args?.cube_id === 'string' ? args.cube_id : undefined;
@@ -164,5 +185,10 @@ export async function runEvictDroneTool(
164
185
  '[LOCAL-MANAGE-COMMIT-UNCONFIRMED] The server returned a successful eviction response for an unexpected drone. The eviction may already be committed. Do not retry blindly; inspect the cube roster first.',
165
186
  );
166
187
  }
167
- return formatEvictDroneSuccess(priorDrone.label, cube.name);
188
+ return {
189
+ text: formatEvictDroneSuccess(priorDrone.label, cube.name),
190
+ droneId: priorDrone.id,
191
+ label: priorDrone.label,
192
+ cubeName: cube.name,
193
+ };
168
194
  }
package/src/index.ts CHANGED
@@ -253,6 +253,22 @@ export function formatUpdatedRoleResult(role: { name: string; id: string; role_c
253
253
  return appendServerAdvisory(`Updated role **${role.name}**${tag} (id: ${role.id}).`, advisory);
254
254
  }
255
255
 
256
+ // gh#496: the unread drain runs on every wake, so its structured payload must
257
+ // stay proportional to the entries it returns. Rosters are deliberately NOT
258
+ // included — entries already carry drone_label/role_name, and borg_roster
259
+ // serves identity mapping when a consumer needs it.
260
+ export function buildReadLogStructuredContent(input: {
261
+ entries: unknown[];
262
+ behind_by: unknown;
263
+ has_more: unknown;
264
+ }): { entries: unknown[]; behind_by: number | null; has_more: boolean } {
265
+ return {
266
+ entries: input.entries,
267
+ behind_by: typeof input.behind_by === 'number' ? input.behind_by : null,
268
+ has_more: input.has_more === true,
269
+ };
270
+ }
271
+
256
272
  export function formatPatchedRoleSectionResult(action: 'replace' | 'insert' | 'delete', heading: string, role: { name: string; id: string }, advisory?: unknown): string {
257
273
  const verb = action === 'replace' ? 'Replaced' : action === 'insert' ? 'Inserted' : 'Deleted';
258
274
  return appendServerAdvisory(`${verb} section **${heading}** in role **${role.name}** (id: ${role.id}).`, advisory);
@@ -419,8 +435,15 @@ export async function main() {
419
435
  isError: true,
420
436
  };
421
437
  }
438
+ const described = {
439
+ name: def.name,
440
+ description: def.description,
441
+ inputSchema: def.inputSchema,
442
+ outputSchema: def.outputSchema ?? null,
443
+ };
422
444
  return {
423
- content: [{ type: 'text', text: JSON.stringify({ name: def.name, description: def.description, inputSchema: def.inputSchema }, null, 2) }],
445
+ content: [{ type: 'text', text: JSON.stringify(described, null, 2) }],
446
+ structuredContent: described,
424
447
  };
425
448
  }
426
449
 
@@ -471,6 +494,7 @@ export async function main() {
471
494
  text: 'Not connected to a cube. Use `borg_assimilate cube_name="<name>"` to join one.',
472
495
  },
473
496
  ],
497
+ structuredContent: { connected: false },
474
498
  };
475
499
  }
476
500
  seedDisplayIdentity(active);
@@ -524,10 +548,12 @@ export async function main() {
524
548
 
525
549
  // gh#285: version-mismatch nudge when on-disk package is newer.
526
550
  let versionHeader = '';
551
+ let onDiskVersion: string | null = null;
527
552
  try {
528
553
  const running = getPackageVersion();
529
554
  const onDisk = getOnDiskVersion();
530
555
  if (running !== 'unknown' && onDisk !== 'unknown' && onDisk !== running) {
556
+ onDiskVersion = onDisk;
531
557
  const [rMaj, rMin, rPat] = running.split('.').map(Number);
532
558
  const [dMaj, dMin, dPat] = onDisk.split('.').map(Number);
533
559
  const onDiskNewer = dMaj > rMaj || (dMaj === rMaj && dMin > rMin) || (dMaj === rMaj && dMin === rMin && dPat > rPat);
@@ -537,7 +563,21 @@ export async function main() {
537
563
  }
538
564
  } catch { /* never break regen */ }
539
565
 
540
- return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(displayedResult, { mode }) }] };
566
+ return {
567
+ content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(displayedResult, { mode }) }],
568
+ structuredContent: {
569
+ connected: true,
570
+ mode,
571
+ cube: displayedResult.cube,
572
+ drone: displayedResult.drone,
573
+ role: displayedResult.role,
574
+ behind_by: typeof displayedResult.behind_by === 'number' ? displayedResult.behind_by : null,
575
+ decision_topics: (displayedResult.decisions ?? []).map((d: any) => d.topic),
576
+ running_version: getPackageVersion(),
577
+ on_disk_version: onDiskVersion,
578
+ wake_path_healthy: inboxMonitorHealthy,
579
+ },
580
+ };
541
581
  }
542
582
 
543
583
  case 'borg_assimilate': {
@@ -583,6 +623,11 @@ export async function main() {
583
623
  ].join('\n');
584
624
  return {
585
625
  content: [{ type: 'text', text: header + formatRegenMarkdown(displayedResult, { mode: 'full' }) }],
626
+ structuredContent: {
627
+ reattached: true,
628
+ cube_name: displayIdentity.cubeName,
629
+ drone_label: displayIdentity.droneLabel,
630
+ },
586
631
  };
587
632
  } catch (err: any) {
588
633
  markDisplayIdentityReadFailed(active!);
@@ -592,7 +637,10 @@ export async function main() {
592
637
  }
593
638
 
594
639
  case 'borg_version': {
595
- return { content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }] };
640
+ return {
641
+ content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }],
642
+ structuredContent: { version: getPackageVersion() },
643
+ };
596
644
  }
597
645
 
598
646
  case 'borg_playbook': {
@@ -615,7 +663,14 @@ export async function main() {
615
663
  : topic
616
664
  ? `No exact match for "${topic}". Full Borg MCP docs index — WebFetch the URL you need:`
617
665
  : `Borg MCP docs index — WebFetch the URL of the section you need:`;
618
- return { content: [{ type: 'text', text: `${header}\n\n${formatDocsIndex(sections)}` }] };
666
+ return {
667
+ content: [{ type: 'text', text: `${header}\n\n${formatDocsIndex(sections)}` }],
668
+ structuredContent: {
669
+ topic: topic || null,
670
+ matched: matched.length > 0,
671
+ sections: sections.map((s) => ({ slug: s.slug, title: s.title, url: s.url, summary: s.summary })),
672
+ },
673
+ };
619
674
  }
620
675
 
621
676
  case 'borg_whoami': {
@@ -644,7 +699,10 @@ export async function main() {
644
699
  // The invocation-local server truth remains authoritative even if
645
700
  // the display-only persistence refresh cannot be written.
646
701
  }
647
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
702
+ return {
703
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
704
+ structuredContent: { ...result },
705
+ };
648
706
  }
649
707
 
650
708
  case 'borg_cube': {
@@ -697,7 +755,10 @@ export async function main() {
697
755
  }
698
756
  lines.push('');
699
757
  lines.push(getDronePlaybook());
700
- return { content: [{ type: 'text', text: lines.join('\n') }] };
758
+ return {
759
+ content: [{ type: 'text', text: lines.join('\n') }],
760
+ structuredContent: { cube, roles },
761
+ };
701
762
  }
702
763
 
703
764
  case 'borg_role': {
@@ -718,7 +779,7 @@ export async function main() {
718
779
  ``,
719
780
  role.detailed_description || '_(no detailed description set)_',
720
781
  ].join('\n');
721
- return { content: [{ type: 'text', text }] };
782
+ return { content: [{ type: 'text', text }], structuredContent: { role } };
722
783
  }
723
784
  const { role } = await getRoleInfo(
724
785
  active.sessionToken,
@@ -730,7 +791,7 @@ export async function main() {
730
791
  ``,
731
792
  role.detailed_description || '_(no detailed description set)_',
732
793
  ].join('\n');
733
- return { content: [{ type: 'text', text }] };
794
+ return { content: [{ type: 'text', text }], structuredContent: { role } };
734
795
  }
735
796
 
736
797
  case 'borg_role-rationale': {
@@ -749,7 +810,10 @@ export async function main() {
749
810
  '',
750
811
  result.body || '_(empty)_',
751
812
  ].join('\n');
752
- return { content: [{ type: 'text', text }] };
813
+ return {
814
+ content: [{ type: 'text', text }],
815
+ structuredContent: { role: result.role, section: result.section, body: result.body },
816
+ };
753
817
  }
754
818
 
755
819
  case 'borg_roster': {
@@ -763,7 +827,10 @@ export async function main() {
763
827
  resolvedSince: resolvedSince ?? null,
764
828
  humanAgo,
765
829
  });
766
- return { content: [{ type: 'text', text }] };
830
+ return {
831
+ content: [{ type: 'text', text }],
832
+ structuredContent: { cube_name: active.name, drones, roles, since: resolvedSince ?? null },
833
+ };
767
834
  }
768
835
 
769
836
  case 'borg_stream-status': {
@@ -803,7 +870,18 @@ export async function main() {
803
870
  cubeName: active?.name ?? null,
804
871
  humanAgo,
805
872
  });
806
- return { content: [{ type: 'text', text: silentInertWarning + text }] };
873
+ return {
874
+ content: [{ type: 'text', text: silentInertWarning + text }],
875
+ structuredContent: {
876
+ status,
877
+ wake_path: wakePath,
878
+ inbox_monitor_healthy: inboxMonitorHealthy,
879
+ inbox_path: inboxPath,
880
+ monitor_state_root: monitorStateRoot,
881
+ drone_label: active?.droneLabel ?? null,
882
+ cube_name: active?.name ?? null,
883
+ },
884
+ };
807
885
  }
808
886
 
809
887
  case 'borg_read-log': {
@@ -849,7 +927,10 @@ export async function main() {
849
927
  `⚠ behind_by: ${behind_by} more unread ${behind_by === 1 ? 'entry' : 'entries'} addressed to you — call \`borg_read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`
850
928
  );
851
929
  }
852
- return { content: [{ type: 'text', text: lines.join('\n') }] };
930
+ return {
931
+ content: [{ type: 'text', text: lines.join('\n') }],
932
+ structuredContent: buildReadLogStructuredContent({ entries, behind_by, has_more }),
933
+ };
853
934
  }
854
935
 
855
936
  case 'borg_read-entry': {
@@ -867,19 +948,28 @@ export async function main() {
867
948
  '',
868
949
  formatLogEntryMarkdown(entry, droneById, roleById),
869
950
  ].join('\n');
870
- return { content: [{ type: 'text', text }] };
951
+ return {
952
+ content: [{ type: 'text', text }],
953
+ structuredContent: { entry, drones, roles },
954
+ };
871
955
  }
872
956
 
873
957
  case 'borg_put-document': {
874
958
  const active = await requireActiveCube();
875
959
  const result = await putDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
876
- return { content: [{ type: 'text', text: `Created cube document.\n\n${formatDocument(result.document)}` }] };
960
+ return {
961
+ content: [{ type: 'text', text: `Created cube document.\n\n${formatDocument(result.document)}` }],
962
+ structuredContent: { document: result.document },
963
+ };
877
964
  }
878
965
 
879
966
  case 'borg_get-document': {
880
967
  const active = await requireActiveCube();
881
968
  const result = await getDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
882
- return { content: [{ type: 'text', text: formatDocument(result.document) }] };
969
+ return {
970
+ content: [{ type: 'text', text: formatDocument(result.document) }],
971
+ structuredContent: { document: result.document },
972
+ };
883
973
  }
884
974
 
885
975
  case 'borg_list-documents': {
@@ -888,13 +978,19 @@ export async function main() {
888
978
  const text = result.documents.length === 0
889
979
  ? `No active or superseded documents in cube "${active.name}".`
890
980
  : result.documents.map(formatDocumentMetadata).join('\n\n');
891
- return { content: [{ type: 'text', text }] };
981
+ return {
982
+ content: [{ type: 'text', text }],
983
+ structuredContent: { documents: result.documents },
984
+ };
892
985
  }
893
986
 
894
987
  case 'borg_remove-document': {
895
988
  const active = await requireActiveCube();
896
989
  const result = await removeDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
897
- return { content: [{ type: 'text', text: `Removed cube document.\n\n${formatDocumentMetadata(result.document)}` }] };
990
+ return {
991
+ content: [{ type: 'text', text: `Removed cube document.\n\n${formatDocumentMetadata(result.document)}` }],
992
+ structuredContent: { document: result.document },
993
+ };
898
994
  }
899
995
 
900
996
  case 'borg_log': {
@@ -918,6 +1014,13 @@ export async function main() {
918
1014
  text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${displayIdentity.droneLabel}; recent cube log already contains this signal.`,
919
1015
  },
920
1016
  ],
1017
+ structuredContent: {
1018
+ suppressed: true,
1019
+ entry: null,
1020
+ recipients: [],
1021
+ unreachable_recipients: [],
1022
+ advisory: null,
1023
+ },
921
1024
  };
922
1025
  }
923
1026
  }
@@ -962,7 +1065,16 @@ export async function main() {
962
1065
  ? `\nAdvisory: this message exceeded ${result.advisory.threshold_bytes} UTF-8 bytes. Store durable detail with borg_put-document, then cite its full id in borg_log.documents with an explicit borg_log.to audience.`
963
1066
  : '';
964
1067
  const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${routed}${unreachable}${citations}${advisory}`;
965
- return { content: [{ type: 'text', text }] };
1068
+ return {
1069
+ content: [{ type: 'text', text }],
1070
+ structuredContent: {
1071
+ suppressed: false,
1072
+ entry: result.entry,
1073
+ recipients: routedRecipients,
1074
+ unreachable_recipients: result.unreachableRecipients ?? [],
1075
+ advisory: result.advisory ?? null,
1076
+ },
1077
+ };
966
1078
  }
967
1079
 
968
1080
  case 'borg_ack': {
@@ -992,6 +1104,7 @@ export async function main() {
992
1104
  : `Acked entry ${entryId} in cube "${active.name}".`,
993
1105
  },
994
1106
  ],
1107
+ structuredContent: { entry_id: entryId, kind, cube_name: active.name },
995
1108
  };
996
1109
  }
997
1110
 
@@ -1003,7 +1116,10 @@ export async function main() {
1003
1116
  args ?? {},
1004
1117
  active.serverTrustIdentity,
1005
1118
  );
1006
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1119
+ return {
1120
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1121
+ structuredContent: { ...result },
1122
+ };
1007
1123
  }
1008
1124
 
1009
1125
  case 'borg_decide': {
@@ -1031,6 +1147,7 @@ export async function main() {
1031
1147
  text: `Recorded ratified decision on "${topic}" in cube "${active.name}"${superseded}. Cite it via borg_decisions; it surfaces in borg_regen.`,
1032
1148
  },
1033
1149
  ],
1150
+ structuredContent: { decision: row, superseded: Boolean(row?.supersedes), cube_name: active.name },
1034
1151
  };
1035
1152
  }
1036
1153
 
@@ -1051,7 +1168,10 @@ export async function main() {
1051
1168
  : decisions
1052
1169
  .map((d: any) => `**${d.topic}:** ${d.decision}${d.rationale ? ` — ${d.rationale}` : ''}`)
1053
1170
  .join('\n');
1054
- return { content: [{ type: 'text', text }] };
1171
+ return {
1172
+ content: [{ type: 'text', text }],
1173
+ structuredContent: { decisions },
1174
+ };
1055
1175
  }
1056
1176
 
1057
1177
  case 'borg_remove-decision': {
@@ -1073,18 +1193,25 @@ export async function main() {
1073
1193
  type: 'text',
1074
1194
  text: `Removed the active ratified decision on "${decision.topic}" from cube "${active.name}".`,
1075
1195
  }],
1196
+ structuredContent: { decision, cube_name: active.name },
1076
1197
  };
1077
1198
  }
1078
1199
 
1079
1200
  case 'borg_list-cubes': {
1080
1201
  const { cubes } = await listCubes();
1081
1202
  if (!cubes.length) {
1082
- return { content: [{ type: 'text', text: 'No cubes yet. Use borg_create-cube to make your first one.' }] };
1203
+ return {
1204
+ content: [{ type: 'text', text: 'No cubes yet. Use borg_create-cube to make your first one.' }],
1205
+ structuredContent: { cubes: [] },
1206
+ };
1083
1207
  }
1084
1208
  const lines = cubes.map((c: any) =>
1085
1209
  `- **${c.name}** (id: ${c.id})\n ${(c.cube_directive || '_(no directive set)_').split('\n')[0].slice(0, 120)}`
1086
1210
  );
1087
- return { content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }] };
1211
+ return {
1212
+ content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }],
1213
+ structuredContent: { cubes },
1214
+ };
1088
1215
  }
1089
1216
 
1090
1217
  case 'borg_create-cube': {
@@ -1126,10 +1253,21 @@ export async function main() {
1126
1253
  ? ' Template cube directive applied (operator passed empty).'
1127
1254
  : '';
1128
1255
  const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
1129
- return { content: [{ type: 'text', text }] };
1256
+ return {
1257
+ content: [{ type: 'text', text }],
1258
+ structuredContent: {
1259
+ cube,
1260
+ template: templateName,
1261
+ roles_created: summary.created,
1262
+ roles_updated: summary.updated,
1263
+ },
1264
+ };
1130
1265
  }
1131
1266
  const text = `Created cube **${cube.name}** (id: ${cube.id}). A default "Drone" role was seeded — rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${cube.name} to join as a drone.`;
1132
- return { content: [{ type: 'text', text }] };
1267
+ return {
1268
+ content: [{ type: 'text', text }],
1269
+ structuredContent: { cube, template: null, roles_created: null, roles_updated: null },
1270
+ };
1133
1271
  }
1134
1272
 
1135
1273
  case 'borg_update-cube': {
@@ -1141,7 +1279,10 @@ export async function main() {
1141
1279
  if (Array.isArray(args?.message_taxonomy)) updates.message_taxonomy = args.message_taxonomy as MessageTaxonomy;
1142
1280
  if (Object.keys(updates).length === 0) throw new Error('Pass at least one of: cube_directive, message_taxonomy.');
1143
1281
  const { cube, advisory } = await updateCube(cubeId, updates);
1144
- return { content: [{ type: 'text', text: formatUpdatedCubeResult(cube, advisory) }] };
1282
+ return {
1283
+ content: [{ type: 'text', text: formatUpdatedCubeResult(cube, advisory) }],
1284
+ structuredContent: { cube, advisory: advisory ?? null },
1285
+ };
1145
1286
  }
1146
1287
 
1147
1288
  case 'borg_patch-taxonomy-class': {
@@ -1167,7 +1308,10 @@ export async function main() {
1167
1308
  label = String(classDef.class ?? '');
1168
1309
  }
1169
1310
  const verb = action === 'add' ? 'Added' : action === 'replace' ? 'Replaced' : 'Removed';
1170
- return { content: [{ type: 'text', text: `${verb} taxonomy class **${label}** in cube **${cube.name}** (id: ${cube.id}).` }] };
1311
+ return {
1312
+ content: [{ type: 'text', text: `${verb} taxonomy class **${label}** in cube **${cube.name}** (id: ${cube.id}).` }],
1313
+ structuredContent: { action, class: label, cube },
1314
+ };
1171
1315
  }
1172
1316
 
1173
1317
  case 'borg_delete-cube': {
@@ -1178,7 +1322,10 @@ export async function main() {
1178
1322
  throw new CubeDeletionConfirmationError(cubeId, confirmCubeId);
1179
1323
  }
1180
1324
  await deleteCube(cubeId, confirmCubeId);
1181
- return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
1325
+ return {
1326
+ content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }],
1327
+ structuredContent: { cube_id: cubeId, deleted: true },
1328
+ };
1182
1329
  }
1183
1330
 
1184
1331
  case 'borg_create-role': {
@@ -1213,7 +1360,10 @@ export async function main() {
1213
1360
  role.is_mandatory ? 'mandatory' : null,
1214
1361
  ].filter(Boolean).join(', ');
1215
1362
  const tag = tags ? ` (${tags})` : '';
1216
- return { content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }] };
1363
+ return {
1364
+ content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }],
1365
+ structuredContent: { role, cube_id: cubeId },
1366
+ };
1217
1367
  }
1218
1368
 
1219
1369
  case 'borg_update-role': {
@@ -1231,7 +1381,10 @@ export async function main() {
1231
1381
  if (typeof args?.default_model === 'string') updates.default_model = args.default_model as string;
1232
1382
  if (Object.keys(updates).length === 0) throw new Error('Pass at least one of: name, short_description, detailed_description, is_default, is_mandatory, is_human_seat, can_broadcast, receives_all_direct.');
1233
1383
  const { role, advisory } = await updateRole(roleId, updates);
1234
- return { content: [{ type: 'text', text: formatUpdatedRoleResult(role, advisory) }] };
1384
+ return {
1385
+ content: [{ type: 'text', text: formatUpdatedRoleResult(role, advisory) }],
1386
+ structuredContent: { role, advisory: advisory ?? null },
1387
+ };
1235
1388
  }
1236
1389
 
1237
1390
  case 'borg_patch-role-section': {
@@ -1259,28 +1412,47 @@ export async function main() {
1259
1412
  ({ role, advisory } = await patchRoleSection(roleId, { action, heading, body }));
1260
1413
  }
1261
1414
  }
1262
- return { content: [{ type: 'text', text: formatPatchedRoleSectionResult(action, heading, role, advisory) }] };
1415
+ return {
1416
+ content: [{ type: 'text', text: formatPatchedRoleSectionResult(action, heading, role, advisory) }],
1417
+ structuredContent: { action, heading, role, advisory: advisory ?? null },
1418
+ };
1263
1419
  }
1264
1420
 
1265
1421
  case 'borg_delete-role': {
1266
1422
  const roleId = args?.role_id as string;
1267
1423
  if (!roleId) throw new Error('role_id is required');
1268
1424
  await deleteRole(roleId);
1269
- return { content: [{ type: 'text', text: `Deleted role ${roleId}.` }] };
1425
+ return {
1426
+ content: [{ type: 'text', text: `Deleted role ${roleId}.` }],
1427
+ structuredContent: { role_id: roleId, deleted: true },
1428
+ };
1270
1429
  }
1271
1430
 
1272
1431
  case 'borg_reassign-drone': {
1432
+ const reassigned = await runReassignDroneTool({
1433
+ droneId: args?.drone_id,
1434
+ roleId: args?.role_id,
1435
+ });
1273
1436
  return {
1274
- content: [{ type: 'text', text: await runReassignDroneTool({
1275
- droneId: args?.drone_id,
1276
- roleId: args?.role_id,
1277
- }) }],
1437
+ content: [{ type: 'text', text: reassigned.text }],
1438
+ structuredContent: {
1439
+ drone: reassigned.drone,
1440
+ role_name: reassigned.roleName,
1441
+ cube_name: reassigned.cubeName,
1442
+ },
1278
1443
  };
1279
1444
  }
1280
1445
 
1281
1446
  case 'borg_evict-drone': {
1447
+ const evicted = await runEvictDroneTool(args);
1282
1448
  return {
1283
- content: [{ type: 'text', text: await runEvictDroneTool(args) }],
1449
+ content: [{ type: 'text', text: evicted.text }],
1450
+ structuredContent: {
1451
+ drone_id: evicted.droneId,
1452
+ label: evicted.label,
1453
+ cube_name: evicted.cubeName,
1454
+ evicted: true,
1455
+ },
1284
1456
  };
1285
1457
  }
1286
1458
 
@@ -1289,7 +1461,10 @@ export async function main() {
1289
1461
  if (!cubeId) throw new Error('cube_id is required');
1290
1462
  const { drones, roles } = await getCube(cubeId);
1291
1463
  if (!drones.length) {
1292
- return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
1464
+ return {
1465
+ content: [{ type: 'text', text: 'No drones in this cube yet.' }],
1466
+ structuredContent: { cube_id: cubeId, drones: [], roles },
1467
+ };
1293
1468
  }
1294
1469
  const rolesById = new Map(roles.map((r: any) => [r.id, r]));
1295
1470
  const lines = drones.map((d: any) => {
@@ -1303,7 +1478,10 @@ export async function main() {
1303
1478
  ...renderRuntimeMetadataLines(d),
1304
1479
  ].join('\n');
1305
1480
  });
1306
- return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n_${RUNTIME_METADATA_ADVISORY}_\n\n${lines.join('\n')}` }] };
1481
+ return {
1482
+ content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n_${RUNTIME_METADATA_ADVISORY}_\n\n${lines.join('\n')}` }],
1483
+ structuredContent: { cube_id: cubeId, drones, roles },
1484
+ };
1307
1485
  }
1308
1486
 
1309
1487
  case 'borg_list-roles': {
@@ -1318,7 +1496,10 @@ export async function main() {
1318
1496
  const cubeId = args?.cube_id as string;
1319
1497
  if (!cubeId) throw new Error('cube_id is required');
1320
1498
  const roles = await listRoles(cubeId);
1321
- return { content: [{ type: 'text', text: renderRoleList(roles, cubeId) }] };
1499
+ return {
1500
+ content: [{ type: 'text', text: renderRoleList(roles, cubeId) }],
1501
+ structuredContent: { cube_id: cubeId, roles },
1502
+ };
1322
1503
  }
1323
1504
 
1324
1505
  case 'borg_list-templates': {
@@ -1327,7 +1508,12 @@ export async function main() {
1327
1508
  const t = getTemplate(n)!;
1328
1509
  return `- **${n}**: ${t.description}`;
1329
1510
  });
1330
- return { content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }] };
1511
+ return {
1512
+ content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }],
1513
+ structuredContent: {
1514
+ templates: names.map((n) => ({ name: n, description: getTemplate(n)!.description })),
1515
+ },
1516
+ };
1331
1517
  }
1332
1518
 
1333
1519
  case 'borg_sync-roles': {
@@ -1344,7 +1530,10 @@ export async function main() {
1344
1530
  : undefined;
1345
1531
  if (!cubeId) throw new Error('cube_id is required');
1346
1532
  const result = await syncRoles(cubeId, templateName, apply, decisions);
1347
- return { content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }] };
1533
+ return {
1534
+ content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }],
1535
+ structuredContent: { cube_id: cubeId, template: templateName, apply, result },
1536
+ };
1348
1537
  }
1349
1538
 
1350
1539
  case 'borg_apply-template': {
@@ -1364,7 +1553,16 @@ export async function main() {
1364
1553
  });
1365
1554
  const { summary, cubeDirectiveNote } = await runApplyTemplateTool(cubeId, template, authority);
1366
1555
 
1367
- return { content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }] };
1556
+ return {
1557
+ content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }],
1558
+ structuredContent: {
1559
+ cube_id: cubeId,
1560
+ template: templateName,
1561
+ roles_created: summary.created,
1562
+ roles_updated: summary.updated,
1563
+ cube_directive_applied: cubeDirectiveNote !== '',
1564
+ },
1565
+ };
1368
1566
  }
1369
1567
 
1370
1568
  default: