@creeperhost/modlens-mcp 1.6.18 → 1.6.20

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/README.md +112 -15
  2. package/TESTING.md +69 -0
  3. package/dist/cli.js +49 -13
  4. package/dist/cli.js.map +1 -1
  5. package/dist/curseforge.d.ts +9 -7
  6. package/dist/curseforge.d.ts.map +1 -1
  7. package/dist/curseforge.js +48 -55
  8. package/dist/curseforge.js.map +1 -1
  9. package/dist/db.d.ts.map +1 -1
  10. package/dist/db.js +31 -1
  11. package/dist/db.js.map +1 -1
  12. package/dist/embeddings.d.ts.map +1 -1
  13. package/dist/embeddings.js +1 -0
  14. package/dist/embeddings.js.map +1 -1
  15. package/dist/feed-the-beast.d.ts +5 -5
  16. package/dist/feed-the-beast.d.ts.map +1 -1
  17. package/dist/feed-the-beast.js +5 -21
  18. package/dist/feed-the-beast.js.map +1 -1
  19. package/dist/java-tools.d.ts +1 -0
  20. package/dist/java-tools.d.ts.map +1 -1
  21. package/dist/java-tools.js +31 -7
  22. package/dist/java-tools.js.map +1 -1
  23. package/dist/launcher.js +15 -0
  24. package/dist/launcher.js.map +1 -1
  25. package/dist/mappings.d.ts +4 -1
  26. package/dist/mappings.d.ts.map +1 -1
  27. package/dist/mappings.js +120 -52
  28. package/dist/mappings.js.map +1 -1
  29. package/dist/minecraft.d.ts +0 -4
  30. package/dist/minecraft.d.ts.map +1 -1
  31. package/dist/minecraft.js +7 -23
  32. package/dist/minecraft.js.map +1 -1
  33. package/dist/mod-artifacts.d.ts +4 -0
  34. package/dist/mod-artifacts.d.ts.map +1 -0
  35. package/dist/mod-artifacts.js +58 -0
  36. package/dist/mod-artifacts.js.map +1 -0
  37. package/dist/modpacks-ch.d.ts +72 -46
  38. package/dist/modpacks-ch.d.ts.map +1 -1
  39. package/dist/modpacks-ch.js +134 -20
  40. package/dist/modpacks-ch.js.map +1 -1
  41. package/dist/modrinth.d.ts +14 -6
  42. package/dist/modrinth.d.ts.map +1 -1
  43. package/dist/modrinth.js +70 -55
  44. package/dist/modrinth.js.map +1 -1
  45. package/dist/platform-adapter.d.ts +1 -0
  46. package/dist/platform-adapter.d.ts.map +1 -1
  47. package/dist/platform.d.ts.map +1 -1
  48. package/dist/platform.js +20 -44
  49. package/dist/platform.js.map +1 -1
  50. package/dist/primer-catalog.d.ts +15 -0
  51. package/dist/primer-catalog.d.ts.map +1 -0
  52. package/dist/primer-catalog.js +43 -0
  53. package/dist/primer-catalog.js.map +1 -0
  54. package/dist/primer-content.d.ts +7 -0
  55. package/dist/primer-content.d.ts.map +1 -0
  56. package/dist/primer-content.js +185 -0
  57. package/dist/primer-content.js.map +1 -0
  58. package/dist/processor.d.ts +5 -0
  59. package/dist/processor.d.ts.map +1 -1
  60. package/dist/processor.js +166 -46
  61. package/dist/processor.js.map +1 -1
  62. package/dist/registry-submission.d.ts +34 -0
  63. package/dist/registry-submission.d.ts.map +1 -0
  64. package/dist/registry-submission.js +128 -0
  65. package/dist/registry-submission.js.map +1 -0
  66. package/dist/repositories/array-fields.d.ts +3 -0
  67. package/dist/repositories/array-fields.d.ts.map +1 -0
  68. package/dist/repositories/array-fields.js +8 -0
  69. package/dist/repositories/array-fields.js.map +1 -0
  70. package/dist/repositories/embeddings-sqlite.d.ts +1 -0
  71. package/dist/repositories/embeddings-sqlite.d.ts.map +1 -1
  72. package/dist/repositories/embeddings-sqlite.js +26 -58
  73. package/dist/repositories/embeddings-sqlite.js.map +1 -1
  74. package/dist/repositories/embeddings.d.ts.map +1 -1
  75. package/dist/repositories/embeddings.js +26 -3
  76. package/dist/repositories/embeddings.js.map +1 -1
  77. package/dist/repositories/index.d.ts +1 -1
  78. package/dist/repositories/index.d.ts.map +1 -1
  79. package/dist/repositories/mod.d.ts +8 -5
  80. package/dist/repositories/mod.d.ts.map +1 -1
  81. package/dist/repositories/mod.js +25 -11
  82. package/dist/repositories/mod.js.map +1 -1
  83. package/dist/repositories/packs.d.ts.map +1 -1
  84. package/dist/repositories/packs.js +10 -2
  85. package/dist/repositories/packs.js.map +1 -1
  86. package/dist/search-adapter.d.ts +2 -0
  87. package/dist/search-adapter.d.ts.map +1 -1
  88. package/dist/search-adapter.js +21 -9
  89. package/dist/search-adapter.js.map +1 -1
  90. package/dist/security.d.ts +2 -0
  91. package/dist/security.d.ts.map +1 -1
  92. package/dist/security.js +5 -1
  93. package/dist/security.js.map +1 -1
  94. package/dist/server.js +80 -35
  95. package/dist/server.js.map +1 -1
  96. package/dist/setup.js +9 -4
  97. package/dist/setup.js.map +1 -1
  98. package/dist/sqlite-json.d.ts +3 -0
  99. package/dist/sqlite-json.d.ts.map +1 -0
  100. package/dist/sqlite-json.js +78 -0
  101. package/dist/sqlite-json.js.map +1 -0
  102. package/dist/sqlite-parameters.d.ts +3 -0
  103. package/dist/sqlite-parameters.d.ts.map +1 -0
  104. package/dist/sqlite-parameters.js +19 -0
  105. package/dist/sqlite-parameters.js.map +1 -0
  106. package/dist/sqlite-schema.d.ts +4 -0
  107. package/dist/sqlite-schema.d.ts.map +1 -0
  108. package/dist/sqlite-schema.js +76 -0
  109. package/dist/sqlite-schema.js.map +1 -0
  110. package/dist/tools/catalog.d.ts +1 -6
  111. package/dist/tools/catalog.d.ts.map +1 -1
  112. package/dist/tools/catalog.js +10 -14
  113. package/dist/tools/catalog.js.map +1 -1
  114. package/dist/tools/compat-check.d.ts.map +1 -1
  115. package/dist/tools/compat-check.js +18 -30
  116. package/dist/tools/compat-check.js.map +1 -1
  117. package/dist/tools/diagnostics.js.map +1 -1
  118. package/dist/tools/docs.d.ts.map +1 -1
  119. package/dist/tools/docs.js +8 -3
  120. package/dist/tools/docs.js.map +1 -1
  121. package/dist/tools/embed-registry.d.ts +20 -4
  122. package/dist/tools/embed-registry.d.ts.map +1 -1
  123. package/dist/tools/embed-registry.js +30 -19
  124. package/dist/tools/embed-registry.js.map +1 -1
  125. package/dist/tools/graphify.d.ts +19 -4
  126. package/dist/tools/graphify.d.ts.map +1 -1
  127. package/dist/tools/graphify.js +61 -13
  128. package/dist/tools/graphify.js.map +1 -1
  129. package/dist/tools/ingest.d.ts.map +1 -1
  130. package/dist/tools/ingest.js +4 -4
  131. package/dist/tools/ingest.js.map +1 -1
  132. package/dist/tools/kubejs.js +1 -1
  133. package/dist/tools/kubejs.js.map +1 -1
  134. package/dist/tools/mc-fts.d.ts.map +1 -1
  135. package/dist/tools/mc-fts.js +9 -4
  136. package/dist/tools/mc-fts.js.map +1 -1
  137. package/dist/tools/mcmeta.d.ts +10 -1
  138. package/dist/tools/mcmeta.d.ts.map +1 -1
  139. package/dist/tools/mcmeta.js +43 -29
  140. package/dist/tools/mcmeta.js.map +1 -1
  141. package/dist/tools/mixin-scan.d.ts +12 -6
  142. package/dist/tools/mixin-scan.d.ts.map +1 -1
  143. package/dist/tools/mixin-scan.js +12 -7
  144. package/dist/tools/mixin-scan.js.map +1 -1
  145. package/dist/tools/mod-data.d.ts.map +1 -1
  146. package/dist/tools/mod-data.js +40 -27
  147. package/dist/tools/mod-data.js.map +1 -1
  148. package/dist/tools/modpacks-ch.d.ts +9 -5
  149. package/dist/tools/modpacks-ch.d.ts.map +1 -1
  150. package/dist/tools/modpacks-ch.js +54 -33
  151. package/dist/tools/modpacks-ch.js.map +1 -1
  152. package/dist/tools/packtools.js +3 -3
  153. package/dist/tools/packtools.js.map +1 -1
  154. package/dist/tools/platform.d.ts +2 -2
  155. package/dist/tools/platform.d.ts.map +1 -1
  156. package/dist/tools/platform.js +33 -23
  157. package/dist/tools/platform.js.map +1 -1
  158. package/dist/tools/primers.d.ts +133 -22
  159. package/dist/tools/primers.d.ts.map +1 -1
  160. package/dist/tools/primers.js +391 -386
  161. package/dist/tools/primers.js.map +1 -1
  162. package/dist/tools/project.d.ts +342 -0
  163. package/dist/tools/project.d.ts.map +1 -0
  164. package/dist/tools/project.js +519 -0
  165. package/dist/tools/project.js.map +1 -0
  166. package/dist/tools/reports.d.ts.map +1 -1
  167. package/dist/tools/reports.js +5 -3
  168. package/dist/tools/reports.js.map +1 -1
  169. package/dist/tools/vanilla-data.d.ts.map +1 -1
  170. package/dist/tools/vanilla-data.js +3 -33
  171. package/dist/tools/vanilla-data.js.map +1 -1
  172. package/dist/tools/vanilla.d.ts +3 -1
  173. package/dist/tools/vanilla.d.ts.map +1 -1
  174. package/dist/tools/vanilla.js +26 -12
  175. package/dist/tools/vanilla.js.map +1 -1
  176. package/dist/tools/version-diff.d.ts.map +1 -1
  177. package/dist/tools/version-diff.js +4 -24
  178. package/dist/tools/version-diff.js.map +1 -1
  179. package/dist/version-ranges.d.ts +5 -0
  180. package/dist/version-ranges.d.ts.map +1 -0
  181. package/dist/version-ranges.js +93 -0
  182. package/dist/version-ranges.js.map +1 -0
  183. package/package.json +9 -1
  184. package/prisma/backends/template.db +0 -0
  185. package/scripts/build-template-db.mjs +3 -1
  186. package/scripts/enable-sqlite-vec.mjs +2 -106
  187. package/scripts/gradle/modlens.init.gradle +154 -0
  188. package/scripts/project-upload.mjs +74 -0
  189. package/scripts/test-minecraft-eras.mjs +202 -0
  190. package/scripts/test-package.mjs +105 -5
  191. package/scripts/test-primers-live.mjs +93 -0
  192. package/scripts/test-project-gradle.mjs +76 -0
  193. package/scripts/test-project-http.mjs +98 -0
  194. package/scripts/test-setup.mjs +18 -0
package/dist/server.js CHANGED
@@ -31,13 +31,14 @@ import { getModGradleFiles, searchGradleFiles, compareGradleDeps, } from "./tool
31
31
  import { generateReport } from "./tools/reports.js";
32
32
  import { findAssetConflicts, findVanillaOverrides, analyzeModSidedness, analyzePackSidedness, computeModComplexity, computePackChangelog, findDataConflicts, buildPackGraph, diffPackConfigs, packHealth } from "./tools/packtools.js";
33
33
  import { indexKubeJsScripts, searchKubeJsScripts, semanticSearchKubeJsScripts } from "./tools/kubejs.js";
34
- import { searchPacksAction, featuredPacksAction, packInfoAction, packManifestAction, syncPackModsAction, resolvePackAction, listRemotePackVersionsAction, ingestPackAction, searchFtbModsAction, ftbModInfoAction, downloadModAction, downloadOverridesAction, listPackVersionsAction, listPackFilesAction, findModInPacksAction } from "./tools/modpacks-ch.js";
34
+ import { searchPacksAction, featuredPacksAction, packInfoAction, packManifestAction, syncPackModsAction, resolvePackAction, listRemotePackVersionsAction, ingestPackAction, searchModsAction, modInfoAction, downloadModAction, downloadOverridesAction, listPackVersionsAction, listPackFilesAction, findModInPacksAction } from "./tools/modpacks-ch.js";
35
35
  import { analyzeCrashLog, findMissingDeps } from "./tools/diagnostics.js";
36
36
  import { checkModCompat } from "./tools/compat-check.js";
37
37
  import { disconnect } from "./db.js";
38
38
  import { mcPaths } from "./minecraft.js";
39
39
  import { findModById, resolveModRefSlim } from "./repositories/mod.js";
40
40
  import { CACHE_ROOT } from "./cache.js";
41
+ import { projectAction, projectToolSchema } from "./tools/project.js";
41
42
  // Load .env — try ~/.modlens/.env first (npx/installed users), then local .env (git-clone users)
42
43
  import { readFileSync, existsSync } from "fs";
43
44
  import { join, dirname } from "path";
@@ -72,7 +73,7 @@ const VERBOSE = process.env.MODLENS_VERBOSE
72
73
  /** Best-effort, bounded JSON for logging tool params (handles circular refs). */
73
74
  function previewArgs(value) {
74
75
  try {
75
- const json = JSON.stringify(value);
76
+ const json = JSON.stringify(value, (key, item) => ["projectKey", "data"].includes(key) ? "[redacted]" : item);
76
77
  if (json == null)
77
78
  return String(value);
78
79
  return json.length > 2000 ? json.slice(0, 2000) + "…(truncated)" : json;
@@ -105,6 +106,12 @@ function resolveDbId(dbId, modId) {
105
106
  const n = typeof modId === "number" ? modId : Number(modId);
106
107
  return Number.isFinite(n) && n > 0 ? n : undefined;
107
108
  }
109
+ function requireModRef(modId, dbId) {
110
+ const ref = dbId ?? modId;
111
+ if (ref === undefined || ref === "")
112
+ throw new Error("modId or dbId is required");
113
+ return ref;
114
+ }
108
115
  /**
109
116
  * Like resolveDbId, but for actions that need a valid DB id — will look up
110
117
  * string modIds via the database so agents can pass e.g. modId:"polylib".
@@ -149,10 +156,12 @@ function safe(fn) {
149
156
  }
150
157
  class JsonBodyParseError extends Error {
151
158
  }
159
+ class HttpBodyTooLargeError extends Error {
160
+ }
152
161
  function createMcpServer() {
153
162
  const server = new McpServer({ name: "modlens", version: pkg.version });
154
163
  // ── 1. mod ────────────────────────────────────────────────────────────────────
155
- server.tool("mod", "Mod database, decompile, and source browser. action=ingest|list|get|search|stats|dependencies|dep_graph|version_conflicts|source_urls|decompile|decompile_status|decompile_class|source|search_source|reindex|batch_ingest|batch_decompile|refresh_metadata|index_fts|search_indexed|index_semantic|search_semantic|get_paths|delete|graph_build|graph_status|graph_query|graph_report|graph_enrich_next|graph_enrich_submit|graph_download|graph_export|graph_submit|embed_export|embed_download|embed_download_all|embed_status|embed_submit. index_fts/search_indexed: BM25-ranked FTS over source code. index_semantic/search_semantic: vector search (requires Ollama). refresh_metadata: re-parse all mods with degraded metadata (filename/@Mod annotation) and upgrade if a higher-quality manifest is found. graph_enrich_next: get next un-enriched chunk for chat enrichment. graph_enrich_submit: submit enriched nodes/edges (chunkIndex, nodes, edges). graph_download: download pre-built graph from registry (targetType=mod|vanilla|modloader with targetId/targetVersion). graph_export: export a mod's local graph as a shareable gzipped bundle. graph_submit: (stub) community graph submission — not yet functional. embed_export/embed_download/embed_status: targetType-aware embeddings actions for mod/vanilla/modloader. embed_download: downloads registry embeddings; protects local embeddings by default (use force=true to overwrite). embed_download_all: download embeddings for all mods. embed_submit: (stub) community embedding submission — not yet functional. Auto-behaviors: decompile_status auto-queues embedding (MODLENS_AUTO_EMBED) and graph build (MODLENS_AUTO_GRAPH). Pass autoEmbed/autoGraph to override per-call.", {
164
+ server.tool("mod", "Mod database, decompile, and source browser. action=ingest|list|get|search|stats|dependencies|dep_graph|version_conflicts|source_urls|decompile|decompile_status|decompile_class|source|search_source|reindex|batch_ingest|batch_decompile|refresh_metadata|index_fts|search_indexed|index_semantic|search_semantic|get_paths|delete|graph_build|graph_status|graph_query|graph_report|graph_enrich_next|graph_enrich_submit|graph_download|graph_export|graph_submit|embed_export|embed_download|embed_download_all|embed_status|embed_submit. index_fts/search_indexed: BM25-ranked FTS over source code. index_semantic/search_semantic: vector search (requires Ollama). refresh_metadata: re-parse all mods with degraded metadata (filename/@Mod annotation) and upgrade if a higher-quality manifest is found. graph_enrich_next: get next un-enriched chunk for chat enrichment. graph_enrich_submit: submit enriched nodes/edges (chunkIndex, nodes, edges). graph_download: download pre-built graph from registry (targetType=mod|vanilla|modloader with targetId/targetVersion). graph_export: export a mod's local graph as a shareable gzipped bundle. graph_submit: validate an exported bundle and open a draft GitHub registry PR (bundlePath required). embed_export/embed_download/embed_status: targetType-aware embeddings actions for mod/vanilla/modloader. embed_download: downloads registry embeddings; protects local embeddings by default (use force=true to overwrite). embed_download_all: download embeddings for all mods. embed_submit: validate an exported bundle and open a draft GitHub registry PR (bundlePath required). Auto-behaviors: decompile_status auto-queues embedding (MODLENS_AUTO_EMBED) and graph build (MODLENS_AUTO_GRAPH). Pass autoEmbed/autoGraph to override per-call.", {
156
165
  action: z.enum([
157
166
  "ingest", "list", "get", "search", "stats", "dependencies", "dep_graph",
158
167
  "version_conflicts", "source_urls", "decompile", "decompile_status",
@@ -193,10 +202,13 @@ function createMcpServer() {
193
202
  targetLoader: z.string().optional().describe("Optional loader hint for registry matching"),
194
203
  targetMcVersion: z.string().optional().describe("Optional MC version hint for registry matching"),
195
204
  model: z.string().optional().describe("Embedding model override for embed_download"),
205
+ bundlePath: z.string().optional().describe("Exported bundle file for graph_submit or embed_submit"),
206
+ contributor: z.string().optional().describe("Contributor alias for a registry submission"),
207
+ note: z.string().optional().describe("Note for the registry pull request"),
196
208
  autoEmbed: z.boolean().optional().describe("Override auto-embed on decompile completion (default: env MODLENS_AUTO_EMBED, true)"),
197
209
  autoGraph: z.boolean().optional().describe("Override auto-graph-build on decompile/source-download (default: env MODLENS_AUTO_GRAPH, true)"),
198
210
  provenance: z.enum(["local", "registry", "community"]).optional().describe("Filter search_semantic results by embedding source (default: all sources)"),
199
- }, safe(async ({ action, jarPath, modId, dbId: rawDbId, query, path, className, loader, mcVersion, hasMixins, decompiled, recursive, skipSource, isRegex, force, limit, directory, indexClasses, replace, budget, backend, chunkIndex, nodes, edges, outputDir, modVersion, targetType, targetId, targetVersion, targetLoader, targetMcVersion, model, autoEmbed, autoGraph, provenance }) => {
211
+ }, safe(async ({ action, jarPath, modId, dbId: rawDbId, query, path, className, loader, mcVersion, hasMixins, decompiled, recursive, skipSource, isRegex, force, limit, directory, indexClasses, replace, budget, backend, chunkIndex, nodes, edges, outputDir, modVersion, targetType, targetId, targetVersion, targetLoader, targetMcVersion, model, autoEmbed, autoGraph, provenance, bundlePath, contributor, note }) => {
200
212
  const dbId = await resolveDbIdAsync(rawDbId, modId);
201
213
  const resolvedTargetType = targetType ?? "mod";
202
214
  const resolvedTargetId = targetId ?? (typeof modId === "string" ? modId : undefined) ?? (resolvedTargetType === "vanilla" ? "minecraft" : undefined);
@@ -210,7 +222,7 @@ function createMcpServer() {
210
222
  result = await listMods({ loader: loader, mcVersion, hasMixins, decompiled, limit: limit ?? 50 });
211
223
  break;
212
224
  case "get":
213
- result = await getModDetails(modId);
225
+ result = await getModDetails(requireModRef(modId, rawDbId));
214
226
  break;
215
227
  case "search":
216
228
  result = await searchMods(query, { loader, mcVersion, limit: limit ?? 20 });
@@ -219,7 +231,7 @@ function createMcpServer() {
219
231
  result = await getDbStats();
220
232
  break;
221
233
  case "dependencies":
222
- result = await getDependencies(modId, recursive ?? false);
234
+ result = await getDependencies(requireModRef(modId, rawDbId), recursive ?? false);
223
235
  break;
224
236
  case "dep_graph":
225
237
  result = await getDependencyGraph(mcVersion);
@@ -337,7 +349,7 @@ function createMcpServer() {
337
349
  result = await exportGraph(dbId, outputDir);
338
350
  break;
339
351
  case "graph_submit":
340
- result = await submitGraph({ bundlePath: outputDir ?? "" });
352
+ result = await submitGraph({ bundlePath: bundlePath ?? outputDir ?? "", contributor, note });
341
353
  break;
342
354
  case "embed_export":
343
355
  result = await exportEmbeddings(resolvedTargetType, outputDir, { dbId, mcVersion: resolvedTargetVersion ?? mcVersion, targetId: resolvedTargetId });
@@ -363,7 +375,7 @@ function createMcpServer() {
363
375
  });
364
376
  break;
365
377
  case "embed_submit":
366
- result = await submitEmbeddings({ bundlePath: outputDir ?? "" });
378
+ result = await submitEmbeddings({ bundlePath: bundlePath ?? outputDir ?? "", contributor, note });
367
379
  break;
368
380
  }
369
381
  return out(result);
@@ -458,7 +470,7 @@ function createMcpServer() {
458
470
  let result;
459
471
  switch (action) {
460
472
  case "targets":
461
- result = await getMixinTargets(modId);
473
+ result = await getMixinTargets(requireModRef(modId, rawDbId));
462
474
  break;
463
475
  case "resolve":
464
476
  result = await resolveMixinTargets(dbId);
@@ -483,7 +495,7 @@ function createMcpServer() {
483
495
  }));
484
496
  // ── 4. platform ───────────────────────────────────────────────────────────────
485
497
  server.tool("platform", "Modrinth/CurseForge platform sync and source download. action=sync_modrinth|sync_curseforge|check_updates|batch_sync|download_source|search|batch_check_updates. " +
486
- "action=search: unified mod search across both Modrinth and CurseForge — deduplicated, sorted by downloads (query required, loader, mcVersion, limit). " +
498
+ "action=search: unified mod search across both Modrinth and CurseForge — deduplicated, sorted by downloads (query required, loader, mcVersion, limit). Missing modpacks.ch loader labels are recovered by inspecting cached/downloaded JARs. " +
487
499
  "action=batch_check_updates: check ALL linked mods for newer versions on Modrinth/CurseForge in one pass (mcVersion, loader, modIdFilter, limit). " +
488
500
  "download_source downloads GitHub source without decompilation — auto-queues graph build (override with autoGraph). Env: MODLENS_AUTO_GRAPH (default: 1).", {
489
501
  action: z.enum(["sync_modrinth", "sync_curseforge", "check_updates", "batch_sync", "download_source", "search", "batch_check_updates"]),
@@ -529,22 +541,22 @@ function createMcpServer() {
529
541
  }));
530
542
  // ── 5. modpacks_ch ───────────────────────────────────────────────────────────
531
543
  server.tool("modpacks_ch", "Search, resolve, list versions for, and ingest arbitrary modpacks so other ModLens tools can analyze the exact pack contents. General workflow: when the user names a modpack and possibly a fuzzy version like 7.1, call action=list_versions with packRef/versionRef, choose the best matching version, then call action=ingest_pack before using analyze_crash_log, reports, pack_tools, dependency checks, mixin scans, source search, or other pack-aware analysis. Crash triage is one common use, not the only use. " +
532
- "Backends: modpacks.ch API (CreeperHost service) for FTB/CurseForge, Modrinth API/.mrpack for Modrinth, and the official Feed The Beast API when namespace=feedthebeast. CurseForge via modpacks.ch requires no CurseForge API key. Modrinth packRef may be a slug/project ID, a modrinth.com modpack/version URL, an api.modrinth.com project/version URL, a cdn.modrinth.com .mrpack URL, or any direct HTTPS .mrpack URL. " +
533
- "action=search|featured|info|manifest|resolve_pack|list_versions|ingest_pack|sync_pack_mods|search_ftb_mods|ftb_mod_info|download_mod|download_overrides|list_pack_versions|list_pack_files|find_mod_in_packs. " +
534
- "namespace=ftb|curseforge|modrinth|feedthebeast (default: ftb). Use feedthebeast for api.feed-the-beast.com packs; use ftb for the modpacks.ch FTB namespace. resolve_pack/list_versions/ingest_pack accept packRef=name/slug/id and versionRef=version id/name/number/partial text. list_versions returns matches for fuzzy refs; list_pack_versions is DB-backed synced-pack history. " +
544
+ "Metadata comes from modpacks.ch for all providers; downloads use the supplied URLs and Modrinth .mrpack archives. Loader-filtered mod queries automatically download and inspect unlabeled JARs; recovered labels report loaderSource=jar. No CurseForge API key is required. Modrinth packRef may be a slug/project ID, a modrinth.com modpack/version URL, an api.modrinth.com project/version URL, a cdn.modrinth.com .mrpack URL, or any direct HTTPS .mrpack URL. " +
545
+ "action=search|featured|info|manifest|resolve_pack|list_versions|ingest_pack|sync_pack_mods|search_mods|mod_info|download_mod|download_overrides|list_pack_versions|list_pack_files|find_mod_in_packs. mod_info returns up to limit versions (default 20). search_ftb_mods and ftb_mod_info remain compatibility aliases. " +
546
+ "namespace=ftb|curseforge|modrinth|feedthebeast (default: ftb). feedthebeast uses the modpacks.ch /ftb catalog; ftb uses /modpack. resolve_pack/list_versions/ingest_pack accept packRef=name/slug/id and versionRef=version id/name/number/partial text. list_versions returns matches for fuzzy refs; list_pack_versions is DB-backed synced-pack history. " +
535
547
  "User-Agent is set per modpacks.ch (CreeperHost) team request for usage tracking.", {
536
- action: z.enum(["search", "featured", "info", "manifest", "resolve_pack", "list_versions", "ingest_pack", "sync_pack_mods", "search_ftb_mods", "ftb_mod_info", "download_mod", "download_overrides", "list_pack_versions", "list_pack_files", "find_mod_in_packs"]),
537
- namespace: z.enum(["ftb", "curseforge", "modrinth", "feedthebeast"]).optional().describe("ftb, curseforge, modrinth, or feedthebeast (default: ftb). feedthebeast uses api.feed-the-beast.com; ftb uses modpacks.ch."),
548
+ action: z.enum(["search", "featured", "info", "manifest", "resolve_pack", "list_versions", "ingest_pack", "sync_pack_mods", "search_mods", "mod_info", "search_ftb_mods", "ftb_mod_info", "download_mod", "download_overrides", "list_pack_versions", "list_pack_files", "find_mod_in_packs"]),
549
+ namespace: z.enum(["ftb", "curseforge", "modrinth", "feedthebeast"]).optional().describe("Provider catalog on modpacks.ch (default: ftb). feedthebeast selects /ftb; ftb selects /modpack."),
538
550
  packId: z.number().optional().describe("Numeric FTB/CurseForge pack ID or internal Modrinth pack key returned by resolve/list actions"),
539
551
  versionId: z.number().optional().describe("Numeric FTB/CurseForge version ID or internal Modrinth version key returned by resolve/list actions"),
540
552
  packRef: z.union([z.number(), z.string()]).optional().describe("Pack name, slug, Modrinth project ID, CurseForge pack ID, FTB/feedthebeast pack ID, Modrinth web/API/CDN URL, or direct HTTPS .mrpack URL. Use for resolve_pack/list_versions/ingest_pack."),
541
553
  versionRef: z.union([z.number(), z.string()]).optional().describe("Version ID, full version name, version number, or fuzzy partial text such as 7.1. Use list_versions first if ambiguous."),
542
554
  packVersionDbId: z.number().optional().describe("Pack version DB id returned by sync_pack_mods (shortcut for list_pack_files)"),
543
- query: z.string().optional().describe("Search query (required for search, search_ftb_mods)"),
544
- modId: z.union([z.number(), z.string()]).optional().describe("FTB mod ID (required for ftb_mod_info, download_mod)"),
545
- fileId: z.number().optional().describe("Exact CF file ID to download (download_mod; skips mcVersion/loader filter)"),
546
- loader: z.string().optional().describe("Loader filter for ftb_mod_info/download_mod (e.g. neoforge, fabric)"),
547
- mcVersionFilter: z.string().optional().describe("MC version filter for ftb_mod_info/download_mod (e.g. 26.1.2)"),
555
+ query: z.string().optional().describe("Search query (required for search, search_mods)"),
556
+ modId: z.union([z.number(), z.string()]).optional().describe("CurseForge project ID or Modrinth project ID/slug (required for mod_info, download_mod)"),
557
+ fileId: z.union([z.number(), z.string()]).optional().describe("Exact CurseForge file ID or Modrinth version ID to download (download_mod; skips mcVersion/loader filter)"),
558
+ loader: z.string().optional().describe("Loader filter for mod_info/download_mod (e.g. forge, fabric, neoforge)"),
559
+ mcVersionFilter: z.string().optional().describe("MC version filter for mod_info/download_mod (e.g. 1.7.10, 1.12.2, 1.21.1)"),
548
560
  force: z.boolean().optional().describe("Force re-download even if cached (download_mod)"),
549
561
  modDbId: z.number().optional().describe("ModLens DB mod id (for find_mod_in_packs)"),
550
562
  cfProject: z.number().optional().describe("CurseForge project id (for find_mod_in_packs)"),
@@ -597,15 +609,17 @@ function createMcpServer() {
597
609
  throw new Error("Use action=ingest_pack for Modrinth packs");
598
610
  result = await syncPackModsAction({ packId, versionId, namespace: ns, fileTypes, skipServer, skipOptional, concurrency, maxFiles });
599
611
  break;
612
+ case "search_mods":
600
613
  case "search_ftb_mods":
601
614
  if (!query)
602
- throw new Error("query is required for action=search_ftb_mods");
603
- result = await searchFtbModsAction(query, limit ?? 20);
615
+ throw new Error("query is required for action=search_mods");
616
+ result = await searchModsAction(query, limit ?? 20);
604
617
  break;
618
+ case "mod_info":
605
619
  case "ftb_mod_info":
606
620
  if (modId === undefined)
607
- throw new Error("modId is required for action=ftb_mod_info");
608
- result = await ftbModInfoAction(modId, { mcVersion: mcVersionFilter, loader });
621
+ throw new Error("modId is required for action=mod_info");
622
+ result = await modInfoAction(modId, { mcVersion: mcVersionFilter, loader, limit });
609
623
  break;
610
624
  case "download_mod":
611
625
  if (modId === undefined)
@@ -889,7 +903,7 @@ function createMcpServer() {
889
903
  return out(result);
890
904
  }));
891
905
  // ── 9. primers ────────────────────────────────────────────────────────────────
892
- server.tool("primers", "Minecraft version migration primers and porting guides. action=ingest|seed|get|by_version|search|list|delete|semantic_search|backfill_embeddings. semantic_search requires Ollama running.", {
906
+ server.tool("primers", "Minecraft migration primers. by_version finds each migration step, including vanilla changes for the selected loader; includeContent=true bundles the Markdown with a shared maxChars budget. Continue with nextCursor until null, keeping the same range and loader. get reads one guide (follow nextStartLine). Missing content is fetched and cached unless fetchContent=false. seed populates the catalogue. Bundles report per-guide failures; retry failed IDs with get. Published guides may leave coverage gaps. semantic_search requires Ollama.", {
893
907
  action: z.enum(["ingest", "seed", "get", "by_version", "search", "list", "delete", "semantic_search", "backfill_embeddings"]),
894
908
  entries: z.array(z.object({
895
909
  fromVersion: z.string(),
@@ -902,27 +916,34 @@ function createMcpServer() {
902
916
  tags: z.array(z.string()).optional(),
903
917
  source: z.string().optional(),
904
918
  fetchContent: z.boolean().optional(),
905
- })).optional().describe("Primer entries to ingest (ingest)"),
906
- id: z.number().optional(),
919
+ })).min(1).optional().describe("Primer entries to ingest; set entries[].fetchContent=true to fetch before saving"),
920
+ id: z.number().int().positive().optional(),
907
921
  fromVersion: z.string().optional(),
908
922
  toVersion: z.string().optional(),
909
- modloader: z.string().optional().describe("neoforge|forge|fabric|quilt"),
923
+ modloader: z.string().optional().describe("neoforge|forge|fabric|quilt|vanilla; discovery includes vanilla guides"),
910
924
  query: z.string().optional(),
911
- limit: z.number().optional(),
912
- }, safe(async ({ action, entries, id, fromVersion, toVersion, modloader, query, limit }) => {
925
+ limit: z.number().int().min(1).max(200).optional(),
926
+ fetchContent: z.boolean().optional().describe("get/seed/by_version: fetch missing content (default true); false permits cached/offline reads"),
927
+ includeContent: z.boolean().optional().describe("by_version: bundle guide bodies (default false); includes vanilla guides for the selected loader"),
928
+ maxChars: z.number().int().min(1000).max(200000).optional().describe("by_version with includeContent: combined text budget (default 60000); up to 20 guides per page"),
929
+ cursor: z.string().min(1).max(2048).optional().describe("by_version with includeContent: nextCursor from the previous page; retain the same range and loader"),
930
+ refresh: z.boolean().optional().describe("get: re-fetch even if cached; failed refresh preserves the cached guide"),
931
+ startLine: z.number().int().positive().optional().describe("get: first line, 1-based (default 1)"),
932
+ maxLines: z.number().int().min(1).max(2000).optional().describe("get: page size (default 400); follow nextStartLine until null"),
933
+ }, safe(async ({ action, entries, id, fromVersion, toVersion, modloader, query, limit, fetchContent, includeContent, maxChars, cursor, refresh, startLine, maxLines }) => {
913
934
  let result;
914
935
  switch (action) {
915
936
  case "ingest":
916
937
  result = await ingestPrimer(entries);
917
938
  break;
918
939
  case "seed":
919
- result = await seedDefaultPrimers();
940
+ result = await seedDefaultPrimers(fetchContent);
920
941
  break;
921
942
  case "get":
922
- result = await getPrimer(id);
943
+ result = await getPrimer(id, { fetchContent, refresh, startLine, maxLines });
923
944
  break;
924
945
  case "by_version":
925
- result = await getPrimersByVersionRange(fromVersion, toVersion, modloader);
946
+ result = await getPrimersByVersionRange(fromVersion, toVersion, modloader, { includeContent, fetchContent, maxChars, cursor });
926
947
  break;
927
948
  case "search":
928
949
  result = await searchPrimers(query, modloader, fromVersion, toVersion, limit);
@@ -940,7 +961,7 @@ function createMcpServer() {
940
961
  result = await backfillPrimerEmbeddings();
941
962
  break;
942
963
  }
943
- return out(result);
964
+ return { ...out(result), ...(result && typeof result === "object" && "failed" in result && Number(result.failed) > 0 ? { isError: true } : {}) };
944
965
  }));
945
966
  // ── 10. mc_registry ───────────────────────────────────────────────────────────
946
967
  server.tool("mc_registry", "Vanilla MC registry and meta data (blocks, commands, registries, sounds, item components). action=blocks|commands|registries|sounds|item_components|registry_entries|mcmeta_versions.", {
@@ -1305,6 +1326,12 @@ function createMcpServer() {
1305
1326
  return out(result);
1306
1327
  }));
1307
1328
  // ── 18. reports ───────────────────────────────────────────────────────────────
1329
+ server.tool("project", "Import and query a private Gradle compile environment, including prepared ATs, mappings and loader patches. " +
1330
+ "Use this instead of mc_source/mod source or member tools when answering about an imported project. " +
1331
+ "Every query requires projectKey and immutable environmentId; no shared vanilla caches are modified. " +
1332
+ "Export with scripts/gradle/modlens.init.gradle; use scripts/project-upload.mjs for remote imports. " +
1333
+ "action=import_local|upload_begin|upload_chunk|upload_finish|upload_abort|list|info|classes|source|search|members|bytecode. " +
1334
+ "list is restricted to the supplied private key. Search covers provided sources and cached decompiles, not all runtime transformations.", projectToolSchema, safe(async (params) => out(await projectAction(params))));
1308
1335
  server.tool("reports", "Generate Markdown reports. report=mixin_conflicts|tag_conflicts|version_conflicts|mod_overview|gradle_deps|pack_compat|dep_graph|sidedness|mod_complexity|pack_changelog. savePath to write to disk.", {
1309
1336
  report: z.enum(["mixin_conflicts", "tag_conflicts", "version_conflicts", "mod_overview", "gradle_deps", "pack_compat", "dep_graph", "sidedness", "mod_complexity", "pack_changelog"]),
1310
1337
  savePath: z.string().optional().describe("Absolute path to save the .md file, e.g. 'C:/reports/mixin_conflicts.md'"),
@@ -1469,8 +1496,23 @@ async function startHttpServer(port) {
1469
1496
  const transports = new Map();
1470
1497
  const readBody = (req) => new Promise((resolve, reject) => {
1471
1498
  const chunks = [];
1472
- req.on("data", (c) => chunks.push(c));
1499
+ let size = 0;
1500
+ let oversized = false;
1501
+ req.on("data", (c) => {
1502
+ size += c.length;
1503
+ if (size > 8 * 1024 * 1024) {
1504
+ if (!oversized) {
1505
+ oversized = true;
1506
+ chunks.length = 0;
1507
+ reject(new HttpBodyTooLargeError("Request body exceeds 8 MiB; use project upload chunks"));
1508
+ }
1509
+ return;
1510
+ }
1511
+ chunks.push(c);
1512
+ });
1473
1513
  req.on("end", () => {
1514
+ if (oversized)
1515
+ return;
1474
1516
  const raw = Buffer.concat(chunks).toString("utf8");
1475
1517
  if (!raw)
1476
1518
  return resolve(undefined);
@@ -1482,6 +1524,7 @@ async function startHttpServer(port) {
1482
1524
  }
1483
1525
  });
1484
1526
  req.on("error", reject);
1527
+ req.on("aborted", () => reject(new Error("Request aborted")));
1485
1528
  });
1486
1529
  const sendJson = (res, status, body) => {
1487
1530
  const payload = JSON.stringify(body);
@@ -1551,6 +1594,8 @@ async function startHttpServer(port) {
1551
1594
  }
1552
1595
  catch (err) {
1553
1596
  if (!res.headersSent) {
1597
+ if (err instanceof HttpBodyTooLargeError)
1598
+ return sendJson(res, 413, { error: err.message });
1554
1599
  if (err instanceof JsonBodyParseError) {
1555
1600
  return sendJson(res, 400, {
1556
1601
  jsonrpc: "2.0",