maven-indexer-mcp 1.0.6 → 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 +161 -47
- package/build/artifact_resolver.js +109 -0
- package/build/config.js +72 -0
- package/build/index.js +8 -13
- package/build/indexer.js +154 -79
- 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/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,8 @@ export class Indexer {
|
|
|
15
15
|
isIndexing = false;
|
|
16
16
|
watcher = null;
|
|
17
17
|
debounceTimer = null;
|
|
18
|
-
constructor() {
|
|
18
|
+
constructor() {
|
|
19
|
+
}
|
|
19
20
|
static getInstance() {
|
|
20
21
|
if (!Indexer.instance) {
|
|
21
22
|
Indexer.instance = new Indexer();
|
|
@@ -44,7 +45,13 @@ export class Indexer {
|
|
|
44
45
|
}
|
|
45
46
|
console.error(`Starting file watcher on ${pathsToWatch.join(', ')}...`);
|
|
46
47
|
this.watcher = chokidar.watch(pathsToWatch, {
|
|
47
|
-
ignored:
|
|
48
|
+
ignored: (p) => {
|
|
49
|
+
// Don't ignore the root paths themselves
|
|
50
|
+
if (pathsToWatch.includes(p))
|
|
51
|
+
return false;
|
|
52
|
+
// Ignore dotfiles/dotdirs
|
|
53
|
+
return path.basename(p).startsWith('.');
|
|
54
|
+
},
|
|
48
55
|
persistent: true,
|
|
49
56
|
ignoreInitial: true,
|
|
50
57
|
depth: 10 // Limit depth to avoid too much overhead? Standard maven repo depth is around 3-5
|
|
@@ -105,13 +112,13 @@ export class Indexer {
|
|
|
105
112
|
let artifacts = [];
|
|
106
113
|
if (repoPath && fsSync.existsSync(repoPath)) {
|
|
107
114
|
console.error(`Scanning Maven repo: ${repoPath}`);
|
|
108
|
-
const mavenArtifacts = await this.scanRepository(repoPath);
|
|
115
|
+
const mavenArtifacts = await this.scanRepository(repoPath, config.normalizedIncludedPackages);
|
|
109
116
|
console.error(`Found ${mavenArtifacts.length} Maven artifacts.`);
|
|
110
117
|
artifacts = artifacts.concat(mavenArtifacts);
|
|
111
118
|
}
|
|
112
119
|
if (gradleRepoPath && fsSync.existsSync(gradleRepoPath)) {
|
|
113
120
|
console.error(`Scanning Gradle repo: ${gradleRepoPath}`);
|
|
114
|
-
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath);
|
|
121
|
+
const gradleArtifacts = await this.scanGradleRepository(gradleRepoPath, config.normalizedIncludedPackages);
|
|
115
122
|
console.error(`Found ${gradleArtifacts.length} Gradle artifacts.`);
|
|
116
123
|
artifacts = artifacts.concat(gradleArtifacts);
|
|
117
124
|
}
|
|
@@ -119,9 +126,10 @@ export class Indexer {
|
|
|
119
126
|
// 2. Persist artifacts and determine what needs indexing
|
|
120
127
|
// We use is_indexed = 0 for new artifacts.
|
|
121
128
|
const insertArtifact = db.prepare(`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
INSERT
|
|
130
|
+
OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
|
|
131
|
+
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
|
|
132
|
+
`);
|
|
125
133
|
// Use a transaction only for the batch insert of artifacts
|
|
126
134
|
db.transaction(() => {
|
|
127
135
|
for (const art of artifacts) {
|
|
@@ -144,10 +152,10 @@ export class Indexer {
|
|
|
144
152
|
}
|
|
145
153
|
// 3. Find artifacts that need indexing (is_indexed = 0)
|
|
146
154
|
const artifactsToIndex = db.prepare(`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
156
|
+
FROM artifacts
|
|
157
|
+
WHERE is_indexed = 0
|
|
158
|
+
`).all();
|
|
151
159
|
console.error(`${artifactsToIndex.length} artifacts need indexing.`);
|
|
152
160
|
// 4. Scan JARs for classes and update DB
|
|
153
161
|
const CHUNK_SIZE = 50;
|
|
@@ -171,8 +179,11 @@ export class Indexer {
|
|
|
171
179
|
}
|
|
172
180
|
/**
|
|
173
181
|
* Recursively scans a directory for Maven artifacts (POM files).
|
|
182
|
+
*
|
|
183
|
+
* @param repoRoot The root directory of the Maven repository.
|
|
184
|
+
* @param normalizedPatterns List of normalized package patterns to include.
|
|
174
185
|
*/
|
|
175
|
-
async scanRepository(
|
|
186
|
+
async scanRepository(repoRoot, normalizedPatterns = []) {
|
|
176
187
|
const results = [];
|
|
177
188
|
const scanDir = async (dir) => {
|
|
178
189
|
let entries;
|
|
@@ -188,7 +199,7 @@ export class Indexer {
|
|
|
188
199
|
const artifactDir = path.dirname(dir);
|
|
189
200
|
const artifactId = path.basename(artifactDir);
|
|
190
201
|
const groupDir = path.dirname(artifactDir);
|
|
191
|
-
const relGroupPath = path.relative(
|
|
202
|
+
const relGroupPath = path.relative(repoRoot, groupDir);
|
|
192
203
|
const groupId = relGroupPath.split(path.sep).join('.');
|
|
193
204
|
if (groupId && artifactId && version && !groupId.startsWith('..')) {
|
|
194
205
|
const sourceJarPath = path.join(dir, `${artifactId}-${version}-sources.jar`);
|
|
@@ -213,14 +224,49 @@ export class Indexer {
|
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
};
|
|
216
|
-
|
|
227
|
+
const startDirs = this.getMavenStartDirs(repoRoot, normalizedPatterns);
|
|
228
|
+
console.error(`Scanning Maven directories: ${startDirs.join(', ')}`);
|
|
229
|
+
for (const startDir of startDirs) {
|
|
230
|
+
await scanDir(startDir);
|
|
231
|
+
}
|
|
217
232
|
return results;
|
|
218
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Calculates the starting directories for Maven scanning based on included packages.
|
|
236
|
+
*
|
|
237
|
+
* @param repoRoot The root of the Maven repository.
|
|
238
|
+
* @param normalizedPatterns The list of normalized included packages.
|
|
239
|
+
*/
|
|
240
|
+
getMavenStartDirs(repoRoot, normalizedPatterns) {
|
|
241
|
+
if (normalizedPatterns.length === 0) {
|
|
242
|
+
return [repoRoot];
|
|
243
|
+
}
|
|
244
|
+
return normalizedPatterns.map(p => path.join(repoRoot, p.split('.').join(path.sep)));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Checks if a group ID is included in the normalized patterns.
|
|
248
|
+
*
|
|
249
|
+
* @param groupId The group ID (e.g., "com.google.guava").
|
|
250
|
+
* @param normalizedPatterns The list of normalized patterns.
|
|
251
|
+
*/
|
|
252
|
+
isGroupIncluded(groupId, normalizedPatterns) {
|
|
253
|
+
if (!normalizedPatterns || normalizedPatterns.length === 0)
|
|
254
|
+
return true;
|
|
255
|
+
for (const pattern of normalizedPatterns) {
|
|
256
|
+
if (groupId === pattern || groupId.startsWith(pattern + '.')) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
219
262
|
/**
|
|
220
263
|
* Scans a Gradle cache directory for artifacts.
|
|
221
264
|
* Structure: group/artifact/version/hash/file
|
|
265
|
+
*
|
|
266
|
+
* @param rootDir The root directory of the Gradle cache (e.g., ~/.gradle/caches/modules-2/files-2.1).
|
|
267
|
+
* @param normalizedPatterns List of normalized package patterns to include.
|
|
222
268
|
*/
|
|
223
|
-
async scanGradleRepository(rootDir) {
|
|
269
|
+
async scanGradleRepository(rootDir, normalizedPatterns = []) {
|
|
224
270
|
const results = [];
|
|
225
271
|
// Helper to read directory safely
|
|
226
272
|
const readDirSafe = async (p) => {
|
|
@@ -236,6 +282,9 @@ export class Indexer {
|
|
|
236
282
|
if (!groupEntry.isDirectory())
|
|
237
283
|
continue;
|
|
238
284
|
const groupId = groupEntry.name;
|
|
285
|
+
if (!this.isGroupIncluded(groupId, normalizedPatterns)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
239
288
|
const groupPath = path.join(rootDir, groupId);
|
|
240
289
|
const artifactDirs = await readDirSafe(groupPath);
|
|
241
290
|
for (const artifactEntry of artifactDirs) {
|
|
@@ -302,7 +351,7 @@ export class Indexer {
|
|
|
302
351
|
await fs.access(jarPath);
|
|
303
352
|
}
|
|
304
353
|
catch {
|
|
305
|
-
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
354
|
+
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
306
355
|
// Or maybe it's a pom-only artifact.
|
|
307
356
|
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
308
357
|
return;
|
|
@@ -333,13 +382,21 @@ export class Indexer {
|
|
|
333
382
|
// Simple check to avoid module-info or invalid names
|
|
334
383
|
if (!info.className.includes('$') && info.className.length > 0) {
|
|
335
384
|
// Filter by includedPackages
|
|
336
|
-
if (this.isPackageIncluded(info.className, config.
|
|
385
|
+
if (this.isPackageIncluded(info.className, config.normalizedIncludedPackages)) {
|
|
337
386
|
classes.push(info.className);
|
|
338
387
|
if (info.superClass && info.superClass !== 'java.lang.Object') {
|
|
339
|
-
inheritance.push({
|
|
388
|
+
inheritance.push({
|
|
389
|
+
className: info.className,
|
|
390
|
+
parent: info.superClass,
|
|
391
|
+
type: 'extends'
|
|
392
|
+
});
|
|
340
393
|
}
|
|
341
394
|
for (const iface of info.interfaces) {
|
|
342
|
-
inheritance.push({
|
|
395
|
+
inheritance.push({
|
|
396
|
+
className: info.className,
|
|
397
|
+
parent: iface,
|
|
398
|
+
type: 'implements'
|
|
399
|
+
});
|
|
343
400
|
}
|
|
344
401
|
}
|
|
345
402
|
}
|
|
@@ -360,13 +417,13 @@ export class Indexer {
|
|
|
360
417
|
try {
|
|
361
418
|
db.transaction(() => {
|
|
362
419
|
const insertClass = db.prepare(`
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
420
|
+
INSERT INTO classes_fts (artifact_id, class_name, simple_name)
|
|
421
|
+
VALUES (?, ?, ?)
|
|
422
|
+
`);
|
|
366
423
|
const insertInheritance = db.prepare(`
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
424
|
+
INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
|
|
425
|
+
VALUES (?, ?, ?, ?)
|
|
426
|
+
`);
|
|
370
427
|
for (const cls of classes) {
|
|
371
428
|
const simpleName = cls.split('.').pop() || cls;
|
|
372
429
|
insertClass.run(artifact.id, cls, simpleName);
|
|
@@ -391,23 +448,17 @@ export class Indexer {
|
|
|
391
448
|
}
|
|
392
449
|
/**
|
|
393
450
|
* Checks if a class package is included in the configuration patterns.
|
|
451
|
+
*
|
|
452
|
+
* @param className The fully qualified class name.
|
|
453
|
+
* @param normalizedPatterns The list of normalized package patterns.
|
|
394
454
|
*/
|
|
395
|
-
isPackageIncluded(className,
|
|
396
|
-
if (
|
|
455
|
+
isPackageIncluded(className, normalizedPatterns) {
|
|
456
|
+
if (!normalizedPatterns || normalizedPatterns.length === 0)
|
|
397
457
|
return true;
|
|
398
|
-
for (const pattern of
|
|
399
|
-
|
|
458
|
+
for (const pattern of normalizedPatterns) {
|
|
459
|
+
// Match exact package or subpackage
|
|
460
|
+
if (className === pattern || className.startsWith(pattern + '.')) {
|
|
400
461
|
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
462
|
}
|
|
412
463
|
}
|
|
413
464
|
return false;
|
|
@@ -419,11 +470,11 @@ export class Indexer {
|
|
|
419
470
|
// Artifact coordinates search
|
|
420
471
|
const db = DB.getInstance();
|
|
421
472
|
const rows = db.prepare(`
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
473
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
474
|
+
FROM artifacts
|
|
475
|
+
WHERE group_id LIKE ?
|
|
476
|
+
OR artifact_id LIKE ? LIMIT 50
|
|
477
|
+
`).all(`%${query}%`, `%${query}%`);
|
|
427
478
|
return rows;
|
|
428
479
|
}
|
|
429
480
|
/**
|
|
@@ -438,12 +489,19 @@ export class Indexer {
|
|
|
438
489
|
// Regex search
|
|
439
490
|
const regex = classNamePattern.substring(6);
|
|
440
491
|
rows = db.prepare(`
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
492
|
+
SELECT c.class_name,
|
|
493
|
+
c.simple_name,
|
|
494
|
+
a.id,
|
|
495
|
+
a.group_id,
|
|
496
|
+
a.artifact_id,
|
|
497
|
+
a.version,
|
|
498
|
+
a.abspath,
|
|
499
|
+
a.has_source
|
|
500
|
+
FROM classes_fts c
|
|
501
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
502
|
+
WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
|
|
445
503
|
LIMIT 100
|
|
446
|
-
|
|
504
|
+
`).all(regex, regex);
|
|
447
505
|
}
|
|
448
506
|
else if (classNamePattern.includes('*') || classNamePattern.includes('?')) {
|
|
449
507
|
// Glob-style search (using LIKE for standard wildcards)
|
|
@@ -451,12 +509,19 @@ export class Indexer {
|
|
|
451
509
|
// But standard glob is * and ?
|
|
452
510
|
const likePattern = classNamePattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
453
511
|
rows = db.prepare(`
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
512
|
+
SELECT c.class_name,
|
|
513
|
+
c.simple_name,
|
|
514
|
+
a.id,
|
|
515
|
+
a.group_id,
|
|
516
|
+
a.artifact_id,
|
|
517
|
+
a.version,
|
|
518
|
+
a.abspath,
|
|
519
|
+
a.has_source
|
|
520
|
+
FROM classes_fts c
|
|
521
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
522
|
+
WHERE c.class_name LIKE ?
|
|
523
|
+
OR c.simple_name LIKE ? LIMIT 100
|
|
524
|
+
`).all(likePattern, likePattern);
|
|
460
525
|
}
|
|
461
526
|
else {
|
|
462
527
|
// Use FTS for smart matching
|
|
@@ -468,13 +533,19 @@ export class Indexer {
|
|
|
468
533
|
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
469
534
|
: `"${escapedPattern}"*`;
|
|
470
535
|
rows = db.prepare(`
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
536
|
+
SELECT c.class_name,
|
|
537
|
+
c.simple_name,
|
|
538
|
+
a.id,
|
|
539
|
+
a.group_id,
|
|
540
|
+
a.artifact_id,
|
|
541
|
+
a.version,
|
|
542
|
+
a.abspath,
|
|
543
|
+
a.has_source
|
|
544
|
+
FROM classes_fts c
|
|
545
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
546
|
+
WHERE c.classes_fts MATCH ?
|
|
547
|
+
ORDER BY rank LIMIT 100
|
|
548
|
+
`).all(query);
|
|
478
549
|
}
|
|
479
550
|
// Group by class name
|
|
480
551
|
const resultMap = new Map();
|
|
@@ -516,16 +587,17 @@ export class Indexer {
|
|
|
516
587
|
}
|
|
517
588
|
// Recursive search for all implementations/subclasses
|
|
518
589
|
const rows = db.prepare(`
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
590
|
+
WITH RECURSIVE hierarchy(class_name, artifact_id) AS (SELECT class_name, artifact_id
|
|
591
|
+
FROM inheritance
|
|
592
|
+
WHERE parent_class_name = ?
|
|
593
|
+
UNION
|
|
594
|
+
SELECT i.class_name, i.artifact_id
|
|
595
|
+
FROM inheritance i
|
|
596
|
+
JOIN hierarchy h ON i.parent_class_name = h.class_name)
|
|
597
|
+
SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
598
|
+
FROM hierarchy h
|
|
599
|
+
JOIN artifacts a ON h.artifact_id = a.id LIMIT 100
|
|
600
|
+
`).all(className);
|
|
529
601
|
console.error(`Searching implementations for ${className}: found ${rows.length} rows.`);
|
|
530
602
|
if (rows.length === 0) {
|
|
531
603
|
// Fallback: Try searching without recursion to see if direct children exist
|
|
@@ -563,9 +635,10 @@ export class Indexer {
|
|
|
563
635
|
getArtifactById(id) {
|
|
564
636
|
const db = DB.getInstance();
|
|
565
637
|
const row = db.prepare(`
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
638
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
639
|
+
FROM artifacts
|
|
640
|
+
WHERE id = ?
|
|
641
|
+
`).get(id);
|
|
569
642
|
if (row) {
|
|
570
643
|
return {
|
|
571
644
|
id: row.id,
|
|
@@ -584,10 +657,12 @@ export class Indexer {
|
|
|
584
657
|
getArtifactByCoordinate(groupId, artifactId, version) {
|
|
585
658
|
const db = DB.getInstance();
|
|
586
659
|
const row = db.prepare(`
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
660
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
661
|
+
FROM artifacts
|
|
662
|
+
WHERE group_id = ?
|
|
663
|
+
AND artifact_id = ?
|
|
664
|
+
AND version = ?
|
|
665
|
+
`).get(groupId, artifactId, version);
|
|
591
666
|
if (row) {
|
|
592
667
|
return {
|
|
593
668
|
id: row.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maven-indexer-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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"
|