maven-indexer-mcp 1.0.5 → 1.0.7
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/README.md +163 -42
- package/build/artifact_resolver.js +109 -0
- package/build/config.js +72 -0
- package/build/index.js +197 -153
- package/build/indexer.js +154 -79
- package/package.json +3 -1
- package/build/reproduce_issue.js +0 -20
package/build/indexer.js
CHANGED
|
@@ -15,7 +15,8 @@ export class Indexer {
|
|
|
15
15
|
isIndexing = false;
|
|
16
16
|
watcher = null;
|
|
17
17
|
debounceTimer = null;
|
|
18
|
-
constructor() {
|
|
18
|
+
constructor() {
|
|
19
|
+
}
|
|
19
20
|
static getInstance() {
|
|
20
21
|
if (!Indexer.instance) {
|
|
21
22
|
Indexer.instance = new Indexer();
|
|
@@ -44,7 +45,13 @@ export class Indexer {
|
|
|
44
45
|
}
|
|
45
46
|
console.error(`Starting file watcher on ${pathsToWatch.join(', ')}...`);
|
|
46
47
|
this.watcher = chokidar.watch(pathsToWatch, {
|
|
47
|
-
ignored:
|
|
48
|
+
ignored: (p) => {
|
|
49
|
+
// Don't ignore the root paths themselves
|
|
50
|
+
if (pathsToWatch.includes(p))
|
|
51
|
+
return false;
|
|
52
|
+
// Ignore dotfiles/dotdirs
|
|
53
|
+
return path.basename(p).startsWith('.');
|
|
54
|
+
},
|
|
48
55
|
persistent: true,
|
|
49
56
|
ignoreInitial: true,
|
|
50
57
|
depth: 10 // Limit depth to avoid too much overhead? Standard maven repo depth is around 3-5
|
|
@@ -105,13 +112,13 @@ export class Indexer {
|
|
|
105
112
|
let artifacts = [];
|
|
106
113
|
if (repoPath && fsSync.existsSync(repoPath)) {
|
|
107
114
|
console.error(`Scanning Maven repo: ${repoPath}`);
|
|
108
|
-
const mavenArtifacts = await this.scanRepository(repoPath);
|
|
115
|
+
const mavenArtifacts = await this.scanRepository(repoPath, config.normalizedIncludedPackages);
|
|
109
116
|
console.error(`Found ${mavenArtifacts.length} Maven artifacts.`);
|
|
110
117
|
artifacts = artifacts.concat(mavenArtifacts);
|
|
111
118
|
}
|
|
112
119
|
if (gradleRepoPath && fsSync.existsSync(gradleRepoPath)) {
|
|
113
120
|
console.error(`Scanning Gradle repo: ${gradleRepoPath}`);
|
|
114
|
-
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath);
|
|
121
|
+
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath, config.normalizedIncludedPackages);
|
|
115
122
|
console.error(`Found ${gradleArtifacts.length} Gradle artifacts.`);
|
|
116
123
|
artifacts = artifacts.concat(gradleArtifacts);
|
|
117
124
|
}
|
|
@@ -119,9 +126,10 @@ export class Indexer {
|
|
|
119
126
|
// 2. Persist artifacts and determine what needs indexing
|
|
120
127
|
// We use is_indexed = 0 for new artifacts.
|
|
121
128
|
const insertArtifact = db.prepare(`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
INSERT
|
|
130
|
+
OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
|
|
131
|
+
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
|
|
132
|
+
`);
|
|
125
133
|
// Use a transaction only for the batch insert of artifacts
|
|
126
134
|
db.transaction(() => {
|
|
127
135
|
for (const art of artifacts) {
|
|
@@ -144,10 +152,10 @@ export class Indexer {
|
|
|
144
152
|
}
|
|
145
153
|
// 3. Find artifacts that need indexing (is_indexed = 0)
|
|
146
154
|
const artifactsToIndex = db.prepare(`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
156
|
+
FROM artifacts
|
|
157
|
+
WHERE is_indexed = 0
|
|
158
|
+
`).all();
|
|
151
159
|
console.error(`${artifactsToIndex.length} artifacts need indexing.`);
|
|
152
160
|
// 4. Scan JARs for classes and update DB
|
|
153
161
|
const CHUNK_SIZE = 50;
|
|
@@ -171,8 +179,11 @@ export class Indexer {
|
|
|
171
179
|
}
|
|
172
180
|
/**
|
|
173
181
|
* Recursively scans a directory for Maven artifacts (POM files).
|
|
182
|
+
*
|
|
183
|
+
* @param repoRoot The root directory of the Maven repository.
|
|
184
|
+
* @param normalizedPatterns List of normalized package patterns to include.
|
|
174
185
|
*/
|
|
175
|
-
async scanRepository(
|
|
186
|
+
async scanRepository(repoRoot, normalizedPatterns = []) {
|
|
176
187
|
const results = [];
|
|
177
188
|
const scanDir = async (dir) => {
|
|
178
189
|
let entries;
|
|
@@ -188,7 +199,7 @@ export class Indexer {
|
|
|
188
199
|
const artifactDir = path.dirname(dir);
|
|
189
200
|
const artifactId = path.basename(artifactDir);
|
|
190
201
|
const groupDir = path.dirname(artifactDir);
|
|
191
|
-
const relGroupPath = path.relative(
|
|
202
|
+
const relGroupPath = path.relative(repoRoot, groupDir);
|
|
192
203
|
const groupId = relGroupPath.split(path.sep).join('.');
|
|
193
204
|
if (groupId && artifactId && version && !groupId.startsWith('..')) {
|
|
194
205
|
const sourceJarPath = path.join(dir, `${artifactId}-${version}-sources.jar`);
|
|
@@ -213,14 +224,49 @@ export class Indexer {
|
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
};
|
|
216
|
-
|
|
227
|
+
const startDirs = this.getMavenStartDirs(repoRoot, normalizedPatterns);
|
|
228
|
+
console.error(`Scanning Maven directories: ${startDirs.join(', ')}`);
|
|
229
|
+
for (const startDir of startDirs) {
|
|
230
|
+
await scanDir(startDir);
|
|
231
|
+
}
|
|
217
232
|
return results;
|
|
218
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Calculates the starting directories for Maven scanning based on included packages.
|
|
236
|
+
*
|
|
237
|
+
* @param repoRoot The root of the Maven repository.
|
|
238
|
+
* @param normalizedPatterns The list of normalized included packages.
|
|
239
|
+
*/
|
|
240
|
+
getMavenStartDirs(repoRoot, normalizedPatterns) {
|
|
241
|
+
if (normalizedPatterns.length === 0) {
|
|
242
|
+
return [repoRoot];
|
|
243
|
+
}
|
|
244
|
+
return normalizedPatterns.map(p => path.join(repoRoot, p.split('.').join(path.sep)));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Checks if a group ID is included in the normalized patterns.
|
|
248
|
+
*
|
|
249
|
+
* @param groupId The group ID (e.g., "com.google.guava").
|
|
250
|
+
* @param normalizedPatterns The list of normalized patterns.
|
|
251
|
+
*/
|
|
252
|
+
isGroupIncluded(groupId, normalizedPatterns) {
|
|
253
|
+
if (!normalizedPatterns || normalizedPatterns.length === 0)
|
|
254
|
+
return true;
|
|
255
|
+
for (const pattern of normalizedPatterns) {
|
|
256
|
+
if (groupId === pattern || groupId.startsWith(pattern + '.')) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
219
262
|
/**
|
|
220
263
|
* Scans a Gradle cache directory for artifacts.
|
|
221
264
|
* Structure: group/artifact/version/hash/file
|
|
265
|
+
*
|
|
266
|
+
* @param rootDir The root directory of the Gradle cache (e.g., ~/.gradle/caches/modules-2/files-2.1).
|
|
267
|
+
* @param normalizedPatterns List of normalized package patterns to include.
|
|
222
268
|
*/
|
|
223
|
-
async scanGradleRepository(rootDir) {
|
|
269
|
+
async scanGradleRepository(rootDir, normalizedPatterns = []) {
|
|
224
270
|
const results = [];
|
|
225
271
|
// Helper to read directory safely
|
|
226
272
|
const readDirSafe = async (p) => {
|
|
@@ -236,6 +282,9 @@ export class Indexer {
|
|
|
236
282
|
if (!groupEntry.isDirectory())
|
|
237
283
|
continue;
|
|
238
284
|
const groupId = groupEntry.name;
|
|
285
|
+
if (!this.isGroupIncluded(groupId, normalizedPatterns)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
239
288
|
const groupPath = path.join(rootDir, groupId);
|
|
240
289
|
const artifactDirs = await readDirSafe(groupPath);
|
|
241
290
|
for (const artifactEntry of artifactDirs) {
|
|
@@ -302,7 +351,7 @@ export class Indexer {
|
|
|
302
351
|
await fs.access(jarPath);
|
|
303
352
|
}
|
|
304
353
|
catch {
|
|
305
|
-
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
354
|
+
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
306
355
|
// Or maybe it's a pom-only artifact.
|
|
307
356
|
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
308
357
|
return;
|
|
@@ -333,13 +382,21 @@ export class Indexer {
|
|
|
333
382
|
// Simple check to avoid module-info or invalid names
|
|
334
383
|
if (!info.className.includes('$') && info.className.length > 0) {
|
|
335
384
|
// Filter by includedPackages
|
|
336
|
-
if (this.isPackageIncluded(info.className, config.
|
|
385
|
+
if (this.isPackageIncluded(info.className, config.normalizedIncludedPackages)) {
|
|
337
386
|
classes.push(info.className);
|
|
338
387
|
if (info.superClass && info.superClass !== 'java.lang.Object') {
|
|
339
|
-
inheritance.push({
|
|
388
|
+
inheritance.push({
|
|
389
|
+
className: info.className,
|
|
390
|
+
parent: info.superClass,
|
|
391
|
+
type: 'extends'
|
|
392
|
+
});
|
|
340
393
|
}
|
|
341
394
|
for (const iface of info.interfaces) {
|
|
342
|
-
inheritance.push({
|
|
395
|
+
inheritance.push({
|
|
396
|
+
className: info.className,
|
|
397
|
+
parent: iface,
|
|
398
|
+
type: 'implements'
|
|
399
|
+
});
|
|
343
400
|
}
|
|
344
401
|
}
|
|
345
402
|
}
|
|
@@ -360,13 +417,13 @@ export class Indexer {
|
|
|
360
417
|
try {
|
|
361
418
|
db.transaction(() => {
|
|
362
419
|
const insertClass = db.prepare(`
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
420
|
+
INSERT INTO classes_fts (artifact_id, class_name, simple_name)
|
|
421
|
+
VALUES (?, ?, ?)
|
|
422
|
+
`);
|
|
366
423
|
const insertInheritance = db.prepare(`
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
424
|
+
INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
|
|
425
|
+
VALUES (?, ?, ?, ?)
|
|
426
|
+
`);
|
|
370
427
|
for (const cls of classes) {
|
|
371
428
|
const simpleName = cls.split('.').pop() || cls;
|
|
372
429
|
insertClass.run(artifact.id, cls, simpleName);
|
|
@@ -391,23 +448,17 @@ export class Indexer {
|
|
|
391
448
|
}
|
|
392
449
|
/**
|
|
393
450
|
* Checks if a class package is included in the configuration patterns.
|
|
451
|
+
*
|
|
452
|
+
* @param className The fully qualified class name.
|
|
453
|
+
* @param normalizedPatterns The list of normalized package patterns.
|
|
394
454
|
*/
|
|
395
|
-
isPackageIncluded(className,
|
|
396
|
-
if (
|
|
455
|
+
isPackageIncluded(className, normalizedPatterns) {
|
|
456
|
+
if (!normalizedPatterns || normalizedPatterns.length === 0)
|
|
397
457
|
return true;
|
|
398
|
-
for (const pattern of
|
|
399
|
-
|
|
458
|
+
for (const pattern of normalizedPatterns) {
|
|
459
|
+
// Match exact package or subpackage
|
|
460
|
+
if (className === pattern || className.startsWith(pattern + '.')) {
|
|
400
461
|
return true;
|
|
401
|
-
if (pattern.endsWith('.*')) {
|
|
402
|
-
const prefix = pattern.slice(0, -2); // "com.example"
|
|
403
|
-
// Match exact package or subpackage
|
|
404
|
-
if (className === prefix || className.startsWith(prefix + '.'))
|
|
405
|
-
return true;
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
// Exact match
|
|
409
|
-
if (className === pattern)
|
|
410
|
-
return true;
|
|
411
462
|
}
|
|
412
463
|
}
|
|
413
464
|
return false;
|
|
@@ -419,11 +470,11 @@ export class Indexer {
|
|
|
419
470
|
// Artifact coordinates search
|
|
420
471
|
const db = DB.getInstance();
|
|
421
472
|
const rows = db.prepare(`
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
473
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
474
|
+
FROM artifacts
|
|
475
|
+
WHERE group_id LIKE ?
|
|
476
|
+
OR artifact_id LIKE ? LIMIT 50
|
|
477
|
+
`).all(`%${query}%`, `%${query}%`);
|
|
427
478
|
return rows;
|
|
428
479
|
}
|
|
429
480
|
/**
|
|
@@ -438,12 +489,19 @@ export class Indexer {
|
|
|
438
489
|
// Regex search
|
|
439
490
|
const regex = classNamePattern.substring(6);
|
|
440
491
|
rows = db.prepare(`
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
492
|
+
SELECT c.class_name,
|
|
493
|
+
c.simple_name,
|
|
494
|
+
a.id,
|
|
495
|
+
a.group_id,
|
|
496
|
+
a.artifact_id,
|
|
497
|
+
a.version,
|
|
498
|
+
a.abspath,
|
|
499
|
+
a.has_source
|
|
500
|
+
FROM classes_fts c
|
|
501
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
502
|
+
WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
|
|
445
503
|
LIMIT 100
|
|
446
|
-
|
|
504
|
+
`).all(regex, regex);
|
|
447
505
|
}
|
|
448
506
|
else if (classNamePattern.includes('*') || classNamePattern.includes('?')) {
|
|
449
507
|
// Glob-style search (using LIKE for standard wildcards)
|
|
@@ -451,12 +509,19 @@ export class Indexer {
|
|
|
451
509
|
// But standard glob is * and ?
|
|
452
510
|
const likePattern = classNamePattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
453
511
|
rows = db.prepare(`
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
512
|
+
SELECT c.class_name,
|
|
513
|
+
c.simple_name,
|
|
514
|
+
a.id,
|
|
515
|
+
a.group_id,
|
|
516
|
+
a.artifact_id,
|
|
517
|
+
a.version,
|
|
518
|
+
a.abspath,
|
|
519
|
+
a.has_source
|
|
520
|
+
FROM classes_fts c
|
|
521
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
522
|
+
WHERE c.class_name LIKE ?
|
|
523
|
+
OR c.simple_name LIKE ? LIMIT 100
|
|
524
|
+
`).all(likePattern, likePattern);
|
|
460
525
|
}
|
|
461
526
|
else {
|
|
462
527
|
// Use FTS for smart matching
|
|
@@ -468,13 +533,19 @@ export class Indexer {
|
|
|
468
533
|
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
469
534
|
: `"${escapedPattern}"*`;
|
|
470
535
|
rows = db.prepare(`
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
536
|
+
SELECT c.class_name,
|
|
537
|
+
c.simple_name,
|
|
538
|
+
a.id,
|
|
539
|
+
a.group_id,
|
|
540
|
+
a.artifact_id,
|
|
541
|
+
a.version,
|
|
542
|
+
a.abspath,
|
|
543
|
+
a.has_source
|
|
544
|
+
FROM classes_fts c
|
|
545
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
546
|
+
WHERE c.classes_fts MATCH ?
|
|
547
|
+
ORDER BY rank LIMIT 100
|
|
548
|
+
`).all(query);
|
|
478
549
|
}
|
|
479
550
|
// Group by class name
|
|
480
551
|
const resultMap = new Map();
|
|
@@ -516,16 +587,17 @@ export class Indexer {
|
|
|
516
587
|
}
|
|
517
588
|
// Recursive search for all implementations/subclasses
|
|
518
589
|
const rows = db.prepare(`
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
590
|
+
WITH RECURSIVE hierarchy(class_name, artifact_id) AS (SELECT class_name, artifact_id
|
|
591
|
+
FROM inheritance
|
|
592
|
+
WHERE parent_class_name = ?
|
|
593
|
+
UNION
|
|
594
|
+
SELECT i.class_name, i.artifact_id
|
|
595
|
+
FROM inheritance i
|
|
596
|
+
JOIN hierarchy h ON i.parent_class_name = h.class_name)
|
|
597
|
+
SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
598
|
+
FROM hierarchy h
|
|
599
|
+
JOIN artifacts a ON h.artifact_id = a.id LIMIT 100
|
|
600
|
+
`).all(className);
|
|
529
601
|
console.error(`Searching implementations for ${className}: found ${rows.length} rows.`);
|
|
530
602
|
if (rows.length === 0) {
|
|
531
603
|
// Fallback: Try searching without recursion to see if direct children exist
|
|
@@ -563,9 +635,10 @@ export class Indexer {
|
|
|
563
635
|
getArtifactById(id) {
|
|
564
636
|
const db = DB.getInstance();
|
|
565
637
|
const row = db.prepare(`
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
638
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
639
|
+
FROM artifacts
|
|
640
|
+
WHERE id = ?
|
|
641
|
+
`).get(id);
|
|
569
642
|
if (row) {
|
|
570
643
|
return {
|
|
571
644
|
id: row.id,
|
|
@@ -584,10 +657,12 @@ export class Indexer {
|
|
|
584
657
|
getArtifactByCoordinate(groupId, artifactId, version) {
|
|
585
658
|
const db = DB.getInstance();
|
|
586
659
|
const row = db.prepare(`
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
660
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
661
|
+
FROM artifacts
|
|
662
|
+
WHERE group_id = ?
|
|
663
|
+
AND artifact_id = ?
|
|
664
|
+
AND version = ?
|
|
665
|
+
`).get(groupId, artifactId, version);
|
|
591
666
|
if (row) {
|
|
592
667
|
return {
|
|
593
668
|
id: row.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maven-indexer-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "MCP server for indexing local Maven repository",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -46,10 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
49
|
+
"@types/semver": "^7.7.1",
|
|
49
50
|
"@types/xml2js": "^0.4.14",
|
|
50
51
|
"@types/yauzl": "^2.10.3",
|
|
51
52
|
"better-sqlite3": "^12.5.0",
|
|
52
53
|
"chokidar": "^5.0.0",
|
|
54
|
+
"semver": "^7.7.3",
|
|
53
55
|
"xml2js": "^0.6.2",
|
|
54
56
|
"yauzl": "^3.2.0",
|
|
55
57
|
"zod": "^4.1.13"
|
package/build/reproduce_issue.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Indexer } from "./indexer.js";
|
|
2
|
-
import { DB } from "./db/index.js";
|
|
3
|
-
async function main() {
|
|
4
|
-
const db = DB.getInstance();
|
|
5
|
-
const indexer = Indexer.getInstance();
|
|
6
|
-
// We assume indexing is done since we just ran it.
|
|
7
|
-
console.log("Searching for 'Kotlin'...");
|
|
8
|
-
const kotlinResults = db.prepare("SELECT class_name FROM classes_fts WHERE class_name LIKE '%Kotlin%' LIMIT 10").all();
|
|
9
|
-
console.log("First 10 Kotlin classes:", kotlinResults);
|
|
10
|
-
console.log("Searching for 'AnAction'...");
|
|
11
|
-
const anActionResults = indexer.searchClass("AnAction");
|
|
12
|
-
console.log(`Found ${anActionResults.length} results for 'AnAction'.`);
|
|
13
|
-
if (anActionResults.length > 0) {
|
|
14
|
-
console.log(anActionResults[0]);
|
|
15
|
-
}
|
|
16
|
-
console.log("Searching for 'StringUtils'...");
|
|
17
|
-
const stringUtilsResults = indexer.searchClass("StringUtils");
|
|
18
|
-
console.log(`Found ${stringUtilsResults.length} results for 'StringUtils'.`);
|
|
19
|
-
}
|
|
20
|
-
main().catch(console.error);
|