maven-indexer-mcp 1.0.2 → 1.0.4
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 +5 -0
- package/build/class_parser.js +120 -0
- package/build/config.js +51 -1
- package/build/db/index.js +21 -0
- package/build/index.js +37 -9
- package/build/indexer.js +171 -33
- package/build/source_parser.js +23 -13
- package/lib/cfr-0.152.jar +0 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ A Model Context Protocol (MCP) server that indexes your local Maven repository (
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
* **Semantic Class Search**: Search for classes by name (e.g., `StringUtils`) or purpose (e.g., `JsonToXml`).
|
|
8
|
+
* **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
|
|
8
9
|
* **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
|
|
9
10
|
* **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
|
|
10
11
|
* **Real-time Monitoring**: Watches the Maven repository for changes (e.g., new `mvn install`) and automatically updates the index.
|
|
@@ -90,6 +91,10 @@ If you prefer to run from source:
|
|
|
90
91
|
* Input: `className`, `artifactId`, `type` ("signatures", "docs", "source")
|
|
91
92
|
* Output: Method signatures, Javadocs, or full source code.
|
|
92
93
|
* **`search_artifacts`**: Search for artifacts by coordinate (groupId, artifactId).
|
|
94
|
+
* **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
|
|
95
|
+
* Input: `className` (e.g. "java.util.List")
|
|
96
|
+
* Output: List of implementation/subclass names and their artifacts.
|
|
97
|
+
* **`refresh_index`**: Trigger a re-scan of the Maven repository.
|
|
93
98
|
|
|
94
99
|
## Development
|
|
95
100
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class ClassParser {
|
|
2
|
+
buffer;
|
|
3
|
+
offset = 0;
|
|
4
|
+
constantPool = [];
|
|
5
|
+
constructor(buffer) {
|
|
6
|
+
this.buffer = buffer;
|
|
7
|
+
}
|
|
8
|
+
static parse(buffer) {
|
|
9
|
+
const parser = new ClassParser(buffer);
|
|
10
|
+
return parser.parse();
|
|
11
|
+
}
|
|
12
|
+
parse() {
|
|
13
|
+
if (this.buffer.length < 10) {
|
|
14
|
+
throw new Error("Invalid class file: too short");
|
|
15
|
+
}
|
|
16
|
+
const magic = this.readU4();
|
|
17
|
+
if (magic !== 0xCAFEBABE) {
|
|
18
|
+
throw new Error("Invalid magic number");
|
|
19
|
+
}
|
|
20
|
+
this.readU2(); // minor
|
|
21
|
+
this.readU2(); // major
|
|
22
|
+
const cpCount = this.readU2();
|
|
23
|
+
this.constantPool = new Array(cpCount);
|
|
24
|
+
// Constant Pool is 1-indexed (1 to count-1)
|
|
25
|
+
for (let i = 1; i < cpCount; i++) {
|
|
26
|
+
const tag = this.readU1();
|
|
27
|
+
switch (tag) {
|
|
28
|
+
case 1: // UTF8
|
|
29
|
+
const len = this.readU2();
|
|
30
|
+
const str = this.buffer.toString('utf-8', this.offset, this.offset + len);
|
|
31
|
+
this.offset += len;
|
|
32
|
+
this.constantPool[i] = { tag, value: str };
|
|
33
|
+
break;
|
|
34
|
+
case 3: // Integer
|
|
35
|
+
case 4: // Float
|
|
36
|
+
this.offset += 4;
|
|
37
|
+
break;
|
|
38
|
+
case 5: // Long
|
|
39
|
+
case 6: // Double
|
|
40
|
+
this.offset += 8;
|
|
41
|
+
i++; // Takes two slots
|
|
42
|
+
break;
|
|
43
|
+
case 7: // Class
|
|
44
|
+
const nameIndex = this.readU2();
|
|
45
|
+
this.constantPool[i] = { tag, nameIndex };
|
|
46
|
+
break;
|
|
47
|
+
case 8: // String
|
|
48
|
+
this.offset += 2;
|
|
49
|
+
break;
|
|
50
|
+
case 9: // Fieldref
|
|
51
|
+
case 10: // Methodref
|
|
52
|
+
case 11: // InterfaceMethodref
|
|
53
|
+
this.offset += 4;
|
|
54
|
+
break;
|
|
55
|
+
case 12: // NameAndType
|
|
56
|
+
this.offset += 4;
|
|
57
|
+
break;
|
|
58
|
+
case 15: // MethodHandle
|
|
59
|
+
this.offset += 3;
|
|
60
|
+
break;
|
|
61
|
+
case 16: // MethodType
|
|
62
|
+
this.offset += 2;
|
|
63
|
+
break;
|
|
64
|
+
case 17: // Dynamic
|
|
65
|
+
case 18: // InvokeDynamic
|
|
66
|
+
this.offset += 4;
|
|
67
|
+
break;
|
|
68
|
+
case 19: // Module
|
|
69
|
+
case 20: // Package
|
|
70
|
+
this.offset += 2;
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unknown constant pool tag: ${tag} at offset ${this.offset - 1}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.readU2(); // Access flags
|
|
77
|
+
const thisClassIndex = this.readU2();
|
|
78
|
+
const superClassIndex = this.readU2();
|
|
79
|
+
const className = this.resolveClass(thisClassIndex);
|
|
80
|
+
const superClass = superClassIndex === 0 ? undefined : this.resolveClass(superClassIndex);
|
|
81
|
+
const interfacesCount = this.readU2();
|
|
82
|
+
const interfaces = [];
|
|
83
|
+
for (let i = 0; i < interfacesCount; i++) {
|
|
84
|
+
const interfaceIndex = this.readU2();
|
|
85
|
+
interfaces.push(this.resolveClass(interfaceIndex));
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
className: className.replace(/\//g, '.'),
|
|
89
|
+
superClass: superClass ? superClass.replace(/\//g, '.') : undefined,
|
|
90
|
+
interfaces: interfaces.map(i => i.replace(/\//g, '.'))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
resolveClass(index) {
|
|
94
|
+
const entry = this.constantPool[index];
|
|
95
|
+
if (!entry || entry.tag !== 7) {
|
|
96
|
+
// Fallback or error?
|
|
97
|
+
return "Unknown";
|
|
98
|
+
}
|
|
99
|
+
const nameEntry = this.constantPool[entry.nameIndex];
|
|
100
|
+
if (!nameEntry || nameEntry.tag !== 1) {
|
|
101
|
+
return "Unknown";
|
|
102
|
+
}
|
|
103
|
+
return nameEntry.value;
|
|
104
|
+
}
|
|
105
|
+
readU1() {
|
|
106
|
+
const val = this.buffer.readUInt8(this.offset);
|
|
107
|
+
this.offset += 1;
|
|
108
|
+
return val;
|
|
109
|
+
}
|
|
110
|
+
readU2() {
|
|
111
|
+
const val = this.buffer.readUInt16BE(this.offset);
|
|
112
|
+
this.offset += 2;
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
readU4() {
|
|
116
|
+
const val = this.buffer.readUInt32BE(this.offset);
|
|
117
|
+
this.offset += 4;
|
|
118
|
+
return val;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/build/config.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import { createWriteStream } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import os from 'os';
|
|
5
|
+
import https from 'https';
|
|
4
6
|
import xml2js from 'xml2js';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
5
8
|
export class Config {
|
|
6
9
|
static instance;
|
|
7
10
|
localRepository = "";
|
|
@@ -69,7 +72,19 @@ export class Config {
|
|
|
69
72
|
}
|
|
70
73
|
else {
|
|
71
74
|
// Fallback to default bundled lib
|
|
72
|
-
|
|
75
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
76
|
+
const __dirname = path.dirname(__filename);
|
|
77
|
+
this.cfrPath = path.resolve(__dirname, '../lib/cfr-0.152.jar');
|
|
78
|
+
}
|
|
79
|
+
if (this.cfrPath && !(await this.fileExists(this.cfrPath))) {
|
|
80
|
+
try {
|
|
81
|
+
console.error(`CFR jar not found at ${this.cfrPath}, attempting to download...`);
|
|
82
|
+
await this.downloadCfr(this.cfrPath);
|
|
83
|
+
console.error(`Successfully downloaded CFR jar to ${this.cfrPath}`);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
console.error(`Failed to download CFR jar: ${e.message}`);
|
|
87
|
+
}
|
|
73
88
|
}
|
|
74
89
|
// Log to stderr so it doesn't interfere with MCP protocol on stdout
|
|
75
90
|
console.error(`Using local repository: ${this.localRepository}`);
|
|
@@ -88,6 +103,41 @@ export class Config {
|
|
|
88
103
|
const dir = path.dirname(this.javaBinary);
|
|
89
104
|
return path.join(dir, 'javap');
|
|
90
105
|
}
|
|
106
|
+
async downloadCfr(destPath) {
|
|
107
|
+
const url = "https://github.com/tangcent/maven-indexer-mcp/raw/refs/heads/main/lib/cfr-0.152.jar";
|
|
108
|
+
const dir = path.dirname(destPath);
|
|
109
|
+
await fs.mkdir(dir, { recursive: true });
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const download = (currentUrl) => {
|
|
112
|
+
https.get(currentUrl, (response) => {
|
|
113
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
114
|
+
if (response.headers.location) {
|
|
115
|
+
download(response.headers.location);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (response.statusCode !== 200) {
|
|
120
|
+
reject(new Error(`Failed to download CFR jar: Status Code ${response.statusCode}`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const file = createWriteStream(destPath);
|
|
124
|
+
response.pipe(file);
|
|
125
|
+
file.on('finish', () => {
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
file.close();
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
file.on('error', (err) => {
|
|
131
|
+
fs.unlink(destPath).catch(() => { });
|
|
132
|
+
reject(err);
|
|
133
|
+
});
|
|
134
|
+
}).on('error', (err) => {
|
|
135
|
+
reject(err);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
download(url);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
91
141
|
async fileExists(filePath) {
|
|
92
142
|
try {
|
|
93
143
|
await fs.access(filePath);
|
package/build/db/index.js
CHANGED
|
@@ -17,6 +17,17 @@ export class DB {
|
|
|
17
17
|
return DB.instance;
|
|
18
18
|
}
|
|
19
19
|
initSchema() {
|
|
20
|
+
// Register REGEXP function
|
|
21
|
+
this.db.function('regexp', { deterministic: true }, (regex, text) => {
|
|
22
|
+
if (!regex || !text)
|
|
23
|
+
return 0;
|
|
24
|
+
try {
|
|
25
|
+
return new RegExp(regex).test(text) ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
20
31
|
this.db.exec(`
|
|
21
32
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
22
33
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -35,6 +46,16 @@ export class DB {
|
|
|
35
46
|
simple_name, -- Just the class name
|
|
36
47
|
tokenize="trigram"
|
|
37
48
|
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS inheritance (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
artifact_id INTEGER NOT NULL,
|
|
53
|
+
class_name TEXT NOT NULL,
|
|
54
|
+
parent_class_name TEXT NOT NULL,
|
|
55
|
+
type TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_inheritance_parent ON inheritance(parent_class_name);
|
|
38
59
|
|
|
39
60
|
-- Cleanup old table if exists
|
|
40
61
|
DROP TABLE IF EXISTS indexed_artifacts;
|
package/build/index.js
CHANGED
|
@@ -21,7 +21,7 @@ indexer.index().then(() => {
|
|
|
21
21
|
return indexer.startWatch();
|
|
22
22
|
}).catch(err => console.error("Initial indexing failed:", err));
|
|
23
23
|
server.registerTool("search_artifacts", {
|
|
24
|
-
description: "Search for artifacts in the local
|
|
24
|
+
description: "Search for Maven artifacts (libraries) in the local repository by coordinate (groupId, artifactId) or keyword. Use this to find available versions of a library.",
|
|
25
25
|
inputSchema: z.object({
|
|
26
26
|
query: z.string().describe("Search query (groupId, artifactId, or keyword)"),
|
|
27
27
|
}),
|
|
@@ -42,7 +42,7 @@ server.registerTool("search_artifacts", {
|
|
|
42
42
|
};
|
|
43
43
|
});
|
|
44
44
|
server.registerTool("search_classes", {
|
|
45
|
-
description: "Search for Java classes.
|
|
45
|
+
description: "Search for Java classes in the local Maven repository. Use this to find classes from external dependencies/libraries when their source code is not available in the current workspace. You can search by class name (e.g. 'StringUtils') or keywords (e.g. 'Json').",
|
|
46
46
|
inputSchema: z.object({
|
|
47
47
|
className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
48
48
|
}),
|
|
@@ -60,8 +60,26 @@ server.registerTool("search_classes", {
|
|
|
60
60
|
content: [{ type: "text", text }]
|
|
61
61
|
};
|
|
62
62
|
});
|
|
63
|
+
server.registerTool("search_implementations", {
|
|
64
|
+
description: "Search for classes that implement a specific interface or extend a specific class. This is useful for finding implementations of SPIs or base classes.",
|
|
65
|
+
inputSchema: z.object({
|
|
66
|
+
className: z.string().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
|
|
67
|
+
}),
|
|
68
|
+
}, async ({ className }) => {
|
|
69
|
+
const matches = indexer.searchImplementations(className);
|
|
70
|
+
const text = matches.length > 0
|
|
71
|
+
? matches.map(m => {
|
|
72
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}`).join("\n ");
|
|
73
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
74
|
+
return `Implementation: ${m.className}\n ${artifacts}${more}`;
|
|
75
|
+
}).join("\n\n")
|
|
76
|
+
: `No implementations found for ${className}. Ensure the index is up to date and the class name is correct.`;
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text }]
|
|
79
|
+
};
|
|
80
|
+
});
|
|
63
81
|
server.registerTool("get_class_details", {
|
|
64
|
-
description: "Get details about a specific class from
|
|
82
|
+
description: "Get details about a specific class from a Maven artifact. Use this to inspect external classes where source code is missing. It can provide method signatures (using javap), Javadocs, or even decompiled source code if the original source jar is unavailable.",
|
|
65
83
|
inputSchema: z.object({
|
|
66
84
|
className: z.string().describe("Fully qualified class name"),
|
|
67
85
|
artifactId: z.number().describe("The internal ID of the artifact (returned by search_classes)"),
|
|
@@ -74,6 +92,7 @@ server.registerTool("get_class_details", {
|
|
|
74
92
|
}
|
|
75
93
|
let detail = null;
|
|
76
94
|
let usedDecompilation = false;
|
|
95
|
+
let lastError = "";
|
|
77
96
|
// 1. If requesting source/docs, try Source JAR first
|
|
78
97
|
if (type === 'source' || type === 'docs') {
|
|
79
98
|
if (artifact.hasSource) {
|
|
@@ -83,6 +102,7 @@ server.registerTool("get_class_details", {
|
|
|
83
102
|
}
|
|
84
103
|
catch (e) {
|
|
85
104
|
// Ignore error and fallthrough to main jar (decompilation)
|
|
105
|
+
lastError = e.message;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
// If not found in source jar (or no source jar), try main jar (decompilation)
|
|
@@ -96,18 +116,26 @@ server.registerTool("get_class_details", {
|
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
catch (e) {
|
|
99
|
-
|
|
119
|
+
console.error(`Decompilation/MainJar access failed: ${e.message}`);
|
|
120
|
+
lastError = e.message;
|
|
100
121
|
}
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
124
|
else {
|
|
104
125
|
// Signatures -> Use Main JAR
|
|
105
126
|
const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
106
|
-
|
|
127
|
+
try {
|
|
128
|
+
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
lastError = e.message;
|
|
132
|
+
}
|
|
107
133
|
}
|
|
108
134
|
try {
|
|
109
135
|
if (!detail) {
|
|
110
|
-
|
|
136
|
+
const debugInfo = `Artifact path: ${artifact.abspath}, hasSource: ${artifact.hasSource}`;
|
|
137
|
+
const errorMsg = lastError ? `\nLast error: ${lastError}` : "";
|
|
138
|
+
return { content: [{ type: "text", text: `Class ${className} not found in artifact ${artifact.artifactId}. \nDebug info: ${debugInfo}${errorMsg}` }] };
|
|
111
139
|
}
|
|
112
140
|
let resultText = `Class: ${detail.className}\n\n`;
|
|
113
141
|
if (usedDecompilation) {
|
|
@@ -131,12 +159,12 @@ server.registerTool("get_class_details", {
|
|
|
131
159
|
}
|
|
132
160
|
});
|
|
133
161
|
server.registerTool("refresh_index", {
|
|
134
|
-
description: "Trigger a re-scan of the Maven repository",
|
|
162
|
+
description: "Trigger a re-scan of the Maven repository. This will re-index all artifacts.",
|
|
135
163
|
}, async () => {
|
|
136
164
|
// Re-run index
|
|
137
|
-
indexer.
|
|
165
|
+
indexer.refresh().catch(console.error);
|
|
138
166
|
return {
|
|
139
|
-
content: [{ type: "text", text: "Index refresh started." }]
|
|
167
|
+
content: [{ type: "text", text: "Index refresh started. All artifacts will be re-indexed." }]
|
|
140
168
|
};
|
|
141
169
|
});
|
|
142
170
|
const transport = new StdioServerTransport();
|
package/build/indexer.js
CHANGED
|
@@ -5,6 +5,7 @@ import yauzl from 'yauzl';
|
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
6
|
import { Config } from './config.js';
|
|
7
7
|
import { DB } from './db/index.js';
|
|
8
|
+
import { ClassParser } from './class_parser.js';
|
|
8
9
|
/**
|
|
9
10
|
* Singleton class responsible for indexing Maven artifacts.
|
|
10
11
|
* It scans the local repository, watches for changes, and indexes Java classes.
|
|
@@ -60,6 +61,19 @@ export class Indexer {
|
|
|
60
61
|
onChange();
|
|
61
62
|
});
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Forces a full re-index of the repository.
|
|
66
|
+
*/
|
|
67
|
+
async refresh() {
|
|
68
|
+
const db = DB.getInstance();
|
|
69
|
+
console.error("Refreshing index...");
|
|
70
|
+
db.transaction(() => {
|
|
71
|
+
db.prepare('UPDATE artifacts SET is_indexed = 0').run();
|
|
72
|
+
db.prepare('DELETE FROM classes_fts').run();
|
|
73
|
+
db.prepare('DELETE FROM inheritance').run();
|
|
74
|
+
});
|
|
75
|
+
return this.index();
|
|
76
|
+
}
|
|
63
77
|
/**
|
|
64
78
|
* Main indexing process.
|
|
65
79
|
* 1. Scans the file system for Maven artifacts.
|
|
@@ -71,15 +85,14 @@ export class Indexer {
|
|
|
71
85
|
return;
|
|
72
86
|
this.isIndexing = true;
|
|
73
87
|
console.error("Starting index...");
|
|
74
|
-
const config = await Config.getInstance();
|
|
75
|
-
const repoPath = config.localRepository;
|
|
76
|
-
const db = DB.getInstance();
|
|
77
|
-
if (!repoPath) {
|
|
78
|
-
console.error("No local repository path found.");
|
|
79
|
-
this.isIndexing = false;
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
88
|
try {
|
|
89
|
+
const config = await Config.getInstance();
|
|
90
|
+
const repoPath = config.localRepository;
|
|
91
|
+
const db = DB.getInstance();
|
|
92
|
+
if (!repoPath) {
|
|
93
|
+
console.error("No local repository path found.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
83
96
|
// 1. Scan for artifacts
|
|
84
97
|
console.error("Scanning repository structure...");
|
|
85
98
|
const artifacts = await this.scanRepository(repoPath);
|
|
@@ -99,6 +112,17 @@ export class Indexer {
|
|
|
99
112
|
});
|
|
100
113
|
}
|
|
101
114
|
});
|
|
115
|
+
// Check if we need to backfill inheritance data (migration)
|
|
116
|
+
const inheritanceCount = db.prepare('SELECT COUNT(*) as count FROM inheritance').get();
|
|
117
|
+
const indexedArtifactsCount = db.prepare('SELECT COUNT(*) as count FROM artifacts WHERE is_indexed = 1').get();
|
|
118
|
+
if (inheritanceCount.count === 0 && indexedArtifactsCount.count > 0) {
|
|
119
|
+
console.error("Detected missing inheritance data. Forcing re-index of classes...");
|
|
120
|
+
db.transaction(() => {
|
|
121
|
+
db.prepare('UPDATE artifacts SET is_indexed = 0').run();
|
|
122
|
+
db.prepare('DELETE FROM classes_fts').run();
|
|
123
|
+
// inheritance is already empty
|
|
124
|
+
});
|
|
125
|
+
}
|
|
102
126
|
// 3. Find artifacts that need indexing (is_indexed = 0)
|
|
103
127
|
const artifactsToIndex = db.prepare(`
|
|
104
128
|
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
@@ -198,20 +222,45 @@ export class Indexer {
|
|
|
198
222
|
return;
|
|
199
223
|
}
|
|
200
224
|
const classes = [];
|
|
225
|
+
const inheritance = [];
|
|
201
226
|
zipfile.readEntry();
|
|
202
227
|
zipfile.on('entry', (entry) => {
|
|
203
228
|
if (entry.fileName.endsWith('.class')) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// Filter by includedPackages
|
|
209
|
-
if (this.isPackageIncluded(className, config.includedPackages)) {
|
|
210
|
-
classes.push(className);
|
|
229
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
230
|
+
if (err || !readStream) {
|
|
231
|
+
zipfile.readEntry();
|
|
232
|
+
return;
|
|
211
233
|
}
|
|
212
|
-
|
|
234
|
+
const chunks = [];
|
|
235
|
+
readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
236
|
+
readStream.on('end', () => {
|
|
237
|
+
const buffer = Buffer.concat(chunks);
|
|
238
|
+
try {
|
|
239
|
+
const info = ClassParser.parse(buffer);
|
|
240
|
+
// Simple check to avoid module-info or invalid names
|
|
241
|
+
if (!info.className.includes('$') && info.className.length > 0) {
|
|
242
|
+
// Filter by includedPackages
|
|
243
|
+
if (this.isPackageIncluded(info.className, config.includedPackages)) {
|
|
244
|
+
classes.push(info.className);
|
|
245
|
+
if (info.superClass && info.superClass !== 'java.lang.Object') {
|
|
246
|
+
inheritance.push({ className: info.className, parent: info.superClass, type: 'extends' });
|
|
247
|
+
}
|
|
248
|
+
for (const iface of info.interfaces) {
|
|
249
|
+
inheritance.push({ className: info.className, parent: iface, type: 'implements' });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
// console.error(`Failed to parse ${entry.fileName}`, e);
|
|
256
|
+
}
|
|
257
|
+
zipfile.readEntry();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
zipfile.readEntry();
|
|
213
263
|
}
|
|
214
|
-
zipfile.readEntry();
|
|
215
264
|
});
|
|
216
265
|
zipfile.on('end', () => {
|
|
217
266
|
// Batch insert classes
|
|
@@ -220,11 +269,18 @@ export class Indexer {
|
|
|
220
269
|
const insertClass = db.prepare(`
|
|
221
270
|
INSERT INTO classes_fts (artifact_id, class_name, simple_name)
|
|
222
271
|
VALUES (?, ?, ?)
|
|
272
|
+
`);
|
|
273
|
+
const insertInheritance = db.prepare(`
|
|
274
|
+
INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
|
|
275
|
+
VALUES (?, ?, ?, ?)
|
|
223
276
|
`);
|
|
224
277
|
for (const cls of classes) {
|
|
225
278
|
const simpleName = cls.split('.').pop() || cls;
|
|
226
279
|
insertClass.run(artifact.id, cls, simpleName);
|
|
227
280
|
}
|
|
281
|
+
for (const item of inheritance) {
|
|
282
|
+
insertInheritance.run(artifact.id, item.className, item.parent, item.type);
|
|
283
|
+
}
|
|
228
284
|
// Mark as indexed
|
|
229
285
|
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
230
286
|
});
|
|
@@ -283,23 +339,50 @@ export class Indexer {
|
|
|
283
339
|
*/
|
|
284
340
|
searchClass(classNamePattern) {
|
|
285
341
|
const db = DB.getInstance();
|
|
286
|
-
// Use FTS for smart matching
|
|
287
|
-
// If pattern has no spaces, we assume it's a prefix or exact match query
|
|
288
|
-
// "String" -> match "String" in simple_name or class_name
|
|
289
|
-
const escapedPattern = classNamePattern.replace(/"/g, '""');
|
|
290
|
-
const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
|
|
291
|
-
const query = safeQuery.length > 0
|
|
292
|
-
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
293
|
-
: `"${escapedPattern}"*`;
|
|
294
342
|
try {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
343
|
+
let rows = [];
|
|
344
|
+
if (classNamePattern.startsWith('regex:')) {
|
|
345
|
+
// Regex search
|
|
346
|
+
const regex = classNamePattern.substring(6);
|
|
347
|
+
rows = db.prepare(`
|
|
348
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
349
|
+
FROM classes_fts c
|
|
350
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
351
|
+
WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
|
|
352
|
+
LIMIT 100
|
|
353
|
+
`).all(regex, regex);
|
|
354
|
+
}
|
|
355
|
+
else if (classNamePattern.includes('*') || classNamePattern.includes('?')) {
|
|
356
|
+
// Glob-style search (using LIKE for standard wildcards)
|
|
357
|
+
// Convert glob wildcards to SQL wildcards if needed, or just rely on user knowing %/_
|
|
358
|
+
// But standard glob is * and ?
|
|
359
|
+
const likePattern = classNamePattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
360
|
+
rows = db.prepare(`
|
|
361
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
362
|
+
FROM classes_fts c
|
|
363
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
364
|
+
WHERE c.class_name LIKE ? OR c.simple_name LIKE ?
|
|
365
|
+
LIMIT 100
|
|
366
|
+
`).all(likePattern, likePattern);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Use FTS for smart matching
|
|
370
|
+
// If pattern has no spaces, we assume it's a prefix or exact match query
|
|
371
|
+
// "String" -> match "String" in simple_name or class_name
|
|
372
|
+
const escapedPattern = classNamePattern.replace(/"/g, '""');
|
|
373
|
+
const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
|
|
374
|
+
const query = safeQuery.length > 0
|
|
375
|
+
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
376
|
+
: `"${escapedPattern}"*`;
|
|
377
|
+
rows = db.prepare(`
|
|
378
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
379
|
+
FROM classes_fts c
|
|
380
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
381
|
+
WHERE c.classes_fts MATCH ?
|
|
382
|
+
ORDER BY rank
|
|
383
|
+
LIMIT 100
|
|
384
|
+
`).all(query);
|
|
385
|
+
}
|
|
303
386
|
// Group by class name
|
|
304
387
|
const resultMap = new Map();
|
|
305
388
|
for (const row of rows) {
|
|
@@ -326,6 +409,61 @@ export class Indexer {
|
|
|
326
409
|
return [];
|
|
327
410
|
}
|
|
328
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Searches for implementations/subclasses of a specific class/interface.
|
|
414
|
+
*/
|
|
415
|
+
searchImplementations(className) {
|
|
416
|
+
const db = DB.getInstance();
|
|
417
|
+
try {
|
|
418
|
+
console.error(`Searching implementations for ${className}...`);
|
|
419
|
+
// Debug: Check if we have any inheritance data at all
|
|
420
|
+
const count = db.prepare("SELECT count(*) as c FROM inheritance").get();
|
|
421
|
+
if (count.c === 0) {
|
|
422
|
+
console.error("WARNING: Inheritance table is empty!");
|
|
423
|
+
}
|
|
424
|
+
// Recursive search for all implementations/subclasses
|
|
425
|
+
const rows = db.prepare(`
|
|
426
|
+
WITH RECURSIVE hierarchy(class_name, artifact_id) AS (
|
|
427
|
+
SELECT class_name, artifact_id FROM inheritance WHERE parent_class_name = ?
|
|
428
|
+
UNION
|
|
429
|
+
SELECT i.class_name, i.artifact_id FROM inheritance i JOIN hierarchy h ON i.parent_class_name = h.class_name
|
|
430
|
+
)
|
|
431
|
+
SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
432
|
+
FROM hierarchy h
|
|
433
|
+
JOIN artifacts a ON h.artifact_id = a.id
|
|
434
|
+
LIMIT 100
|
|
435
|
+
`).all(className);
|
|
436
|
+
console.error(`Searching implementations for ${className}: found ${rows.length} rows.`);
|
|
437
|
+
if (rows.length === 0) {
|
|
438
|
+
// Fallback: Try searching without recursion to see if direct children exist
|
|
439
|
+
const direct = db.prepare('SELECT count(*) as c FROM inheritance WHERE parent_class_name = ?').get(className);
|
|
440
|
+
console.error(`Direct implementations check for ${className}: ${direct.c}`);
|
|
441
|
+
}
|
|
442
|
+
const resultMap = new Map();
|
|
443
|
+
for (const row of rows) {
|
|
444
|
+
const art = {
|
|
445
|
+
id: row.id,
|
|
446
|
+
groupId: row.group_id,
|
|
447
|
+
artifactId: row.artifact_id,
|
|
448
|
+
version: row.version,
|
|
449
|
+
abspath: row.abspath,
|
|
450
|
+
hasSource: Boolean(row.has_source)
|
|
451
|
+
};
|
|
452
|
+
if (!resultMap.has(row.class_name)) {
|
|
453
|
+
resultMap.set(row.class_name, []);
|
|
454
|
+
}
|
|
455
|
+
resultMap.get(row.class_name).push(art);
|
|
456
|
+
}
|
|
457
|
+
return Array.from(resultMap.entries()).map(([className, artifacts]) => ({
|
|
458
|
+
className,
|
|
459
|
+
artifacts
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
console.error("Search implementations failed", e);
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
329
467
|
/**
|
|
330
468
|
* Retrieves an artifact by its database ID.
|
|
331
469
|
*/
|
package/build/source_parser.js
CHANGED
|
@@ -59,29 +59,38 @@ export class SourceParser {
|
|
|
59
59
|
// If it is the sources jar, decompilation will fail (as it doesn't contain .class files),
|
|
60
60
|
// returning null.
|
|
61
61
|
if (type === 'source' || type === 'docs') {
|
|
62
|
-
return this.decompileClass(jarPath, className);
|
|
62
|
+
return this.decompileClass(jarPath, className, type);
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
-
static async decompileClass(jarPath, className) {
|
|
66
|
+
static async decompileClass(jarPath, className, type) {
|
|
67
|
+
const config = await Config.getInstance();
|
|
68
|
+
const cfrPath = config.getCfrJarPath();
|
|
69
|
+
if (!cfrPath || !fs.existsSync(cfrPath)) {
|
|
70
|
+
throw new Error(`CFR jar not found at ${cfrPath}`);
|
|
71
|
+
}
|
|
72
|
+
// Use java -cp cfr.jar:target.jar org.benf.cfr.reader.Main className
|
|
73
|
+
const classpath = `${cfrPath}${path.delimiter}${jarPath}`;
|
|
74
|
+
const cmd = `java -cp "${classpath}" org.benf.cfr.reader.Main "${className}"`;
|
|
75
|
+
// console.error("Running decompile command:", cmd);
|
|
67
76
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// If
|
|
72
|
-
|
|
77
|
+
const { stdout, stderr } = await execAsync(cmd);
|
|
78
|
+
if (!stdout && stderr) {
|
|
79
|
+
console.error(`CFR stderr for ${className}:`, stderr);
|
|
80
|
+
// If stderr has content but stdout is empty, it might be an error
|
|
81
|
+
throw new Error(`CFR stderr: ${stderr}`);
|
|
82
|
+
}
|
|
83
|
+
if (stdout) {
|
|
84
|
+
return this.parse(className, stdout, type);
|
|
73
85
|
}
|
|
74
|
-
// Use java -cp cfr.jar:target.jar org.benf.cfr.reader.Main className
|
|
75
|
-
const classpath = `${cfrPath}${path.delimiter}${jarPath}`;
|
|
76
|
-
const { stdout } = await execAsync(`java -cp "${classpath}" org.benf.cfr.reader.Main "${className}"`);
|
|
77
86
|
return {
|
|
78
87
|
className,
|
|
79
88
|
source: stdout // Return as source
|
|
80
89
|
};
|
|
81
90
|
}
|
|
82
91
|
catch (e) {
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
console.error(`CFR failed for ${className} in ${jarPath}:`, e.message);
|
|
93
|
+
throw e; // Rethrow to let caller handle
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
static async getSignaturesWithJavap(jarPath, className) {
|
|
@@ -123,7 +132,8 @@ export class SourceParser {
|
|
|
123
132
|
let inDoc = false;
|
|
124
133
|
// Regex to match method signatures (public/protected, return type, name, args)
|
|
125
134
|
// ignoring annotations for simplicity
|
|
126
|
-
|
|
135
|
+
// Expanded to match decompiled code better (e.g., might not have throws, might be abstract)
|
|
136
|
+
const methodRegex = /^\s*(public|protected)\s+(?:[\w<>?\[\]]+\s+)+(\w+)\s*\([^)]*\)/;
|
|
127
137
|
for (const line of lines) {
|
|
128
138
|
const trimmed = line.trim();
|
|
129
139
|
// Javadoc extraction
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maven-indexer-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "MCP server for indexing local Maven repository",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"build",
|
|
12
|
+
"lib",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|