@gethmy/mcp 2.3.3 → 2.4.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.
@@ -15,6 +15,7 @@ import {
15
15
  consolidateMemories,
16
16
  } from "./consolidation.js";
17
17
  import { findSimilarEntities } from "./graph-expansion.js";
18
+ import { type AuditReport, runMemoryAudit } from "./memory-audit.js";
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // Types
@@ -39,7 +40,8 @@ export type CleanupStep =
39
40
  | "consolidate"
40
41
  | "orphans"
41
42
  | "duplicates"
42
- | "backfill";
43
+ | "backfill"
44
+ | "audit";
43
45
 
44
46
  export interface CleanupOptions {
45
47
  dryRun?: boolean;
@@ -47,6 +49,45 @@ export interface CleanupOptions {
47
49
  maxAgeDays?: number;
48
50
  minClusterSize?: number;
49
51
  orphanAgeDays?: number;
52
+ auditArchiveBelow?: number;
53
+ auditDeleteBelow?: number;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Purge types
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export interface PurgeFilters {
61
+ tier?: "draft" | "episode" | "reference";
62
+ scope?: string;
63
+ type?: string;
64
+ olderThanDays?: number;
65
+ maxConfidence?: number;
66
+ tags?: string[];
67
+ }
68
+
69
+ export interface PurgeOptions {
70
+ dryRun?: boolean;
71
+ filters: PurgeFilters;
72
+ }
73
+
74
+ export interface PurgeReport {
75
+ success: boolean;
76
+ dryRun: boolean;
77
+ timestamp: string;
78
+ workspace: { id: string; projectId: string };
79
+ filters: PurgeFilters;
80
+ matched: number;
81
+ purged: number;
82
+ items: Array<{
83
+ id: string;
84
+ title: string;
85
+ type: string;
86
+ tier: string;
87
+ confidence: number;
88
+ ageDays: number;
89
+ }>;
90
+ errors: Array<{ entityId: string; message: string }>;
50
91
  }
51
92
 
52
93
  interface PruneStepResult {
@@ -98,6 +139,15 @@ interface BackfillStepResult {
98
139
  errors: Array<{ entity_id: string; error: string }>;
99
140
  }
100
141
 
142
+ interface AuditStepResult {
143
+ scanned: number;
144
+ legacyCount: number;
145
+ buckets: { keep: number; review: number; archive: number; delete: number };
146
+ actions: { flaggedReview: number; archived: number; deleted: number };
147
+ lowestScore: number | null;
148
+ report: AuditReport;
149
+ }
150
+
101
151
  export interface CleanupReport {
102
152
  success: boolean;
103
153
  dryRun: boolean;
@@ -116,6 +166,7 @@ export interface CleanupReport {
116
166
  orphans?: OrphanStepResult;
117
167
  duplicates?: DuplicateStepResult;
118
168
  backfill?: BackfillStepResult;
169
+ audit?: AuditStepResult;
119
170
  };
120
171
 
121
172
  errors: Array<{ step: string; message: string }>;
@@ -128,6 +179,7 @@ const ALL_STEPS: CleanupStep[] = [
128
179
  "orphans",
129
180
  "duplicates",
130
181
  "backfill",
182
+ "audit",
131
183
  ];
132
184
 
133
185
  const MS_PER_DAY = 1000 * 60 * 60 * 24;
@@ -322,6 +374,55 @@ export async function runMemoryCleanup(
322
374
  }
323
375
  }
324
376
 
377
+ // Stage 6: Quality audit — rate every entity against modern standards
378
+ if (steps.includes("audit")) {
379
+ try {
380
+ const auditReport = await runMemoryAudit(client, workspaceId, projectId, {
381
+ dryRun,
382
+ archiveBelow: options?.auditArchiveBelow,
383
+ deleteBelow: options?.auditDeleteBelow,
384
+ });
385
+ const low =
386
+ auditReport.lowest.length > 0 ? auditReport.lowest[0].score : null;
387
+ report.steps.audit = {
388
+ scanned: auditReport.summary.scanned,
389
+ legacyCount: auditReport.summary.legacyCount,
390
+ buckets: {
391
+ keep: auditReport.summary.keep,
392
+ review: auditReport.summary.review,
393
+ archive: auditReport.summary.archive,
394
+ delete: auditReport.summary.delete,
395
+ },
396
+ actions: auditReport.actionsTaken,
397
+ lowestScore: low,
398
+ report: auditReport,
399
+ };
400
+ report.summary.issuesFound +=
401
+ auditReport.summary.review +
402
+ auditReport.summary.archive +
403
+ auditReport.summary.delete;
404
+ if (!dryRun) {
405
+ report.summary.actionsTaken +=
406
+ auditReport.actionsTaken.flaggedReview +
407
+ auditReport.actionsTaken.archived +
408
+ auditReport.actionsTaken.deleted;
409
+ }
410
+ for (const err of auditReport.errors) {
411
+ report.errors.push({
412
+ step: `audit:${err.step}`,
413
+ message: err.entityId
414
+ ? `${err.entityId}: ${err.message}`
415
+ : err.message,
416
+ });
417
+ }
418
+ } catch (err) {
419
+ report.errors.push({
420
+ step: "audit",
421
+ message: (err as Error).message,
422
+ });
423
+ }
424
+ }
425
+
325
426
  report.healthReport = generateHealthReport(report);
326
427
  return report;
327
428
  }
@@ -639,6 +740,30 @@ function generateHealthReport(report: CleanupReport): string {
639
740
  }
640
741
  }
641
742
 
743
+ // Audit
744
+ if (report.steps.audit) {
745
+ const a = report.steps.audit;
746
+ lines.push("## Quality Audit");
747
+ lines.push(
748
+ `Scanned ${a.scanned} entities. Legacy signals on ${a.legacyCount}.`,
749
+ );
750
+ lines.push(
751
+ `Buckets — keep: ${a.buckets.keep}, review: ${a.buckets.review}, archive: ${a.buckets.archive}, delete: ${a.buckets.delete}.`,
752
+ );
753
+ if (!report.dryRun) {
754
+ lines.push(
755
+ `Actions — flagged: ${a.actions.flaggedReview}, archived: ${a.actions.archived}, deleted: ${a.actions.deleted}.`,
756
+ );
757
+ }
758
+ if (a.report.lowest.length > 0) {
759
+ const worst = a.report.lowest[0];
760
+ lines.push(
761
+ `Lowest score: **${worst.score}** — "${worst.title}" (${worst.reasons.slice(0, 2).join(", ") || "—"}).`,
762
+ );
763
+ }
764
+ lines.push("");
765
+ }
766
+
642
767
  // Errors
643
768
  if (report.errors.length > 0) {
644
769
  lines.push("## Errors");
@@ -654,3 +779,124 @@ function generateHealthReport(report: CleanupReport): string {
654
779
 
655
780
  return lines.join("\n");
656
781
  }
782
+
783
+ // ---------------------------------------------------------------------------
784
+ // Purge — filtered bulk deletion
785
+ // ---------------------------------------------------------------------------
786
+
787
+ export async function purgeMemories(
788
+ client: HarmonyApiClient,
789
+ workspaceId: string,
790
+ projectId: string,
791
+ options: PurgeOptions,
792
+ ): Promise<PurgeReport> {
793
+ const dryRun = options.dryRun !== false;
794
+ const { filters } = options;
795
+
796
+ // Safety: require at least one narrowing filter
797
+ const hasFilter =
798
+ filters.tier ||
799
+ filters.scope ||
800
+ filters.type ||
801
+ filters.olderThanDays !== undefined ||
802
+ filters.maxConfidence !== undefined ||
803
+ (filters.tags && filters.tags.length > 0);
804
+
805
+ if (!hasFilter) {
806
+ throw new Error(
807
+ "At least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags) is required.",
808
+ );
809
+ }
810
+
811
+ // Paginate through all matching entities
812
+ const allMatches: MemoryEntity[] = [];
813
+ let offset = 0;
814
+ const pageSize = 100;
815
+ const now = Date.now();
816
+
817
+ while (true) {
818
+ const result = await client.listMemoryEntities({
819
+ workspace_id: workspaceId,
820
+ project_id: projectId,
821
+ type: filters.type,
822
+ scope: filters.scope,
823
+ tags: filters.tags,
824
+ limit: pageSize,
825
+ offset,
826
+ });
827
+
828
+ const entities = (result.entities || []) as MemoryEntity[];
829
+ if (entities.length === 0) break;
830
+
831
+ // Client-side filtering for fields the API doesn't support natively
832
+ for (const entity of entities) {
833
+ if (filters.tier && entity.memory_tier !== filters.tier) continue;
834
+ if (
835
+ filters.maxConfidence !== undefined &&
836
+ entity.confidence > filters.maxConfidence
837
+ )
838
+ continue;
839
+ if (filters.olderThanDays !== undefined) {
840
+ const ref = entity.last_accessed_at || entity.created_at;
841
+ const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY;
842
+ if (ageDays < filters.olderThanDays) continue;
843
+ }
844
+ allMatches.push(entity);
845
+ }
846
+
847
+ if (entities.length < pageSize) break;
848
+ offset += pageSize;
849
+ }
850
+
851
+ // Build preview items
852
+ const items = allMatches.map((e) => ({
853
+ id: e.id,
854
+ title: e.title,
855
+ type: e.type,
856
+ tier: e.memory_tier,
857
+ confidence: e.confidence,
858
+ ageDays: Math.round(
859
+ (now - new Date(e.last_accessed_at || e.created_at).getTime()) /
860
+ MS_PER_DAY,
861
+ ),
862
+ }));
863
+
864
+ // Execute deletions if not dry-run
865
+ const errors: Array<{ entityId: string; message: string }> = [];
866
+ let purged = 0;
867
+
868
+ if (!dryRun) {
869
+ // Delete in batches to avoid overwhelming the API
870
+ for (let i = 0; i < allMatches.length; i += CONCURRENCY_LIMIT) {
871
+ const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT);
872
+ const results = await Promise.allSettled(
873
+ batch.map((e) => client.deleteMemoryEntity(e.id)),
874
+ );
875
+ for (let j = 0; j < results.length; j++) {
876
+ if (results[j].status === "fulfilled") {
877
+ purged++;
878
+ } else {
879
+ errors.push({
880
+ entityId: batch[j].id,
881
+ message:
882
+ results[j].status === "rejected"
883
+ ? String((results[j] as PromiseRejectedResult).reason)
884
+ : "Unknown error",
885
+ });
886
+ }
887
+ }
888
+ }
889
+ }
890
+
891
+ return {
892
+ success: errors.length === 0,
893
+ dryRun,
894
+ timestamp: new Date().toISOString(),
895
+ workspace: { id: workspaceId, projectId },
896
+ filters,
897
+ matched: allMatches.length,
898
+ purged: dryRun ? 0 : purged,
899
+ items,
900
+ errors,
901
+ };
902
+ }
package/src/server.ts CHANGED
@@ -58,7 +58,12 @@ import {
58
58
  } from "./context-assembly.js";
59
59
  import { autoExpandGraph } from "./graph-expansion.js";
60
60
  import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
61
- import { runMemoryCleanup } from "./memory-cleanup.js";
61
+ import { runMemoryAudit } from "./memory-audit.js";
62
+ import {
63
+ type PurgeFilters,
64
+ purgeMemories,
65
+ runMemoryCleanup,
66
+ } from "./memory-cleanup.js";
62
67
  import { onboardNewUser } from "./onboard.js";
63
68
  import type { PromptVariant } from "./prompt-builder.js";
64
69
 
@@ -721,6 +726,22 @@ const TOOLS = {
721
726
  type: "number",
722
727
  description: "Updated time estimate",
723
728
  },
729
+ actions: {
730
+ type: "array",
731
+ items: {
732
+ type: "object",
733
+ properties: {
734
+ description: {
735
+ type: "string",
736
+ description:
737
+ "What was done, e.g. 'Edited CardDetailSheet.tsx — added done toggle'",
738
+ },
739
+ },
740
+ required: ["description"],
741
+ },
742
+ description:
743
+ "Actions performed since last update. Each becomes a visible activity log entry.",
744
+ },
724
745
  },
725
746
  required: ["cardId", "agentIdentifier", "agentName"],
726
747
  },
@@ -1641,7 +1662,7 @@ const TOOLS = {
1641
1662
 
1642
1663
  harmony_cleanup_memories: {
1643
1664
  description:
1644
- "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and backfill embeddings. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
1665
+ "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, backfill embeddings, and optionally run a quality audit. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
1645
1666
  inputSchema: {
1646
1667
  type: "object",
1647
1668
  properties: {
@@ -1662,10 +1683,17 @@ const TOOLS = {
1662
1683
  type: "array",
1663
1684
  items: {
1664
1685
  type: "string",
1665
- enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"],
1686
+ enum: [
1687
+ "prune",
1688
+ "consolidate",
1689
+ "orphans",
1690
+ "duplicates",
1691
+ "backfill",
1692
+ "audit",
1693
+ ],
1666
1694
  },
1667
1695
  description:
1668
- "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill.",
1696
+ "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit.",
1669
1697
  },
1670
1698
  maxAgeDays: {
1671
1699
  type: "number",
@@ -1679,6 +1707,106 @@ const TOOLS = {
1679
1707
  type: "number",
1680
1708
  description: "Min age in days for orphan detection (default: 14)",
1681
1709
  },
1710
+ auditArchiveBelow: {
1711
+ type: "number",
1712
+ description: "Audit: archive entities scoring below this (default: 40)",
1713
+ },
1714
+ auditDeleteBelow: {
1715
+ type: "number",
1716
+ description: "Audit: delete entities scoring below this (default: 20)",
1717
+ },
1718
+ },
1719
+ required: [],
1720
+ },
1721
+ },
1722
+ harmony_audit_memories: {
1723
+ description:
1724
+ "Rate every memory against state-of-the-art quality standards (confidence, decay, structural completeness, content, tier-age fit, access). Flags legacy entities from before recent optimizations (default confidence, missing embeddings, stuck drafts). Buckets: keep (≥70), review (40-69), archive (20-39), delete (<20). Dry-run by default.",
1725
+ inputSchema: {
1726
+ type: "object",
1727
+ properties: {
1728
+ workspaceId: {
1729
+ type: "string",
1730
+ description: "Workspace ID (optional if context set)",
1731
+ },
1732
+ projectId: {
1733
+ type: "string",
1734
+ description: "Project ID (optional)",
1735
+ },
1736
+ dryRun: {
1737
+ type: "boolean",
1738
+ description:
1739
+ "Preview audit without flagging/archiving/deleting (default: true)",
1740
+ },
1741
+ archiveBelow: {
1742
+ type: "number",
1743
+ description:
1744
+ "Score threshold below which entities are archived (confidence set to 0.25). Default: 40",
1745
+ },
1746
+ deleteBelow: {
1747
+ type: "number",
1748
+ description:
1749
+ "Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete.",
1750
+ },
1751
+ limit: {
1752
+ type: "number",
1753
+ description:
1754
+ "Max number of entities to audit (default: 500). Paginated fetch.",
1755
+ },
1756
+ },
1757
+ required: [],
1758
+ },
1759
+ },
1760
+ harmony_purge_memories: {
1761
+ description:
1762
+ "Bulk-delete memory entities matching filters within a project. Requires at least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags). Dry-run by default — preview what would be deleted before executing.",
1763
+ inputSchema: {
1764
+ type: "object",
1765
+ properties: {
1766
+ workspaceId: {
1767
+ type: "string",
1768
+ description: "Workspace ID (optional if context set)",
1769
+ },
1770
+ projectId: {
1771
+ type: "string",
1772
+ description:
1773
+ "Project ID (required — purge is project-scoped). Falls back to active project context.",
1774
+ },
1775
+ dryRun: {
1776
+ type: "boolean",
1777
+ description:
1778
+ "Preview what would be deleted without executing (default: true)",
1779
+ },
1780
+ tier: {
1781
+ type: "string",
1782
+ enum: ["draft", "episode", "reference"],
1783
+ description: 'Filter by memory tier (e.g. "draft")',
1784
+ },
1785
+ scope: {
1786
+ type: "string",
1787
+ description:
1788
+ 'Filter by scope (e.g. "private", "project", "workspace")',
1789
+ },
1790
+ type: {
1791
+ type: "string",
1792
+ description:
1793
+ 'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")',
1794
+ },
1795
+ olderThanDays: {
1796
+ type: "number",
1797
+ description:
1798
+ "Only include entities not accessed in at least this many days",
1799
+ },
1800
+ maxConfidence: {
1801
+ type: "number",
1802
+ description:
1803
+ "Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)",
1804
+ },
1805
+ tags: {
1806
+ type: "array",
1807
+ items: { type: "string" },
1808
+ description: "Only include entities matching these tags",
1809
+ },
1682
1810
  },
1683
1811
  required: [],
1684
1812
  },
@@ -2643,18 +2771,26 @@ async function handleToolCall(
2643
2771
  args.progressPercent !== undefined
2644
2772
  ? z.number().min(0).max(100).parse(args.progressPercent)
2645
2773
  : undefined;
2646
- // Merge any pending memory actions into the progress update
2647
- const callerRecentActions = args.recentActions as
2648
- | { action: string; ts: string }[]
2774
+ // Convert actions parameter to recentActions format and merge with memory actions
2775
+ const callerActions = args.actions as
2776
+ | { description: string }[]
2649
2777
  | undefined;
2778
+ const now = new Date().toISOString();
2779
+ const callerRecentActions: { action: string; ts: string }[] = [
2780
+ ...((args.recentActions as { action: string; ts: string }[]) || []),
2781
+ ...(callerActions || []).map((a) => ({
2782
+ action: a.description,
2783
+ ts: now,
2784
+ })),
2785
+ ];
2650
2786
  const memSession = getMemorySession(cardId);
2651
2787
  let mergedRecentActions: { action: string; ts: string }[] | undefined;
2652
2788
  if (memSession?.dirty) {
2653
2789
  mergedRecentActions = mergeMemoryActionsInto(
2654
2790
  cardId,
2655
- callerRecentActions || [],
2791
+ callerRecentActions,
2656
2792
  );
2657
- } else if (callerRecentActions) {
2793
+ } else if (callerRecentActions.length > 0) {
2658
2794
  mergedRecentActions = callerRecentActions;
2659
2795
  }
2660
2796
 
@@ -4013,6 +4149,37 @@ async function handleToolCall(
4013
4149
  };
4014
4150
  }
4015
4151
 
4152
+ case "harmony_audit_memories": {
4153
+ const workspaceId =
4154
+ (args.workspaceId as string) || deps.getActiveWorkspaceId();
4155
+ if (!workspaceId) {
4156
+ throw new Error(
4157
+ "No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
4158
+ );
4159
+ }
4160
+ const projectId =
4161
+ (args.projectId as string) || deps.getActiveProjectId() || undefined;
4162
+
4163
+ const report = await runMemoryAudit(client, workspaceId, projectId, {
4164
+ dryRun: args.dryRun as boolean | undefined,
4165
+ archiveBelow: args.archiveBelow as number | undefined,
4166
+ deleteBelow: args.deleteBelow as number | undefined,
4167
+ limit: args.limit as number | undefined,
4168
+ });
4169
+
4170
+ return {
4171
+ success: report.success,
4172
+ dryRun: report.dryRun,
4173
+ summary: report.summary,
4174
+ distribution: report.distribution,
4175
+ legacyBreakdown: report.legacyBreakdown,
4176
+ actionsTaken: report.actionsTaken,
4177
+ lowest: report.lowest,
4178
+ errors: report.errors,
4179
+ healthReport: report.healthReport,
4180
+ };
4181
+ }
4182
+
4016
4183
  case "harmony_cleanup_memories": {
4017
4184
  const workspaceId =
4018
4185
  (args.workspaceId as string) || deps.getActiveWorkspaceId();
@@ -4030,6 +4197,7 @@ async function handleToolCall(
4030
4197
  "orphans",
4031
4198
  "duplicates",
4032
4199
  "backfill",
4200
+ "audit",
4033
4201
  ];
4034
4202
  const rawSteps = args.steps as string[] | undefined;
4035
4203
  const steps = rawSteps?.filter((s) => validSteps.includes(s));
@@ -4041,10 +4209,14 @@ async function handleToolCall(
4041
4209
 
4042
4210
  const report = await runMemoryCleanup(client, workspaceId, projectId, {
4043
4211
  dryRun: args.dryRun as boolean | undefined,
4044
- steps,
4212
+ steps: steps as
4213
+ | ("prune" | "consolidate" | "orphans" | "duplicates" | "backfill" | "audit")[]
4214
+ | undefined,
4045
4215
  maxAgeDays: args.maxAgeDays as number | undefined,
4046
4216
  minClusterSize: args.minClusterSize as number | undefined,
4047
4217
  orphanAgeDays: args.orphanAgeDays as number | undefined,
4218
+ auditArchiveBelow: args.auditArchiveBelow as number | undefined,
4219
+ auditDeleteBelow: args.auditDeleteBelow as number | undefined,
4048
4220
  });
4049
4221
 
4050
4222
  return {
@@ -4056,6 +4228,49 @@ async function handleToolCall(
4056
4228
  };
4057
4229
  }
4058
4230
 
4231
+ case "harmony_purge_memories": {
4232
+ const workspaceId =
4233
+ (args.workspaceId as string) || deps.getActiveWorkspaceId();
4234
+ if (!workspaceId) {
4235
+ throw new Error(
4236
+ "No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
4237
+ );
4238
+ }
4239
+ const projectId = (args.projectId as string) || deps.getActiveProjectId();
4240
+ if (!projectId) {
4241
+ throw new Error(
4242
+ "No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.",
4243
+ );
4244
+ }
4245
+
4246
+ const filters: PurgeFilters = {};
4247
+ if (args.tier) filters.tier = args.tier as PurgeFilters["tier"];
4248
+ if (args.scope) filters.scope = args.scope as string;
4249
+ if (args.type) filters.type = args.type as string;
4250
+ if (args.olderThanDays !== undefined)
4251
+ filters.olderThanDays = args.olderThanDays as number;
4252
+ if (args.maxConfidence !== undefined)
4253
+ filters.maxConfidence = args.maxConfidence as number;
4254
+ if (args.tags) filters.tags = args.tags as string[];
4255
+
4256
+ const report = await purgeMemories(client, workspaceId, projectId, {
4257
+ dryRun: args.dryRun as boolean | undefined,
4258
+ filters,
4259
+ });
4260
+
4261
+ return {
4262
+ success: report.success,
4263
+ dryRun: report.dryRun,
4264
+ matched: report.matched,
4265
+ purged: report.purged,
4266
+ items: report.items,
4267
+ errors: report.errors,
4268
+ message: report.dryRun
4269
+ ? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.`
4270
+ : `Purged ${report.purged} of ${report.matched} matching entities.`,
4271
+ };
4272
+ }
4273
+
4059
4274
  default:
4060
4275
  throw new Error(`Unknown tool: ${name}`);
4061
4276
  }
@@ -4109,6 +4324,7 @@ export class HarmonyMCPServer {
4109
4324
  );
4110
4325
 
4111
4326
  // Graceful shutdown: end all auto-sessions
4327
+ let exitCode = 0;
4112
4328
  const handleShutdown = async () => {
4113
4329
  try {
4114
4330
  await shutdownAllSessions();
@@ -4116,10 +4332,20 @@ export class HarmonyMCPServer {
4116
4332
  // Best-effort
4117
4333
  }
4118
4334
  destroyAutoSession();
4119
- process.exit(0);
4335
+ process.exit(exitCode);
4120
4336
  };
4121
4337
  process.on("SIGINT", handleShutdown);
4122
4338
  process.on("SIGTERM", handleShutdown);
4339
+ process.on("uncaughtException", (err) => {
4340
+ console.error("MCP server uncaught exception:", err);
4341
+ exitCode = 1;
4342
+ handleShutdown();
4343
+ });
4344
+ process.on("unhandledRejection", (reason) => {
4345
+ console.error("MCP server unhandled rejection:", reason);
4346
+ exitCode = 1;
4347
+ handleShutdown();
4348
+ });
4123
4349
 
4124
4350
  // Attempt startup sync (non-blocking, best-effort)
4125
4351
  try {