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/index.js
CHANGED
|
@@ -5,12 +5,14 @@ import path from 'path';
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { Indexer } from "./indexer.js";
|
|
7
7
|
import { SourceParser } from "./source_parser.js";
|
|
8
|
+
import { ArtifactResolver } from "./artifact_resolver.js";
|
|
8
9
|
const server = new McpServer({
|
|
9
10
|
name: "maven-indexer",
|
|
10
11
|
version: "1.0.0",
|
|
11
12
|
}, {
|
|
12
13
|
capabilities: {
|
|
13
14
|
tools: {},
|
|
15
|
+
prompts: {},
|
|
14
16
|
},
|
|
15
17
|
});
|
|
16
18
|
// Start indexing in the background
|
|
@@ -20,188 +22,230 @@ indexer.index().then(() => {
|
|
|
20
22
|
// Start watching for changes after initial index
|
|
21
23
|
return indexer.startWatch();
|
|
22
24
|
}).catch(err => console.error("Initial indexing failed:", err));
|
|
23
|
-
server.registerTool("search_artifacts", {
|
|
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
|
-
inputSchema: z.object({
|
|
26
|
-
query: z.string().describe("Search query (groupId, artifactId, or keyword)"),
|
|
27
|
-
}),
|
|
28
|
-
}, async ({ query }) => {
|
|
29
|
-
const matches = indexer.search(query);
|
|
30
|
-
// Limit results to avoid overflow
|
|
31
|
-
const limitedMatches = matches.slice(0, 20);
|
|
32
|
-
const text = limitedMatches.length > 0
|
|
33
|
-
? limitedMatches.map(a => `${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
|
|
34
|
-
: "No artifacts found matching the query.";
|
|
35
|
-
return {
|
|
36
|
-
content: [
|
|
37
|
-
{
|
|
38
|
-
type: "text",
|
|
39
|
-
text: `Found ${matches.length} matches${matches.length > 20 ? ' (showing first 20)' : ''}:\n${text}`,
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
server.registerTool("search_classes", {
|
|
45
|
-
description: "Search for Java classes in the local Maven repository. WHEN TO USE: 1. You cannot find a class definition in the current project source (it's likely a dependency). 2. You need to read the source code, method signatures, or Javadocs of an external library class. 3. You need to verify which version of a library class is being used. Examples: 'Show me the source of StringUtils', 'What methods are available on DateTimeUtils?', 'Where is this class imported from?'.",
|
|
46
|
-
inputSchema: z.object({
|
|
47
|
-
className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
48
|
-
}),
|
|
49
|
-
}, async ({ className }) => {
|
|
50
|
-
const matches = indexer.searchClass(className);
|
|
51
|
-
const text = matches.length > 0
|
|
52
|
-
? matches.map(m => {
|
|
53
|
-
// Group by artifact ID to allow easy selection
|
|
54
|
-
const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
|
|
55
|
-
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
56
|
-
return `Class: ${m.className}\n ${artifacts}${more}`;
|
|
57
|
-
}).join("\n\n")
|
|
58
|
-
: "No classes found matching the query. Try different keywords.";
|
|
59
|
-
return {
|
|
60
|
-
content: [{ type: "text", text }]
|
|
61
|
-
};
|
|
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, especially in external libraries.",
|
|
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 => `${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
|
-
});
|
|
81
25
|
server.registerTool("get_class_details", {
|
|
82
|
-
description: "
|
|
26
|
+
description: "Retrieve the source code for a class from the local Maven/Gradle cache (containing internal company libraries). This tool identifies the containing artifact and returns the source code. It prefers actual source files but will fall back to decompilation if necessary. Use this primarily for internal company libraries that are not present in the current workspace. IMPORTANT: Even if the code compiles and imports work, the source code might not be in the current workspace (it comes from a compiled internal library). Use this tool to see the actual implementation of those internal libraries. Supports batch queries.",
|
|
83
27
|
inputSchema: z.object({
|
|
84
|
-
className: z.string().describe("Fully qualified class name"),
|
|
85
|
-
|
|
28
|
+
className: z.string().optional().describe("Fully qualified class name"),
|
|
29
|
+
classNames: z.array(z.string()).optional().describe("Batch class names"),
|
|
30
|
+
coordinate: z.string().optional().describe("The Maven coordinate of the artifact (groupId:artifactId:version). Optional: if not provided, the tool will automatically find the best match (preferring artifacts with source code). Applies to all classes in batch mode."),
|
|
86
31
|
type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
|
|
87
32
|
}),
|
|
88
|
-
}, async ({ className, coordinate, type }) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
33
|
+
}, async ({ className, classNames, coordinate, type }) => {
|
|
34
|
+
const resolveOne = async (clsName, coord) => {
|
|
35
|
+
let targetArtifact;
|
|
36
|
+
if (coord) {
|
|
37
|
+
const parts = coord.split(':');
|
|
38
|
+
if (parts.length === 3) {
|
|
39
|
+
targetArtifact = indexer.getArtifactByCoordinate(parts[0], parts[1], parts[2]);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return "Invalid coordinate format. Expected groupId:artifactId:version";
|
|
43
|
+
}
|
|
44
|
+
if (!targetArtifact) {
|
|
45
|
+
return `Artifact ${coord} not found in index.`;
|
|
46
|
+
}
|
|
94
47
|
}
|
|
95
48
|
else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!exactMatch) {
|
|
108
|
-
// If no exact match, but we have some results, list them
|
|
109
|
-
if (matches.length > 0) {
|
|
110
|
-
const suggestions = matches.map(m => `- ${m.className}`).join("\n");
|
|
111
|
-
return { content: [{ type: "text", text: `Class '${className}' not found exactly. Did you mean:\n${suggestions}` }] };
|
|
49
|
+
// Auto-resolve artifact if coordinate is missing
|
|
50
|
+
const matches = indexer.searchClass(clsName);
|
|
51
|
+
// Find exact match for class name
|
|
52
|
+
const exactMatch = matches.find(m => m.className === clsName);
|
|
53
|
+
if (!exactMatch) {
|
|
54
|
+
// If no exact match, but we have some results, list them
|
|
55
|
+
if (matches.length > 0) {
|
|
56
|
+
const suggestions = matches.map(m => `- ${m.className}`).join("\n");
|
|
57
|
+
return `Class '${clsName}' not found exactly. Did you mean:\n${suggestions}`;
|
|
58
|
+
}
|
|
59
|
+
return `Class '${clsName}' not found in the index. Try 'search_classes' with a keyword if you are unsure of the full name.`;
|
|
112
60
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const artifacts = exactMatch.artifacts.sort((a, b) => {
|
|
118
|
-
if (a.hasSource !== b.hasSource) {
|
|
119
|
-
return a.hasSource ? -1 : 1; // source comes first
|
|
61
|
+
// We have an exact match, choose the best artifact
|
|
62
|
+
const bestArtifact = await ArtifactResolver.resolveBestArtifact(exactMatch.artifacts);
|
|
63
|
+
if (!bestArtifact) {
|
|
64
|
+
return `Class '${clsName}' found but no artifacts are associated with it (database inconsistency).`;
|
|
120
65
|
}
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
if (artifacts.length === 0) {
|
|
124
|
-
return { content: [{ type: "text", text: `Class '${className}' found but no artifacts are associated with it (database inconsistency).` }] };
|
|
66
|
+
targetArtifact = bestArtifact;
|
|
125
67
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
68
|
+
const artifact = targetArtifact;
|
|
69
|
+
let detail = null;
|
|
70
|
+
let usedDecompilation = false;
|
|
71
|
+
let lastError = "";
|
|
72
|
+
// 1. If requesting source/docs, try Source JAR first
|
|
73
|
+
if (type === 'source' || type === 'docs') {
|
|
74
|
+
if (artifact.hasSource) {
|
|
75
|
+
const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
|
|
76
|
+
try {
|
|
77
|
+
detail = await SourceParser.getClassDetail(sourceJarPath, clsName, type);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
// Ignore error and fallthrough to main jar (decompilation)
|
|
81
|
+
lastError = e.message;
|
|
82
|
+
}
|
|
139
83
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
84
|
+
// If not found in source jar (or no source jar), try main jar (decompilation)
|
|
85
|
+
if (!detail) {
|
|
86
|
+
let mainJarPath = artifact.abspath;
|
|
87
|
+
if (!mainJarPath.endsWith('.jar')) {
|
|
88
|
+
mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// SourceParser will try to decompile if source file not found in jar
|
|
92
|
+
detail = await SourceParser.getClassDetail(mainJarPath, clsName, type);
|
|
93
|
+
if (detail && detail.source) {
|
|
94
|
+
usedDecompilation = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error(`Decompilation/MainJar access failed: ${e.message}`);
|
|
99
|
+
lastError = e.message;
|
|
100
|
+
}
|
|
143
101
|
}
|
|
144
102
|
}
|
|
145
|
-
|
|
146
|
-
|
|
103
|
+
else {
|
|
104
|
+
// Signatures -> Use Main JAR
|
|
147
105
|
let mainJarPath = artifact.abspath;
|
|
148
106
|
if (!mainJarPath.endsWith('.jar')) {
|
|
149
107
|
mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
150
108
|
}
|
|
151
109
|
try {
|
|
152
|
-
|
|
153
|
-
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
154
|
-
if (detail && detail.source) {
|
|
155
|
-
usedDecompilation = true;
|
|
156
|
-
}
|
|
110
|
+
detail = await SourceParser.getClassDetail(mainJarPath, clsName, type);
|
|
157
111
|
}
|
|
158
112
|
catch (e) {
|
|
159
|
-
console.error(`Decompilation/MainJar access failed: ${e.message}`);
|
|
160
113
|
lastError = e.message;
|
|
161
114
|
}
|
|
162
115
|
}
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// Signatures -> Use Main JAR
|
|
166
|
-
let mainJarPath = artifact.abspath;
|
|
167
|
-
if (!mainJarPath.endsWith('.jar')) {
|
|
168
|
-
mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
169
|
-
}
|
|
170
116
|
try {
|
|
171
|
-
|
|
117
|
+
if (!detail) {
|
|
118
|
+
const debugInfo = `Artifact path: ${artifact.abspath}, hasSource: ${artifact.hasSource}`;
|
|
119
|
+
const errorMsg = lastError ? `\nLast error: ${lastError}` : "";
|
|
120
|
+
return `Class ${clsName} not found in artifact ${artifact.artifactId}. \nDebug info: ${debugInfo}${errorMsg}`;
|
|
121
|
+
}
|
|
122
|
+
let resultText = `### Class: ${detail.className}\n`;
|
|
123
|
+
resultText += `Artifact: ${artifact.groupId}:${artifact.artifactId}:${artifact.version}\n\n`; // Inform user which artifact was used
|
|
124
|
+
if (usedDecompilation) {
|
|
125
|
+
resultText += "*Source code decompiled from binary class file.*\n\n";
|
|
126
|
+
}
|
|
127
|
+
if (type === 'source') {
|
|
128
|
+
const lang = detail.language || 'java';
|
|
129
|
+
resultText += "```" + lang + "\n" + detail.source + "\n```";
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (detail.doc) {
|
|
133
|
+
resultText += "Documentation:\n" + detail.doc + "\n\n";
|
|
134
|
+
}
|
|
135
|
+
if (detail.signatures) {
|
|
136
|
+
resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return resultText;
|
|
172
140
|
}
|
|
173
141
|
catch (e) {
|
|
174
|
-
|
|
142
|
+
return `Error reading source: ${e.message}`;
|
|
175
143
|
}
|
|
144
|
+
};
|
|
145
|
+
const allNames = [];
|
|
146
|
+
if (className)
|
|
147
|
+
allNames.push(className);
|
|
148
|
+
if (classNames)
|
|
149
|
+
allNames.push(...classNames);
|
|
150
|
+
if (allNames.length === 0) {
|
|
151
|
+
return { content: [{ type: "text", text: "No class name provided." }] };
|
|
176
152
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
resultText += "Documentation:\n" + detail.doc + "\n\n";
|
|
195
|
-
}
|
|
196
|
-
if (detail.signatures) {
|
|
197
|
-
resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return { content: [{ type: "text", text: resultText }] };
|
|
153
|
+
const results = await Promise.all(allNames.map(name => resolveOne(name, coordinate)));
|
|
154
|
+
return { content: [{ type: "text", text: results.join("\n\n") }] };
|
|
155
|
+
});
|
|
156
|
+
server.registerTool("search_artifacts", {
|
|
157
|
+
description: "Search for internal company artifacts and libraries in the local Maven repository and Gradle caches by coordinate (groupId, artifactId) or keyword. Use this primarily for internal company packages or to find available versions of internal projects that are locally built. Also supports searching third-party libraries in the local cache. Supports batch queries.",
|
|
158
|
+
inputSchema: z.object({
|
|
159
|
+
query: z.string().optional().describe("Search query (groupId, artifactId, or keyword)"),
|
|
160
|
+
queries: z.array(z.string()).optional().describe("Batch search queries"),
|
|
161
|
+
}),
|
|
162
|
+
}, async ({ query, queries }) => {
|
|
163
|
+
const allQueries = [];
|
|
164
|
+
if (query)
|
|
165
|
+
allQueries.push(query);
|
|
166
|
+
if (queries)
|
|
167
|
+
allQueries.push(...queries);
|
|
168
|
+
if (allQueries.length === 0) {
|
|
169
|
+
return { content: [{ type: "text", text: "No query provided." }] };
|
|
201
170
|
}
|
|
202
|
-
|
|
203
|
-
|
|
171
|
+
const results = allQueries.map(q => {
|
|
172
|
+
const matches = indexer.search(q);
|
|
173
|
+
// Limit results to avoid overflow
|
|
174
|
+
const limitedMatches = matches.slice(0, 20);
|
|
175
|
+
const text = limitedMatches.length > 0
|
|
176
|
+
? limitedMatches.map(a => `${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
|
|
177
|
+
: "No artifacts found matching the query.";
|
|
178
|
+
return `### Results for "${q}" (Found ${matches.length}${matches.length > 20 ? ', showing first 20' : ''}):\n${text}`;
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: results.join("\n\n"),
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
server.registerTool("search_classes", {
|
|
190
|
+
description: "Search for Java classes in internal company libraries found in the local Maven/Gradle caches. Essential for finding classes in internal company libraries that are not part of the current workspace source code. Use this when you see an import (e.g., 'com.company.util.Helper') but cannot find the definition. Do not assume that because the code compiles or the import exists, the source is local. It often comes from a compiled internal library. This tool helps locate the defining artifact. Supports batch queries.",
|
|
191
|
+
inputSchema: z.object({
|
|
192
|
+
className: z.string().optional().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
193
|
+
classNames: z.array(z.string()).optional().describe("Batch class names"),
|
|
194
|
+
}),
|
|
195
|
+
}, async ({ className, classNames }) => {
|
|
196
|
+
const allNames = [];
|
|
197
|
+
if (className)
|
|
198
|
+
allNames.push(className);
|
|
199
|
+
if (classNames)
|
|
200
|
+
allNames.push(...classNames);
|
|
201
|
+
if (allNames.length === 0) {
|
|
202
|
+
return { content: [{ type: "text", text: "No class name provided." }] };
|
|
203
|
+
}
|
|
204
|
+
const results = allNames.map(name => {
|
|
205
|
+
const matches = indexer.searchClass(name);
|
|
206
|
+
const text = matches.length > 0
|
|
207
|
+
? matches.map(m => {
|
|
208
|
+
// Group by artifact ID to allow easy selection
|
|
209
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
|
|
210
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
211
|
+
return `Class: ${m.className}\n ${artifacts}${more}`;
|
|
212
|
+
}).join("\n\n")
|
|
213
|
+
: "No classes found matching the query. Try different keywords.";
|
|
214
|
+
return `### Results for "${name}":\n${text}`;
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: results.join("\n\n") }]
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
server.registerTool("search_implementations", {
|
|
221
|
+
description: "Search for internal implementations of an interface or base class. This is particularly useful for finding implementations of SPIs or base classes within internal company libraries in the local Maven/Gradle cache. Supports batch queries.",
|
|
222
|
+
inputSchema: z.object({
|
|
223
|
+
className: z.string().optional().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
|
|
224
|
+
classNames: z.array(z.string()).optional().describe("Batch class names"),
|
|
225
|
+
}),
|
|
226
|
+
}, async ({ className, classNames }) => {
|
|
227
|
+
const allNames = [];
|
|
228
|
+
if (className)
|
|
229
|
+
allNames.push(className);
|
|
230
|
+
if (classNames)
|
|
231
|
+
allNames.push(...classNames);
|
|
232
|
+
if (allNames.length === 0) {
|
|
233
|
+
return { content: [{ type: "text", text: "No class name provided." }] };
|
|
204
234
|
}
|
|
235
|
+
const results = allNames.map(name => {
|
|
236
|
+
const matches = indexer.searchImplementations(name);
|
|
237
|
+
const text = matches.length > 0
|
|
238
|
+
? matches.map(m => {
|
|
239
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}`).join("\n ");
|
|
240
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
241
|
+
return `Implementation: ${m.className}\n ${artifacts}${more}`;
|
|
242
|
+
}).join("\n\n")
|
|
243
|
+
: `No implementations found for ${name}. Ensure the index is up to date and the class name is correct.`;
|
|
244
|
+
return `### Results for "${name}":\n${text}`;
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: results.join("\n\n") }]
|
|
248
|
+
};
|
|
205
249
|
});
|
|
206
250
|
server.registerTool("refresh_index", {
|
|
207
251
|
description: "Trigger a re-scan of the Maven repository. This will re-index all artifacts.",
|