maven-indexer-mcp 1.0.4 → 1.0.6

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 CHANGED
@@ -1,6 +1,12 @@
1
1
  # Maven Indexer MCP Server
2
2
 
3
- A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and provides AI agents with tools to search for Java classes, method signatures, and source code.
3
+ A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and Gradle cache (`~/.gradle/caches/modules-2/files-2.1`) to provide AI agents with tools to search for Java classes, method signatures, and source code.
4
+
5
+ **Key Use Case**: While AI models are well-versed in popular public libraries (like Spring, Apache Commons, Guava), they often struggle with:
6
+ 1. **Internal Company Packages**: Private libraries that are not public.
7
+ 2. **Non-Well-Known Public Packages**: Niche or less popular open-source libraries.
8
+
9
+ This server bridges that gap by allowing the AI to "read" your local dependencies, effectively giving it knowledge of your private and obscure libraries.
4
10
 
5
11
  ## Features
6
12
 
@@ -8,7 +14,7 @@ A Model Context Protocol (MCP) server that indexes your local Maven repository (
8
14
  * **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
9
15
  * **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
10
16
  * **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
11
- * **Real-time Monitoring**: Watches the Maven repository for changes (e.g., new `mvn install`) and automatically updates the index.
17
+ * **Real-time Monitoring**: Watches the repositories for changes (e.g., new `mvn install`) and automatically updates the index.
12
18
  * **Efficient Persistence**: Uses SQLite to store the index, handling large repositories with minimal memory footprint.
13
19
 
14
20
  ## Getting Started
@@ -26,13 +32,14 @@ Add the following config to your MCP client:
26
32
  }
27
33
  ```
28
34
 
29
- This will automatically download and run the latest version of the server. It will auto-detect your Maven repository location (usually `~/.m2/repository`).
35
+ This will automatically download and run the latest version of the server. It will auto-detect your Maven repository location (usually `~/.m2/repository`) and Gradle cache.
30
36
 
31
37
  ### Configuration (Optional)
32
38
 
33
39
  If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to the configuration:
34
40
 
35
41
  * **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if your repository is in a non-standard location.
42
+ * **`GRADLE_REPO_PATH`**: Absolute path to your Gradle cache (e.g., `/Users/yourname/.gradle/caches/modules-2/files-2.1`).
36
43
  * **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
37
44
  * **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server will attempt to use its bundled CFR version.
38
45
 
@@ -46,6 +53,7 @@ Example with optional configuration:
46
53
  "args": ["-y", "maven-indexer-mcp@latest"],
47
54
  "env": {
48
55
  "MAVEN_REPO": "/Users/yourname/.m2/repository",
56
+ "GRADLE_REPO_PATH": "/Users/yourname/.gradle/caches/modules-2/files-2.1",
49
57
  "INCLUDED_PACKAGES": "com.mycompany.*",
50
58
  "MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
51
59
  }
@@ -84,14 +92,23 @@ If you prefer to run from source:
84
92
 
85
93
  ## Available Tools
86
94
 
87
- * **`search_classes`**: Search for Java classes.
95
+ * **`search_classes`**: Search for Java classes in the local Maven repository and Gradle caches.
96
+ * **WHEN TO USE**:
97
+ 1. **Internal/Private Code**: You need to find a class from a company-internal library.
98
+ 2. **Obscure Libraries**: You are using a less common public library that the AI doesn't know well.
99
+ 3. **Version Verification**: You need to check exactly which version of a class is present locally.
100
+ * *Note*: For well-known libraries (e.g., standard Java lib, Spring), the AI likely knows the class structure already, so this tool is less critical.
101
+ * **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this class imported from?".
88
102
  * Input: `className` (e.g., "StringUtils", "Json parser")
89
103
  * Output: List of matching classes with their artifacts.
90
- * **`get_class_details`**: Get detailed information about a class.
91
- * Input: `className`, `artifactId`, `type` ("signatures", "docs", "source")
104
+ * **`get_class_details`**: Decompile and read the source code of external libraries/dependencies. **Use this instead of 'SearchCodebase' for classes that are imported but defined in JAR files.**
105
+ * **Key Value**: "Don't guess what the internal library does—read the code."
106
+ * **Tip**: Essential for internal/proprietary code where documentation is scarce or non-existent.
107
+ * Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
92
108
  * Output: Method signatures, Javadocs, or full source code.
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.
109
+ * **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those with source code attached).
110
+ * **`search_artifacts`**: Search for artifacts in Maven/Gradle caches by coordinate (groupId, artifactId).
111
+ * **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class. Useful for finding SPI implementations in external libraries.
95
112
  * Input: `className` (e.g. "java.util.List")
96
113
  * Output: List of implementation/subclass names and their artifacts.
97
114
  * **`refresh_index`**: Trigger a re-scan of the Maven repository.
package/build/config.js CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
8
8
  export class Config {
9
9
  static instance;
10
10
  localRepository = "";
11
+ gradleRepository = "";
11
12
  javaBinary = "java";
12
13
  includedPackages = ["*"];
13
14
  cfrPath = null;
@@ -52,6 +53,18 @@ export class Config {
52
53
  repoPath = path.join(os.homedir(), '.m2', 'repository');
53
54
  }
54
55
  this.localRepository = repoPath;
56
+ // Load Gradle Repository
57
+ let gradleRepoPath = null;
58
+ if (process.env.GRADLE_REPO_PATH) {
59
+ gradleRepoPath = process.env.GRADLE_REPO_PATH;
60
+ }
61
+ else if (process.env.GRADLE_USER_HOME) {
62
+ gradleRepoPath = path.join(process.env.GRADLE_USER_HOME, 'caches', 'modules-2', 'files-2.1');
63
+ }
64
+ else {
65
+ gradleRepoPath = path.join(os.homedir(), '.gradle', 'caches', 'modules-2', 'files-2.1');
66
+ }
67
+ this.gradleRepository = gradleRepoPath;
55
68
  // Load Java Path
56
69
  if (process.env.JAVA_HOME) {
57
70
  this.javaBinary = path.join(process.env.JAVA_HOME, 'bin', 'java');
package/build/index.js CHANGED
@@ -11,6 +11,7 @@ const server = new McpServer({
11
11
  }, {
12
12
  capabilities: {
13
13
  tools: {},
14
+ prompts: {},
14
15
  },
15
16
  });
16
17
  // Start indexing in the background
@@ -20,144 +21,237 @@ indexer.index().then(() => {
20
21
  // Start watching for changes after initial index
21
22
  return indexer.startWatch();
22
23
  }).catch(err => console.error("Initial indexing failed:", err));
24
+ server.registerTool("get_class_details", {
25
+ 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.",
26
+ inputSchema: z.object({
27
+ className: z.string().optional().describe("Fully qualified class name"),
28
+ classNames: z.array(z.string()).optional().describe("Batch class names"),
29
+ 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."),
30
+ type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
31
+ }),
32
+ }, async ({ className, classNames, coordinate, type }) => {
33
+ const resolveOne = async (clsName, coord) => {
34
+ let targetArtifact;
35
+ if (coord) {
36
+ const parts = coord.split(':');
37
+ if (parts.length === 3) {
38
+ targetArtifact = indexer.getArtifactByCoordinate(parts[0], parts[1], parts[2]);
39
+ }
40
+ else {
41
+ return "Invalid coordinate format. Expected groupId:artifactId:version";
42
+ }
43
+ if (!targetArtifact) {
44
+ return `Artifact ${coord} not found in index.`;
45
+ }
46
+ }
47
+ else {
48
+ // Auto-resolve artifact if coordinate is missing
49
+ const matches = indexer.searchClass(clsName);
50
+ // Find exact match for class name
51
+ const exactMatch = matches.find(m => m.className === clsName);
52
+ if (!exactMatch) {
53
+ // If no exact match, but we have some results, list them
54
+ if (matches.length > 0) {
55
+ const suggestions = matches.map(m => `- ${m.className}`).join("\n");
56
+ return `Class '${clsName}' not found exactly. Did you mean:\n${suggestions}`;
57
+ }
58
+ return `Class '${clsName}' not found in the index. Try 'search_classes' with a keyword if you are unsure of the full name.`;
59
+ }
60
+ // We have an exact match, choose the best artifact
61
+ // Strategy: 1. Prefer hasSource=true. 2. Prefer highest ID (likely newest).
62
+ const artifacts = exactMatch.artifacts.sort((a, b) => {
63
+ if (a.hasSource !== b.hasSource) {
64
+ return a.hasSource ? -1 : 1; // source comes first
65
+ }
66
+ return b.id - a.id; // higher ID comes first
67
+ });
68
+ if (artifacts.length === 0) {
69
+ return `Class '${clsName}' found but no artifacts are associated with it (database inconsistency).`;
70
+ }
71
+ targetArtifact = artifacts[0];
72
+ }
73
+ const artifact = targetArtifact;
74
+ let detail = null;
75
+ let usedDecompilation = false;
76
+ let lastError = "";
77
+ // 1. If requesting source/docs, try Source JAR first
78
+ if (type === 'source' || type === 'docs') {
79
+ if (artifact.hasSource) {
80
+ const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
81
+ try {
82
+ detail = await SourceParser.getClassDetail(sourceJarPath, clsName, type);
83
+ }
84
+ catch (e) {
85
+ // Ignore error and fallthrough to main jar (decompilation)
86
+ lastError = e.message;
87
+ }
88
+ }
89
+ // If not found in source jar (or no source jar), try main jar (decompilation)
90
+ if (!detail) {
91
+ let mainJarPath = artifact.abspath;
92
+ if (!mainJarPath.endsWith('.jar')) {
93
+ mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
94
+ }
95
+ try {
96
+ // SourceParser will try to decompile if source file not found in jar
97
+ detail = await SourceParser.getClassDetail(mainJarPath, clsName, type);
98
+ if (detail && detail.source) {
99
+ usedDecompilation = true;
100
+ }
101
+ }
102
+ catch (e) {
103
+ console.error(`Decompilation/MainJar access failed: ${e.message}`);
104
+ lastError = e.message;
105
+ }
106
+ }
107
+ }
108
+ else {
109
+ // Signatures -> Use Main JAR
110
+ let mainJarPath = artifact.abspath;
111
+ if (!mainJarPath.endsWith('.jar')) {
112
+ mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
113
+ }
114
+ try {
115
+ detail = await SourceParser.getClassDetail(mainJarPath, clsName, type);
116
+ }
117
+ catch (e) {
118
+ lastError = e.message;
119
+ }
120
+ }
121
+ try {
122
+ if (!detail) {
123
+ const debugInfo = `Artifact path: ${artifact.abspath}, hasSource: ${artifact.hasSource}`;
124
+ const errorMsg = lastError ? `\nLast error: ${lastError}` : "";
125
+ return `Class ${clsName} not found in artifact ${artifact.artifactId}. \nDebug info: ${debugInfo}${errorMsg}`;
126
+ }
127
+ let resultText = `### Class: ${detail.className}\n`;
128
+ resultText += `Artifact: ${artifact.groupId}:${artifact.artifactId}:${artifact.version}\n\n`; // Inform user which artifact was used
129
+ if (usedDecompilation) {
130
+ resultText += "*Source code decompiled from binary class file.*\n\n";
131
+ }
132
+ if (type === 'source') {
133
+ const lang = detail.language || 'java';
134
+ resultText += "```" + lang + "\n" + detail.source + "\n```";
135
+ }
136
+ else {
137
+ if (detail.doc) {
138
+ resultText += "Documentation:\n" + detail.doc + "\n\n";
139
+ }
140
+ if (detail.signatures) {
141
+ resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
142
+ }
143
+ }
144
+ return resultText;
145
+ }
146
+ catch (e) {
147
+ return `Error reading source: ${e.message}`;
148
+ }
149
+ };
150
+ const allNames = [];
151
+ if (className)
152
+ allNames.push(className);
153
+ if (classNames)
154
+ allNames.push(...classNames);
155
+ if (allNames.length === 0) {
156
+ return { content: [{ type: "text", text: "No class name provided." }] };
157
+ }
158
+ const results = await Promise.all(allNames.map(name => resolveOne(name, coordinate)));
159
+ return { content: [{ type: "text", text: results.join("\n\n") }] };
160
+ });
23
161
  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.",
162
+ 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.",
25
163
  inputSchema: z.object({
26
- query: z.string().describe("Search query (groupId, artifactId, or keyword)"),
164
+ query: z.string().optional().describe("Search query (groupId, artifactId, or keyword)"),
165
+ queries: z.array(z.string()).optional().describe("Batch search queries"),
27
166
  }),
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 => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
34
- : "No artifacts found matching the query.";
167
+ }, async ({ query, queries }) => {
168
+ const allQueries = [];
169
+ if (query)
170
+ allQueries.push(query);
171
+ if (queries)
172
+ allQueries.push(...queries);
173
+ if (allQueries.length === 0) {
174
+ return { content: [{ type: "text", text: "No query provided." }] };
175
+ }
176
+ const results = allQueries.map(q => {
177
+ const matches = indexer.search(q);
178
+ // Limit results to avoid overflow
179
+ const limitedMatches = matches.slice(0, 20);
180
+ const text = limitedMatches.length > 0
181
+ ? limitedMatches.map(a => `${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
182
+ : "No artifacts found matching the query.";
183
+ return `### Results for "${q}" (Found ${matches.length}${matches.length > 20 ? ', showing first 20' : ''}):\n${text}`;
184
+ });
35
185
  return {
36
186
  content: [
37
187
  {
38
188
  type: "text",
39
- text: `Found ${matches.length} matches${matches.length > 20 ? ' (showing first 20)' : ''}:\n${text}`,
189
+ text: results.join("\n\n"),
40
190
  },
41
191
  ],
42
192
  };
43
193
  });
44
194
  server.registerTool("search_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').",
195
+ 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.",
46
196
  inputSchema: z.object({
47
- className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
197
+ className: z.string().optional().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
198
+ classNames: z.array(z.string()).optional().describe("Batch class names"),
48
199
  }),
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 => `[ID: ${a.id}] ${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.";
200
+ }, async ({ className, classNames }) => {
201
+ const allNames = [];
202
+ if (className)
203
+ allNames.push(className);
204
+ if (classNames)
205
+ allNames.push(...classNames);
206
+ if (allNames.length === 0) {
207
+ return { content: [{ type: "text", text: "No class name provided." }] };
208
+ }
209
+ const results = allNames.map(name => {
210
+ const matches = indexer.searchClass(name);
211
+ const text = matches.length > 0
212
+ ? matches.map(m => {
213
+ // Group by artifact ID to allow easy selection
214
+ const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
215
+ const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
216
+ return `Class: ${m.className}\n ${artifacts}${more}`;
217
+ }).join("\n\n")
218
+ : "No classes found matching the query. Try different keywords.";
219
+ return `### Results for "${name}":\n${text}`;
220
+ });
59
221
  return {
60
- content: [{ type: "text", text }]
222
+ content: [{ type: "text", text: results.join("\n\n") }]
61
223
  };
62
224
  });
63
225
  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.",
226
+ 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.",
65
227
  inputSchema: z.object({
66
- className: z.string().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
228
+ className: z.string().optional().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
229
+ classNames: z.array(z.string()).optional().describe("Batch class names"),
67
230
  }),
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.`;
231
+ }, async ({ className, classNames }) => {
232
+ const allNames = [];
233
+ if (className)
234
+ allNames.push(className);
235
+ if (classNames)
236
+ allNames.push(...classNames);
237
+ if (allNames.length === 0) {
238
+ return { content: [{ type: "text", text: "No class name provided." }] };
239
+ }
240
+ const results = allNames.map(name => {
241
+ const matches = indexer.searchImplementations(name);
242
+ const text = matches.length > 0
243
+ ? matches.map(m => {
244
+ const artifacts = m.artifacts.slice(0, 5).map(a => `${a.groupId}:${a.artifactId}:${a.version}`).join("\n ");
245
+ const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
246
+ return `Implementation: ${m.className}\n ${artifacts}${more}`;
247
+ }).join("\n\n")
248
+ : `No implementations found for ${name}. Ensure the index is up to date and the class name is correct.`;
249
+ return `### Results for "${name}":\n${text}`;
250
+ });
77
251
  return {
78
- content: [{ type: "text", text }]
252
+ content: [{ type: "text", text: results.join("\n\n") }]
79
253
  };
80
254
  });
81
- server.registerTool("get_class_details", {
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.",
83
- inputSchema: z.object({
84
- className: z.string().describe("Fully qualified class name"),
85
- artifactId: z.number().describe("The internal ID of the artifact (returned by search_classes)"),
86
- type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
87
- }),
88
- }, async ({ className, artifactId, type }) => {
89
- const artifact = indexer.getArtifactById(artifactId);
90
- if (!artifact) {
91
- return { content: [{ type: "text", text: "Artifact not found." }] };
92
- }
93
- let detail = null;
94
- let usedDecompilation = false;
95
- let lastError = "";
96
- // 1. If requesting source/docs, try Source JAR first
97
- if (type === 'source' || type === 'docs') {
98
- if (artifact.hasSource) {
99
- const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
100
- try {
101
- detail = await SourceParser.getClassDetail(sourceJarPath, className, type);
102
- }
103
- catch (e) {
104
- // Ignore error and fallthrough to main jar (decompilation)
105
- lastError = e.message;
106
- }
107
- }
108
- // If not found in source jar (or no source jar), try main jar (decompilation)
109
- if (!detail) {
110
- const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
111
- try {
112
- // SourceParser will try to decompile if source file not found in jar
113
- detail = await SourceParser.getClassDetail(mainJarPath, className, type);
114
- if (detail && detail.source) {
115
- usedDecompilation = true;
116
- }
117
- }
118
- catch (e) {
119
- console.error(`Decompilation/MainJar access failed: ${e.message}`);
120
- lastError = e.message;
121
- }
122
- }
123
- }
124
- else {
125
- // Signatures -> Use Main JAR
126
- const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
127
- try {
128
- detail = await SourceParser.getClassDetail(mainJarPath, className, type);
129
- }
130
- catch (e) {
131
- lastError = e.message;
132
- }
133
- }
134
- try {
135
- if (!detail) {
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}` }] };
139
- }
140
- let resultText = `Class: ${detail.className}\n\n`;
141
- if (usedDecompilation) {
142
- resultText += "*Source code decompiled from binary class file.*\n\n";
143
- }
144
- if (type === 'source') {
145
- resultText += "```java\n" + detail.source + "\n```";
146
- }
147
- else {
148
- if (detail.doc) {
149
- resultText += "Documentation:\n" + detail.doc + "\n\n";
150
- }
151
- if (detail.signatures) {
152
- resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
153
- }
154
- }
155
- return { content: [{ type: "text", text: resultText }] };
156
- }
157
- catch (e) {
158
- return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
159
- }
160
- });
161
255
  server.registerTool("refresh_index", {
162
256
  description: "Trigger a re-scan of the Maven repository. This will re-index all artifacts.",
163
257
  }, async () => {
package/build/indexer.js CHANGED
@@ -28,16 +28,22 @@ export class Indexer {
28
28
  */
29
29
  async startWatch() {
30
30
  const config = await Config.getInstance();
31
- const repoPath = config.localRepository;
32
- if (!repoPath || !fsSync.existsSync(repoPath)) {
33
- console.error("Repository path not found, skipping watch mode.");
31
+ const pathsToWatch = [];
32
+ if (config.localRepository && fsSync.existsSync(config.localRepository)) {
33
+ pathsToWatch.push(config.localRepository);
34
+ }
35
+ if (config.gradleRepository && fsSync.existsSync(config.gradleRepository)) {
36
+ pathsToWatch.push(config.gradleRepository);
37
+ }
38
+ if (pathsToWatch.length === 0) {
39
+ console.error("No repository paths found, skipping watch mode.");
34
40
  return;
35
41
  }
36
42
  if (this.watcher) {
37
43
  return;
38
44
  }
39
- console.error(`Starting file watcher on ${repoPath}...`);
40
- this.watcher = chokidar.watch(repoPath, {
45
+ console.error(`Starting file watcher on ${pathsToWatch.join(', ')}...`);
46
+ this.watcher = chokidar.watch(pathsToWatch, {
41
47
  ignored: /(^|[\/\\])\../, // ignore dotfiles
42
48
  persistent: true,
43
49
  ignoreInitial: true,
@@ -88,15 +94,28 @@ export class Indexer {
88
94
  try {
89
95
  const config = await Config.getInstance();
90
96
  const repoPath = config.localRepository;
97
+ const gradleRepoPath = config.gradleRepository;
91
98
  const db = DB.getInstance();
92
- if (!repoPath) {
93
- console.error("No local repository path found.");
99
+ if (!repoPath && !gradleRepoPath) {
100
+ console.error("No repository path found.");
94
101
  return;
95
102
  }
96
103
  // 1. Scan for artifacts
97
104
  console.error("Scanning repository structure...");
98
- const artifacts = await this.scanRepository(repoPath);
99
- console.error(`Found ${artifacts.length} artifacts on disk.`);
105
+ let artifacts = [];
106
+ if (repoPath && fsSync.existsSync(repoPath)) {
107
+ console.error(`Scanning Maven repo: ${repoPath}`);
108
+ const mavenArtifacts = await this.scanRepository(repoPath);
109
+ console.error(`Found ${mavenArtifacts.length} Maven artifacts.`);
110
+ artifacts = artifacts.concat(mavenArtifacts);
111
+ }
112
+ if (gradleRepoPath && fsSync.existsSync(gradleRepoPath)) {
113
+ console.error(`Scanning Gradle repo: ${gradleRepoPath}`);
114
+ const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath);
115
+ console.error(`Found ${gradleArtifacts.length} Gradle artifacts.`);
116
+ artifacts = artifacts.concat(gradleArtifacts);
117
+ }
118
+ console.error(`Found ${artifacts.length} total artifacts on disk.`);
100
119
  // 2. Persist artifacts and determine what needs indexing
101
120
  // We use is_indexed = 0 for new artifacts.
102
121
  const insertArtifact = db.prepare(`
@@ -197,12 +216,86 @@ export class Indexer {
197
216
  await scanDir(rootDir);
198
217
  return results;
199
218
  }
219
+ /**
220
+ * Scans a Gradle cache directory for artifacts.
221
+ * Structure: group/artifact/version/hash/file
222
+ */
223
+ async scanGradleRepository(rootDir) {
224
+ const results = [];
225
+ // Helper to read directory safely
226
+ const readDirSafe = async (p) => {
227
+ try {
228
+ return await fs.readdir(p, { withFileTypes: true });
229
+ }
230
+ catch (e) {
231
+ return [];
232
+ }
233
+ };
234
+ const groupDirs = await readDirSafe(rootDir);
235
+ for (const groupEntry of groupDirs) {
236
+ if (!groupEntry.isDirectory())
237
+ continue;
238
+ const groupId = groupEntry.name;
239
+ const groupPath = path.join(rootDir, groupId);
240
+ const artifactDirs = await readDirSafe(groupPath);
241
+ for (const artifactEntry of artifactDirs) {
242
+ if (!artifactEntry.isDirectory())
243
+ continue;
244
+ const artifactId = artifactEntry.name;
245
+ const artifactPath = path.join(groupPath, artifactId);
246
+ const versionDirs = await readDirSafe(artifactPath);
247
+ for (const versionEntry of versionDirs) {
248
+ if (!versionEntry.isDirectory())
249
+ continue;
250
+ const version = versionEntry.name;
251
+ const versionPath = path.join(artifactPath, version);
252
+ const hashDirs = await readDirSafe(versionPath);
253
+ let jarPath = null;
254
+ let hasSource = false;
255
+ // We need to iterate all hash dirs to find the jar and source jar
256
+ for (const hashEntry of hashDirs) {
257
+ if (!hashEntry.isDirectory())
258
+ continue;
259
+ const hashPath = path.join(versionPath, hashEntry.name);
260
+ const files = await readDirSafe(hashPath);
261
+ for (const file of files) {
262
+ if (file.isFile()) {
263
+ if (file.name.endsWith('.jar')) {
264
+ if (file.name.endsWith('-sources.jar')) {
265
+ hasSource = true;
266
+ }
267
+ else if (!file.name.endsWith('-javadoc.jar')) {
268
+ // This should be the main jar
269
+ jarPath = path.join(hashPath, file.name);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ if (jarPath) {
276
+ results.push({
277
+ id: 0, // Placeholder
278
+ groupId,
279
+ artifactId,
280
+ version,
281
+ abspath: jarPath, // Full path to JAR
282
+ hasSource
283
+ });
284
+ }
285
+ }
286
+ }
287
+ }
288
+ return results;
289
+ }
200
290
  /**
201
291
  * Extracts classes from the artifact's JAR and indexes them.
202
292
  * Updates the 'is_indexed' flag upon completion.
203
293
  */
204
294
  async indexArtifactClasses(artifact) {
205
- const jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
295
+ let jarPath = artifact.abspath;
296
+ if (!jarPath.endsWith('.jar')) {
297
+ jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
298
+ }
206
299
  const db = DB.getInstance();
207
300
  const config = await Config.getInstance();
208
301
  try {
@@ -485,4 +578,26 @@ export class Indexer {
485
578
  }
486
579
  return undefined;
487
580
  }
581
+ /**
582
+ * Retrieves an artifact by its Maven coordinate.
583
+ */
584
+ getArtifactByCoordinate(groupId, artifactId, version) {
585
+ const db = DB.getInstance();
586
+ const row = db.prepare(`
587
+ SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
588
+ FROM artifacts
589
+ WHERE group_id = ? AND artifact_id = ? AND version = ?
590
+ `).get(groupId, artifactId, version);
591
+ if (row) {
592
+ return {
593
+ id: row.id,
594
+ groupId: row.groupId,
595
+ artifactId: row.artifactId,
596
+ version: row.version,
597
+ abspath: row.abspath,
598
+ hasSource: Boolean(row.hasSource)
599
+ };
600
+ }
601
+ return undefined;
602
+ }
488
603
  }
@@ -28,6 +28,7 @@ export class SourceParser {
28
28
  zipfile.on('entry', (entry) => {
29
29
  if (candidates.includes(entry.fileName)) {
30
30
  found = true;
31
+ const language = entry.fileName.endsWith('.kt') ? 'kotlin' : 'java';
31
32
  zipfile.openReadStream(entry, (err, readStream) => {
32
33
  if (err || !readStream) {
33
34
  resolve(null);
@@ -37,7 +38,7 @@ export class SourceParser {
37
38
  readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
38
39
  readStream.on('end', () => {
39
40
  const source = Buffer.concat(chunks).toString('utf-8');
40
- resolve(SourceParser.parse(className, source, type));
41
+ resolve(SourceParser.parse(className, source, type, language));
41
42
  });
42
43
  });
43
44
  }
@@ -81,11 +82,12 @@ export class SourceParser {
81
82
  throw new Error(`CFR stderr: ${stderr}`);
82
83
  }
83
84
  if (stdout) {
84
- return this.parse(className, stdout, type);
85
+ return this.parse(className, stdout, type, 'java');
85
86
  }
86
87
  return {
87
88
  className,
88
- source: stdout // Return as source
89
+ source: stdout, // Return as source
90
+ language: 'java'
89
91
  };
90
92
  }
91
93
  catch (e) {
@@ -108,7 +110,8 @@ export class SourceParser {
108
110
  l !== '}');
109
111
  return {
110
112
  className,
111
- signatures: lines
113
+ signatures: lines,
114
+ language: 'java'
112
115
  };
113
116
  }
114
117
  catch (e) {
@@ -118,9 +121,9 @@ export class SourceParser {
118
121
  return null;
119
122
  }
120
123
  }
121
- static parse(className, source, type) {
124
+ static parse(className, source, type, language = 'java') {
122
125
  if (type === 'source') {
123
- return { className, source };
126
+ return { className, source, language };
124
127
  }
125
128
  // Very simple regex-based parsing to extract methods and javadocs
126
129
  // This is heuristic and won't be perfect, but it's fast and dependency-free
@@ -169,7 +172,8 @@ export class SourceParser {
169
172
  return {
170
173
  className,
171
174
  signatures,
172
- doc: type === 'docs' ? allDocs.join('\n\n') : undefined
175
+ doc: type === 'docs' ? allDocs.join('\n\n') : undefined,
176
+ language
173
177
  };
174
178
  }
175
179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-indexer-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "MCP server for indexing local Maven repository",
5
5
  "main": "build/index.js",
6
6
  "type": "module",