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 CHANGED
@@ -1,21 +1,28 @@
1
1
  # Maven Indexer MCP Server
2
2
 
3
- A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and Gradle cache (`~/.gradle/caches/modules-2/files-2.1`) to provide AI agents with tools to search for Java classes, method signatures, and source code.
3
+ [![npm version](https://img.shields.io/npm/v/maven-indexer-mcp.svg?style=flat)](https://www.npmjs.com/package/maven-indexer-mcp)
4
+ [![Tests](https://github.com/tangcent/maven-indexer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/tangcent/maven-indexer-mcp/actions/workflows/ci.yml)
4
5
 
5
- **Key Use Case**: While AI models are well-versed in popular public libraries (like Spring, Apache Commons, Guava), they often struggle with:
6
- 1. **Internal Company Packages**: Private libraries that are not public.
7
- 2. **Non-Well-Known Public Packages**: Niche or less popular open-source libraries.
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
- This server bridges that gap by allowing the AI to "read" your local dependencies, effectively giving it knowledge of your private and obscure libraries.
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
- * **Semantic Class Search**: Search for classes by name (e.g., `StringUtils`) or purpose (e.g., `JsonToXml`).
14
- * **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
15
- * **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
16
- * **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
17
- * **Real-time Monitoring**: Watches the repositories for changes (e.g., new `mvn install`) and automatically updates the index.
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": ["-y", "maven-indexer-mcp@latest"]
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 location (usually `~/.m2/repository`) and Gradle cache.
45
+ This will automatically download and run the latest version of the server. It will auto-detect your Maven repository
46
+ location (usually `~/.m2/repository`) and Gradle cache.
36
47
 
37
- ### Configuration (Optional)
48
+ ### MCP Client configuration
49
+
50
+ <details>
51
+ <summary>Cline</summary>
52
+
53
+ Follow <a href="https://docs.cline.bot/mcp/configuring-mcp-servers">Cline's MCP guide</a> and use the config provided
54
+ above.
55
+ </details>
56
+
57
+ <details>
58
+ <summary>Codex</summary>
59
+
60
+ Follow the <a href="https://github.com/openai/codex/blob/main/docs/advanced.md#model-context-protocol-mcp">configure MCP
61
+ guide</a> using the standard config from above.
62
+ </details>
63
+
64
+ <details>
65
+ <summary>Cursor</summary>
66
+
67
+ **Click the button to install:**
68
+
69
+ [![Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=maven-indexer&config=eyJjb21tYW5kIjoibnB4IC15IG1hdmVuLWluZGV4ZXItbWNwQGxhdGVzdCJ9)
70
+
71
+ **Or install manually:**
72
+
73
+ Go to `Cursor Settings` -> `MCP` -> `New MCP Server`. Use the config provided above.
74
+
75
+ </details>
76
+
77
+ <details>
78
+ <summary>JetBrains AI Assistant & Junie</summary>
79
+
80
+ Go to `Settings | Tools | AI Assistant | Model Context Protocol (MCP)` -> `Add`. Use the config provided above.
81
+ The same way `maven-indexer` can be configured for JetBrains Junie in `Settings | Tools | Junie | MCP Settings` ->`Add`.
82
+ Use the config provided above.
83
+ </details>
84
+
85
+ <details>
86
+ <summary>Kiro</summary>
87
+
88
+ In **Kiro Settings**, go to `Configure MCP` > `Open Workspace or User MCP Config` > Use the configuration snippet
89
+ provided above.
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
- If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to the configuration:
94
+ </details>
40
95
 
41
- * **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if your repository is in a non-standard location.
42
- * **`GRADLE_REPO_PATH`**: Absolute path to your Gradle cache (e.g., `/Users/yourname/.gradle/caches/modules-2/files-2.1`).
43
- * **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
44
- * **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server will attempt to use its bundled CFR version.
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": ["-y", "maven-indexer-mcp@latest"],
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. Clone the repository:
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. Install dependencies and build:
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. Use the absolute path in your config:
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
- * **`search_classes`**: Search for Java classes in the local Maven repository and Gradle caches.
96
- * **WHEN TO USE**:
97
- 1. **Internal/Private Code**: You need to find a class from a company-internal library.
98
- 2. **Obscure Libraries**: You are using a less common public library that the AI doesn't know well.
99
- 3. **Version Verification**: You need to check exactly which version of a class is present locally.
100
- * *Note*: For well-known libraries (e.g., standard Java lib, Spring), the AI likely knows the class structure already, so this tool is less critical.
101
- * **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this class imported from?".
102
- * Input: `className` (e.g., "StringUtils", "Json parser")
103
- * Output: List of matching classes with their artifacts.
104
- * **`get_class_details`**: Decompile and read the source code of external libraries/dependencies. **Use this instead of 'SearchCodebase' for classes that are imported but defined in JAR files.**
105
- * **Key Value**: "Don't guess what the internal library does—read the code."
106
- * **Tip**: Essential for internal/proprietary code where documentation is scarce or non-existent.
107
- * Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
108
- * Output: Method signatures, Javadocs, or full source code.
109
- * **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those with source code attached).
110
- * **`search_artifacts`**: Search for artifacts in Maven/Gradle caches by coordinate (groupId, artifactId).
111
- * **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class. Useful for finding SPI implementations in external libraries.
112
- * Input: `className` (e.g. "java.util.List")
113
- * Output: List of implementation/subclass names and their artifacts.
114
- * **`refresh_index`**: Trigger a re-scan of the Maven repository.
203
+ * **`search_classes`**: Search for Java classes in the local Maven repository and Gradle caches.
204
+ * **WHEN TO USE**:
205
+ 1. **Internal/Private Code**: You need to find a class from a company-internal library.
206
+ 2. **Obscure Libraries**: You are using a less common public library that the AI doesn't know well.
207
+ 3. **Version Verification**: You need to check exactly which version of a class is present locally.
208
+
209
+ * *Note*: For well-known libraries (e.g., standard Java lib, Spring), the AI likely knows the class structure
210
+ already, so this tool is less critical.
211
+ * **Examples**: "Show me the source of StringUtils", "What methods are available on DateTimeUtils?", "Where is this
212
+ class imported from?".
213
+ * Input: `className` (e.g., "StringUtils", "Json parser")
214
+ * Output: List of matching classes with their artifacts.
215
+ * **`get_class_details`**: Decompile and read the source code of external libraries/dependencies. **Use this instead
216
+ of 'SearchCodebase' for classes that are imported but defined in JAR files.**
217
+ * **Key Value**: "Don't guess what the internal library does—read the code."
218
+ * **Tip**: Essential for internal/proprietary code where documentation is scarce or non-existent.
219
+ * Input: `className` (required), `artifactId` (optional), `type` ("signatures", "docs", "source")
220
+ * Output: Method signatures, Javadocs, or full source code.
221
+ * **Note**: If `artifactId` is omitted, the tool automatically selects the best available artifact (preferring those
222
+ with source code attached).
223
+ * **`search_artifacts`**: Search for artifacts in Maven/Gradle caches by coordinate (groupId, artifactId).
224
+ * **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
225
+ Useful for finding SPI implementations in external libraries.
226
+ * Input: `className` (e.g. "java.util.List")
227
+ * Output: List of implementation/subclass names and their artifacts.
228
+ * **`refresh_index`**: Trigger a re-scan of the Maven repository.
115
229
 
116
230
  ## Development
117
231
 
118
- * **Run tests**: `npm test`
119
- * **Watch mode**: `npm run watch`
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 **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
+ 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
- // Strategy: 1. Prefer hasSource=true. 2. Prefer highest ID (likely newest).
62
- const artifacts = exactMatch.artifacts.sort((a, b) => {
63
- if (a.hasSource !== b.hasSource) {
64
- return a.hasSource ? -1 : 1; // source comes first
65
- }
66
- return b.id - a.id; // higher ID comes first
67
- });
68
- if (artifacts.length === 0) {
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 = artifacts[0];
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 **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.",
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 **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.",
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 **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.",
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: /(^|[\/\\])\../, // ignore dotfiles
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
- INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
123
- VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
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
- SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
148
- FROM artifacts
149
- WHERE is_indexed = 0
150
- `).all();
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(rootDir) {
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(rootDir, groupDir);
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
- await scanDir(rootDir);
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.includedPackages)) {
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({ className: info.className, parent: info.superClass, type: 'extends' });
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({ className: info.className, parent: iface, type: 'implements' });
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
- INSERT INTO classes_fts (artifact_id, class_name, simple_name)
364
- VALUES (?, ?, ?)
365
- `);
420
+ INSERT INTO classes_fts (artifact_id, class_name, simple_name)
421
+ VALUES (?, ?, ?)
422
+ `);
366
423
  const insertInheritance = db.prepare(`
367
- INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
368
- VALUES (?, ?, ?, ?)
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, patterns) {
396
- if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*'))
455
+ isPackageIncluded(className, normalizedPatterns) {
456
+ if (!normalizedPatterns || normalizedPatterns.length === 0)
397
457
  return true;
398
- for (const pattern of patterns) {
399
- if (pattern === '*')
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
- SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
423
- FROM artifacts
424
- WHERE group_id LIKE ? OR artifact_id LIKE ?
425
- LIMIT 50
426
- `).all(`%${query}%`, `%${query}%`);
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
- SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
442
- FROM classes_fts c
443
- JOIN artifacts a ON c.artifact_id = a.id
444
- WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
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
- `).all(regex, regex);
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
- SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
455
- FROM classes_fts c
456
- JOIN artifacts a ON c.artifact_id = a.id
457
- WHERE c.class_name LIKE ? OR c.simple_name LIKE ?
458
- LIMIT 100
459
- `).all(likePattern, likePattern);
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
- SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
472
- FROM classes_fts c
473
- JOIN artifacts a ON c.artifact_id = a.id
474
- WHERE c.classes_fts MATCH ?
475
- ORDER BY rank
476
- LIMIT 100
477
- `).all(query);
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
- WITH RECURSIVE hierarchy(class_name, artifact_id) AS (
520
- SELECT class_name, artifact_id FROM inheritance WHERE parent_class_name = ?
521
- UNION
522
- SELECT i.class_name, i.artifact_id FROM inheritance i JOIN hierarchy h ON i.parent_class_name = h.class_name
523
- )
524
- SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
525
- FROM hierarchy h
526
- JOIN artifacts a ON h.artifact_id = a.id
527
- LIMIT 100
528
- `).all(className);
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
- SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
567
- FROM artifacts WHERE id = ?
568
- `).get(id);
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
- SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
588
- FROM artifacts
589
- WHERE group_id = ? AND artifact_id = ? AND version = ?
590
- `).get(groupId, artifactId, version);
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.6",
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"