@adhisang/minecraft-modding-mcp 3.2.0 → 4.1.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +52 -32
  3. package/dist/build-suggested-call.d.ts +29 -0
  4. package/dist/build-suggested-call.js +58 -0
  5. package/dist/cache-registry.d.ts +3 -1
  6. package/dist/cache-registry.js +59 -7
  7. package/dist/config.d.ts +10 -1
  8. package/dist/config.js +52 -1
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +18 -18
  10. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  11. package/dist/entry-tools/batch-class-members-service.d.ts +34 -0
  12. package/dist/entry-tools/batch-class-members-service.js +97 -0
  13. package/dist/entry-tools/batch-class-source-service.d.ts +37 -0
  14. package/dist/entry-tools/batch-class-source-service.js +100 -0
  15. package/dist/entry-tools/batch-mappings-service.d.ts +36 -0
  16. package/dist/entry-tools/batch-mappings-service.js +66 -0
  17. package/dist/entry-tools/batch-runner.d.ts +72 -0
  18. package/dist/entry-tools/batch-runner.js +90 -0
  19. package/dist/entry-tools/batch-symbol-exists-service.d.ts +46 -0
  20. package/dist/entry-tools/batch-symbol-exists-service.js +113 -0
  21. package/dist/entry-tools/compare-minecraft-service.d.ts +6 -6
  22. package/dist/entry-tools/inspect-minecraft/handlers/artifact.d.ts +5 -0
  23. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +83 -0
  24. package/dist/entry-tools/inspect-minecraft/handlers/class-members.d.ts +6 -0
  25. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +80 -0
  26. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.d.ts +5 -0
  27. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +248 -0
  28. package/dist/entry-tools/inspect-minecraft/handlers/class-source.d.ts +5 -0
  29. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +60 -0
  30. package/dist/entry-tools/inspect-minecraft/handlers/file.d.ts +5 -0
  31. package/dist/entry-tools/inspect-minecraft/handlers/file.js +54 -0
  32. package/dist/entry-tools/inspect-minecraft/handlers/list-files.d.ts +5 -0
  33. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +100 -0
  34. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +5 -0
  35. package/dist/entry-tools/inspect-minecraft/handlers/search.js +155 -0
  36. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +6 -0
  37. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +49 -0
  38. package/dist/entry-tools/inspect-minecraft/internal.d.ts +1042 -0
  39. package/dist/entry-tools/inspect-minecraft/internal.js +448 -0
  40. package/dist/entry-tools/inspect-minecraft-service.d.ts +213 -328
  41. package/dist/entry-tools/inspect-minecraft-service.js +20 -1238
  42. package/dist/entry-tools/manage-cache-service.d.ts +16 -16
  43. package/dist/entry-tools/validate-project/cases/access-transformer.d.ts +6 -0
  44. package/dist/entry-tools/validate-project/cases/access-transformer.js +106 -0
  45. package/dist/entry-tools/validate-project/cases/access-widener.d.ts +6 -0
  46. package/dist/entry-tools/validate-project/cases/access-widener.js +86 -0
  47. package/dist/entry-tools/validate-project/cases/mixin.d.ts +6 -0
  48. package/dist/entry-tools/validate-project/cases/mixin.js +90 -0
  49. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +97 -0
  50. package/dist/entry-tools/validate-project/cases/project-summary.js +346 -0
  51. package/dist/entry-tools/validate-project/internal.d.ts +135 -0
  52. package/dist/entry-tools/validate-project/internal.js +287 -0
  53. package/dist/entry-tools/validate-project-service.d.ts +63 -47
  54. package/dist/entry-tools/validate-project-service.js +12 -482
  55. package/dist/entry-tools/verify-mixin-target-service.d.ts +133 -0
  56. package/dist/entry-tools/verify-mixin-target-service.js +323 -0
  57. package/dist/error-mapping.d.ts +40 -0
  58. package/dist/error-mapping.js +139 -0
  59. package/dist/errors.d.ts +6 -0
  60. package/dist/errors.js +6 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +170 -1314
  63. package/dist/lru-list.d.ts +31 -0
  64. package/dist/lru-list.js +102 -0
  65. package/dist/mapping/internal-types.d.ts +54 -0
  66. package/dist/mapping/internal-types.js +14 -0
  67. package/dist/mapping/loaders/mojang.d.ts +2 -0
  68. package/dist/mapping/loaders/mojang.js +64 -0
  69. package/dist/mapping/loaders/tiny-loom.d.ts +2 -0
  70. package/dist/mapping/loaders/tiny-loom.js +73 -0
  71. package/dist/mapping/loaders/tiny-maven.d.ts +2 -0
  72. package/dist/mapping/loaders/tiny-maven.js +104 -0
  73. package/dist/mapping/loaders/types.d.ts +14 -0
  74. package/dist/mapping/loaders/types.js +2 -0
  75. package/dist/mapping/lookup.d.ts +52 -0
  76. package/dist/mapping/lookup.js +496 -0
  77. package/dist/mapping/parsers/normalize.d.ts +10 -0
  78. package/dist/mapping/parsers/normalize.js +52 -0
  79. package/dist/mapping/parsers/proguard.d.ts +20 -0
  80. package/dist/mapping/parsers/proguard.js +138 -0
  81. package/dist/mapping/parsers/symbol-records.d.ts +27 -0
  82. package/dist/mapping/parsers/symbol-records.js +216 -0
  83. package/dist/mapping/parsers/tiny.d.ts +9 -0
  84. package/dist/mapping/parsers/tiny.js +96 -0
  85. package/dist/mapping/types.d.ts +147 -0
  86. package/dist/mapping/types.js +2 -0
  87. package/dist/mapping-pipeline-service.d.ts +10 -1
  88. package/dist/mapping-pipeline-service.js +16 -3
  89. package/dist/mapping-service.d.ts +15 -144
  90. package/dist/mapping-service.js +179 -1119
  91. package/dist/mixin/access-validators.d.ts +9 -0
  92. package/dist/mixin/access-validators.js +257 -0
  93. package/dist/mixin/annotation-validators.d.ts +5 -0
  94. package/dist/mixin/annotation-validators.js +162 -0
  95. package/dist/mixin/helpers.d.ts +28 -0
  96. package/dist/mixin/helpers.js +315 -0
  97. package/dist/mixin/parsed-validator.d.ts +8 -0
  98. package/dist/mixin/parsed-validator.js +337 -0
  99. package/dist/mixin/types.d.ts +208 -0
  100. package/dist/mixin/types.js +28 -0
  101. package/dist/mixin-validator.d.ts +9 -201
  102. package/dist/mixin-validator.js +8 -1005
  103. package/dist/observability.d.ts +18 -1
  104. package/dist/observability.js +44 -1
  105. package/dist/response-utils.d.ts +44 -10
  106. package/dist/response-utils.js +131 -17
  107. package/dist/source/access-validate.d.ts +4 -0
  108. package/dist/source/access-validate.js +254 -0
  109. package/dist/source/artifact-resolver.d.ts +110 -0
  110. package/dist/source/artifact-resolver.js +1174 -0
  111. package/dist/source/cache-metrics.d.ts +26 -0
  112. package/dist/source/cache-metrics.js +172 -0
  113. package/dist/source/class-source/members-builder.d.ts +34 -0
  114. package/dist/source/class-source/members-builder.js +46 -0
  115. package/dist/source/class-source/snippet-builder.d.ts +19 -0
  116. package/dist/source/class-source/snippet-builder.js +46 -0
  117. package/dist/source/class-source-helpers.d.ts +34 -0
  118. package/dist/source/class-source-helpers.js +140 -0
  119. package/dist/source/class-source.d.ts +42 -0
  120. package/dist/source/class-source.js +883 -0
  121. package/dist/source/descriptor-utils.d.ts +6 -0
  122. package/dist/source/descriptor-utils.js +37 -0
  123. package/dist/source/file-access.d.ts +4 -0
  124. package/dist/source/file-access.js +102 -0
  125. package/dist/source/indexer.d.ts +82 -0
  126. package/dist/source/indexer.js +505 -0
  127. package/dist/source/lifecycle/diff-utils.d.ts +9 -0
  128. package/dist/source/lifecycle/diff-utils.js +107 -0
  129. package/dist/source/lifecycle/diff.d.ts +2 -0
  130. package/dist/source/lifecycle/diff.js +265 -0
  131. package/dist/source/lifecycle/mapping-helpers.d.ts +22 -0
  132. package/dist/source/lifecycle/mapping-helpers.js +327 -0
  133. package/dist/source/lifecycle/runtime-check.d.ts +2 -0
  134. package/dist/source/lifecycle/runtime-check.js +142 -0
  135. package/dist/source/lifecycle/trace.d.ts +2 -0
  136. package/dist/source/lifecycle/trace.js +231 -0
  137. package/dist/source/lifecycle.d.ts +4 -0
  138. package/dist/source/lifecycle.js +5 -0
  139. package/dist/source/search.d.ts +51 -0
  140. package/dist/source/search.js +676 -0
  141. package/dist/source/shared-utils.d.ts +6 -0
  142. package/dist/source/shared-utils.js +55 -0
  143. package/dist/source/state.d.ts +21 -0
  144. package/dist/source/state.js +19 -0
  145. package/dist/source/symbol-resolver.d.ts +3 -0
  146. package/dist/source/symbol-resolver.js +212 -0
  147. package/dist/source/validate-mixin/pipeline/mapping-health.d.ts +3 -0
  148. package/dist/source/validate-mixin/pipeline/mapping-health.js +41 -0
  149. package/dist/source/validate-mixin/pipeline/parse.d.ts +2 -0
  150. package/dist/source/validate-mixin/pipeline/parse.js +10 -0
  151. package/dist/source/validate-mixin/pipeline/resolve.d.ts +3 -0
  152. package/dist/source/validate-mixin/pipeline/resolve.js +78 -0
  153. package/dist/source/validate-mixin/pipeline/target-lookup.d.ts +6 -0
  154. package/dist/source/validate-mixin/pipeline/target-lookup.js +260 -0
  155. package/dist/source/validate-mixin/pipeline-context.d.ts +72 -0
  156. package/dist/source/validate-mixin/pipeline-context.js +93 -0
  157. package/dist/source/validate-mixin.d.ts +22 -0
  158. package/dist/source/validate-mixin.js +799 -0
  159. package/dist/source/workspace-target.d.ts +18 -0
  160. package/dist/source/workspace-target.js +305 -0
  161. package/dist/source-resolver.d.ts +9 -1
  162. package/dist/source-resolver.js +14 -6
  163. package/dist/source-service.d.ts +178 -105
  164. package/dist/source-service.js +72 -5312
  165. package/dist/stage-emitter.d.ts +13 -0
  166. package/dist/stage-emitter.js +30 -0
  167. package/dist/stdio-supervisor.d.ts +61 -0
  168. package/dist/stdio-supervisor.js +326 -9
  169. package/dist/storage/artifacts-repo.d.ts +4 -1
  170. package/dist/storage/artifacts-repo.js +33 -5
  171. package/dist/storage/files-repo.d.ts +0 -2
  172. package/dist/storage/files-repo.js +0 -11
  173. package/dist/storage/migrations.d.ts +1 -1
  174. package/dist/storage/migrations.js +10 -2
  175. package/dist/storage/schema.d.ts +2 -0
  176. package/dist/storage/schema.js +25 -0
  177. package/dist/tool-contract-manifest.d.ts +1 -1
  178. package/dist/tool-contract-manifest.js +23 -6
  179. package/dist/tool-guidance.d.ts +82 -0
  180. package/dist/tool-guidance.js +734 -0
  181. package/dist/tool-schema-registry.d.ts +16 -0
  182. package/dist/tool-schema-registry.js +37 -0
  183. package/dist/tool-schemas.d.ts +3518 -0
  184. package/dist/tool-schemas.js +813 -0
  185. package/dist/types.d.ts +39 -0
  186. package/dist/version-service.js +7 -6
  187. package/dist/workspace-context-cache.d.ts +32 -0
  188. package/dist/workspace-context-cache.js +66 -0
  189. package/dist/workspace-mapping-service.d.ts +16 -0
  190. package/dist/workspace-mapping-service.js +173 -1
  191. package/docs/README-ja.md +414 -0
  192. package/docs/examples.md +483 -0
  193. package/docs/tool-reference.md +459 -0
  194. package/package.json +5 -2
@@ -0,0 +1,13 @@
1
+ export type StageEmitter = (stage: string, meta?: Record<string, unknown>) => Promise<void>;
2
+ export declare const NOOP_STAGE_EMITTER: StageEmitter;
3
+ export type StageEmitterExtra = {
4
+ requestId?: string | number;
5
+ sendNotification?: (notification: {
6
+ method: string;
7
+ params?: unknown;
8
+ }) => Promise<void>;
9
+ };
10
+ export type StageEmitterOptions = {
11
+ disabled?: boolean;
12
+ };
13
+ export declare function makeStageEmitter(extra: StageEmitterExtra | undefined, options?: StageEmitterOptions): StageEmitter;
@@ -0,0 +1,30 @@
1
+ import { performance } from "node:perf_hooks";
2
+ const STAGE_PROGRESS_DISABLED = process.env.MIXIN_STAGE_PROGRESS_OFF === "1";
3
+ export const NOOP_STAGE_EMITTER = async () => {
4
+ /* noop */
5
+ };
6
+ export function makeStageEmitter(extra, options = {}) {
7
+ if (options.disabled ?? STAGE_PROGRESS_DISABLED) {
8
+ return NOOP_STAGE_EMITTER;
9
+ }
10
+ if (!extra) {
11
+ return NOOP_STAGE_EMITTER;
12
+ }
13
+ const requestId = extra.requestId;
14
+ const sendNotification = extra.sendNotification;
15
+ if (requestId === undefined || typeof sendNotification !== "function") {
16
+ return NOOP_STAGE_EMITTER;
17
+ }
18
+ return async (stage, meta) => {
19
+ await sendNotification({
20
+ method: "$/stageUpdate",
21
+ params: {
22
+ stage,
23
+ meta: meta ?? null,
24
+ t: performance.now(),
25
+ requestId
26
+ }
27
+ });
28
+ };
29
+ }
30
+ //# sourceMappingURL=stage-emitter.js.map
@@ -1,12 +1,72 @@
1
+ import type { JSONRPCResponse } from "@modelcontextprotocol/sdk/types.js";
1
2
  type SupervisorOptions = {
2
3
  entryFile: string;
3
4
  };
5
+ type RequestId = string | number;
6
+ export type ExitInfo = {
7
+ code: number | null;
8
+ signal: NodeJS.Signals | null;
9
+ };
10
+ export type RetryRecommendation = "narrow-query" | "clear-cache" | "report-bug" | "same-request";
11
+ export type RestartContext = {
12
+ toolName: string | undefined;
13
+ durationMs: number;
14
+ lastStage: string | undefined;
15
+ lastStageElapsedMs: number | undefined;
16
+ lastStageMeta: unknown;
17
+ exit: ExitInfo;
18
+ toolArgsRedacted: Record<string, unknown> | undefined;
19
+ /** `true` when redaction replaced any value with a sentinel placeholder; the synthetic envelope omits `suggestedCall` in that case. */
20
+ toolArgsRedactedModified: boolean;
21
+ retryRecommendation: RetryRecommendation;
22
+ };
23
+ export type PendingRequestSnapshot = {
24
+ id: RequestId;
25
+ method?: string;
26
+ toolName?: string;
27
+ toolArgsRedacted?: Record<string, unknown>;
28
+ /** `true` when toolArgsRedacted contains sentinel placeholders (truncate/redact/overflow). */
29
+ toolArgsRedactedModified?: boolean;
30
+ startedAt: number;
31
+ lastStage?: string;
32
+ lastStageStartedAt?: number;
33
+ lastStageMeta?: unknown;
34
+ };
35
+ export declare function buildLegacyJsonRpcError(id: RequestId): JSONRPCResponse;
36
+ export type RedactedToolArgs = {
37
+ args: Record<string, unknown>;
38
+ /** `true` when redaction replaced any value with a sentinel; supervisor uses this to decide whether to surface `suggestedCall`. */
39
+ modified: boolean;
40
+ };
41
+ export declare function redactToolArgs(args: unknown): RedactedToolArgs;
42
+ export declare function decideRetryRecommendation(ctx: Pick<RestartContext, "toolName" | "lastStage" | "lastStageMeta" | "exit">, recentRestartTimestamps: number[]): RetryRecommendation;
43
+ export declare function buildSyntheticCallToolResult(id: RequestId, ctx: RestartContext): JSONRPCResponse;
44
+ export declare function pruneRestartTimestamps(timestamps: number[], now: number, windowMs?: number): number[];
45
+ /**
46
+ * Group a worker-exit batch by toolName and produce the per-tool pruned
47
+ * timestamp set plus the post-exit snapshot. Grouping ensures one exit
48
+ * counts as one death per tool regardless of how many concurrent pending
49
+ * requests it killed. `prunedByTool` feeds `buildWorkerRestartReply`;
50
+ * `updatedByTool` is what the caller writes back to `recentRestarts`.
51
+ */
52
+ export declare function buildExitTimestampGroups(pendingToolNames: Iterable<string | undefined>, recentRestarts: Map<string, number[]>, now: number): {
53
+ prunedByTool: Map<string, number[]>;
54
+ updatedByTool: Map<string, number[]>;
55
+ };
56
+ export type BuildWorkerRestartReplyOptions = {
57
+ structuredRestartDisabled?: boolean;
58
+ };
59
+ export declare function buildWorkerRestartReply(req: PendingRequestSnapshot, exit: ExitInfo, now: number, recentRestartTimestamps: number[], options?: BuildWorkerRestartReplyOptions): {
60
+ reply: JSONRPCResponse;
61
+ updatedTimestamps: number[];
62
+ };
4
63
  export declare class StdioSupervisor {
5
64
  private readonly entryFile;
6
65
  private readonly clientReader;
7
66
  private readonly workerReader;
8
67
  private readonly queuedMessages;
9
68
  private readonly pendingRequests;
69
+ private readonly recentRestarts;
10
70
  private child;
11
71
  private childReady;
12
72
  private shuttingDown;
@@ -33,6 +93,7 @@ export declare class StdioSupervisor {
33
93
  private readonly handleWorkerProcessError;
34
94
  private readonly handleWorkerExit;
35
95
  private handleWorkerMessage;
96
+ private applyStageUpdate;
36
97
  private handleWorkerReady;
37
98
  private isInitializationResponse;
38
99
  private flushQueue;
@@ -2,10 +2,20 @@ import { spawn } from "node:child_process";
2
2
  import process from "node:process";
3
3
  import { encodeJsonRpcMessage, JsonRpcFrameReader } from "./json-rpc-framing.js";
4
4
  import { log } from "./logger.js";
5
+ import { buildSuggestedCall } from "./build-suggested-call.js";
6
+ import { getToolSchema } from "./tool-schema-registry.js";
5
7
  const DEFAULT_CLIENT_MODE = "line";
6
8
  const WORKER_MODE_ENV = "MCP_STDIO_WORKER_MODE";
7
9
  const WORKER_READY_MARKER = "__MCP_STDIO_WORKER_READY__";
8
10
  const SUPERVISOR_DEBUG_ENABLED = process.env.MCP_SUPERVISOR_DEBUG === "1";
11
+ const STRUCTURED_RESTART_DISABLED = process.env.SUPERVISOR_STRUCTURED_RESTART_OFF === "1";
12
+ const RESTART_WINDOW_MS = 60_000;
13
+ const RESTART_REPEAT_THRESHOLD = 3;
14
+ const REDACT_KEY_PATTERNS = [/secret/i, /token/i, /apikey/i, /password/i];
15
+ const PRESERVED_PATH_KEYS = new Set(["projectPath", "sourcePath", "mixinConfigPath"]);
16
+ const MAX_STRING_BYTES = 256;
17
+ const MAX_ARRAY_LENGTH = 8;
18
+ const MAX_OBJECT_KEYS = 16;
9
19
  function isRequest(message) {
10
20
  return "method" in message && "id" in message;
11
21
  }
@@ -23,7 +33,7 @@ function getTrackedRequestId(message) {
23
33
  function requestKey(id) {
24
34
  return `${typeof id}:${String(id)}`;
25
35
  }
26
- function buildWorkerRestartError(id) {
36
+ export function buildLegacyJsonRpcError(id) {
27
37
  return {
28
38
  jsonrpc: "2.0",
29
39
  id,
@@ -33,6 +43,252 @@ function buildWorkerRestartError(id) {
33
43
  }
34
44
  };
35
45
  }
46
+ function byteLengthUtf8(value) {
47
+ return Buffer.byteLength(value, "utf8");
48
+ }
49
+ function isRedactKey(key) {
50
+ return REDACT_KEY_PATTERNS.some((re) => re.test(key));
51
+ }
52
+ function redactValue(value, counter, keyName) {
53
+ if (keyName !== undefined && !PRESERVED_PATH_KEYS.has(keyName) && isRedactKey(keyName)) {
54
+ counter.modified = true;
55
+ return "<redacted>";
56
+ }
57
+ if (value === null || value === undefined) {
58
+ return value;
59
+ }
60
+ const valueType = typeof value;
61
+ if (valueType === "string") {
62
+ const str = value;
63
+ if (keyName !== undefined && PRESERVED_PATH_KEYS.has(keyName)) {
64
+ return str;
65
+ }
66
+ const byteLen = byteLengthUtf8(str);
67
+ if (byteLen > MAX_STRING_BYTES) {
68
+ counter.modified = true;
69
+ return `<truncated:${byteLen} bytes>`;
70
+ }
71
+ return str;
72
+ }
73
+ if (valueType === "number" || valueType === "boolean") {
74
+ return value;
75
+ }
76
+ if (Array.isArray(value)) {
77
+ if (value.length > MAX_ARRAY_LENGTH) {
78
+ counter.modified = true;
79
+ const head = value
80
+ .slice(0, MAX_ARRAY_LENGTH)
81
+ .map((item) => redactValue(item, counter));
82
+ head.push(`<+${value.length - MAX_ARRAY_LENGTH} more>`);
83
+ return head;
84
+ }
85
+ return value.map((item) => redactValue(item, counter));
86
+ }
87
+ if (valueType === "object") {
88
+ const entries = Object.entries(value);
89
+ const limited = entries.slice(0, MAX_OBJECT_KEYS);
90
+ const result = {};
91
+ for (const [key, child] of limited) {
92
+ result[key] = redactValue(child, counter, key);
93
+ }
94
+ if (entries.length > MAX_OBJECT_KEYS) {
95
+ counter.modified = true;
96
+ result["<+more>"] = `<+${entries.length - MAX_OBJECT_KEYS} more keys>`;
97
+ }
98
+ return result;
99
+ }
100
+ return undefined;
101
+ }
102
+ export function redactToolArgs(args) {
103
+ if (args === null || args === undefined || typeof args !== "object" || Array.isArray(args)) {
104
+ return { args: {}, modified: false };
105
+ }
106
+ const counter = { modified: false };
107
+ const redacted = redactValue(args, counter);
108
+ if (redacted && typeof redacted === "object" && !Array.isArray(redacted)) {
109
+ return { args: redacted, modified: counter.modified };
110
+ }
111
+ return { args: {}, modified: counter.modified };
112
+ }
113
+ export function decideRetryRecommendation(ctx, recentRestartTimestamps) {
114
+ if (ctx.lastStage === "target-lookup") {
115
+ const meta = ctx.lastStageMeta;
116
+ const targetTotal = typeof meta?.targetTotal === "number" ? meta.targetTotal : undefined;
117
+ if (targetTotal !== undefined && targetTotal > 5) {
118
+ return "narrow-query";
119
+ }
120
+ }
121
+ if (ctx.exit.signal === "SIGABRT" || ctx.exit.signal === "SIGSEGV" || ctx.exit.signal === "SIGKILL") {
122
+ return "clear-cache";
123
+ }
124
+ if (recentRestartTimestamps.length >= RESTART_REPEAT_THRESHOLD) {
125
+ return "report-bug";
126
+ }
127
+ return "same-request";
128
+ }
129
+ export function buildSyntheticCallToolResult(id, ctx) {
130
+ const tool = ctx.toolName ?? "unknown";
131
+ const hints = [];
132
+ if (ctx.lastStage) {
133
+ const indexNote = (() => {
134
+ const meta = ctx.lastStageMeta;
135
+ if (meta &&
136
+ typeof meta.targetIndex === "number" &&
137
+ typeof meta.targetTotal === "number") {
138
+ return `at index ${meta.targetIndex}/${meta.targetTotal}`;
139
+ }
140
+ return "";
141
+ })();
142
+ const recommendation = (() => {
143
+ switch (ctx.retryRecommendation) {
144
+ case "narrow-query":
145
+ return "narrow mixinConfigPath to one file and retry";
146
+ case "clear-cache":
147
+ return "clear cache and retry";
148
+ case "report-bug":
149
+ return "report a bug — repeated worker exits";
150
+ case "same-request":
151
+ default:
152
+ return "retry the same request";
153
+ }
154
+ })();
155
+ hints.push(`lastStage=${ctx.lastStage}${indexNote ? ` ${indexNote}` : ""} — ${recommendation}`);
156
+ }
157
+ else {
158
+ hints.push("worker exited before stage tracking began");
159
+ }
160
+ const error = {
161
+ type: "about:blank/mcp/worker-restart",
162
+ title: "MCP worker restarted",
163
+ detail: `The worker process exited while handling tools/call (${tool}).`,
164
+ status: 503,
165
+ code: "ERR_WORKER_RESTART",
166
+ instance: `urn:mcp:request:${String(id)}`,
167
+ hints
168
+ };
169
+ // Mutated args carry sentinel placeholders (`<truncated:…>`, `<redacted>`)
170
+ // that are not safe to retry. Surface them on `meta.restart.redactedToolArgs`
171
+ // for diagnostics instead.
172
+ if (ctx.toolArgsRedacted !== undefined && !ctx.toolArgsRedactedModified) {
173
+ // Only emit `suggestedCall` when the resolved tool name is a
174
+ // currently-registered public tool. The "unknown" fallback above and
175
+ // unregistered names (typo, disabled tool, version-skewed) cannot
176
+ // produce a re-callable payload; buildSuggestedCall fails open for
177
+ // unregistered names so the registration check belongs here. The
178
+ // diagnostic `restart.redactedToolArgs` field below still surfaces the
179
+ // args.
180
+ if (ctx.toolName !== undefined &&
181
+ ctx.toolName.length > 0 &&
182
+ getToolSchema(ctx.toolName) !== undefined) {
183
+ const gated = buildSuggestedCall({
184
+ tool: ctx.toolName,
185
+ params: ctx.toolArgsRedacted
186
+ });
187
+ if (gated.suggestedCall) {
188
+ error.suggestedCall = gated.suggestedCall;
189
+ }
190
+ }
191
+ }
192
+ if (ctx.lastStage !== undefined) {
193
+ error.failedStage = ctx.lastStage;
194
+ }
195
+ const restart = {
196
+ tool,
197
+ durationMs: ctx.durationMs,
198
+ lastStage: ctx.lastStage ?? null,
199
+ lastStageElapsedMs: ctx.lastStageElapsedMs ?? null,
200
+ lastStageMeta: ctx.lastStageMeta ?? null,
201
+ exit: {
202
+ code: ctx.exit.code,
203
+ signal: ctx.exit.signal
204
+ },
205
+ retryRecommendation: ctx.retryRecommendation
206
+ };
207
+ // Diagnostic echo (paired with the modified flag); not retryable.
208
+ if (ctx.toolArgsRedacted !== undefined) {
209
+ restart.redactedToolArgs = ctx.toolArgsRedacted;
210
+ restart.redactedToolArgsModified = ctx.toolArgsRedactedModified;
211
+ }
212
+ const meta = {
213
+ synthetic: true,
214
+ syntheticSource: "supervisor",
215
+ restart
216
+ };
217
+ const structuredContent = { error, meta };
218
+ return {
219
+ jsonrpc: "2.0",
220
+ id,
221
+ result: {
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: JSON.stringify(structuredContent)
226
+ }
227
+ ],
228
+ isError: true,
229
+ structuredContent
230
+ }
231
+ };
232
+ }
233
+ export function pruneRestartTimestamps(timestamps, now, windowMs = RESTART_WINDOW_MS) {
234
+ const cutoff = now - windowMs;
235
+ return timestamps.filter((t) => t >= cutoff);
236
+ }
237
+ /**
238
+ * Group a worker-exit batch by toolName and produce the per-tool pruned
239
+ * timestamp set plus the post-exit snapshot. Grouping ensures one exit
240
+ * counts as one death per tool regardless of how many concurrent pending
241
+ * requests it killed. `prunedByTool` feeds `buildWorkerRestartReply`;
242
+ * `updatedByTool` is what the caller writes back to `recentRestarts`.
243
+ */
244
+ export function buildExitTimestampGroups(pendingToolNames, recentRestarts, now) {
245
+ const prunedByTool = new Map();
246
+ const updatedByTool = new Map();
247
+ for (const raw of pendingToolNames) {
248
+ const toolName = raw ?? "unknown";
249
+ if (prunedByTool.has(toolName))
250
+ continue;
251
+ const pruned = pruneRestartTimestamps(recentRestarts.get(toolName) ?? [], now);
252
+ prunedByTool.set(toolName, pruned);
253
+ updatedByTool.set(toolName, [...pruned, now]);
254
+ }
255
+ return { prunedByTool, updatedByTool };
256
+ }
257
+ export function buildWorkerRestartReply(req, exit, now, recentRestartTimestamps, options = {}) {
258
+ if (options.structuredRestartDisabled || req.method !== "tools/call") {
259
+ return {
260
+ reply: buildLegacyJsonRpcError(req.id),
261
+ updatedTimestamps: recentRestartTimestamps
262
+ };
263
+ }
264
+ const pruned = pruneRestartTimestamps(recentRestartTimestamps, now);
265
+ // Include the current restart in the count so "3 deaths in 60s →
266
+ // report-bug" fires on the third event, not the fourth.
267
+ const updatedTimestamps = [...pruned, now];
268
+ const recommendation = decideRetryRecommendation({
269
+ toolName: req.toolName,
270
+ lastStage: req.lastStage,
271
+ lastStageMeta: req.lastStageMeta,
272
+ exit
273
+ }, updatedTimestamps);
274
+ const ctx = {
275
+ toolName: req.toolName,
276
+ durationMs: Math.max(0, now - req.startedAt),
277
+ lastStage: req.lastStage,
278
+ lastStageElapsedMs: req.lastStageStartedAt !== undefined
279
+ ? Math.max(0, now - req.lastStageStartedAt)
280
+ : undefined,
281
+ lastStageMeta: req.lastStageMeta,
282
+ exit,
283
+ toolArgsRedacted: req.toolArgsRedacted,
284
+ toolArgsRedactedModified: req.toolArgsRedactedModified ?? false,
285
+ retryRecommendation: recommendation
286
+ };
287
+ return {
288
+ reply: buildSyntheticCallToolResult(req.id, ctx),
289
+ updatedTimestamps
290
+ };
291
+ }
36
292
  function debugSupervisor(event, details) {
37
293
  if (!SUPERVISOR_DEBUG_ENABLED) {
38
294
  return;
@@ -45,6 +301,7 @@ export class StdioSupervisor {
45
301
  workerReader = new JsonRpcFrameReader();
46
302
  queuedMessages = [];
47
303
  pendingRequests = new Map();
304
+ recentRestarts = new Map();
48
305
  child;
49
306
  childReady = false;
50
307
  shuttingDown = false;
@@ -121,10 +378,21 @@ export class StdioSupervisor {
121
378
  if (isRequest(message)) {
122
379
  const id = getTrackedRequestId(message);
123
380
  if (id !== undefined) {
124
- this.pendingRequests.set(requestKey(id), {
381
+ const pending = {
125
382
  id,
126
- method: message.method
127
- });
383
+ method: message.method,
384
+ startedAt: performance.now()
385
+ };
386
+ if (message.method === "tools/call") {
387
+ const params = (message.params ?? {});
388
+ if (typeof params.name === "string") {
389
+ pending.toolName = params.name;
390
+ }
391
+ const redacted = redactToolArgs(params.arguments);
392
+ pending.toolArgsRedacted = redacted.args;
393
+ pending.toolArgsRedactedModified = redacted.modified;
394
+ }
395
+ this.pendingRequests.set(requestKey(id), pending);
128
396
  }
129
397
  if (message.method === "initialize") {
130
398
  this.initializeSentToWorker = true;
@@ -202,7 +470,7 @@ export class StdioSupervisor {
202
470
  signal,
203
471
  pendingRequests: this.pendingRequests.size
204
472
  });
205
- this.failPendingRequestsOnWorkerExit();
473
+ this.failPendingRequestsOnWorkerExit({ code, signal });
206
474
  this.scheduleRestart();
207
475
  };
208
476
  handleWorkerMessage(message) {
@@ -212,6 +480,12 @@ export class StdioSupervisor {
212
480
  id: "id" in message ? message.id : undefined,
213
481
  replayingInitialization: this.replayingInitialization
214
482
  });
483
+ if (isNotification(message) && typeof message.method === "string" && message.method.startsWith("$/")) {
484
+ if (message.method === "$/stageUpdate") {
485
+ this.applyStageUpdate(message.params);
486
+ }
487
+ return;
488
+ }
215
489
  if (this.isInitializationResponse(message)) {
216
490
  const id = getTrackedRequestId(message);
217
491
  if (id !== undefined) {
@@ -240,6 +514,33 @@ export class StdioSupervisor {
240
514
  }
241
515
  this.writeToClient(message);
242
516
  }
517
+ applyStageUpdate(params) {
518
+ if (params === null || params === undefined || typeof params !== "object") {
519
+ return;
520
+ }
521
+ const p = params;
522
+ const requestId = typeof p.requestId === "string" || typeof p.requestId === "number"
523
+ ? p.requestId
524
+ : undefined;
525
+ if (requestId === undefined)
526
+ return;
527
+ const pending = this.pendingRequests.get(requestKey(requestId));
528
+ if (!pending)
529
+ return;
530
+ // Refresh `lastStageStartedAt` only on stage transitions so
531
+ // `lastStageElapsedMs` measures from stage entry, not from the latest
532
+ // per-target emit. `lastStageMeta` still updates on every emit so
533
+ // restart envelopes carry the latest progress payload.
534
+ if (typeof p.stage === "string" && p.stage !== pending.lastStage) {
535
+ pending.lastStage = p.stage;
536
+ pending.lastStageStartedAt = performance.now();
537
+ }
538
+ else if (pending.lastStageStartedAt === undefined) {
539
+ // First emit for this request; bootstrap the stage timer.
540
+ pending.lastStageStartedAt = performance.now();
541
+ }
542
+ pending.lastStageMeta = p.meta;
543
+ }
243
544
  handleWorkerReady() {
244
545
  debugSupervisor("worker_ready", {
245
546
  hasInitializeRequest: this.initializeRequest !== undefined,
@@ -278,16 +579,32 @@ export class StdioSupervisor {
278
579
  this.forwardToWorker(message);
279
580
  }
280
581
  }
281
- failPendingRequestsOnWorkerExit() {
582
+ failPendingRequestsOnWorkerExit(exit) {
282
583
  const preservedInitializeKey = this.initializeRequest && !this.clientInitialized
283
584
  ? requestKey(this.initializeRequest.id)
284
585
  : undefined;
586
+ const now = performance.now();
587
+ // Record this exit once per toolName regardless of how many concurrent
588
+ // pending requests it killed; per-request recording would inflate the
589
+ // count to N and falsely escalate retryRecommendation to "report-bug".
590
+ const pendingToolNames = [];
591
+ for (const [key, pending] of this.pendingRequests.entries()) {
592
+ if (key === preservedInitializeKey)
593
+ continue;
594
+ pendingToolNames.push(pending.toolName);
595
+ }
596
+ const { prunedByTool, updatedByTool } = buildExitTimestampGroups(pendingToolNames, this.recentRestarts, now);
597
+ for (const [toolName, updated] of updatedByTool) {
598
+ this.recentRestarts.set(toolName, updated);
599
+ }
285
600
  for (const [key, pending] of [...this.pendingRequests.entries()]) {
286
- if (key === preservedInitializeKey) {
601
+ if (key === preservedInitializeKey)
287
602
  continue;
288
- }
289
603
  this.pendingRequests.delete(key);
290
- this.writeToClient(buildWorkerRestartError(pending.id));
604
+ const toolName = pending.toolName ?? "unknown";
605
+ const pruned = prunedByTool.get(toolName) ?? [];
606
+ const { reply } = buildWorkerRestartReply(pending, exit, now, pruned, { structuredRestartDisabled: STRUCTURED_RESTART_DISABLED });
607
+ this.writeToClient(reply);
291
608
  }
292
609
  }
293
610
  writeToClient(message) {
@@ -3,6 +3,7 @@ import type { ArtifactProvenance, ArtifactRow, SourceMapping, SourceOrigin } fro
3
3
  type SqliteDatabase = InstanceType<typeof Database>;
4
4
  interface UpsertArtifactInput {
5
5
  artifactId: string;
6
+ alias?: string;
6
7
  origin: SourceOrigin;
7
8
  coordinate?: string;
8
9
  version?: string;
@@ -27,6 +28,7 @@ export declare class ArtifactsRepo {
27
28
  private readonly upsertStmt;
28
29
  private readonly getStmt;
29
30
  private readonly touchStmt;
31
+ private readonly setAliasStmt;
30
32
  private readonly deleteStmt;
31
33
  private readonly listStmt;
32
34
  private readonly countStmt;
@@ -34,8 +36,9 @@ export declare class ArtifactsRepo {
34
36
  private readonly listLruWithContentBytesStmt;
35
37
  constructor(db: SqliteDatabase);
36
38
  upsertArtifact(input: UpsertArtifactInput): void;
37
- getArtifact(artifactId: string): ArtifactRow | undefined;
39
+ getArtifact(artifactIdOrAlias: string): ArtifactRow | undefined;
38
40
  touchArtifact(artifactId: string, timestamp: string): void;
41
+ setAlias(artifactId: string, alias: string): void;
39
42
  deleteArtifact(artifactId: string): void;
40
43
  listArtifactsByLru(limit: number): ArtifactRow[];
41
44
  countArtifacts(): number;
@@ -35,6 +35,7 @@ function parseQualityFlags(value) {
35
35
  function toArtifactRow(record) {
36
36
  return {
37
37
  artifactId: record.artifact_id,
38
+ alias: record.alias ?? undefined,
38
39
  origin: record.origin,
39
40
  coordinate: record.coordinate ?? undefined,
40
41
  version: record.version ?? undefined,
@@ -59,6 +60,7 @@ export class ArtifactsRepo {
59
60
  upsertStmt;
60
61
  getStmt;
61
62
  touchStmt;
63
+ setAliasStmt;
62
64
  deleteStmt;
63
65
  listStmt;
64
66
  countStmt;
@@ -68,11 +70,12 @@ export class ArtifactsRepo {
68
70
  this.db = db;
69
71
  this.upsertStmt = this.db.prepare(`
70
72
  INSERT INTO artifacts (
71
- artifact_id, origin, coordinate, version, binary_jar_path, source_jar_path, repo_url, requested_mapping, mapping_applied, provenance_json, quality_flags_json, artifact_signature, is_decompiled, created_at, updated_at
73
+ artifact_id, alias, origin, coordinate, version, binary_jar_path, source_jar_path, repo_url, requested_mapping, mapping_applied, provenance_json, quality_flags_json, artifact_signature, is_decompiled, created_at, updated_at
72
74
  ) VALUES (
73
- @artifact_id, @origin, @coordinate, @version, @binary_jar_path, @source_jar_path, @repo_url, @requested_mapping, @mapping_applied, @provenance_json, @quality_flags_json, @artifact_signature, @is_decompiled, @created_at, @updated_at
75
+ @artifact_id, @alias, @origin, @coordinate, @version, @binary_jar_path, @source_jar_path, @repo_url, @requested_mapping, @mapping_applied, @provenance_json, @quality_flags_json, @artifact_signature, @is_decompiled, @created_at, @updated_at
74
76
  )
75
77
  ON CONFLICT(artifact_id) DO UPDATE SET
78
+ alias = excluded.alias,
76
79
  origin = excluded.origin,
77
80
  coordinate = excluded.coordinate,
78
81
  version = excluded.version,
@@ -87,9 +90,12 @@ export class ArtifactsRepo {
87
90
  is_decompiled = excluded.is_decompiled,
88
91
  updated_at = excluded.updated_at
89
92
  `);
93
+ // artifact_id is a 64-char SHA hex; alias is `<type>-<...>-<6charhex>` containing
94
+ // dashes/letters. The two namespaces cannot collide, so OR-matching is unambiguous.
90
95
  this.getStmt = this.db.prepare(`
91
96
  SELECT
92
97
  artifact_id,
98
+ alias,
93
99
  origin,
94
100
  coordinate,
95
101
  version,
@@ -105,17 +111,28 @@ export class ArtifactsRepo {
105
111
  created_at,
106
112
  updated_at
107
113
  FROM artifacts
108
- WHERE artifact_id = ?
114
+ WHERE artifact_id = ? OR alias = ?
115
+ LIMIT 1
109
116
  `);
110
117
  this.touchStmt = this.db.prepare(`
111
118
  UPDATE artifacts
112
119
  SET updated_at = ?
113
120
  WHERE artifact_id = ?
121
+ `);
122
+ // Persists alias on cache-hit / migrated-row paths where upsertArtifact would
123
+ // otherwise be skipped. Conditional WHERE keeps it idempotent and avoids a
124
+ // pointless write when alias is already correct.
125
+ this.setAliasStmt = this.db.prepare(`
126
+ UPDATE artifacts
127
+ SET alias = ?
128
+ WHERE artifact_id = ?
129
+ AND (alias IS NULL OR alias <> ?)
114
130
  `);
115
131
  this.deleteStmt = this.db.prepare(`DELETE FROM artifacts WHERE artifact_id = ?`);
116
132
  this.listStmt = this.db.prepare(`
117
133
  SELECT
118
134
  artifact_id,
135
+ alias,
119
136
  origin,
120
137
  coordinate,
121
138
  version,
@@ -157,6 +174,7 @@ export class ArtifactsRepo {
157
174
  upsertArtifact(input) {
158
175
  this.upsertStmt.run({
159
176
  artifact_id: input.artifactId,
177
+ alias: input.alias ?? null,
160
178
  origin: input.origin,
161
179
  coordinate: input.coordinate ?? null,
162
180
  version: input.version ?? null,
@@ -173,8 +191,11 @@ export class ArtifactsRepo {
173
191
  updated_at: input.timestamp
174
192
  });
175
193
  }
176
- getArtifact(artifactId) {
177
- const row = this.getStmt.get([artifactId]);
194
+ // Accepts either an artifact_id (64-char SHA hex) or a human-readable alias
195
+ // (e.g. `mc-1.21.10-mojang-merged-5ad2e7`). The two key namespaces cannot
196
+ // collide because aliases always contain `-` and a non-hex prefix.
197
+ getArtifact(artifactIdOrAlias) {
198
+ const row = this.getStmt.get([artifactIdOrAlias, artifactIdOrAlias]);
178
199
  if (!row) {
179
200
  return undefined;
180
201
  }
@@ -183,6 +204,13 @@ export class ArtifactsRepo {
183
204
  touchArtifact(artifactId, timestamp) {
184
205
  this.touchStmt.run([timestamp, artifactId]);
185
206
  }
207
+ // Backfills or rotates the alias for an existing row. Used by warm-cache
208
+ // resolveArtifact paths where upsertArtifact is skipped, so a freshly
209
+ // computed alias still reaches the DB and stays in sync with the value the
210
+ // caller just received in the response.
211
+ setAlias(artifactId, alias) {
212
+ this.setAliasStmt.run([alias, artifactId, alias]);
213
+ }
186
214
  deleteArtifact(artifactId) {
187
215
  this.deleteStmt.run([artifactId]);
188
216
  }
@@ -43,8 +43,6 @@ export declare class FilesRepo {
43
43
  private readonly db;
44
44
  private readonly deleteStmt;
45
45
  private readonly insertFilesStmt;
46
- private readonly insertFtsStmt;
47
- private readonly deleteFtsStmt;
48
46
  private readonly getContentStmt;
49
47
  private readonly listStmt;
50
48
  private readonly listRowsStmt;