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 CHANGED
@@ -1,15 +1,28 @@
1
1
  # Maven Indexer MCP Server
2
2
 
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.
3
+ [![npm version](https://img.shields.io/npm/v/maven-indexer-mcp.svg?style=flat)](https://www.npmjs.com/package/maven-indexer-mcp)
4
+ [![Tests](https://github.com/tangcent/maven-indexer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/tangcent/maven-indexer-mcp/actions/workflows/ci.yml)
5
+
6
+ A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and Gradle cache (
7
+ `~/.gradle/caches/modules-2/files-2.1`) to provide AI agents with tools to search for Java classes, method signatures,
8
+ and source code.
9
+
10
+ **Key Use Case**: While AI models are well-versed in popular public libraries (like Spring, Apache Commons, Guava), they
11
+ often struggle with:
12
+
13
+ 1. **Internal Company Packages**: Private libraries that are not public.
14
+ 2. **Non-Well-Known Public Packages**: Niche or less popular open-source libraries.
15
+
16
+ This server bridges that gap by allowing the AI to "read" your local dependencies, effectively giving it knowledge of
17
+ your private and obscure libraries.
4
18
 
5
19
  ## Features
6
20
 
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.
9
- * **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
10
- * **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
11
- * **Real-time Monitoring**: Watches the repositories for changes (e.g., new `mvn install`) and automatically updates the index.
12
- * **Efficient Persistence**: Uses SQLite to store the index, handling large repositories with minimal memory footprint.
21
+ * **Semantic Class Search**: Search for classes by name or purpose.
22
+ * **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
23
+ * **On-Demand Analysis**: Extracts method signatures and Javadocs directly from JARs.
24
+ * **Source Code Retrieval**: Provides full source code if available.
25
+ * **Real-time Monitoring**: Automatically updates the index when repositories change.
13
26
 
14
27
  ## Getting Started
15
28
 
@@ -20,22 +33,116 @@ Add the following config to your MCP client:
20
33
  "mcpServers": {
21
34
  "maven-indexer": {
22
35
  "command": "npx",
23
- "args": ["-y", "maven-indexer-mcp@latest"]
36
+ "args": [
37
+ "-y",
38
+ "maven-indexer-mcp@latest"
39
+ ]
24
40
  }
25
41
  }
26
42
  }
27
43
  ```
28
44
 
29
- 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.
45
+ This will automatically download and run the latest version of the server. It will auto-detect your Maven repository
46
+ location (usually `~/.m2/repository`) and Gradle cache.
30
47
 
31
- ### Configuration (Optional)
48
+ ### MCP Client configuration
49
+
50
+ <details>
51
+ <summary>Cline</summary>
52
+
53
+ Follow <a href="https://docs.cline.bot/mcp/configuring-mcp-servers">Cline's MCP guide</a> and use the config provided
54
+ above.
55
+ </details>
56
+
57
+ <details>
58
+ <summary>Codex</summary>
59
+
60
+ Follow the <a href="https://github.com/openai/codex/blob/main/docs/advanced.md#model-context-protocol-mcp">configure MCP
61
+ guide</a> using the standard config from above.
62
+ </details>
63
+
64
+ <details>
65
+ <summary>Cursor</summary>
66
+
67
+ **Click the button to install:**
68
+
69
+ [![Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=maven-indexer&config=eyJjb21tYW5kIjoibnB4IC15IG1hdmVuLWluZGV4ZXItbWNwQGxhdGVzdCJ9)
70
+
71
+ **Or install manually:**
72
+
73
+ Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above.
74
+
75
+ </details>
76
+
77
+ <details>
78
+ <summary>JetBrains AI Assistant & Junie</summary>
79
+
80
+ Go to `Settings | Tools | AI Assistant | Model Context Protocol (MCP)` -> `Add`. Use the config provided above.
81
+ The same way `maven-indexer` can be configured for JetBrains Junie in `Settings | Tools | Junie | MCP Settings` ->`Add`.
82
+ Use the config provided above.
83
+ </details>
84
+
85
+ <details>
86
+ <summary>Kiro</summary>
87
+
88
+ In **Kiro Settings**, go to `Configure MCP` > `Open Workspace or User MCP Config` > Use the configuration snippet
89
+ provided above.
32
90
 
33
- If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to the configuration:
91
+ Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Config`. Use the configuration snippet
92
+ provided above.
34
93
 
35
- * **`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.
36
- * **`GRADLE_REPO_PATH`**: Absolute path to your Gradle cache (e.g., `/Users/yourname/.gradle/caches/modules-2/files-2.1`).
37
- * **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
38
- * **`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.
94
+ </details>
95
+
96
+ <details>
97
+ <summary>Qoder</summary>
98
+
99
+ In **Qoder Settings**, go to `MCP Server` > `+ Add` > Use the configuration snippet provided above.
100
+
101
+ Alternatively, follow the <a href="https://docs.qoder.com/user-guide/chat/model-context-protocol">MCP guide</a> and use
102
+ the standard config from above.
103
+
104
+ </details>
105
+
106
+ <details>
107
+ <summary>Trae</summary>
108
+
109
+ Go to `Settings` -> `MCP` -> `+ Add` -> `Add Manually` to add an MCP Server. Use the config provided above.
110
+ </details>
111
+
112
+ <details>
113
+ <summary>Windsurf</summary>
114
+
115
+ Follow the <a href="https://docs.windsurf.com/windsurf/cascade/mcp#mcp-config-json">configure MCP guide</a>
116
+ using the standard config from above.
117
+ </details>
118
+
119
+ ## Your first prompt
120
+
121
+ Enter the following prompt in your MCP Client to check if everything is working:
122
+
123
+ ```text
124
+ Find the class `StringUtils` in my local maven repository and show me its methods.
125
+ ```
126
+
127
+ Your MCP client should read the class `StringUtils` from your local Maven repository and show its methods.
128
+
129
+ ### Configuration (Optional)
130
+
131
+ If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to
132
+ the configuration:
133
+
134
+ * **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if
135
+ your repository is in a non-standard location.
136
+ * **`GRADLE_REPO_PATH`**: Absolute path to your Gradle cache (e.g.,
137
+ `/Users/yourname/.gradle/caches/modules-2/files-2.1`).
138
+ * **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`).
139
+ Default is `*` (index everything).
140
+ * **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server
141
+ will attempt to use its bundled CFR version.
142
+ * **`VERSION_RESOLUTION_STRATEGY`**: (Optional) Strategy to choose the version when multiple versions of an artifact are found and no specific coordinate is provided.
143
+ * `semver`: (Default) Prefer the highest semantic version (e.g. 1.2.0 > 1.1.9).
144
+ * `latest-published`: Prefer the version with the latest publish time (checks `*.pom.lastUpdated` first, then file modification time).
145
+ * `latest-used`: Prefer the version most recently imported/used by the user (based on file creation time).
39
146
 
40
147
  Example with optional configuration:
41
148
 
@@ -44,12 +151,16 @@ Example with optional configuration:
44
151
  "mcpServers": {
45
152
  "maven-indexer": {
46
153
  "command": "npx",
47
- "args": ["-y", "maven-indexer-mcp@latest"],
154
+ "args": [
155
+ "-y",
156
+ "maven-indexer-mcp@latest"
157
+ ],
48
158
  "env": {
49
159
  "MAVEN_REPO": "/Users/yourname/.m2/repository",
50
160
  "GRADLE_REPO_PATH": "/Users/yourname/.gradle/caches/modules-2/files-2.1",
51
161
  "INCLUDED_PACKAGES": "com.mycompany.*",
52
- "MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
162
+ "MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar",
163
+ "VERSION_RESOLUTION_STRATEGY": "semver"
53
164
  }
54
165
  }
55
166
  }
@@ -60,19 +171,22 @@ Example with optional configuration:
60
171
 
61
172
  If you prefer to run from source:
62
173
 
63
- 1. Clone the repository:
174
+ 1. Clone the repository:
175
+
64
176
  ```bash
65
177
  git clone https://github.com/tangcent/maven-indexer-mcp.git
66
178
  cd maven-indexer-mcp
67
179
  ```
68
180
 
69
- 2. Install dependencies and build:
181
+ 2. Install dependencies and build:
182
+
70
183
  ```bash
71
184
  npm install
72
185
  npm run build
73
186
  ```
74
187
 
75
- 3. Use the absolute path in your config:
188
+ 3. Use the absolute path in your config:
189
+
76
190
  ```json
77
191
  {
78
192
  "mcpServers": {
@@ -86,31 +200,38 @@ If you prefer to run from source:
86
200
 
87
201
  ## Available Tools
88
202
 
89
- * **`search_classes`**: Search for Java classes in the local Maven repository (dependencies).
90
- * **WHEN TO USE**:
91
- 1. You cannot find a class definition in the current project source (it's likely a dependency).
92
- 2. You need to read the source code, method signatures, or Javadocs of an external library class.
93
- 3. You need to verify which version of a library class is being used.
94
- * **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this class imported from?".
95
- * Input: `className` (e.g., "StringUtils", "Json parser")
96
- * Output: List of matching classes with their artifacts.
97
- * **`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.**
98
- * **Key Value**: "Don't guess what the library does—read the code."
99
- * **Tip**: When reviewing usages of an external class, use this to retrieve the class definition to understand the context fully.
100
- * Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
101
- * Output: Method signatures, Javadocs, or full source code.
102
- * **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those with source code attached).
103
- * **`search_artifacts`**: Search for artifacts by coordinate (groupId, artifactId).
104
- * **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class. Useful for finding SPI implementations in external libraries.
105
- * Input: `className` (e.g. "java.util.List")
106
- * Output: List of implementation/subclass names and their artifacts.
107
- * **`refresh_index`**: Trigger a re-scan of the Maven repository.
203
+ * **`search_classes`**: Search for Java classes in the local Maven repository and Gradle caches.
204
+ * **WHEN TO USE**:
205
+ 1. **Internal/Private Code**: You need to find a class from a company-internal library.
206
+ 2. **Obscure Libraries**: You are using a less common public library that the AI doesn't know well.
207
+ 3. **Version Verification**: You need to check exactly which version of a class is present locally.
208
+
209
+ * *Note*: For well-known libraries (e.g., standard Java lib, Spring), the AI likely knows the class structure
210
+ already, so this tool is less critical.
211
+ * **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this
212
+ class imported from?".
213
+ * Input: `className` (e.g., "StringUtils", "Json parser")
214
+ * Output: List of matching classes with their artifacts.
215
+ * **`get_class_details`**: Decompile and read the source code of external libraries/dependencies. **Use this instead
216
+ of 'SearchCodebase' for classes that are imported but defined in JAR files.**
217
+ * **Key Value**: "Don't guess what the internal library does—read the code."
218
+ * **Tip**: Essential for internal/proprietary code where documentation is scarce or non-existent.
219
+ * Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
220
+ * Output: Method signatures, Javadocs, or full source code.
221
+ * **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those
222
+ with source code attached).
223
+ * **`search_artifacts`**: Search for artifacts in Maven/Gradle caches by coordinate (groupId, artifactId).
224
+ * **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
225
+ Useful for finding SPI implementations in external libraries.
226
+ * Input: `className` (e.g. "java.util.List")
227
+ * Output: List of implementation/subclass names and their artifacts.
228
+ * **`refresh_index`**: Trigger a re-scan of the Maven repository.
108
229
 
109
230
  ## Development
110
231
 
111
- * **Run tests**: `npm test`
112
- * **Watch mode**: `npm run watch`
232
+ * **Run tests**: `npm test`
233
+ * **Watch mode**: `npm run watch`
113
234
 
114
235
  ## License
115
236
 
116
- ISC
237
+ [ISC](LICENSE)
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import semver from 'semver';
4
+ import { Config } from './config.js';
5
+ export class ArtifactResolver {
6
+ static strategies = {
7
+ 'semver': ArtifactResolver.compareSemver,
8
+ 'latest-published': ArtifactResolver.compareLatestPublished,
9
+ 'latest-used': ArtifactResolver.compareLatestUsed
10
+ };
11
+ static async resolveBestArtifact(artifacts) {
12
+ if (!artifacts || artifacts.length === 0)
13
+ return undefined;
14
+ const config = await Config.getInstance();
15
+ const comparator = this.strategies[config.versionResolutionStrategy] || this.strategies['semver'];
16
+ const sorted = [...artifacts].sort((a, b) => {
17
+ // 1. Always prefer source if available
18
+ if (a.hasSource !== b.hasSource) {
19
+ return a.hasSource ? -1 : 1; // source comes first
20
+ }
21
+ // 2. Apply configured strategy
22
+ try {
23
+ return comparator(a, b);
24
+ }
25
+ catch (e) {
26
+ // Fallback to ID comparison (likely insert order)
27
+ return b.id - a.id;
28
+ }
29
+ });
30
+ return sorted[0];
31
+ }
32
+ static compareSemver(a, b) {
33
+ const vA = semver.coerce(a.version);
34
+ const vB = semver.coerce(b.version);
35
+ if (vA && vB) {
36
+ const comparison = semver.rcompare(vA, vB);
37
+ if (comparison !== 0)
38
+ return comparison;
39
+ }
40
+ if (semver.valid(a.version) && semver.valid(b.version)) {
41
+ return semver.rcompare(a.version, b.version);
42
+ }
43
+ // Fallback for non-semver strings
44
+ return b.id - a.id;
45
+ }
46
+ static getArtifactFileTime(artifact, timeType) {
47
+ let p = artifact.abspath;
48
+ try {
49
+ if (fs.statSync(p).isDirectory()) {
50
+ // Try jar first, then pom
51
+ const jarPath = path.join(p, `${artifact.artifactId}-${artifact.version}.jar`);
52
+ if (fs.existsSync(jarPath))
53
+ return fs.statSync(jarPath)[timeType].getTime();
54
+ const pomPath = path.join(p, `${artifact.artifactId}-${artifact.version}.pom`);
55
+ if (fs.existsSync(pomPath))
56
+ return fs.statSync(pomPath)[timeType].getTime();
57
+ }
58
+ return fs.statSync(p)[timeType].getTime();
59
+ }
60
+ catch {
61
+ return 0;
62
+ }
63
+ }
64
+ static getArtifactPublishTime(artifact) {
65
+ let p = artifact.abspath;
66
+ try {
67
+ // 1. Try to read .lastUpdated file (priority)
68
+ if (fs.statSync(p).isDirectory()) {
69
+ const pomPath = path.join(p, `${artifact.artifactId}-${artifact.version}.pom`);
70
+ const lastUpdatedPath = `${pomPath}.lastUpdated`;
71
+ if (fs.existsSync(lastUpdatedPath)) {
72
+ try {
73
+ const content = fs.readFileSync(lastUpdatedPath, 'utf-8');
74
+ // Try to find property with timestamp
75
+ const matches = content.match(/lastUpdated=(\d{13})/g);
76
+ if (matches) {
77
+ let maxTime = 0;
78
+ for (const match of matches) {
79
+ const ts = parseInt(match.split('=')[1]);
80
+ if (!isNaN(ts) && ts > maxTime)
81
+ maxTime = ts;
82
+ }
83
+ if (maxTime > 0)
84
+ return maxTime;
85
+ }
86
+ }
87
+ catch (e) {
88
+ // ignore parse error
89
+ }
90
+ }
91
+ }
92
+ // 2. Fallback to mtime
93
+ return ArtifactResolver.getArtifactFileTime(artifact, 'mtime');
94
+ }
95
+ catch {
96
+ return 0;
97
+ }
98
+ }
99
+ static compareLatestPublished(a, b) {
100
+ const tA = ArtifactResolver.getArtifactPublishTime(a);
101
+ const tB = ArtifactResolver.getArtifactPublishTime(b);
102
+ return tB - tA; // Newer first
103
+ }
104
+ static compareLatestUsed(a, b) {
105
+ const tA = ArtifactResolver.getArtifactFileTime(a, 'birthtime');
106
+ const tB = ArtifactResolver.getArtifactFileTime(b, 'birthtime');
107
+ return tB - tA; // Newer first
108
+ }
109
+ }
package/build/config.js CHANGED
@@ -11,7 +11,9 @@ export class Config {
11
11
  gradleRepository = "";
12
12
  javaBinary = "java";
13
13
  includedPackages = ["*"];
14
+ normalizedIncludedPackages = [];
14
15
  cfrPath = null;
16
+ versionResolutionStrategy = 'semver';
15
17
  constructor() { }
16
18
  static async getInstance() {
17
19
  if (!Config.instance) {
@@ -79,6 +81,7 @@ export class Config {
79
81
  .map(p => p.trim())
80
82
  .filter(p => p.length > 0);
81
83
  }
84
+ this.normalizedIncludedPackages = this.normalizeScanPatterns(this.includedPackages);
82
85
  // Load CFR Path
83
86
  if (process.env.MAVEN_INDEXER_CFR_PATH) {
84
87
  this.cfrPath = process.env.MAVEN_INDEXER_CFR_PATH;
@@ -89,6 +92,24 @@ export class Config {
89
92
  const __dirname = path.dirname(__filename);
90
93
  this.cfrPath = path.resolve(__dirname, '../lib/cfr-0.152.jar');
91
94
  }
95
+ if (process.env.VERSION_RESOLUTION_STRATEGY) {
96
+ const strategy = process.env.VERSION_RESOLUTION_STRATEGY.toLowerCase();
97
+ if (strategy === 'semver' || strategy === 'latest-published' || strategy === 'latest-used') {
98
+ this.versionResolutionStrategy = strategy;
99
+ }
100
+ else if (strategy === 'semver-latest') {
101
+ // Backward compatibility
102
+ this.versionResolutionStrategy = 'semver';
103
+ }
104
+ else if (strategy === 'date-latest' || strategy === 'modification-time' || strategy === 'publish-time') {
105
+ // Backward compatibility
106
+ this.versionResolutionStrategy = 'latest-published';
107
+ }
108
+ else if (strategy === 'creation-time' || strategy === 'usage-time') {
109
+ // Backward compatibility
110
+ this.versionResolutionStrategy = 'latest-used';
111
+ }
112
+ }
92
113
  if (this.cfrPath && !(await this.fileExists(this.cfrPath))) {
93
114
  try {
94
115
  console.error(`CFR jar not found at ${this.cfrPath}, attempting to download...`);
@@ -174,4 +195,55 @@ export class Config {
174
195
  }
175
196
  return null;
176
197
  }
198
+ /**
199
+ * Normalizes package patterns for scanning.
200
+ * 1. Filters out empty or whitespace-only patterns.
201
+ * 2. Removes wildcards ('*', '.*').
202
+ * 3. Sorts and removes duplicates/sub-packages.
203
+ *
204
+ * @param patterns The raw patterns from configuration.
205
+ * @returns A list of normalized package prefixes. Returns [] if all packages should be scanned.
206
+ */
207
+ normalizeScanPatterns(patterns) {
208
+ if (!patterns || patterns.length === 0) {
209
+ return [];
210
+ }
211
+ // 1. Filter empty/blank patterns and remove wildcards
212
+ let clean = patterns
213
+ .map(p => p.trim())
214
+ .filter(p => p.length > 0) // Ignore empty strings
215
+ .map(p => {
216
+ if (p.endsWith('.*'))
217
+ return p.slice(0, -2);
218
+ if (p === '*')
219
+ return ''; // Will be filtered out later if we want strict prefixes, but '*' usually means ALL
220
+ return p;
221
+ });
222
+ // If any pattern became empty string (meaning '*') or was originally '*', it implies "Scan All"
223
+ if (clean.includes('')) {
224
+ return [];
225
+ }
226
+ if (clean.length === 0) {
227
+ // Usually empty config means Scan All.
228
+ return [];
229
+ }
230
+ // 2. Sort to ensure parents come before children
231
+ clean.sort();
232
+ // 3. Remove duplicates and sub-packages
233
+ const result = [];
234
+ for (const p of clean) {
235
+ if (result.length === 0) {
236
+ result.push(p);
237
+ continue;
238
+ }
239
+ const last = result[result.length - 1];
240
+ // Check if 'p' is sub-package of 'last'
241
+ // e.g. last="com.test", p="com.test.demo" -> p starts with last + '.'
242
+ if (p === last || p.startsWith(last + '.')) {
243
+ continue;
244
+ }
245
+ result.push(p);
246
+ }
247
+ return result;
248
+ }
177
249
  }