@adhisang/minecraft-modding-mcp 4.2.1 → 5.0.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.
- package/CHANGELOG.md +33 -0
- package/README.md +5 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +15 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +124 -28
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +1 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +30 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +1 -1
- package/dist/entry-tools/response-contract.js +1 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +113 -22
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/index.js +138 -71
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/lookup.d.ts +1 -1
- package/dist/mapping/lookup.js +7 -1
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +178 -36
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +53 -7
- package/dist/mixin/parsed-validator.js +2 -2
- package/dist/mixin-parser.js +48 -4
- package/dist/mod-decompile-service.js +2 -1
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +95 -40
- package/dist/nbt/java-nbt-codec.js +6 -0
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/registry-service.js +25 -1
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +48 -4
- package/dist/source/class-source-helpers.js +67 -11
- package/dist/source/class-source.js +97 -20
- package/dist/source/indexer.js +5 -2
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +94 -22
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +15 -9
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-service.d.ts +28 -7
- package/dist/storage/files-repo.d.ts +12 -0
- package/dist/storage/files-repo.js +24 -0
- package/dist/storage/symbols-repo.d.ts +6 -0
- package/dist/storage/symbols-repo.js +6 -0
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +4 -1
- package/dist/tool-guidance.js +37 -12
- package/dist/tool-schemas.d.ts +559 -494
- package/dist/tool-schemas.js +99 -63
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +18 -0
- package/dist/warning-details.js +63 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +22 -17
- package/package.json +1 -1
|
@@ -3,6 +3,15 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import { isSecureJarEntryPath } from "./path-resolver.js";
|
|
5
5
|
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
6
|
+
// Test-only instrumentation: counts real jar opens at the single openZipFile
|
|
7
|
+
// chokepoint so perf tests can assert N-opens-per-call collapses to 1.
|
|
8
|
+
let zipOpenCount = 0;
|
|
9
|
+
export function __getZipOpenCount() {
|
|
10
|
+
return zipOpenCount;
|
|
11
|
+
}
|
|
12
|
+
export function __resetZipOpenCount() {
|
|
13
|
+
zipOpenCount = 0;
|
|
14
|
+
}
|
|
6
15
|
function toErrorMessage(value) {
|
|
7
16
|
if (value instanceof Error) {
|
|
8
17
|
return value.message;
|
|
@@ -23,7 +32,7 @@ export function hasJavaSourceExtension(entryPath) {
|
|
|
23
32
|
}
|
|
24
33
|
return true;
|
|
25
34
|
}
|
|
26
|
-
function openZipFile(jarPath) {
|
|
35
|
+
export function openZipFile(jarPath) {
|
|
27
36
|
return new Promise((resolve, reject) => {
|
|
28
37
|
yauzl.open(jarPath, {
|
|
29
38
|
lazyEntries: true,
|
|
@@ -33,6 +42,7 @@ function openZipFile(jarPath) {
|
|
|
33
42
|
reject(new Error(`Failed to read jar "${jarPath}": ${toErrorMessage(error)}`));
|
|
34
43
|
return;
|
|
35
44
|
}
|
|
45
|
+
zipOpenCount += 1;
|
|
36
46
|
resolve(zipFile);
|
|
37
47
|
});
|
|
38
48
|
});
|
|
@@ -193,6 +203,66 @@ export async function readJarEntryAsBuffer(jarPath, entryPath) {
|
|
|
193
203
|
}
|
|
194
204
|
});
|
|
195
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Opens a jar ONCE, drains its central directory into a name->entry index, and
|
|
208
|
+
* serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
|
|
209
|
+
* single logical operation reads many entries from the same jar (e.g. walking a
|
|
210
|
+
* class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
|
|
211
|
+
* which re-opens and re-scans the jar each time. Callers MUST close() the reader
|
|
212
|
+
* (in a finally) to release the file descriptor. getEntryBuffer throws the same
|
|
213
|
+
* INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
|
|
214
|
+
* readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
|
|
215
|
+
*/
|
|
216
|
+
export async function createJarEntryReader(jarPath) {
|
|
217
|
+
const zipFile = await openZipFile(jarPath);
|
|
218
|
+
let closed = false;
|
|
219
|
+
const index = new Map();
|
|
220
|
+
try {
|
|
221
|
+
while (true) {
|
|
222
|
+
const entry = await readNextEntry(zipFile);
|
|
223
|
+
if (!entry) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!index.has(entry.fileName)) {
|
|
230
|
+
index.set(entry.fileName, entry);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
zipFile.close();
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
async getEntryBuffer(entryPath) {
|
|
240
|
+
const normalizedTargetPath = entryPath.replaceAll("\\", "/");
|
|
241
|
+
if (!isSecureJarEntryPath(normalizedTargetPath)) {
|
|
242
|
+
throw createError({
|
|
243
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
244
|
+
message: `Entry path "${normalizedTargetPath}" is not allowed.`,
|
|
245
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const entry = index.get(normalizedTargetPath);
|
|
249
|
+
if (!entry) {
|
|
250
|
+
throw createError({
|
|
251
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
252
|
+
message: `Entry "${normalizedTargetPath}" was not found in "${jarPath}".`,
|
|
253
|
+
details: { jarPath, entryPath: normalizedTargetPath }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return readEntryStream(zipFile, entry, jarPath);
|
|
257
|
+
},
|
|
258
|
+
close() {
|
|
259
|
+
if (!closed) {
|
|
260
|
+
closed = true;
|
|
261
|
+
zipFile.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
196
266
|
export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options = {}) {
|
|
197
267
|
return withZipFile(jarPath, async (zipFile) => {
|
|
198
268
|
const entries = [];
|
|
@@ -226,6 +296,48 @@ export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options
|
|
|
226
296
|
}
|
|
227
297
|
});
|
|
228
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
|
|
301
|
+
* ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
|
|
302
|
+
* inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
|
|
303
|
+
* sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
|
|
304
|
+
* and its finally-close, but takes an injectable open seam for open-count assertions.
|
|
305
|
+
*/
|
|
306
|
+
export async function collectMatchedJarEntriesAsBuffers(jarPath, predicate, options = {}, deps = {}) {
|
|
307
|
+
const open = deps.openZipFile ?? openZipFile;
|
|
308
|
+
const zipFile = await open(jarPath);
|
|
309
|
+
try {
|
|
310
|
+
const entries = [];
|
|
311
|
+
const maxEntries = options.maxEntries == null ? undefined : Math.max(1, Math.trunc(options.maxEntries));
|
|
312
|
+
while (true) {
|
|
313
|
+
const entry = await readNextEntry(zipFile);
|
|
314
|
+
if (!entry) {
|
|
315
|
+
return entries;
|
|
316
|
+
}
|
|
317
|
+
if (!isSecureJarEntryPath(entry.fileName)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!predicate(entry.fileName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const contentBuffer = await readEntryStream(zipFile, entry, jarPath, options.maxBytes);
|
|
325
|
+
entries.push({ filePath: entry.fileName, content: contentBuffer });
|
|
326
|
+
if (maxEntries != null && entries.length >= maxEntries) {
|
|
327
|
+
return entries;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!options.continueOnError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
zipFile.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
229
341
|
export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
|
|
230
342
|
const zipFile = await openZipFile(jarPath);
|
|
231
343
|
try {
|
|
@@ -285,7 +397,7 @@ function countMatches(input, pattern) {
|
|
|
285
397
|
}
|
|
286
398
|
return count;
|
|
287
399
|
}
|
|
288
|
-
export async function detectFabricLikeInputNamespace(inputJar) {
|
|
400
|
+
export async function detectFabricLikeInputNamespace(inputJar, deps = {}) {
|
|
289
401
|
const warnings = [];
|
|
290
402
|
const classEntries = (await listJarEntries(inputJar))
|
|
291
403
|
.filter((entry) => entry.endsWith(".class"))
|
|
@@ -297,16 +409,15 @@ export async function detectFabricLikeInputNamespace(inputJar) {
|
|
|
297
409
|
warnings
|
|
298
410
|
};
|
|
299
411
|
}
|
|
412
|
+
// Read all sampled class entries in a SINGLE jar open instead of re-opening the
|
|
413
|
+
// jar once per entry. The sample set and latin1 decode are unchanged, so scores —
|
|
414
|
+
// and therefore the detected namespace — are identical.
|
|
415
|
+
const sampleSet = new Set(classEntries);
|
|
416
|
+
const matched = await collectMatchedJarEntriesAsBuffers(inputJar, (name) => sampleSet.has(name), { continueOnError: true }, deps);
|
|
300
417
|
let mojangScore = 0;
|
|
301
418
|
let intermediaryScore = 0;
|
|
302
|
-
for (const
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
419
|
+
for (const { content } of matched) {
|
|
420
|
+
const text = content.toString("latin1");
|
|
310
421
|
mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
|
|
311
422
|
intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
|
|
312
423
|
intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
|
package/dist/source-service.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as indexer from "./source/indexer.js";
|
|
|
11
11
|
import * as workspaceTarget from "./source/workspace-target.js";
|
|
12
12
|
import * as validateMixinModule from "./source/validate-mixin.js";
|
|
13
13
|
import * as artifactResolver from "./source/artifact-resolver.js";
|
|
14
|
+
import type { WireMembersBlock } from "./source/class-source/members-builder.js";
|
|
14
15
|
import { type StageEmitter } from "./stage-emitter.js";
|
|
15
16
|
import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
|
|
16
17
|
import { type WorkspaceContextCache } from "./workspace-context-cache.js";
|
|
@@ -30,7 +31,8 @@ export type ResolveArtifactInput = {
|
|
|
30
31
|
scope?: ArtifactScope;
|
|
31
32
|
preferProjectVersion?: boolean;
|
|
32
33
|
strictVersion?: boolean;
|
|
33
|
-
|
|
34
|
+
/** When true, compute the optional `sampleEntries` listing (detail=full / include=samples). */
|
|
35
|
+
includeSampleEntries?: boolean;
|
|
34
36
|
};
|
|
35
37
|
export type ResolveArtifactOutput = {
|
|
36
38
|
artifactId: string;
|
|
@@ -112,6 +114,8 @@ export type SearchClassSourceInput = {
|
|
|
112
114
|
export type SearchClassSourceOutput = {
|
|
113
115
|
hits: SearchSourceHit[];
|
|
114
116
|
nextCursor?: string;
|
|
117
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored (results restart from page one). */
|
|
118
|
+
cursorIgnored?: boolean;
|
|
115
119
|
mappingApplied: SourceMapping;
|
|
116
120
|
returnedNamespace: SourceMapping;
|
|
117
121
|
artifactContents: ArtifactContentsSummary;
|
|
@@ -205,6 +209,10 @@ export type GetClassSourceOutput = {
|
|
|
205
209
|
};
|
|
206
210
|
truncated: boolean;
|
|
207
211
|
charsTruncated?: boolean;
|
|
212
|
+
/** True when startLine begins past EOF; the returned range is empty (not clamped). */
|
|
213
|
+
outOfRange?: boolean;
|
|
214
|
+
/** First not-yet-returned line when truncated; pass back as startLine to continue. */
|
|
215
|
+
nextStartLine?: number;
|
|
208
216
|
origin: ResolvedSourceArtifact["origin"];
|
|
209
217
|
artifactId: string;
|
|
210
218
|
requestedMapping: SourceMapping;
|
|
@@ -213,6 +221,11 @@ export type GetClassSourceOutput = {
|
|
|
213
221
|
provenance: ArtifactProvenance;
|
|
214
222
|
qualityFlags: string[];
|
|
215
223
|
artifactContents: ArtifactContentsSummary;
|
|
224
|
+
/** Replayable get-class-source call that reads from nextStartLine (truncated, non-file output only). */
|
|
225
|
+
suggestedCall?: {
|
|
226
|
+
tool: string;
|
|
227
|
+
params: Record<string, unknown>;
|
|
228
|
+
};
|
|
216
229
|
outputFile?: string;
|
|
217
230
|
warnings: string[];
|
|
218
231
|
};
|
|
@@ -245,11 +258,14 @@ export type GetClassMembersInput = {
|
|
|
245
258
|
includeInherited?: boolean;
|
|
246
259
|
memberPattern?: string;
|
|
247
260
|
maxMembers?: number;
|
|
261
|
+
cursor?: string;
|
|
248
262
|
projectPath?: string;
|
|
249
263
|
gradleUserHome?: string;
|
|
250
264
|
scope?: ArtifactScope;
|
|
251
265
|
preferProjectVersion?: boolean;
|
|
252
266
|
strictVersion?: boolean;
|
|
267
|
+
/** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
|
|
268
|
+
includeDescriptors?: boolean;
|
|
253
269
|
};
|
|
254
270
|
export type DecompiledMember = {
|
|
255
271
|
name: string;
|
|
@@ -265,11 +281,7 @@ export type DecompiledFallback = {
|
|
|
265
281
|
export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
|
|
266
282
|
export type GetClassMembersOutput = {
|
|
267
283
|
className: string;
|
|
268
|
-
members:
|
|
269
|
-
constructors: SignatureMember[];
|
|
270
|
-
fields: SignatureMember[];
|
|
271
|
-
methods: SignatureMember[];
|
|
272
|
-
};
|
|
284
|
+
members: WireMembersBlock;
|
|
273
285
|
counts: {
|
|
274
286
|
constructors: number;
|
|
275
287
|
fields: number;
|
|
@@ -277,6 +289,10 @@ export type GetClassMembersOutput = {
|
|
|
277
289
|
total: number;
|
|
278
290
|
};
|
|
279
291
|
truncated: boolean;
|
|
292
|
+
/** Continuation cursor when more members remain; pass back as cursor to fetch the next page. */
|
|
293
|
+
nextCursor?: string;
|
|
294
|
+
/** True when a provided cursor was malformed or belonged to a different query and was ignored. */
|
|
295
|
+
cursorIgnored?: boolean;
|
|
280
296
|
context: ExplorerResponseContext;
|
|
281
297
|
origin: ResolvedSourceArtifact["origin"];
|
|
282
298
|
artifactId: string;
|
|
@@ -420,7 +436,12 @@ export type ValidateMixinInput = {
|
|
|
420
436
|
path: string;
|
|
421
437
|
};
|
|
422
438
|
sourceRoots?: string[];
|
|
423
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Optional when input.mode='project', or preferProjectVersion=true with a
|
|
441
|
+
* projectPath set: the dispatcher detects the Minecraft version from
|
|
442
|
+
* gradle.properties before any version-dependent stage runs.
|
|
443
|
+
*/
|
|
444
|
+
version?: string;
|
|
424
445
|
mapping?: SourceMapping;
|
|
425
446
|
sourcePriority?: MappingSourcePriority;
|
|
426
447
|
scope?: ArtifactScope;
|
|
@@ -47,6 +47,7 @@ export declare class FilesRepo {
|
|
|
47
47
|
private readonly listStmt;
|
|
48
48
|
private readonly listRowsStmt;
|
|
49
49
|
private readonly searchPathStmt;
|
|
50
|
+
private readonly searchContentLikePathsStmt;
|
|
50
51
|
private readonly searchFtsStmt;
|
|
51
52
|
private readonly getByPathsStmtCache;
|
|
52
53
|
private readonly classLookupStmtCache;
|
|
@@ -58,6 +59,17 @@ export declare class FilesRepo {
|
|
|
58
59
|
getFileContent(artifactId: string, filePath: string): FileRow | undefined;
|
|
59
60
|
listFiles(artifactId: string, options: ListFilesOptions): PagedResult<string>;
|
|
60
61
|
listFileRows(artifactId: string, options: ListFileRowsOptions): PagedResult<FileRow>;
|
|
62
|
+
/**
|
|
63
|
+
* Narrow the literal/contains text-scan candidate set without hydrating content:
|
|
64
|
+
* returns the file_paths whose content matches `content LIKE %needle%` (ASCII
|
|
65
|
+
* case-insensitive). The result is a SUPERSET of the JS contains/exact match set
|
|
66
|
+
* for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
|
|
67
|
+
* file_path is selected — content stays in the DB until getFileContentsByPaths.
|
|
68
|
+
*/
|
|
69
|
+
searchContentLikeCandidatePaths(artifactId: string, needle: string, limit: number): {
|
|
70
|
+
filePaths: string[];
|
|
71
|
+
scannedRows: number;
|
|
72
|
+
};
|
|
61
73
|
getFileContentsByPaths(artifactId: string, filePaths: string[]): FileRow[];
|
|
62
74
|
searchFileCandidates(artifactId: string, options: SearchFilesOptions): PagedResult<SearchFileCandidateResult> & {
|
|
63
75
|
scannedRows: number;
|
|
@@ -121,6 +121,10 @@ function buildIndexedMatchQuery(query, match) {
|
|
|
121
121
|
return token;
|
|
122
122
|
}).join(" ");
|
|
123
123
|
}
|
|
124
|
+
/** Escape LIKE wildcards so the needle is matched literally under ESCAPE '\'. */
|
|
125
|
+
function escapeLikeNeedle(value) {
|
|
126
|
+
return value.replace(/[\\%_]/g, "\\$&");
|
|
127
|
+
}
|
|
124
128
|
export class FilesRepo {
|
|
125
129
|
db;
|
|
126
130
|
deleteStmt;
|
|
@@ -129,6 +133,7 @@ export class FilesRepo {
|
|
|
129
133
|
listStmt;
|
|
130
134
|
listRowsStmt;
|
|
131
135
|
searchPathStmt;
|
|
136
|
+
searchContentLikePathsStmt;
|
|
132
137
|
searchFtsStmt;
|
|
133
138
|
getByPathsStmtCache = new Map();
|
|
134
139
|
classLookupStmtCache = new Map();
|
|
@@ -167,6 +172,13 @@ export class FilesRepo {
|
|
|
167
172
|
WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'
|
|
168
173
|
ORDER BY file_path ASC
|
|
169
174
|
LIMIT ?
|
|
175
|
+
`);
|
|
176
|
+
this.searchContentLikePathsStmt = this.db.prepare(`
|
|
177
|
+
SELECT file_path
|
|
178
|
+
FROM files
|
|
179
|
+
WHERE artifact_id = ? AND content LIKE ? ESCAPE '\\'
|
|
180
|
+
ORDER BY file_path ASC
|
|
181
|
+
LIMIT ?
|
|
170
182
|
`);
|
|
171
183
|
this.searchFtsStmt = this.db.prepare(`
|
|
172
184
|
SELECT file_path, rank
|
|
@@ -239,6 +251,18 @@ export class FilesRepo {
|
|
|
239
251
|
nextCursor: nextCursorFromRows(rows)
|
|
240
252
|
};
|
|
241
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Narrow the literal/contains text-scan candidate set without hydrating content:
|
|
256
|
+
* returns the file_paths whose content matches `content LIKE %needle%` (ASCII
|
|
257
|
+
* case-insensitive). The result is a SUPERSET of the JS contains/exact match set
|
|
258
|
+
* for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
|
|
259
|
+
* file_path is selected — content stays in the DB until getFileContentsByPaths.
|
|
260
|
+
*/
|
|
261
|
+
searchContentLikeCandidatePaths(artifactId, needle, limit) {
|
|
262
|
+
const pattern = `%${escapeLikeNeedle(needle)}%`;
|
|
263
|
+
const rows = this.searchContentLikePathsStmt.all(artifactId, pattern, Math.max(1, limit));
|
|
264
|
+
return { filePaths: rows.map((row) => row.file_path), scannedRows: rows.length };
|
|
265
|
+
}
|
|
242
266
|
getFileContentsByPaths(artifactId, filePaths) {
|
|
243
267
|
if (filePaths.length === 0) {
|
|
244
268
|
return [];
|
|
@@ -22,6 +22,12 @@ export interface FindScopedSymbolsOptions {
|
|
|
22
22
|
query: string;
|
|
23
23
|
match: ScopedSymbolMatch;
|
|
24
24
|
symbolKind?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Restrict results to these symbol kinds via a bound `symbol_kind IN (...)` clause.
|
|
27
|
+
* Combined with the singular `symbolKind` it ANDs (both apply). An empty array is
|
|
28
|
+
* ignored (degrades to no kind filter) to avoid emitting an invalid `IN ()`.
|
|
29
|
+
*/
|
|
30
|
+
symbolKinds?: string[];
|
|
25
31
|
packagePrefix?: string;
|
|
26
32
|
filePathLike?: string;
|
|
27
33
|
limit?: number;
|
|
@@ -260,6 +260,12 @@ export class SymbolsRepo {
|
|
|
260
260
|
where.push("symbol_kind = ?");
|
|
261
261
|
params.push(options.symbolKind);
|
|
262
262
|
}
|
|
263
|
+
const symbolKinds = options.symbolKinds;
|
|
264
|
+
if (symbolKinds && symbolKinds.length > 0) {
|
|
265
|
+
const placeholders = symbolKinds.map(() => "?").join(", ");
|
|
266
|
+
where.push(`symbol_kind IN (${placeholders})`);
|
|
267
|
+
params.push(...symbolKinds);
|
|
268
|
+
}
|
|
263
269
|
if (options.packagePrefix?.trim()) {
|
|
264
270
|
const normalizedPrefix = options.packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
|
|
265
271
|
where.push("file_path LIKE ? ESCAPE '\\'");
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
// A type token may carry spaced generics (Map<String, Integer>), arrays (int[][]),
|
|
2
|
+
// wildcards (List<? extends Foo>), and FQNs (java.util.Map). It must not contain "=".
|
|
3
|
+
const TYPE_TOKEN = "[\\w.$][\\w.$<>\\[\\],?\\s]*";
|
|
4
|
+
const MODIFIER = "(?:public|private|protected|abstract|final|static|native|synchronized|default|strictfp|transient|volatile)";
|
|
5
|
+
const MODIFIER_OR_ANNOTATION_RUN = `(?:(?:@[\\w.]+|${MODIFIER})\\s+)*`;
|
|
6
|
+
const TYPE_PARAMS = "(?:<[^>]+>\\s*)?";
|
|
7
|
+
const THROWS_CLAUSE = "(?:\\s+throws\\s+[\\w.$,\\s]+)?";
|
|
8
|
+
const CLASS_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}(class|interface|enum|record)\\s+([A-Za-z_$][\\w$]*)`);
|
|
9
|
+
// Method/constructor with a body: return type is optional (constructors have none); ends with "{".
|
|
10
|
+
const METHOD_BLOCK_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_PARAMS}(?:${TYPE_TOKEN}\\s+)?([A-Za-z_$][\\w$]*)\\s*\\([^)]*\\)${THROWS_CLAUSE}\\s*\\{`);
|
|
11
|
+
// Abstract/interface method declaration: a return type is REQUIRED; ends with ";".
|
|
12
|
+
// Requiring the return type is what separates "void onTick();" from a bare call "doThing();".
|
|
13
|
+
const METHOD_ABSTRACT_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_PARAMS}${TYPE_TOKEN}\\s+([A-Za-z_$][\\w$]*)\\s*\\([^)]*\\)${THROWS_CLAUSE}\\s*;`);
|
|
14
|
+
const FIELD_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_TOKEN}\\s+([A-Za-z_$][\\w$]*)\\s*(?:=|;|,)`);
|
|
15
|
+
// Lines that begin with one of these keywords are statements, not declarations. They are
|
|
16
|
+
// skipped wholesale so that "return helper();" / "for (...)" / "new Foo() {" never produce
|
|
17
|
+
// phantom members. "default"/"synchronized" are intentionally excluded (they double as
|
|
18
|
+
// member modifiers); the method regexes reject their statement forms structurally.
|
|
19
|
+
const STATEMENT_LINE_KEYWORDS = new Set([
|
|
20
|
+
"if", "for", "while", "switch", "catch", "return", "throw", "else", "do",
|
|
21
|
+
"case", "break", "continue", "assert", "super", "this", "try", "new", "yield"
|
|
22
|
+
]);
|
|
23
|
+
// Defence-in-depth: even if a keyword is captured as a member name, drop it.
|
|
4
24
|
const NOISE_TOKENS = new Set(["if", "for", "while", "switch", "catch", "return", "new", "throw"]);
|
|
5
25
|
function normalizeLine(line) {
|
|
6
26
|
return line.replace(/\s+/g, " ").trim();
|
|
@@ -8,6 +28,10 @@ function normalizeLine(line) {
|
|
|
8
28
|
function isNoiseToken(token) {
|
|
9
29
|
return NOISE_TOKENS.has(token);
|
|
10
30
|
}
|
|
31
|
+
function isStatementLine(line) {
|
|
32
|
+
const firstWord = /^([A-Za-z_$][\w$]*)/.exec(line)?.[1];
|
|
33
|
+
return firstWord != null && STATEMENT_LINE_KEYWORDS.has(firstWord);
|
|
34
|
+
}
|
|
11
35
|
function lineIndexToLine(lineNo) {
|
|
12
36
|
return lineNo + 1;
|
|
13
37
|
}
|
|
@@ -17,8 +41,13 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
17
41
|
const qualifiedName = filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
18
42
|
for (let index = 0; index < lines.length; index += 1) {
|
|
19
43
|
const rawLine = lines[index] ?? "";
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
44
|
+
const normalized = normalizeLine(rawLine);
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Drop a single leading "}"/"{" so "} public void foo() {" still parses.
|
|
49
|
+
const line = normalized.replace(/^[}{]\s*/, "");
|
|
50
|
+
if (!line || isStatementLine(line)) {
|
|
22
51
|
continue;
|
|
23
52
|
}
|
|
24
53
|
const classMatch = line.match(CLASS_DECLARATION);
|
|
@@ -35,7 +64,7 @@ export function extractSymbolsFromSource(filePath, content) {
|
|
|
35
64
|
}
|
|
36
65
|
continue;
|
|
37
66
|
}
|
|
38
|
-
const methodMatch = line.match(
|
|
67
|
+
const methodMatch = line.match(METHOD_BLOCK_DECLARATION) ?? line.match(METHOD_ABSTRACT_DECLARATION);
|
|
39
68
|
if (methodMatch) {
|
|
40
69
|
const symbolName = methodMatch[1];
|
|
41
70
|
if (symbolName && !isNoiseToken(symbolName)) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice `text` to at most `maxChars` UTF-16 code units without splitting a
|
|
3
|
+
* surrogate pair. A naive `text.slice(0, maxChars)` can leave a lone high
|
|
4
|
+
* surrogate at the boundary (e.g. when an emoji straddles the cut), producing
|
|
5
|
+
* an invalid string; this drops that dangling half so callers never emit one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sliceToMaxCharsSafe(text: string, maxChars: number): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice `text` to at most `maxChars` UTF-16 code units without splitting a
|
|
3
|
+
* surrogate pair. A naive `text.slice(0, maxChars)` can leave a lone high
|
|
4
|
+
* surrogate at the boundary (e.g. when an emoji straddles the cut), producing
|
|
5
|
+
* an invalid string; this drops that dangling half so callers never emit one.
|
|
6
|
+
*/
|
|
7
|
+
export function sliceToMaxCharsSafe(text, maxChars) {
|
|
8
|
+
if (maxChars <= 0) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
if (text.length <= maxChars) {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
let cut = text.slice(0, maxChars);
|
|
15
|
+
const lastUnit = cut.charCodeAt(cut.length - 1);
|
|
16
|
+
if (lastUnit >= 0xd800 && lastUnit <= 0xdbff) {
|
|
17
|
+
cut = cut.slice(0, -1);
|
|
18
|
+
}
|
|
19
|
+
return cut;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=text-truncate.js.map
|
|
@@ -15,6 +15,16 @@ function tinyRemapperDownloadUrl(version) {
|
|
|
15
15
|
export async function resolveTinyRemapperJar(cacheDir, overridePath, fetchFn) {
|
|
16
16
|
// 1. Environment / config override
|
|
17
17
|
if (overridePath) {
|
|
18
|
+
if (!existsSync(overridePath)) {
|
|
19
|
+
throw createError({
|
|
20
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
21
|
+
message: `Configured tiny-remapper jar does not exist: ${overridePath}`,
|
|
22
|
+
details: {
|
|
23
|
+
path: overridePath,
|
|
24
|
+
nextAction: "Point tinyRemapperJarPath / MCP_TINY_REMAPPER_JAR at an existing fat jar, or unset it to download automatically."
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
18
28
|
return overridePath;
|
|
19
29
|
}
|
|
20
30
|
const version = process.env.MCP_TINY_REMAPPER_VERSION ?? DEFAULT_VERSION;
|
|
@@ -67,14 +67,14 @@ const SECTION_ROWS = {
|
|
|
67
67
|
"mapping-symbols": {
|
|
68
68
|
en: [
|
|
69
69
|
"| `find-mapping` | Look up mapping candidates for class, field, or method symbols |",
|
|
70
|
-
"| `resolve-method-mapping-exact` |
|
|
70
|
+
"| `resolve-method-mapping-exact` | Strict shortcut for `find-mapping` (kind=method, signatureMode=exact); requires the full owner+name+descriptor triple |",
|
|
71
71
|
"| `get-class-api-matrix` | Show one class API across `obfuscated`, `mojang`, `intermediary`, and `yarn` |",
|
|
72
72
|
"| `resolve-workspace-symbol` | Resolve compile-visible symbol names from a Gradle workspace |",
|
|
73
73
|
"| `check-symbol-exists` | Check whether a class, field, or method exists in a namespace |"
|
|
74
74
|
],
|
|
75
75
|
ja: [
|
|
76
76
|
"| `find-mapping` | クラス、フィールド、メソッドのシンボルに対するマッピング候補を調べる |",
|
|
77
|
-
"| `resolve-method-mapping-exact` | owner
|
|
77
|
+
"| `resolve-method-mapping-exact` | `find-mapping`(kind=method, signatureMode=exact)の厳密版ショートカット。owner+name+descriptor の三つ組が必須 |",
|
|
78
78
|
"| `get-class-api-matrix` | 1 つのクラス API を `obfuscated`、`mojang`、`intermediary`、`yarn` で見比べる |",
|
|
79
79
|
"| `resolve-workspace-symbol` | Gradle ワークスペースからコンパイル時に見えるシンボル名を解決する |",
|
|
80
80
|
"| `check-symbol-exists` | 名前空間内でクラス、フィールド、メソッドが存在するかを確認する |"
|
|
@@ -3,6 +3,13 @@ const DEFAULT_OPTIONS = {
|
|
|
3
3
|
maxConcurrent: 1,
|
|
4
4
|
maxQueue: 2
|
|
5
5
|
};
|
|
6
|
+
// Heavy tools that have a batch equivalent able to collapse many same-kind
|
|
7
|
+
// queries into a single gated call. Used to point overflow guidance at the
|
|
8
|
+
// right batch-* tool instead of just telling the caller to retry serially.
|
|
9
|
+
const BATCH_EQUIVALENTS = {
|
|
10
|
+
"find-mapping": "batch-mappings",
|
|
11
|
+
"resolve-method-mapping-exact": "batch-mappings"
|
|
12
|
+
};
|
|
6
13
|
export class ToolExecutionGate {
|
|
7
14
|
maxConcurrent;
|
|
8
15
|
maxQueue;
|
|
@@ -17,6 +24,10 @@ export class ToolExecutionGate {
|
|
|
17
24
|
return this.execute({ tool, task: task });
|
|
18
25
|
}
|
|
19
26
|
if (this.queue.length >= this.maxQueue) {
|
|
27
|
+
const batchTool = BATCH_EQUIVALENTS[tool];
|
|
28
|
+
const nextAction = batchTool
|
|
29
|
+
? `Retry after the current heavy analysis request completes. To run many ${tool} queries efficiently, collapse them into a single ${batchTool} call instead of issuing them in parallel.`
|
|
30
|
+
: "Retry after the current heavy analysis request completes. Avoid sending multiple heavy mapping/version analysis tools in parallel.";
|
|
20
31
|
return Promise.reject(createError({
|
|
21
32
|
code: ERROR_CODES.LIMIT_EXCEEDED,
|
|
22
33
|
message: `Heavy tool queue is full; "${tool}" was not started.`,
|
|
@@ -26,7 +37,7 @@ export class ToolExecutionGate {
|
|
|
26
37
|
queuedCount: this.queue.length,
|
|
27
38
|
maxConcurrent: this.maxConcurrent,
|
|
28
39
|
maxQueue: this.maxQueue,
|
|
29
|
-
nextAction
|
|
40
|
+
nextAction
|
|
30
41
|
}
|
|
31
42
|
}));
|
|
32
43
|
}
|
package/dist/tool-guidance.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
|
+
import type { WarningDetail } from "./warning-details.js";
|
|
2
3
|
import { type ExampleCall, type ProblemDetails, type ProblemFieldError, type SuggestedCall } from "./error-mapping.js";
|
|
3
4
|
import type { SourceTargetInput } from "./types.js";
|
|
4
5
|
export type ToolMeta = {
|
|
@@ -6,6 +7,8 @@ export type ToolMeta = {
|
|
|
6
7
|
tool: string;
|
|
7
8
|
durationMs: number;
|
|
8
9
|
warnings: string[];
|
|
10
|
+
/** Structured companion to `warnings`: one classified entry per warning string, referencing its text via `warnings[detail.index]`. */
|
|
11
|
+
warningDetails?: WarningDetail[];
|
|
9
12
|
detailApplied?: "summary" | "standard" | "full";
|
|
10
13
|
includeApplied?: string[];
|
|
11
14
|
truncated?: Record<string, unknown>;
|
|
@@ -29,7 +32,7 @@ export declare const SUGGESTED_CALL_DEFAULTS: {
|
|
|
29
32
|
readonly explain: false;
|
|
30
33
|
readonly preferProjectMapping: false;
|
|
31
34
|
readonly minSeverity: "all";
|
|
32
|
-
readonly reportMode: "
|
|
35
|
+
readonly reportMode: "summary-first";
|
|
33
36
|
readonly treatInfoAsWarning: true;
|
|
34
37
|
readonly includeIssues: true;
|
|
35
38
|
};
|