borgmcp 4.1.0 → 4.2.0

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
@@ -419,8 +419,15 @@ export async function main() {
419
419
  isError: true,
420
420
  };
421
421
  }
422
+ const described = {
423
+ name: def.name,
424
+ description: def.description,
425
+ inputSchema: def.inputSchema,
426
+ outputSchema: def.outputSchema ?? null,
427
+ };
422
428
  return {
423
- content: [{ type: 'text', text: JSON.stringify({ name: def.name, description: def.description, inputSchema: def.inputSchema }, null, 2) }],
429
+ content: [{ type: 'text', text: JSON.stringify(described, null, 2) }],
430
+ structuredContent: described,
424
431
  };
425
432
  }
426
433
 
@@ -471,6 +478,7 @@ export async function main() {
471
478
  text: 'Not connected to a cube. Use `borg_assimilate cube_name="<name>"` to join one.',
472
479
  },
473
480
  ],
481
+ structuredContent: { connected: false },
474
482
  };
475
483
  }
476
484
  seedDisplayIdentity(active);
@@ -524,10 +532,12 @@ export async function main() {
524
532
 
525
533
  // gh#285: version-mismatch nudge when on-disk package is newer.
526
534
  let versionHeader = '';
535
+ let onDiskVersion: string | null = null;
527
536
  try {
528
537
  const running = getPackageVersion();
529
538
  const onDisk = getOnDiskVersion();
530
539
  if (running !== 'unknown' && onDisk !== 'unknown' && onDisk !== running) {
540
+ onDiskVersion = onDisk;
531
541
  const [rMaj, rMin, rPat] = running.split('.').map(Number);
532
542
  const [dMaj, dMin, dPat] = onDisk.split('.').map(Number);
533
543
  const onDiskNewer = dMaj > rMaj || (dMaj === rMaj && dMin > rMin) || (dMaj === rMaj && dMin === rMin && dPat > rPat);
@@ -537,7 +547,21 @@ export async function main() {
537
547
  }
538
548
  } catch { /* never break regen */ }
539
549
 
540
- return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(displayedResult, { mode }) }] };
550
+ return {
551
+ content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(displayedResult, { mode }) }],
552
+ structuredContent: {
553
+ connected: true,
554
+ mode,
555
+ cube: displayedResult.cube,
556
+ drone: displayedResult.drone,
557
+ role: displayedResult.role,
558
+ behind_by: typeof displayedResult.behind_by === 'number' ? displayedResult.behind_by : null,
559
+ decision_topics: (displayedResult.decisions ?? []).map((d: any) => d.topic),
560
+ running_version: getPackageVersion(),
561
+ on_disk_version: onDiskVersion,
562
+ wake_path_healthy: inboxMonitorHealthy,
563
+ },
564
+ };
541
565
  }
542
566
 
543
567
  case 'borg_assimilate': {
@@ -583,6 +607,11 @@ export async function main() {
583
607
  ].join('\n');
584
608
  return {
585
609
  content: [{ type: 'text', text: header + formatRegenMarkdown(displayedResult, { mode: 'full' }) }],
610
+ structuredContent: {
611
+ reattached: true,
612
+ cube_name: displayIdentity.cubeName,
613
+ drone_label: displayIdentity.droneLabel,
614
+ },
586
615
  };
587
616
  } catch (err: any) {
588
617
  markDisplayIdentityReadFailed(active!);
@@ -592,7 +621,10 @@ export async function main() {
592
621
  }
593
622
 
594
623
  case 'borg_version': {
595
- return { content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }] };
624
+ return {
625
+ content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }],
626
+ structuredContent: { version: getPackageVersion() },
627
+ };
596
628
  }
597
629
 
598
630
  case 'borg_playbook': {
@@ -615,7 +647,14 @@ export async function main() {
615
647
  : topic
616
648
  ? `No exact match for "${topic}". Full Borg MCP docs index — WebFetch the URL you need:`
617
649
  : `Borg MCP docs index — WebFetch the URL of the section you need:`;
618
- return { content: [{ type: 'text', text: `${header}\n\n${formatDocsIndex(sections)}` }] };
650
+ return {
651
+ content: [{ type: 'text', text: `${header}\n\n${formatDocsIndex(sections)}` }],
652
+ structuredContent: {
653
+ topic: topic || null,
654
+ matched: matched.length > 0,
655
+ sections: sections.map((s) => ({ slug: s.slug, title: s.title, url: s.url, summary: s.summary })),
656
+ },
657
+ };
619
658
  }
620
659
 
621
660
  case 'borg_whoami': {
@@ -644,7 +683,10 @@ export async function main() {
644
683
  // The invocation-local server truth remains authoritative even if
645
684
  // the display-only persistence refresh cannot be written.
646
685
  }
647
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
686
+ return {
687
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
688
+ structuredContent: { ...result },
689
+ };
648
690
  }
649
691
 
650
692
  case 'borg_cube': {
@@ -697,7 +739,10 @@ export async function main() {
697
739
  }
698
740
  lines.push('');
699
741
  lines.push(getDronePlaybook());
700
- return { content: [{ type: 'text', text: lines.join('\n') }] };
742
+ return {
743
+ content: [{ type: 'text', text: lines.join('\n') }],
744
+ structuredContent: { cube, roles },
745
+ };
701
746
  }
702
747
 
703
748
  case 'borg_role': {
@@ -718,7 +763,7 @@ export async function main() {
718
763
  ``,
719
764
  role.detailed_description || '_(no detailed description set)_',
720
765
  ].join('\n');
721
- return { content: [{ type: 'text', text }] };
766
+ return { content: [{ type: 'text', text }], structuredContent: { role } };
722
767
  }
723
768
  const { role } = await getRoleInfo(
724
769
  active.sessionToken,
@@ -730,7 +775,7 @@ export async function main() {
730
775
  ``,
731
776
  role.detailed_description || '_(no detailed description set)_',
732
777
  ].join('\n');
733
- return { content: [{ type: 'text', text }] };
778
+ return { content: [{ type: 'text', text }], structuredContent: { role } };
734
779
  }
735
780
 
736
781
  case 'borg_role-rationale': {
@@ -749,7 +794,10 @@ export async function main() {
749
794
  '',
750
795
  result.body || '_(empty)_',
751
796
  ].join('\n');
752
- return { content: [{ type: 'text', text }] };
797
+ return {
798
+ content: [{ type: 'text', text }],
799
+ structuredContent: { role: result.role, section: result.section, body: result.body },
800
+ };
753
801
  }
754
802
 
755
803
  case 'borg_roster': {
@@ -763,7 +811,10 @@ export async function main() {
763
811
  resolvedSince: resolvedSince ?? null,
764
812
  humanAgo,
765
813
  });
766
- return { content: [{ type: 'text', text }] };
814
+ return {
815
+ content: [{ type: 'text', text }],
816
+ structuredContent: { cube_name: active.name, drones, roles, since: resolvedSince ?? null },
817
+ };
767
818
  }
768
819
 
769
820
  case 'borg_stream-status': {
@@ -803,7 +854,18 @@ export async function main() {
803
854
  cubeName: active?.name ?? null,
804
855
  humanAgo,
805
856
  });
806
- return { content: [{ type: 'text', text: silentInertWarning + text }] };
857
+ return {
858
+ content: [{ type: 'text', text: silentInertWarning + text }],
859
+ structuredContent: {
860
+ status,
861
+ wake_path: wakePath,
862
+ inbox_monitor_healthy: inboxMonitorHealthy,
863
+ inbox_path: inboxPath,
864
+ monitor_state_root: monitorStateRoot,
865
+ drone_label: active?.droneLabel ?? null,
866
+ cube_name: active?.name ?? null,
867
+ },
868
+ };
807
869
  }
808
870
 
809
871
  case 'borg_read-log': {
@@ -849,7 +911,16 @@ export async function main() {
849
911
  `⚠ 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
912
  );
851
913
  }
852
- return { content: [{ type: 'text', text: lines.join('\n') }] };
914
+ return {
915
+ content: [{ type: 'text', text: lines.join('\n') }],
916
+ structuredContent: {
917
+ entries,
918
+ drones,
919
+ roles,
920
+ behind_by: typeof behind_by === 'number' ? behind_by : null,
921
+ has_more: has_more === true,
922
+ },
923
+ };
853
924
  }
854
925
 
855
926
  case 'borg_read-entry': {
@@ -867,19 +938,28 @@ export async function main() {
867
938
  '',
868
939
  formatLogEntryMarkdown(entry, droneById, roleById),
869
940
  ].join('\n');
870
- return { content: [{ type: 'text', text }] };
941
+ return {
942
+ content: [{ type: 'text', text }],
943
+ structuredContent: { entry, drones, roles },
944
+ };
871
945
  }
872
946
 
873
947
  case 'borg_put-document': {
874
948
  const active = await requireActiveCube();
875
949
  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)}` }] };
950
+ return {
951
+ content: [{ type: 'text', text: `Created cube document.\n\n${formatDocument(result.document)}` }],
952
+ structuredContent: { document: result.document },
953
+ };
877
954
  }
878
955
 
879
956
  case 'borg_get-document': {
880
957
  const active = await requireActiveCube();
881
958
  const result = await getDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
882
- return { content: [{ type: 'text', text: formatDocument(result.document) }] };
959
+ return {
960
+ content: [{ type: 'text', text: formatDocument(result.document) }],
961
+ structuredContent: { document: result.document },
962
+ };
883
963
  }
884
964
 
885
965
  case 'borg_list-documents': {
@@ -888,13 +968,19 @@ export async function main() {
888
968
  const text = result.documents.length === 0
889
969
  ? `No active or superseded documents in cube "${active.name}".`
890
970
  : result.documents.map(formatDocumentMetadata).join('\n\n');
891
- return { content: [{ type: 'text', text }] };
971
+ return {
972
+ content: [{ type: 'text', text }],
973
+ structuredContent: { documents: result.documents },
974
+ };
892
975
  }
893
976
 
894
977
  case 'borg_remove-document': {
895
978
  const active = await requireActiveCube();
896
979
  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)}` }] };
980
+ return {
981
+ content: [{ type: 'text', text: `Removed cube document.\n\n${formatDocumentMetadata(result.document)}` }],
982
+ structuredContent: { document: result.document },
983
+ };
898
984
  }
899
985
 
900
986
  case 'borg_log': {
@@ -918,6 +1004,13 @@ export async function main() {
918
1004
  text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${displayIdentity.droneLabel}; recent cube log already contains this signal.`,
919
1005
  },
920
1006
  ],
1007
+ structuredContent: {
1008
+ suppressed: true,
1009
+ entry: null,
1010
+ recipients: [],
1011
+ unreachable_recipients: [],
1012
+ advisory: null,
1013
+ },
921
1014
  };
922
1015
  }
923
1016
  }
@@ -962,7 +1055,16 @@ export async function main() {
962
1055
  ? `\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
1056
  : '';
964
1057
  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 }] };
1058
+ return {
1059
+ content: [{ type: 'text', text }],
1060
+ structuredContent: {
1061
+ suppressed: false,
1062
+ entry: result.entry,
1063
+ recipients: routedRecipients,
1064
+ unreachable_recipients: result.unreachableRecipients ?? [],
1065
+ advisory: result.advisory ?? null,
1066
+ },
1067
+ };
966
1068
  }
967
1069
 
968
1070
  case 'borg_ack': {
@@ -992,6 +1094,7 @@ export async function main() {
992
1094
  : `Acked entry ${entryId} in cube "${active.name}".`,
993
1095
  },
994
1096
  ],
1097
+ structuredContent: { entry_id: entryId, kind, cube_name: active.name },
995
1098
  };
996
1099
  }
997
1100
 
@@ -1003,7 +1106,10 @@ export async function main() {
1003
1106
  args ?? {},
1004
1107
  active.serverTrustIdentity,
1005
1108
  );
1006
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1109
+ return {
1110
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1111
+ structuredContent: { ...result },
1112
+ };
1007
1113
  }
1008
1114
 
1009
1115
  case 'borg_decide': {
@@ -1031,6 +1137,7 @@ export async function main() {
1031
1137
  text: `Recorded ratified decision on "${topic}" in cube "${active.name}"${superseded}. Cite it via borg_decisions; it surfaces in borg_regen.`,
1032
1138
  },
1033
1139
  ],
1140
+ structuredContent: { decision: row, superseded: Boolean(row?.supersedes), cube_name: active.name },
1034
1141
  };
1035
1142
  }
1036
1143
 
@@ -1051,7 +1158,10 @@ export async function main() {
1051
1158
  : decisions
1052
1159
  .map((d: any) => `**${d.topic}:** ${d.decision}${d.rationale ? ` — ${d.rationale}` : ''}`)
1053
1160
  .join('\n');
1054
- return { content: [{ type: 'text', text }] };
1161
+ return {
1162
+ content: [{ type: 'text', text }],
1163
+ structuredContent: { decisions },
1164
+ };
1055
1165
  }
1056
1166
 
1057
1167
  case 'borg_remove-decision': {
@@ -1073,18 +1183,25 @@ export async function main() {
1073
1183
  type: 'text',
1074
1184
  text: `Removed the active ratified decision on "${decision.topic}" from cube "${active.name}".`,
1075
1185
  }],
1186
+ structuredContent: { decision, cube_name: active.name },
1076
1187
  };
1077
1188
  }
1078
1189
 
1079
1190
  case 'borg_list-cubes': {
1080
1191
  const { cubes } = await listCubes();
1081
1192
  if (!cubes.length) {
1082
- return { content: [{ type: 'text', text: 'No cubes yet. Use borg_create-cube to make your first one.' }] };
1193
+ return {
1194
+ content: [{ type: 'text', text: 'No cubes yet. Use borg_create-cube to make your first one.' }],
1195
+ structuredContent: { cubes: [] },
1196
+ };
1083
1197
  }
1084
1198
  const lines = cubes.map((c: any) =>
1085
1199
  `- **${c.name}** (id: ${c.id})\n ${(c.cube_directive || '_(no directive set)_').split('\n')[0].slice(0, 120)}`
1086
1200
  );
1087
- return { content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }] };
1201
+ return {
1202
+ content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }],
1203
+ structuredContent: { cubes },
1204
+ };
1088
1205
  }
1089
1206
 
1090
1207
  case 'borg_create-cube': {
@@ -1126,10 +1243,21 @@ export async function main() {
1126
1243
  ? ' Template cube directive applied (operator passed empty).'
1127
1244
  : '';
1128
1245
  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 }] };
1246
+ return {
1247
+ content: [{ type: 'text', text }],
1248
+ structuredContent: {
1249
+ cube,
1250
+ template: templateName,
1251
+ roles_created: summary.created,
1252
+ roles_updated: summary.updated,
1253
+ },
1254
+ };
1130
1255
  }
1131
1256
  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 }] };
1257
+ return {
1258
+ content: [{ type: 'text', text }],
1259
+ structuredContent: { cube, template: null, roles_created: null, roles_updated: null },
1260
+ };
1133
1261
  }
1134
1262
 
1135
1263
  case 'borg_update-cube': {
@@ -1141,7 +1269,10 @@ export async function main() {
1141
1269
  if (Array.isArray(args?.message_taxonomy)) updates.message_taxonomy = args.message_taxonomy as MessageTaxonomy;
1142
1270
  if (Object.keys(updates).length === 0) throw new Error('Pass at least one of: cube_directive, message_taxonomy.');
1143
1271
  const { cube, advisory } = await updateCube(cubeId, updates);
1144
- return { content: [{ type: 'text', text: formatUpdatedCubeResult(cube, advisory) }] };
1272
+ return {
1273
+ content: [{ type: 'text', text: formatUpdatedCubeResult(cube, advisory) }],
1274
+ structuredContent: { cube, advisory: advisory ?? null },
1275
+ };
1145
1276
  }
1146
1277
 
1147
1278
  case 'borg_patch-taxonomy-class': {
@@ -1167,7 +1298,10 @@ export async function main() {
1167
1298
  label = String(classDef.class ?? '');
1168
1299
  }
1169
1300
  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}).` }] };
1301
+ return {
1302
+ content: [{ type: 'text', text: `${verb} taxonomy class **${label}** in cube **${cube.name}** (id: ${cube.id}).` }],
1303
+ structuredContent: { action, class: label, cube },
1304
+ };
1171
1305
  }
1172
1306
 
1173
1307
  case 'borg_delete-cube': {
@@ -1178,7 +1312,10 @@ export async function main() {
1178
1312
  throw new CubeDeletionConfirmationError(cubeId, confirmCubeId);
1179
1313
  }
1180
1314
  await deleteCube(cubeId, confirmCubeId);
1181
- return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
1315
+ return {
1316
+ content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }],
1317
+ structuredContent: { cube_id: cubeId, deleted: true },
1318
+ };
1182
1319
  }
1183
1320
 
1184
1321
  case 'borg_create-role': {
@@ -1213,7 +1350,10 @@ export async function main() {
1213
1350
  role.is_mandatory ? 'mandatory' : null,
1214
1351
  ].filter(Boolean).join(', ');
1215
1352
  const tag = tags ? ` (${tags})` : '';
1216
- return { content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }] };
1353
+ return {
1354
+ content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }],
1355
+ structuredContent: { role, cube_id: cubeId },
1356
+ };
1217
1357
  }
1218
1358
 
1219
1359
  case 'borg_update-role': {
@@ -1231,7 +1371,10 @@ export async function main() {
1231
1371
  if (typeof args?.default_model === 'string') updates.default_model = args.default_model as string;
1232
1372
  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
1373
  const { role, advisory } = await updateRole(roleId, updates);
1234
- return { content: [{ type: 'text', text: formatUpdatedRoleResult(role, advisory) }] };
1374
+ return {
1375
+ content: [{ type: 'text', text: formatUpdatedRoleResult(role, advisory) }],
1376
+ structuredContent: { role, advisory: advisory ?? null },
1377
+ };
1235
1378
  }
1236
1379
 
1237
1380
  case 'borg_patch-role-section': {
@@ -1259,28 +1402,47 @@ export async function main() {
1259
1402
  ({ role, advisory } = await patchRoleSection(roleId, { action, heading, body }));
1260
1403
  }
1261
1404
  }
1262
- return { content: [{ type: 'text', text: formatPatchedRoleSectionResult(action, heading, role, advisory) }] };
1405
+ return {
1406
+ content: [{ type: 'text', text: formatPatchedRoleSectionResult(action, heading, role, advisory) }],
1407
+ structuredContent: { action, heading, role, advisory: advisory ?? null },
1408
+ };
1263
1409
  }
1264
1410
 
1265
1411
  case 'borg_delete-role': {
1266
1412
  const roleId = args?.role_id as string;
1267
1413
  if (!roleId) throw new Error('role_id is required');
1268
1414
  await deleteRole(roleId);
1269
- return { content: [{ type: 'text', text: `Deleted role ${roleId}.` }] };
1415
+ return {
1416
+ content: [{ type: 'text', text: `Deleted role ${roleId}.` }],
1417
+ structuredContent: { role_id: roleId, deleted: true },
1418
+ };
1270
1419
  }
1271
1420
 
1272
1421
  case 'borg_reassign-drone': {
1422
+ const reassigned = await runReassignDroneTool({
1423
+ droneId: args?.drone_id,
1424
+ roleId: args?.role_id,
1425
+ });
1273
1426
  return {
1274
- content: [{ type: 'text', text: await runReassignDroneTool({
1275
- droneId: args?.drone_id,
1276
- roleId: args?.role_id,
1277
- }) }],
1427
+ content: [{ type: 'text', text: reassigned.text }],
1428
+ structuredContent: {
1429
+ drone: reassigned.drone,
1430
+ role_name: reassigned.roleName,
1431
+ cube_name: reassigned.cubeName,
1432
+ },
1278
1433
  };
1279
1434
  }
1280
1435
 
1281
1436
  case 'borg_evict-drone': {
1437
+ const evicted = await runEvictDroneTool(args);
1282
1438
  return {
1283
- content: [{ type: 'text', text: await runEvictDroneTool(args) }],
1439
+ content: [{ type: 'text', text: evicted.text }],
1440
+ structuredContent: {
1441
+ drone_id: evicted.droneId,
1442
+ label: evicted.label,
1443
+ cube_name: evicted.cubeName,
1444
+ evicted: true,
1445
+ },
1284
1446
  };
1285
1447
  }
1286
1448
 
@@ -1289,7 +1451,10 @@ export async function main() {
1289
1451
  if (!cubeId) throw new Error('cube_id is required');
1290
1452
  const { drones, roles } = await getCube(cubeId);
1291
1453
  if (!drones.length) {
1292
- return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
1454
+ return {
1455
+ content: [{ type: 'text', text: 'No drones in this cube yet.' }],
1456
+ structuredContent: { cube_id: cubeId, drones: [], roles },
1457
+ };
1293
1458
  }
1294
1459
  const rolesById = new Map(roles.map((r: any) => [r.id, r]));
1295
1460
  const lines = drones.map((d: any) => {
@@ -1303,7 +1468,10 @@ export async function main() {
1303
1468
  ...renderRuntimeMetadataLines(d),
1304
1469
  ].join('\n');
1305
1470
  });
1306
- return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n_${RUNTIME_METADATA_ADVISORY}_\n\n${lines.join('\n')}` }] };
1471
+ return {
1472
+ content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n_${RUNTIME_METADATA_ADVISORY}_\n\n${lines.join('\n')}` }],
1473
+ structuredContent: { cube_id: cubeId, drones, roles },
1474
+ };
1307
1475
  }
1308
1476
 
1309
1477
  case 'borg_list-roles': {
@@ -1318,7 +1486,10 @@ export async function main() {
1318
1486
  const cubeId = args?.cube_id as string;
1319
1487
  if (!cubeId) throw new Error('cube_id is required');
1320
1488
  const roles = await listRoles(cubeId);
1321
- return { content: [{ type: 'text', text: renderRoleList(roles, cubeId) }] };
1489
+ return {
1490
+ content: [{ type: 'text', text: renderRoleList(roles, cubeId) }],
1491
+ structuredContent: { cube_id: cubeId, roles },
1492
+ };
1322
1493
  }
1323
1494
 
1324
1495
  case 'borg_list-templates': {
@@ -1327,7 +1498,12 @@ export async function main() {
1327
1498
  const t = getTemplate(n)!;
1328
1499
  return `- **${n}**: ${t.description}`;
1329
1500
  });
1330
- return { content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }] };
1501
+ return {
1502
+ content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }],
1503
+ structuredContent: {
1504
+ templates: names.map((n) => ({ name: n, description: getTemplate(n)!.description })),
1505
+ },
1506
+ };
1331
1507
  }
1332
1508
 
1333
1509
  case 'borg_sync-roles': {
@@ -1344,7 +1520,10 @@ export async function main() {
1344
1520
  : undefined;
1345
1521
  if (!cubeId) throw new Error('cube_id is required');
1346
1522
  const result = await syncRoles(cubeId, templateName, apply, decisions);
1347
- return { content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }] };
1523
+ return {
1524
+ content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }],
1525
+ structuredContent: { cube_id: cubeId, template: templateName, apply, result },
1526
+ };
1348
1527
  }
1349
1528
 
1350
1529
  case 'borg_apply-template': {
@@ -1364,7 +1543,16 @@ export async function main() {
1364
1543
  });
1365
1544
  const { summary, cubeDirectiveNote } = await runApplyTemplateTool(cubeId, template, authority);
1366
1545
 
1367
- return { content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }] };
1546
+ return {
1547
+ content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }],
1548
+ structuredContent: {
1549
+ cube_id: cubeId,
1550
+ template: templateName,
1551
+ roles_created: summary.created,
1552
+ roles_updated: summary.updated,
1553
+ cube_directive_applied: cubeDirectiveNote !== '',
1554
+ },
1555
+ };
1368
1556
  }
1369
1557
 
1370
1558
  default: