maven-indexer-mcp 1.0.6 → 1.0.8
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 +161 -47
- package/build/artifact_resolver.js +109 -0
- package/build/config.js +72 -0
- package/build/db/index.js +16 -3
- package/build/index.js +8 -13
- package/build/indexer.js +225 -109
- package/build/simple_watcher.js +121 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
# Maven Indexer MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/maven-indexer-mcp)
|
|
4
|
+
[](https://github.com/tangcent/maven-indexer-mcp/actions/workflows/ci.yml)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
1
|
|
7
|
-
|
|
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.
|
|
8
9
|
|
|
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.
|
|
10
18
|
|
|
11
19
|
## Features
|
|
12
20
|
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* **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.
|
|
19
26
|
|
|
20
27
|
## Getting Started
|
|
21
28
|
|
|
@@ -26,22 +33,116 @@ Add the following config to your MCP client:
|
|
|
26
33
|
"mcpServers": {
|
|
27
34
|
"maven-indexer": {
|
|
28
35
|
"command": "npx",
|
|
29
|
-
"args": [
|
|
36
|
+
"args": [
|
|
37
|
+
"-y",
|
|
38
|
+
"maven-indexer-mcp@latest"
|
|
39
|
+
]
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
```
|
|
34
44
|
|
|
35
|
-
This will automatically download and run the latest version of the server. It will auto-detect your Maven repository
|
|
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.
|
|
36
47
|
|
|
37
|
-
###
|
|
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
|
+
[](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.
|
|
90
|
+
|
|
91
|
+
Or, from the IDE **Activity Bar** > `Kiro` > `MCP Servers` > `Click Open MCP Config`. Use the configuration snippet
|
|
92
|
+
provided above.
|
|
38
93
|
|
|
39
|
-
|
|
94
|
+
</details>
|
|
40
95
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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).
|
|
45
146
|
|
|
46
147
|
Example with optional configuration:
|
|
47
148
|
|
|
@@ -50,12 +151,16 @@ Example with optional configuration:
|
|
|
50
151
|
"mcpServers": {
|
|
51
152
|
"maven-indexer": {
|
|
52
153
|
"command": "npx",
|
|
53
|
-
"args": [
|
|
154
|
+
"args": [
|
|
155
|
+
"-y",
|
|
156
|
+
"maven-indexer-mcp@latest"
|
|
157
|
+
],
|
|
54
158
|
"env": {
|
|
55
159
|
"MAVEN_REPO": "/Users/yourname/.m2/repository",
|
|
56
160
|
"GRADLE_REPO_PATH": "/Users/yourname/.gradle/caches/modules-2/files-2.1",
|
|
57
161
|
"INCLUDED_PACKAGES": "com.mycompany.*",
|
|
58
|
-
"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"
|
|
59
164
|
}
|
|
60
165
|
}
|
|
61
166
|
}
|
|
@@ -66,19 +171,22 @@ Example with optional configuration:
|
|
|
66
171
|
|
|
67
172
|
If you prefer to run from source:
|
|
68
173
|
|
|
69
|
-
1.
|
|
174
|
+
1. Clone the repository:
|
|
175
|
+
|
|
70
176
|
```bash
|
|
71
177
|
git clone https://github.com/tangcent/maven-indexer-mcp.git
|
|
72
178
|
cd maven-indexer-mcp
|
|
73
179
|
```
|
|
74
180
|
|
|
75
|
-
2.
|
|
181
|
+
2. Install dependencies and build:
|
|
182
|
+
|
|
76
183
|
```bash
|
|
77
184
|
npm install
|
|
78
185
|
npm run build
|
|
79
186
|
```
|
|
80
187
|
|
|
81
|
-
3.
|
|
188
|
+
3. Use the absolute path in your config:
|
|
189
|
+
|
|
82
190
|
```json
|
|
83
191
|
{
|
|
84
192
|
"mcpServers": {
|
|
@@ -92,32 +200,38 @@ If you prefer to run from source:
|
|
|
92
200
|
|
|
93
201
|
## Available Tools
|
|
94
202
|
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
1.
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
115
229
|
|
|
116
230
|
## Development
|
|
117
231
|
|
|
118
|
-
*
|
|
119
|
-
*
|
|
232
|
+
* **Run tests**: `npm test`
|
|
233
|
+
* **Watch mode**: `npm run watch`
|
|
120
234
|
|
|
121
235
|
## License
|
|
122
236
|
|
|
123
|
-
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
|
}
|
package/build/db/index.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
3
5
|
export class DB {
|
|
4
6
|
static instance;
|
|
5
7
|
db;
|
|
6
8
|
constructor() {
|
|
7
9
|
// Check environment variable for DB path (useful for testing)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
if (process.env.DB_FILE) {
|
|
11
|
+
this.db = new Database(process.env.DB_FILE);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
// Use home directory for the database file
|
|
15
|
+
const homeDir = os.homedir();
|
|
16
|
+
const configDir = path.join(homeDir, '.maven-indexer-mcp');
|
|
17
|
+
// Ensure the directory exists
|
|
18
|
+
if (!fs.existsSync(configDir)) {
|
|
19
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
const dbPath = path.join(configDir, 'maven-index.sqlite');
|
|
22
|
+
this.db = new Database(dbPath);
|
|
23
|
+
}
|
|
11
24
|
this.initSchema();
|
|
12
25
|
}
|
|
13
26
|
static getInstance() {
|
package/build/index.js
CHANGED
|
@@ -5,6 +5,7 @@ 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",
|
|
@@ -22,7 +23,7 @@ indexer.index().then(() => {
|
|
|
22
23
|
return indexer.startWatch();
|
|
23
24
|
}).catch(err => console.error("Initial indexing failed:", err));
|
|
24
25
|
server.registerTool("get_class_details", {
|
|
25
|
-
description: "Retrieve the source code for a class from the
|
|
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.",
|
|
26
27
|
inputSchema: z.object({
|
|
27
28
|
className: z.string().optional().describe("Fully qualified class name"),
|
|
28
29
|
classNames: z.array(z.string()).optional().describe("Batch class names"),
|
|
@@ -58,17 +59,11 @@ server.registerTool("get_class_details", {
|
|
|
58
59
|
return `Class '${clsName}' not found in the index. Try 'search_classes' with a keyword if you are unsure of the full name.`;
|
|
59
60
|
}
|
|
60
61
|
// We have an exact match, choose the best artifact
|
|
61
|
-
|
|
62
|
-
|
|
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) {
|
|
62
|
+
const bestArtifact = await ArtifactResolver.resolveBestArtifact(exactMatch.artifacts);
|
|
63
|
+
if (!bestArtifact) {
|
|
69
64
|
return `Class '${clsName}' found but no artifacts are associated with it (database inconsistency).`;
|
|
70
65
|
}
|
|
71
|
-
targetArtifact =
|
|
66
|
+
targetArtifact = bestArtifact;
|
|
72
67
|
}
|
|
73
68
|
const artifact = targetArtifact;
|
|
74
69
|
let detail = null;
|
|
@@ -159,7 +154,7 @@ server.registerTool("get_class_details", {
|
|
|
159
154
|
return { content: [{ type: "text", text: results.join("\n\n") }] };
|
|
160
155
|
});
|
|
161
156
|
server.registerTool("search_artifacts", {
|
|
162
|
-
description: "Search for
|
|
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.",
|
|
163
158
|
inputSchema: z.object({
|
|
164
159
|
query: z.string().optional().describe("Search query (groupId, artifactId, or keyword)"),
|
|
165
160
|
queries: z.array(z.string()).optional().describe("Batch search queries"),
|
|
@@ -192,7 +187,7 @@ server.registerTool("search_artifacts", {
|
|
|
192
187
|
};
|
|
193
188
|
});
|
|
194
189
|
server.registerTool("search_classes", {
|
|
195
|
-
description: "Search for Java classes in
|
|
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.",
|
|
196
191
|
inputSchema: z.object({
|
|
197
192
|
className: z.string().optional().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
198
193
|
classNames: z.array(z.string()).optional().describe("Batch class names"),
|
|
@@ -223,7 +218,7 @@ server.registerTool("search_classes", {
|
|
|
223
218
|
};
|
|
224
219
|
});
|
|
225
220
|
server.registerTool("search_implementations", {
|
|
226
|
-
description: "Search for
|
|
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.",
|
|
227
222
|
inputSchema: z.object({
|
|
228
223
|
className: z.string().optional().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
|
|
229
224
|
classNames: z.array(z.string()).optional().describe("Batch class names"),
|
package/build/indexer.js
CHANGED
|
@@ -15,7 +15,9 @@ export class Indexer {
|
|
|
15
15
|
isIndexing = false;
|
|
16
16
|
watcher = null;
|
|
17
17
|
debounceTimer = null;
|
|
18
|
-
|
|
18
|
+
pollingTimer = null;
|
|
19
|
+
constructor() {
|
|
20
|
+
}
|
|
19
21
|
static getInstance() {
|
|
20
22
|
if (!Indexer.instance) {
|
|
21
23
|
Indexer.instance = new Indexer();
|
|
@@ -23,49 +25,95 @@ export class Indexer {
|
|
|
23
25
|
return Indexer.instance;
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
|
-
* Starts watching the local repository for changes
|
|
27
|
-
*
|
|
28
|
+
* Starts watching the local repository for changes - ULTRA SIMPLE VERSION
|
|
29
|
+
* Just watch the root directories without recursion
|
|
28
30
|
*/
|
|
29
31
|
async startWatch() {
|
|
30
32
|
const config = await Config.getInstance();
|
|
31
|
-
const
|
|
33
|
+
const watchPaths = [];
|
|
34
|
+
// Simple: Just add the main repository paths
|
|
32
35
|
if (config.localRepository && fsSync.existsSync(config.localRepository)) {
|
|
33
|
-
|
|
36
|
+
watchPaths.push(config.localRepository);
|
|
34
37
|
}
|
|
35
38
|
if (config.gradleRepository && fsSync.existsSync(config.gradleRepository)) {
|
|
36
|
-
|
|
39
|
+
watchPaths.push(config.gradleRepository);
|
|
37
40
|
}
|
|
38
|
-
if (
|
|
41
|
+
if (watchPaths.length === 0) {
|
|
39
42
|
console.error("No repository paths found, skipping watch mode.");
|
|
40
43
|
return;
|
|
41
44
|
}
|
|
42
|
-
if (this.watcher) {
|
|
45
|
+
if (this.watcher || this.pollingTimer) {
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
|
-
console.error(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
console.error(`🔍 Starting ultra-simple file watcher on: ${watchPaths.join(', ')}`);
|
|
49
|
+
try {
|
|
50
|
+
// ULTRA SIMPLE: Watch only root directories, no recursion
|
|
51
|
+
this.watcher = chokidar.watch(watchPaths, {
|
|
52
|
+
// Watch only the root directory itself, not subdirectories
|
|
53
|
+
depth: 0,
|
|
54
|
+
// Don't trigger for files that already exist
|
|
55
|
+
ignoreInitial: true,
|
|
56
|
+
// Wait for files to finish writing before triggering
|
|
57
|
+
awaitWriteFinish: {
|
|
58
|
+
stabilityThreshold: 2000,
|
|
59
|
+
pollInterval: 100
|
|
60
|
+
},
|
|
61
|
+
// Don't crash on permission errors
|
|
62
|
+
ignorePermissionErrors: true
|
|
63
|
+
});
|
|
64
|
+
// SIMPLE: Watch for any changes in the root directories
|
|
65
|
+
// This will catch when new directories are created (which means new artifacts)
|
|
66
|
+
this.watcher
|
|
67
|
+
.on('addDir', (dirPath) => {
|
|
68
|
+
console.error(`� New directory detected: ${path.basename(dirPath)}`);
|
|
69
|
+
this.triggerReindex();
|
|
70
|
+
})
|
|
71
|
+
.on('unlinkDir', (dirPath) => {
|
|
72
|
+
console.error(`🗑️ Directory removed: ${path.basename(dirPath)}`);
|
|
73
|
+
this.triggerReindex();
|
|
74
|
+
})
|
|
75
|
+
.on('error', (error) => {
|
|
76
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
77
|
+
console.error(`❌ Watcher error: ${errorMessage}`);
|
|
78
|
+
this.fallbackToPolling();
|
|
79
|
+
});
|
|
80
|
+
console.error('✅ Ultra-simple file watcher started successfully');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
84
|
+
console.error(`❌ Failed to start watcher: ${errorMessage}`);
|
|
85
|
+
this.fallbackToPolling();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Falls back to polling mode if watcher fails.
|
|
90
|
+
* Polls the repository every 1 min
|
|
91
|
+
*/
|
|
92
|
+
fallbackToPolling() {
|
|
93
|
+
if (this.pollingTimer) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.error('⚠️ Falling back to polling mode (every 10s)...');
|
|
97
|
+
if (this.watcher) {
|
|
98
|
+
this.watcher.close().catch(err => console.error(`Error closing watcher: ${err}`));
|
|
99
|
+
this.watcher = null;
|
|
100
|
+
}
|
|
101
|
+
this.pollingTimer = setInterval(() => {
|
|
102
|
+
this.index().catch(console.error);
|
|
103
|
+
}, 60000);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Trigger reindexing with debouncing (wait a bit for multiple changes)
|
|
107
|
+
*/
|
|
108
|
+
triggerReindex() {
|
|
109
|
+
if (this.debounceTimer) {
|
|
110
|
+
clearTimeout(this.debounceTimer);
|
|
111
|
+
}
|
|
112
|
+
// Wait 3 seconds after the last change before reindexing
|
|
113
|
+
this.debounceTimer = setTimeout(() => {
|
|
114
|
+
console.error('🔄 Changes detected - triggering reindex...');
|
|
115
|
+
this.index().catch(console.error);
|
|
116
|
+
}, 3000);
|
|
69
117
|
}
|
|
70
118
|
/**
|
|
71
119
|
* Forces a full re-index of the repository.
|
|
@@ -105,13 +153,13 @@ export class Indexer {
|
|
|
105
153
|
let artifacts = [];
|
|
106
154
|
if (repoPath && fsSync.existsSync(repoPath)) {
|
|
107
155
|
console.error(`Scanning Maven repo: ${repoPath}`);
|
|
108
|
-
const mavenArtifacts = await this.scanRepository(repoPath);
|
|
156
|
+
const mavenArtifacts = await this.scanRepository(repoPath, config.normalizedIncludedPackages);
|
|
109
157
|
console.error(`Found ${mavenArtifacts.length} Maven artifacts.`);
|
|
110
158
|
artifacts = artifacts.concat(mavenArtifacts);
|
|
111
159
|
}
|
|
112
160
|
if (gradleRepoPath && fsSync.existsSync(gradleRepoPath)) {
|
|
113
161
|
console.error(`Scanning Gradle repo: ${gradleRepoPath}`);
|
|
114
|
-
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath);
|
|
162
|
+
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath, config.normalizedIncludedPackages);
|
|
115
163
|
console.error(`Found ${gradleArtifacts.length} Gradle artifacts.`);
|
|
116
164
|
artifacts = artifacts.concat(gradleArtifacts);
|
|
117
165
|
}
|
|
@@ -119,9 +167,10 @@ export class Indexer {
|
|
|
119
167
|
// 2. Persist artifacts and determine what needs indexing
|
|
120
168
|
// We use is_indexed = 0 for new artifacts.
|
|
121
169
|
const insertArtifact = db.prepare(`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
INSERT
|
|
171
|
+
OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
|
|
172
|
+
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
|
|
173
|
+
`);
|
|
125
174
|
// Use a transaction only for the batch insert of artifacts
|
|
126
175
|
db.transaction(() => {
|
|
127
176
|
for (const art of artifacts) {
|
|
@@ -144,10 +193,10 @@ export class Indexer {
|
|
|
144
193
|
}
|
|
145
194
|
// 3. Find artifacts that need indexing (is_indexed = 0)
|
|
146
195
|
const artifactsToIndex = db.prepare(`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
196
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
197
|
+
FROM artifacts
|
|
198
|
+
WHERE is_indexed = 0
|
|
199
|
+
`).all();
|
|
151
200
|
console.error(`${artifactsToIndex.length} artifacts need indexing.`);
|
|
152
201
|
// 4. Scan JARs for classes and update DB
|
|
153
202
|
const CHUNK_SIZE = 50;
|
|
@@ -171,8 +220,11 @@ export class Indexer {
|
|
|
171
220
|
}
|
|
172
221
|
/**
|
|
173
222
|
* Recursively scans a directory for Maven artifacts (POM files).
|
|
223
|
+
*
|
|
224
|
+
* @param repoRoot The root directory of the Maven repository.
|
|
225
|
+
* @param normalizedPatterns List of normalized package patterns to include.
|
|
174
226
|
*/
|
|
175
|
-
async scanRepository(
|
|
227
|
+
async scanRepository(repoRoot, normalizedPatterns = []) {
|
|
176
228
|
const results = [];
|
|
177
229
|
const scanDir = async (dir) => {
|
|
178
230
|
let entries;
|
|
@@ -188,7 +240,7 @@ export class Indexer {
|
|
|
188
240
|
const artifactDir = path.dirname(dir);
|
|
189
241
|
const artifactId = path.basename(artifactDir);
|
|
190
242
|
const groupDir = path.dirname(artifactDir);
|
|
191
|
-
const relGroupPath = path.relative(
|
|
243
|
+
const relGroupPath = path.relative(repoRoot, groupDir);
|
|
192
244
|
const groupId = relGroupPath.split(path.sep).join('.');
|
|
193
245
|
if (groupId && artifactId && version && !groupId.startsWith('..')) {
|
|
194
246
|
const sourceJarPath = path.join(dir, `${artifactId}-${version}-sources.jar`);
|
|
@@ -213,14 +265,49 @@ export class Indexer {
|
|
|
213
265
|
}
|
|
214
266
|
}
|
|
215
267
|
};
|
|
216
|
-
|
|
268
|
+
const startDirs = this.getMavenStartDirs(repoRoot, normalizedPatterns);
|
|
269
|
+
console.error(`Scanning Maven directories: ${startDirs.join(', ')}`);
|
|
270
|
+
for (const startDir of startDirs) {
|
|
271
|
+
await scanDir(startDir);
|
|
272
|
+
}
|
|
217
273
|
return results;
|
|
218
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Calculates the starting directories for Maven scanning based on included packages.
|
|
277
|
+
*
|
|
278
|
+
* @param repoRoot The root of the Maven repository.
|
|
279
|
+
* @param normalizedPatterns The list of normalized included packages.
|
|
280
|
+
*/
|
|
281
|
+
getMavenStartDirs(repoRoot, normalizedPatterns) {
|
|
282
|
+
if (normalizedPatterns.length === 0) {
|
|
283
|
+
return [repoRoot];
|
|
284
|
+
}
|
|
285
|
+
return normalizedPatterns.map(p => path.join(repoRoot, p.split('.').join(path.sep)));
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Checks if a group ID is included in the normalized patterns.
|
|
289
|
+
*
|
|
290
|
+
* @param groupId The group ID (e.g., "com.google.guava").
|
|
291
|
+
* @param normalizedPatterns The list of normalized patterns.
|
|
292
|
+
*/
|
|
293
|
+
isGroupIncluded(groupId, normalizedPatterns) {
|
|
294
|
+
if (!normalizedPatterns || normalizedPatterns.length === 0)
|
|
295
|
+
return true;
|
|
296
|
+
for (const pattern of normalizedPatterns) {
|
|
297
|
+
if (groupId === pattern || groupId.startsWith(pattern + '.')) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
219
303
|
/**
|
|
220
304
|
* Scans a Gradle cache directory for artifacts.
|
|
221
305
|
* Structure: group/artifact/version/hash/file
|
|
306
|
+
*
|
|
307
|
+
* @param rootDir The root directory of the Gradle cache (e.g., ~/.gradle/caches/modules-2/files-2.1).
|
|
308
|
+
* @param normalizedPatterns List of normalized package patterns to include.
|
|
222
309
|
*/
|
|
223
|
-
async scanGradleRepository(rootDir) {
|
|
310
|
+
async scanGradleRepository(rootDir, normalizedPatterns = []) {
|
|
224
311
|
const results = [];
|
|
225
312
|
// Helper to read directory safely
|
|
226
313
|
const readDirSafe = async (p) => {
|
|
@@ -236,6 +323,9 @@ export class Indexer {
|
|
|
236
323
|
if (!groupEntry.isDirectory())
|
|
237
324
|
continue;
|
|
238
325
|
const groupId = groupEntry.name;
|
|
326
|
+
if (!this.isGroupIncluded(groupId, normalizedPatterns)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
239
329
|
const groupPath = path.join(rootDir, groupId);
|
|
240
330
|
const artifactDirs = await readDirSafe(groupPath);
|
|
241
331
|
for (const artifactEntry of artifactDirs) {
|
|
@@ -302,7 +392,7 @@ export class Indexer {
|
|
|
302
392
|
await fs.access(jarPath);
|
|
303
393
|
}
|
|
304
394
|
catch {
|
|
305
|
-
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
395
|
+
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
306
396
|
// Or maybe it's a pom-only artifact.
|
|
307
397
|
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
308
398
|
return;
|
|
@@ -333,13 +423,21 @@ export class Indexer {
|
|
|
333
423
|
// Simple check to avoid module-info or invalid names
|
|
334
424
|
if (!info.className.includes('$') && info.className.length > 0) {
|
|
335
425
|
// Filter by includedPackages
|
|
336
|
-
if (this.isPackageIncluded(info.className, config.
|
|
426
|
+
if (this.isPackageIncluded(info.className, config.normalizedIncludedPackages)) {
|
|
337
427
|
classes.push(info.className);
|
|
338
428
|
if (info.superClass && info.superClass !== 'java.lang.Object') {
|
|
339
|
-
inheritance.push({
|
|
429
|
+
inheritance.push({
|
|
430
|
+
className: info.className,
|
|
431
|
+
parent: info.superClass,
|
|
432
|
+
type: 'extends'
|
|
433
|
+
});
|
|
340
434
|
}
|
|
341
435
|
for (const iface of info.interfaces) {
|
|
342
|
-
inheritance.push({
|
|
436
|
+
inheritance.push({
|
|
437
|
+
className: info.className,
|
|
438
|
+
parent: iface,
|
|
439
|
+
type: 'implements'
|
|
440
|
+
});
|
|
343
441
|
}
|
|
344
442
|
}
|
|
345
443
|
}
|
|
@@ -360,13 +458,13 @@ export class Indexer {
|
|
|
360
458
|
try {
|
|
361
459
|
db.transaction(() => {
|
|
362
460
|
const insertClass = db.prepare(`
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
461
|
+
INSERT INTO classes_fts (artifact_id, class_name, simple_name)
|
|
462
|
+
VALUES (?, ?, ?)
|
|
463
|
+
`);
|
|
366
464
|
const insertInheritance = db.prepare(`
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
465
|
+
INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
|
|
466
|
+
VALUES (?, ?, ?, ?)
|
|
467
|
+
`);
|
|
370
468
|
for (const cls of classes) {
|
|
371
469
|
const simpleName = cls.split('.').pop() || cls;
|
|
372
470
|
insertClass.run(artifact.id, cls, simpleName);
|
|
@@ -391,23 +489,17 @@ export class Indexer {
|
|
|
391
489
|
}
|
|
392
490
|
/**
|
|
393
491
|
* Checks if a class package is included in the configuration patterns.
|
|
492
|
+
*
|
|
493
|
+
* @param className The fully qualified class name.
|
|
494
|
+
* @param normalizedPatterns The list of normalized package patterns.
|
|
394
495
|
*/
|
|
395
|
-
isPackageIncluded(className,
|
|
396
|
-
if (
|
|
496
|
+
isPackageIncluded(className, normalizedPatterns) {
|
|
497
|
+
if (!normalizedPatterns || normalizedPatterns.length === 0)
|
|
397
498
|
return true;
|
|
398
|
-
for (const pattern of
|
|
399
|
-
|
|
499
|
+
for (const pattern of normalizedPatterns) {
|
|
500
|
+
// Match exact package or subpackage
|
|
501
|
+
if (className === pattern || className.startsWith(pattern + '.')) {
|
|
400
502
|
return true;
|
|
401
|
-
if (pattern.endsWith('.*')) {
|
|
402
|
-
const prefix = pattern.slice(0, -2); // "com.example"
|
|
403
|
-
// Match exact package or subpackage
|
|
404
|
-
if (className === prefix || className.startsWith(prefix + '.'))
|
|
405
|
-
return true;
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
// Exact match
|
|
409
|
-
if (className === pattern)
|
|
410
|
-
return true;
|
|
411
503
|
}
|
|
412
504
|
}
|
|
413
505
|
return false;
|
|
@@ -419,11 +511,11 @@ export class Indexer {
|
|
|
419
511
|
// Artifact coordinates search
|
|
420
512
|
const db = DB.getInstance();
|
|
421
513
|
const rows = db.prepare(`
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
514
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
515
|
+
FROM artifacts
|
|
516
|
+
WHERE group_id LIKE ?
|
|
517
|
+
OR artifact_id LIKE ? LIMIT 50
|
|
518
|
+
`).all(`%${query}%`, `%${query}%`);
|
|
427
519
|
return rows;
|
|
428
520
|
}
|
|
429
521
|
/**
|
|
@@ -438,12 +530,19 @@ export class Indexer {
|
|
|
438
530
|
// Regex search
|
|
439
531
|
const regex = classNamePattern.substring(6);
|
|
440
532
|
rows = db.prepare(`
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
533
|
+
SELECT c.class_name,
|
|
534
|
+
c.simple_name,
|
|
535
|
+
a.id,
|
|
536
|
+
a.group_id,
|
|
537
|
+
a.artifact_id,
|
|
538
|
+
a.version,
|
|
539
|
+
a.abspath,
|
|
540
|
+
a.has_source
|
|
541
|
+
FROM classes_fts c
|
|
542
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
543
|
+
WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
|
|
445
544
|
LIMIT 100
|
|
446
|
-
|
|
545
|
+
`).all(regex, regex);
|
|
447
546
|
}
|
|
448
547
|
else if (classNamePattern.includes('*') || classNamePattern.includes('?')) {
|
|
449
548
|
// Glob-style search (using LIKE for standard wildcards)
|
|
@@ -451,12 +550,19 @@ export class Indexer {
|
|
|
451
550
|
// But standard glob is * and ?
|
|
452
551
|
const likePattern = classNamePattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
453
552
|
rows = db.prepare(`
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
553
|
+
SELECT c.class_name,
|
|
554
|
+
c.simple_name,
|
|
555
|
+
a.id,
|
|
556
|
+
a.group_id,
|
|
557
|
+
a.artifact_id,
|
|
558
|
+
a.version,
|
|
559
|
+
a.abspath,
|
|
560
|
+
a.has_source
|
|
561
|
+
FROM classes_fts c
|
|
562
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
563
|
+
WHERE c.class_name LIKE ?
|
|
564
|
+
OR c.simple_name LIKE ? LIMIT 100
|
|
565
|
+
`).all(likePattern, likePattern);
|
|
460
566
|
}
|
|
461
567
|
else {
|
|
462
568
|
// Use FTS for smart matching
|
|
@@ -468,13 +574,19 @@ export class Indexer {
|
|
|
468
574
|
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
469
575
|
: `"${escapedPattern}"*`;
|
|
470
576
|
rows = db.prepare(`
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
577
|
+
SELECT c.class_name,
|
|
578
|
+
c.simple_name,
|
|
579
|
+
a.id,
|
|
580
|
+
a.group_id,
|
|
581
|
+
a.artifact_id,
|
|
582
|
+
a.version,
|
|
583
|
+
a.abspath,
|
|
584
|
+
a.has_source
|
|
585
|
+
FROM classes_fts c
|
|
586
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
587
|
+
WHERE c.classes_fts MATCH ?
|
|
588
|
+
ORDER BY rank LIMIT 100
|
|
589
|
+
`).all(query);
|
|
478
590
|
}
|
|
479
591
|
// Group by class name
|
|
480
592
|
const resultMap = new Map();
|
|
@@ -516,16 +628,17 @@ export class Indexer {
|
|
|
516
628
|
}
|
|
517
629
|
// Recursive search for all implementations/subclasses
|
|
518
630
|
const rows = db.prepare(`
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
631
|
+
WITH RECURSIVE hierarchy(class_name, artifact_id) AS (SELECT class_name, artifact_id
|
|
632
|
+
FROM inheritance
|
|
633
|
+
WHERE parent_class_name = ?
|
|
634
|
+
UNION
|
|
635
|
+
SELECT i.class_name, i.artifact_id
|
|
636
|
+
FROM inheritance i
|
|
637
|
+
JOIN hierarchy h ON i.parent_class_name = h.class_name)
|
|
638
|
+
SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
639
|
+
FROM hierarchy h
|
|
640
|
+
JOIN artifacts a ON h.artifact_id = a.id LIMIT 100
|
|
641
|
+
`).all(className);
|
|
529
642
|
console.error(`Searching implementations for ${className}: found ${rows.length} rows.`);
|
|
530
643
|
if (rows.length === 0) {
|
|
531
644
|
// Fallback: Try searching without recursion to see if direct children exist
|
|
@@ -563,9 +676,10 @@ export class Indexer {
|
|
|
563
676
|
getArtifactById(id) {
|
|
564
677
|
const db = DB.getInstance();
|
|
565
678
|
const row = db.prepare(`
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
679
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
680
|
+
FROM artifacts
|
|
681
|
+
WHERE id = ?
|
|
682
|
+
`).get(id);
|
|
569
683
|
if (row) {
|
|
570
684
|
return {
|
|
571
685
|
id: row.id,
|
|
@@ -584,10 +698,12 @@ export class Indexer {
|
|
|
584
698
|
getArtifactByCoordinate(groupId, artifactId, version) {
|
|
585
699
|
const db = DB.getInstance();
|
|
586
700
|
const row = db.prepare(`
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
701
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
702
|
+
FROM artifacts
|
|
703
|
+
WHERE group_id = ?
|
|
704
|
+
AND artifact_id = ?
|
|
705
|
+
AND version = ?
|
|
706
|
+
`).get(groupId, artifactId, version);
|
|
591
707
|
if (row) {
|
|
592
708
|
return {
|
|
593
709
|
id: row.id,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIMPLIFIED FILE WATCHER for Maven Indexer
|
|
3
|
+
*
|
|
4
|
+
* This is a cleaned-up version that's easier to understand.
|
|
5
|
+
* It watches for new JAR and POM files in your Maven/Gradle repositories.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
export class SimpleWatcher {
|
|
10
|
+
watcher = null;
|
|
11
|
+
onChangeCallback;
|
|
12
|
+
constructor(onChangeCallback) {
|
|
13
|
+
this.onChangeCallback = onChangeCallback;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Start watching - SIMPLE VERSION
|
|
17
|
+
* Just watch the main folders and filter for JAR/POM files
|
|
18
|
+
*/
|
|
19
|
+
async startWatching(mavenRepoPath, gradleRepoPath) {
|
|
20
|
+
if (this.watcher) {
|
|
21
|
+
console.log('Watcher already running');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const watchPaths = [];
|
|
25
|
+
// Add Maven repo if it exists
|
|
26
|
+
if (mavenRepoPath && fs.existsSync(mavenRepoPath)) {
|
|
27
|
+
watchPaths.push(mavenRepoPath);
|
|
28
|
+
}
|
|
29
|
+
// Add Gradle repo if it exists
|
|
30
|
+
if (gradleRepoPath && fs.existsSync(gradleRepoPath)) {
|
|
31
|
+
watchPaths.push(gradleRepoPath);
|
|
32
|
+
}
|
|
33
|
+
if (watchPaths.length === 0) {
|
|
34
|
+
console.log('No repository paths to watch');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(`🔍 Watching for new JAR/POM files in: ${watchPaths.join(', ')}`);
|
|
38
|
+
// SIMPLE: Use chokidar with basic ignore patterns
|
|
39
|
+
const chokidar = await import('chokidar');
|
|
40
|
+
this.watcher = chokidar.watch(watchPaths, {
|
|
41
|
+
// Ignore common junk files (simple patterns)
|
|
42
|
+
ignored: [
|
|
43
|
+
'**/.*', // Hidden files
|
|
44
|
+
'**/*.tmp', // Temp files
|
|
45
|
+
'**/*.sha1', // Checksum files
|
|
46
|
+
'**/*.sha256', // Checksum files
|
|
47
|
+
'**/*.md5', // Checksum files
|
|
48
|
+
],
|
|
49
|
+
// Don't watch too deep (Maven repos are usually 3-4 levels)
|
|
50
|
+
depth: 6,
|
|
51
|
+
// Don't trigger for files that already exist
|
|
52
|
+
ignoreInitial: true,
|
|
53
|
+
// Wait for files to finish writing before triggering
|
|
54
|
+
awaitWriteFinish: {
|
|
55
|
+
stabilityThreshold: 2000, // Wait 2 seconds after file stops changing
|
|
56
|
+
pollInterval: 100
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// SIMPLE: Just watch for new files being added
|
|
60
|
+
this.watcher
|
|
61
|
+
.on('add', (filePath) => {
|
|
62
|
+
// Only care about JAR and POM files
|
|
63
|
+
if (filePath.endsWith('.jar') || filePath.endsWith('.pom')) {
|
|
64
|
+
console.log(`📦 New file detected: ${path.basename(filePath)}`);
|
|
65
|
+
this.triggerReindex();
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.on('unlink', (filePath) => {
|
|
69
|
+
// Also watch for files being deleted
|
|
70
|
+
if (filePath.endsWith('.jar') || filePath.endsWith('.pom')) {
|
|
71
|
+
console.log(`🗑️ File removed: ${path.basename(filePath)}`);
|
|
72
|
+
this.triggerReindex();
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.on('error', (error) => {
|
|
76
|
+
console.log(`❌ Watcher error: ${error.message}`);
|
|
77
|
+
});
|
|
78
|
+
console.log('✅ File watcher started successfully');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stop watching
|
|
82
|
+
*/
|
|
83
|
+
stopWatching() {
|
|
84
|
+
if (this.watcher) {
|
|
85
|
+
this.watcher.close();
|
|
86
|
+
this.watcher = null;
|
|
87
|
+
console.log('🛑 File watcher stopped');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Trigger reindexing with debouncing (wait a bit for multiple changes)
|
|
92
|
+
*/
|
|
93
|
+
debounceTimer = null;
|
|
94
|
+
triggerReindex() {
|
|
95
|
+
// Clear any existing timer
|
|
96
|
+
if (this.debounceTimer) {
|
|
97
|
+
clearTimeout(this.debounceTimer);
|
|
98
|
+
}
|
|
99
|
+
// Wait 3 seconds after the last change before reindexing
|
|
100
|
+
this.debounceTimer = setTimeout(() => {
|
|
101
|
+
console.log('🔄 Changes detected - triggering reindex...');
|
|
102
|
+
this.onChangeCallback();
|
|
103
|
+
}, 3000);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* USAGE EXAMPLE:
|
|
108
|
+
*
|
|
109
|
+
* const watcher = new SimpleWatcher(() => {
|
|
110
|
+
* console.log('Time to reindex the repository!');
|
|
111
|
+
* // Put your reindexing code here
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* await watcher.startWatching(
|
|
115
|
+
* '/Users/tangcent/.m2/repository',
|
|
116
|
+
* '/Users/tangcent/.gradle/caches/modules-2/files-2.1'
|
|
117
|
+
* );
|
|
118
|
+
*
|
|
119
|
+
* // Later... when you want to stop
|
|
120
|
+
* watcher.stopWatching();
|
|
121
|
+
**/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maven-indexer-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "MCP server for indexing local Maven repository",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -46,10 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
49
|
+
"@types/semver": "^7.7.1",
|
|
49
50
|
"@types/xml2js": "^0.4.14",
|
|
50
51
|
"@types/yauzl": "^2.10.3",
|
|
51
52
|
"better-sqlite3": "^12.5.0",
|
|
52
53
|
"chokidar": "^5.0.0",
|
|
54
|
+
"semver": "^7.7.3",
|
|
53
55
|
"xml2js": "^0.6.2",
|
|
54
56
|
"yauzl": "^3.2.0",
|
|
55
57
|
"zod": "^4.1.13"
|