maven-indexer-mcp 1.0.0
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/LICENSE +13 -0
- package/README.md +99 -0
- package/build/config.js +99 -0
- package/build/db/index.js +53 -0
- package/build/index.js +174 -0
- package/build/indexer.js +322 -0
- package/build/source_parser.js +127 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2024 tangcent
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
4
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
5
|
+
copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
8
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
9
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
10
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
11
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
12
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
13
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Maven Indexer MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that indexes your local Maven repository (`~/.m2/repository`) and provides AI agents with tools to search for Java classes, method signatures, and source code.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
* **Semantic Class Search**: Search for classes by name (e.g., `StringUtils`) or purpose (e.g., `JsonToXml`).
|
|
8
|
+
* **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
|
|
9
|
+
* **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
|
|
10
|
+
* **Real-time Monitoring**: Watches the Maven repository for changes (e.g., new `mvn install`) and automatically updates the index.
|
|
11
|
+
* **Efficient Persistence**: Uses SQLite to store the index, handling large repositories with minimal memory footprint.
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
Add the following config to your MCP client:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"maven-indexer": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "maven-indexer-mcp@latest"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This will automatically download and run the latest version of the server. It will auto-detect your Maven repository location (usually `~/.m2/repository`).
|
|
29
|
+
|
|
30
|
+
### Configuration (Optional)
|
|
31
|
+
|
|
32
|
+
If the auto-detection fails, or if you want to filter which packages are indexed, you can add environment variables to the configuration:
|
|
33
|
+
|
|
34
|
+
* **`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.
|
|
35
|
+
* **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
|
|
36
|
+
|
|
37
|
+
Example with optional configuration:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"maven-indexer": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "maven-indexer-mcp@latest"],
|
|
45
|
+
"env": {
|
|
46
|
+
"MAVEN_REPO": "/Users/yourname/.m2/repository",
|
|
47
|
+
"INCLUDED_PACKAGES": "com.mycompany.*"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Local Development
|
|
55
|
+
|
|
56
|
+
If you prefer to run from source:
|
|
57
|
+
|
|
58
|
+
1. Clone the repository:
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/tangcent/maven-indexer-mcp.git
|
|
61
|
+
cd maven-indexer-mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
2. Install dependencies and build:
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
3. Use the absolute path in your config:
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"maven-indexer": {
|
|
75
|
+
"command": "node",
|
|
76
|
+
"args": ["/absolute/path/to/maven-indexer-mcp/build/index.js"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Available Tools
|
|
83
|
+
|
|
84
|
+
* **`search_classes`**: Search for Java classes.
|
|
85
|
+
* Input: `className` (e.g., "StringUtils", "Json parser")
|
|
86
|
+
* Output: List of matching classes with their artifacts.
|
|
87
|
+
* **`get_class_details`**: Get detailed information about a class.
|
|
88
|
+
* Input: `className`, `artifactId`, `type` ("signatures", "docs", "source")
|
|
89
|
+
* Output: Method signatures, Javadocs, or full source code.
|
|
90
|
+
* **`search_artifacts`**: Search for artifacts by coordinate (groupId, artifactId).
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
* **Run tests**: `npm test`
|
|
95
|
+
* **Watch mode**: `npm run watch`
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
ISC
|
package/build/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import xml2js from 'xml2js';
|
|
5
|
+
export class Config {
|
|
6
|
+
static instance;
|
|
7
|
+
localRepository = "";
|
|
8
|
+
javaBinary = "java";
|
|
9
|
+
includedPackages = ["*"];
|
|
10
|
+
constructor() { }
|
|
11
|
+
static async getInstance() {
|
|
12
|
+
if (!Config.instance) {
|
|
13
|
+
Config.instance = new Config();
|
|
14
|
+
await Config.instance.load();
|
|
15
|
+
}
|
|
16
|
+
return Config.instance;
|
|
17
|
+
}
|
|
18
|
+
// For testing
|
|
19
|
+
static reset() {
|
|
20
|
+
Config.instance = undefined;
|
|
21
|
+
}
|
|
22
|
+
async load() {
|
|
23
|
+
let repoPath = null;
|
|
24
|
+
// 1. Check environment variable
|
|
25
|
+
if (process.env.MAVEN_REPO_PATH) {
|
|
26
|
+
repoPath = process.env.MAVEN_REPO_PATH;
|
|
27
|
+
}
|
|
28
|
+
else if (process.env.MAVEN_REPO) {
|
|
29
|
+
repoPath = process.env.MAVEN_REPO;
|
|
30
|
+
}
|
|
31
|
+
// 2. Try user settings
|
|
32
|
+
if (!repoPath) {
|
|
33
|
+
const homeDir = os.homedir();
|
|
34
|
+
const userSettingsPath = path.join(homeDir, '.m2', 'settings.xml');
|
|
35
|
+
if (await this.fileExists(userSettingsPath)) {
|
|
36
|
+
repoPath = await this.parseSettings(userSettingsPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 3. Try global settings if not found
|
|
40
|
+
if (!repoPath && process.env.M2_HOME) {
|
|
41
|
+
const globalSettingsPath = path.join(process.env.M2_HOME, 'conf', 'settings.xml');
|
|
42
|
+
if (await this.fileExists(globalSettingsPath)) {
|
|
43
|
+
repoPath = await this.parseSettings(globalSettingsPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// 4. Default
|
|
47
|
+
if (!repoPath) {
|
|
48
|
+
repoPath = path.join(os.homedir(), '.m2', 'repository');
|
|
49
|
+
}
|
|
50
|
+
this.localRepository = repoPath;
|
|
51
|
+
// Load Java Path
|
|
52
|
+
if (process.env.JAVA_HOME) {
|
|
53
|
+
this.javaBinary = path.join(process.env.JAVA_HOME, 'bin', 'java');
|
|
54
|
+
}
|
|
55
|
+
// Allow explicit override
|
|
56
|
+
if (process.env.JAVA_PATH) {
|
|
57
|
+
this.javaBinary = process.env.JAVA_PATH;
|
|
58
|
+
}
|
|
59
|
+
// Load Included Packages
|
|
60
|
+
if (process.env.INCLUDED_PACKAGES) {
|
|
61
|
+
this.includedPackages = process.env.INCLUDED_PACKAGES.split(',')
|
|
62
|
+
.map(p => p.trim())
|
|
63
|
+
.filter(p => p.length > 0);
|
|
64
|
+
}
|
|
65
|
+
// Log to stderr so it doesn't interfere with MCP protocol on stdout
|
|
66
|
+
console.error(`Using local repository: ${this.localRepository}`);
|
|
67
|
+
console.error(`Using Java binary: ${this.javaBinary}`);
|
|
68
|
+
console.error(`Included packages: ${JSON.stringify(this.includedPackages)}`);
|
|
69
|
+
}
|
|
70
|
+
getJavapPath() {
|
|
71
|
+
if (this.javaBinary === 'java')
|
|
72
|
+
return 'javap';
|
|
73
|
+
const dir = path.dirname(this.javaBinary);
|
|
74
|
+
return path.join(dir, 'javap');
|
|
75
|
+
}
|
|
76
|
+
async fileExists(filePath) {
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(filePath);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async parseSettings(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
88
|
+
const parser = new xml2js.Parser();
|
|
89
|
+
const result = await parser.parseStringPromise(content);
|
|
90
|
+
if (result.settings && result.settings.localRepository && result.settings.localRepository[0]) {
|
|
91
|
+
return result.settings.localRepository[0];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error(`Failed to parse ${filePath}:`, error);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export class DB {
|
|
4
|
+
static instance;
|
|
5
|
+
db;
|
|
6
|
+
constructor() {
|
|
7
|
+
// Check environment variable for DB path (useful for testing)
|
|
8
|
+
const dbName = process.env.DB_FILE || 'maven-index.sqlite';
|
|
9
|
+
const dbPath = path.join(process.cwd(), dbName);
|
|
10
|
+
this.db = new Database(dbPath);
|
|
11
|
+
this.initSchema();
|
|
12
|
+
}
|
|
13
|
+
static getInstance() {
|
|
14
|
+
if (!DB.instance) {
|
|
15
|
+
DB.instance = new DB();
|
|
16
|
+
}
|
|
17
|
+
return DB.instance;
|
|
18
|
+
}
|
|
19
|
+
initSchema() {
|
|
20
|
+
this.db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
group_id TEXT NOT NULL,
|
|
24
|
+
artifact_id TEXT NOT NULL,
|
|
25
|
+
version TEXT NOT NULL,
|
|
26
|
+
abspath TEXT NOT NULL,
|
|
27
|
+
has_source INTEGER DEFAULT 0,
|
|
28
|
+
UNIQUE(group_id, artifact_id, version)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS classes_fts USING fts5(
|
|
32
|
+
artifact_id UNINDEXED,
|
|
33
|
+
class_name, -- Fully qualified name
|
|
34
|
+
simple_name, -- Just the class name
|
|
35
|
+
tokenize="trigram"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Helper table to track indexed artifacts to avoid re-indexing unchanged ones (simplification: just track ID)
|
|
39
|
+
CREATE TABLE IF NOT EXISTS indexed_artifacts (
|
|
40
|
+
artifact_id INTEGER PRIMARY KEY
|
|
41
|
+
);
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
getDb() {
|
|
45
|
+
return this.db;
|
|
46
|
+
}
|
|
47
|
+
prepare(sql) {
|
|
48
|
+
return this.db.prepare(sql);
|
|
49
|
+
}
|
|
50
|
+
transaction(fn) {
|
|
51
|
+
return this.db.transaction(fn)();
|
|
52
|
+
}
|
|
53
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { Indexer } from "./indexer.js";
|
|
7
|
+
import { SourceParser } from "./source_parser.js";
|
|
8
|
+
const server = new Server({
|
|
9
|
+
name: "maven-indexer",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
}, {
|
|
12
|
+
capabilities: {
|
|
13
|
+
tools: {},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
// Start indexing in the background
|
|
17
|
+
const indexer = Indexer.getInstance();
|
|
18
|
+
// We trigger indexing but don't await it so server can start
|
|
19
|
+
indexer.index().then(() => {
|
|
20
|
+
// Start watching for changes after initial index
|
|
21
|
+
return indexer.startWatch();
|
|
22
|
+
}).catch(err => console.error("Initial indexing failed:", err));
|
|
23
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
24
|
+
return {
|
|
25
|
+
tools: [
|
|
26
|
+
{
|
|
27
|
+
name: "search_artifacts",
|
|
28
|
+
description: "Search for artifacts in the local Maven repository",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
query: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Search query (groupId, artifactId, or keyword)",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["query"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "search_classes",
|
|
42
|
+
description: "Search for Java classes. Can be used to find classes by name or to find classes for a specific purpose (by searching keywords in class names).",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
className: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml').",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ["className"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "get_class_details",
|
|
56
|
+
description: "Get details about a specific class from an artifact, including method signatures and javadocs (if source is available).",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
className: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Fully qualified class name",
|
|
63
|
+
},
|
|
64
|
+
artifactId: {
|
|
65
|
+
type: "number",
|
|
66
|
+
description: "The internal ID of the artifact (returned by search_classes)",
|
|
67
|
+
},
|
|
68
|
+
type: {
|
|
69
|
+
type: "string",
|
|
70
|
+
enum: ["signatures", "docs", "source"],
|
|
71
|
+
description: "Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code).",
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
required: ["className", "artifactId", "type"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "refresh_index",
|
|
79
|
+
description: "Trigger a re-scan of the Maven repository",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
89
|
+
if (request.params.name === "search_artifacts") {
|
|
90
|
+
const query = String(request.params.arguments?.query);
|
|
91
|
+
const matches = indexer.search(query);
|
|
92
|
+
// Limit results to avoid overflow
|
|
93
|
+
const limitedMatches = matches.slice(0, 20);
|
|
94
|
+
const text = limitedMatches.length > 0
|
|
95
|
+
? limitedMatches.map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
|
|
96
|
+
: "No artifacts found matching the query.";
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Found ${matches.length} matches${matches.length > 20 ? ' (showing first 20)' : ''}:\n${text}`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (request.params.name === "search_classes") {
|
|
107
|
+
const className = String(request.params.arguments?.className);
|
|
108
|
+
const matches = indexer.searchClass(className);
|
|
109
|
+
const text = matches.length > 0
|
|
110
|
+
? matches.map(m => {
|
|
111
|
+
// Group by artifact ID to allow easy selection
|
|
112
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
|
|
113
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
114
|
+
return `Class: ${m.className}\n ${artifacts}${more}`;
|
|
115
|
+
}).join("\n\n")
|
|
116
|
+
: "No classes found matching the query. Try different keywords.";
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text }]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (request.params.name === "get_class_details") {
|
|
122
|
+
const className = String(request.params.arguments?.className);
|
|
123
|
+
const artifactId = Number(request.params.arguments?.artifactId);
|
|
124
|
+
const type = String(request.params.arguments?.type);
|
|
125
|
+
const artifact = indexer.getArtifactById(artifactId);
|
|
126
|
+
if (!artifact) {
|
|
127
|
+
return { content: [{ type: "text", text: "Artifact not found." }] };
|
|
128
|
+
}
|
|
129
|
+
let jarPath;
|
|
130
|
+
if (type === 'signatures') {
|
|
131
|
+
// Use Main JAR for signatures (javap)
|
|
132
|
+
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Use Source JAR for docs and full source
|
|
136
|
+
if (!artifact.hasSource) {
|
|
137
|
+
return { content: [{ type: "text", text: `Artifact ${artifact.groupId}:${artifact.artifactId}:${artifact.version} does not have a sources jar available locally.` }] };
|
|
138
|
+
}
|
|
139
|
+
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const detail = await SourceParser.getClassDetail(jarPath, className, type);
|
|
143
|
+
if (!detail) {
|
|
144
|
+
return { content: [{ type: "text", text: `Class ${className} not found in ${type === 'signatures' ? 'artifact' : 'sources'} of ${artifact.artifactId}.` }] };
|
|
145
|
+
}
|
|
146
|
+
let resultText = `Class: ${detail.className}\n\n`;
|
|
147
|
+
if (type === 'source') {
|
|
148
|
+
resultText += "```java\n" + detail.source + "\n```";
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (detail.doc) {
|
|
152
|
+
resultText += "Documentation:\n" + detail.doc + "\n\n";
|
|
153
|
+
}
|
|
154
|
+
if (detail.signatures) {
|
|
155
|
+
resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { content: [{ type: "text", text: resultText }] };
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (request.params.name === "refresh_index") {
|
|
165
|
+
// Re-run index
|
|
166
|
+
indexer.index().catch(console.error);
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: "text", text: "Index refresh started." }]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
throw new Error("Tool not found");
|
|
172
|
+
});
|
|
173
|
+
const transport = new StdioServerTransport();
|
|
174
|
+
await server.connect(transport);
|
package/build/indexer.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yauzl from 'yauzl';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import { Config } from './config.js';
|
|
7
|
+
import { DB } from './db/index.js';
|
|
8
|
+
export class Indexer {
|
|
9
|
+
static instance;
|
|
10
|
+
isIndexing = false;
|
|
11
|
+
watcher = null;
|
|
12
|
+
debounceTimer = null;
|
|
13
|
+
constructor() { }
|
|
14
|
+
static getInstance() {
|
|
15
|
+
if (!Indexer.instance) {
|
|
16
|
+
Indexer.instance = new Indexer();
|
|
17
|
+
}
|
|
18
|
+
return Indexer.instance;
|
|
19
|
+
}
|
|
20
|
+
async startWatch() {
|
|
21
|
+
const config = await Config.getInstance();
|
|
22
|
+
const repoPath = config.localRepository;
|
|
23
|
+
if (!repoPath || !fsSync.existsSync(repoPath)) {
|
|
24
|
+
console.error("Repository path not found, skipping watch mode.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (this.watcher) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.error(`Starting file watcher on ${repoPath}...`);
|
|
31
|
+
this.watcher = chokidar.watch(repoPath, {
|
|
32
|
+
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
33
|
+
persistent: true,
|
|
34
|
+
ignoreInitial: true,
|
|
35
|
+
depth: 10 // Limit depth to avoid too much overhead? Standard maven repo depth is around 3-5
|
|
36
|
+
});
|
|
37
|
+
const onChange = () => {
|
|
38
|
+
if (this.debounceTimer)
|
|
39
|
+
clearTimeout(this.debounceTimer);
|
|
40
|
+
this.debounceTimer = setTimeout(() => {
|
|
41
|
+
console.error("Repository change detected. Triggering re-index...");
|
|
42
|
+
this.index().catch(console.error);
|
|
43
|
+
}, 5000); // Debounce for 5 seconds
|
|
44
|
+
};
|
|
45
|
+
this.watcher
|
|
46
|
+
.on('add', (path) => {
|
|
47
|
+
if (path.endsWith('.jar') || path.endsWith('.pom'))
|
|
48
|
+
onChange();
|
|
49
|
+
})
|
|
50
|
+
.on('unlink', (path) => {
|
|
51
|
+
if (path.endsWith('.jar') || path.endsWith('.pom'))
|
|
52
|
+
onChange();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async index() {
|
|
56
|
+
if (this.isIndexing)
|
|
57
|
+
return;
|
|
58
|
+
this.isIndexing = true;
|
|
59
|
+
console.error("Starting index...");
|
|
60
|
+
const config = await Config.getInstance();
|
|
61
|
+
const repoPath = config.localRepository;
|
|
62
|
+
const db = DB.getInstance();
|
|
63
|
+
if (!repoPath) {
|
|
64
|
+
console.error("No local repository path found.");
|
|
65
|
+
this.isIndexing = false;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
// 1. Scan for artifacts
|
|
70
|
+
console.error("Scanning repository structure...");
|
|
71
|
+
const artifacts = await this.scanRepository(repoPath);
|
|
72
|
+
console.error(`Found ${artifacts.length} artifacts on disk.`);
|
|
73
|
+
// 2. Persist artifacts and determine what needs indexing
|
|
74
|
+
const artifactsToIndex = [];
|
|
75
|
+
db.transaction(() => {
|
|
76
|
+
const insertArtifact = db.prepare(`
|
|
77
|
+
INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source)
|
|
78
|
+
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource)
|
|
79
|
+
`);
|
|
80
|
+
const selectId = db.prepare(`
|
|
81
|
+
SELECT id FROM artifacts
|
|
82
|
+
WHERE group_id = @groupId AND artifact_id = @artifactId AND version = @version
|
|
83
|
+
`);
|
|
84
|
+
const checkIndexed = db.prepare(`
|
|
85
|
+
SELECT 1 FROM indexed_artifacts WHERE artifact_id = ?
|
|
86
|
+
`);
|
|
87
|
+
for (const art of artifacts) {
|
|
88
|
+
insertArtifact.run({
|
|
89
|
+
...art,
|
|
90
|
+
hasSource: art.hasSource ? 1 : 0
|
|
91
|
+
});
|
|
92
|
+
const row = selectId.get({
|
|
93
|
+
groupId: art.groupId,
|
|
94
|
+
artifactId: art.artifactId,
|
|
95
|
+
version: art.version
|
|
96
|
+
});
|
|
97
|
+
if (row) {
|
|
98
|
+
art.id = row.id;
|
|
99
|
+
const isIndexed = checkIndexed.get(art.id);
|
|
100
|
+
if (!isIndexed) {
|
|
101
|
+
artifactsToIndex.push(art);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
console.error(`${artifactsToIndex.length} artifacts need indexing.`);
|
|
107
|
+
// 3. Scan JARs for classes and update DB
|
|
108
|
+
const CHUNK_SIZE = 50;
|
|
109
|
+
let processedCount = 0;
|
|
110
|
+
for (let i = 0; i < artifactsToIndex.length; i += CHUNK_SIZE) {
|
|
111
|
+
const chunk = artifactsToIndex.slice(i, i + CHUNK_SIZE);
|
|
112
|
+
await Promise.all(chunk.map(artifact => this.indexArtifactClasses(artifact)));
|
|
113
|
+
processedCount += chunk.length;
|
|
114
|
+
if (processedCount % 100 === 0) {
|
|
115
|
+
console.error(`Processed ${processedCount}/${artifactsToIndex.length} artifacts...`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
console.error(`Indexing complete.`);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error("Indexing failed", e);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.isIndexing = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async scanRepository(rootDir) {
|
|
128
|
+
const results = [];
|
|
129
|
+
const scanDir = async (dir) => {
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const pomFiles = entries.filter(e => e.isFile() && e.name.endsWith('.pom'));
|
|
138
|
+
if (pomFiles.length > 0) {
|
|
139
|
+
const version = path.basename(dir);
|
|
140
|
+
const artifactDir = path.dirname(dir);
|
|
141
|
+
const artifactId = path.basename(artifactDir);
|
|
142
|
+
const groupDir = path.dirname(artifactDir);
|
|
143
|
+
const relGroupPath = path.relative(rootDir, groupDir);
|
|
144
|
+
const groupId = relGroupPath.split(path.sep).join('.');
|
|
145
|
+
if (groupId && artifactId && version && !groupId.startsWith('..')) {
|
|
146
|
+
const sourceJarPath = path.join(dir, `${artifactId}-${version}-sources.jar`);
|
|
147
|
+
// Use sync check for speed in this context or cache it
|
|
148
|
+
const hasSource = fsSync.existsSync(sourceJarPath);
|
|
149
|
+
results.push({
|
|
150
|
+
id: 0, // Placeholder
|
|
151
|
+
groupId,
|
|
152
|
+
artifactId,
|
|
153
|
+
version,
|
|
154
|
+
abspath: dir,
|
|
155
|
+
hasSource
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
if (entry.name.startsWith('.'))
|
|
163
|
+
continue;
|
|
164
|
+
await scanDir(path.join(dir, entry.name));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
await scanDir(rootDir);
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
async indexArtifactClasses(artifact) {
|
|
172
|
+
const jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
173
|
+
const db = DB.getInstance();
|
|
174
|
+
const config = await Config.getInstance();
|
|
175
|
+
try {
|
|
176
|
+
await fs.access(jarPath);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
180
|
+
// Or maybe it's a pom-only artifact.
|
|
181
|
+
db.prepare('INSERT OR IGNORE INTO indexed_artifacts (artifact_id) VALUES (?)').run(artifact.id);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
|
|
186
|
+
if (err || !zipfile) {
|
|
187
|
+
resolve();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const classes = [];
|
|
191
|
+
zipfile.readEntry();
|
|
192
|
+
zipfile.on('entry', (entry) => {
|
|
193
|
+
if (entry.fileName.endsWith('.class')) {
|
|
194
|
+
// Convert path/to/MyClass.class -> path.to.MyClass
|
|
195
|
+
const className = entry.fileName.slice(0, -6).replace(/\//g, '.');
|
|
196
|
+
// Simple check to avoid module-info or invalid names
|
|
197
|
+
if (!className.includes('$') && className.length > 0) {
|
|
198
|
+
// Filter by includedPackages
|
|
199
|
+
if (this.isPackageIncluded(className, config.includedPackages)) {
|
|
200
|
+
classes.push(className);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
zipfile.readEntry();
|
|
205
|
+
});
|
|
206
|
+
zipfile.on('end', () => {
|
|
207
|
+
// Batch insert classes
|
|
208
|
+
try {
|
|
209
|
+
db.transaction(() => {
|
|
210
|
+
const insertClass = db.prepare(`
|
|
211
|
+
INSERT INTO classes_fts (artifact_id, class_name, simple_name)
|
|
212
|
+
VALUES (?, ?, ?)
|
|
213
|
+
`);
|
|
214
|
+
for (const cls of classes) {
|
|
215
|
+
const simpleName = cls.split('.').pop() || cls;
|
|
216
|
+
insertClass.run(artifact.id, cls, simpleName);
|
|
217
|
+
}
|
|
218
|
+
db.prepare('INSERT OR IGNORE INTO indexed_artifacts (artifact_id) VALUES (?)').run(artifact.id);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
console.error(`Failed to insert classes for ${artifact.groupId}:${artifact.artifactId}`, e);
|
|
223
|
+
}
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
zipfile.on('error', () => {
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
isPackageIncluded(className, patterns) {
|
|
233
|
+
if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*'))
|
|
234
|
+
return true;
|
|
235
|
+
for (const pattern of patterns) {
|
|
236
|
+
if (pattern === '*')
|
|
237
|
+
return true;
|
|
238
|
+
if (pattern.endsWith('.*')) {
|
|
239
|
+
const prefix = pattern.slice(0, -2); // "com.example"
|
|
240
|
+
// Match exact package or subpackage
|
|
241
|
+
if (className === prefix || className.startsWith(prefix + '.'))
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Exact match
|
|
246
|
+
if (className === pattern)
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
search(query) {
|
|
253
|
+
// Artifact coordinates search
|
|
254
|
+
const db = DB.getInstance();
|
|
255
|
+
const rows = db.prepare(`
|
|
256
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
257
|
+
FROM artifacts
|
|
258
|
+
WHERE group_id LIKE ? OR artifact_id LIKE ?
|
|
259
|
+
LIMIT 50
|
|
260
|
+
`).all(`%${query}%`, `%${query}%`);
|
|
261
|
+
return rows;
|
|
262
|
+
}
|
|
263
|
+
searchClass(classNamePattern) {
|
|
264
|
+
const db = DB.getInstance();
|
|
265
|
+
// Use FTS for smart matching
|
|
266
|
+
// If pattern has no spaces, we assume it's a prefix or exact match query
|
|
267
|
+
// "String" -> match "String" in simple_name or class_name
|
|
268
|
+
const query = `"${classNamePattern}"* OR ${classNamePattern}`;
|
|
269
|
+
try {
|
|
270
|
+
const rows = db.prepare(`
|
|
271
|
+
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
272
|
+
FROM classes_fts c
|
|
273
|
+
JOIN artifacts a ON c.artifact_id = a.id
|
|
274
|
+
WHERE c.classes_fts MATCH ?
|
|
275
|
+
ORDER BY rank
|
|
276
|
+
LIMIT 100
|
|
277
|
+
`).all(query);
|
|
278
|
+
// Group by class name
|
|
279
|
+
const resultMap = new Map();
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
const art = {
|
|
282
|
+
id: row.id,
|
|
283
|
+
groupId: row.group_id,
|
|
284
|
+
artifactId: row.artifact_id,
|
|
285
|
+
version: row.version,
|
|
286
|
+
abspath: row.abspath,
|
|
287
|
+
hasSource: Boolean(row.has_source)
|
|
288
|
+
};
|
|
289
|
+
if (!resultMap.has(row.class_name)) {
|
|
290
|
+
resultMap.set(row.class_name, []);
|
|
291
|
+
}
|
|
292
|
+
resultMap.get(row.class_name).push(art);
|
|
293
|
+
}
|
|
294
|
+
return Array.from(resultMap.entries()).map(([className, artifacts]) => ({
|
|
295
|
+
className,
|
|
296
|
+
artifacts
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
console.error("Search failed", e);
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
getArtifactById(id) {
|
|
305
|
+
const db = DB.getInstance();
|
|
306
|
+
const row = db.prepare(`
|
|
307
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
308
|
+
FROM artifacts WHERE id = ?
|
|
309
|
+
`).get(id);
|
|
310
|
+
if (row) {
|
|
311
|
+
return {
|
|
312
|
+
id: row.id,
|
|
313
|
+
groupId: row.groupId,
|
|
314
|
+
artifactId: row.artifactId,
|
|
315
|
+
version: row.version,
|
|
316
|
+
abspath: row.abspath,
|
|
317
|
+
hasSource: Boolean(row.hasSource)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import yauzl from 'yauzl';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { Config } from './config.js';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export class SourceParser {
|
|
7
|
+
static async getClassDetail(jarPath, className, type) {
|
|
8
|
+
if (type === 'signatures') {
|
|
9
|
+
return this.getSignaturesWithJavap(jarPath, className);
|
|
10
|
+
}
|
|
11
|
+
// className: com.example.MyClass
|
|
12
|
+
// internalPath: com/example/MyClass.java
|
|
13
|
+
const internalPath = className.replace(/\./g, '/') + '.java';
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
|
|
16
|
+
if (err || !zipfile) {
|
|
17
|
+
resolve(null);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let found = false;
|
|
21
|
+
zipfile.readEntry();
|
|
22
|
+
zipfile.on('entry', (entry) => {
|
|
23
|
+
if (entry.fileName === internalPath) {
|
|
24
|
+
found = true;
|
|
25
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
26
|
+
if (err || !readStream) {
|
|
27
|
+
resolve(null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const chunks = [];
|
|
31
|
+
readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
32
|
+
readStream.on('end', () => {
|
|
33
|
+
const source = Buffer.concat(chunks).toString('utf-8');
|
|
34
|
+
resolve(SourceParser.parse(className, source, type));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
zipfile.readEntry();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
zipfile.on('end', () => {
|
|
43
|
+
if (!found)
|
|
44
|
+
resolve(null);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
static async getSignaturesWithJavap(jarPath, className) {
|
|
50
|
+
try {
|
|
51
|
+
const config = await Config.getInstance();
|
|
52
|
+
const javap = config.getJavapPath();
|
|
53
|
+
// Use -public to show public members (closest to API surface)
|
|
54
|
+
// or default (protected)
|
|
55
|
+
const { stdout } = await execAsync(`"${javap}" -cp "${jarPath}" "${className}"`);
|
|
56
|
+
const lines = stdout.split('\n')
|
|
57
|
+
.map(l => l.trim())
|
|
58
|
+
.filter(l => l.length > 0 &&
|
|
59
|
+
!l.startsWith('Compiled from') &&
|
|
60
|
+
!l.includes('static {};') &&
|
|
61
|
+
l !== '}');
|
|
62
|
+
return {
|
|
63
|
+
className,
|
|
64
|
+
signatures: lines
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
// Fallback or error
|
|
69
|
+
// If javap fails (e.g. class not found in main jar?), return null
|
|
70
|
+
// console.error(`javap failed for ${className} in ${jarPath}:`, e);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
static parse(className, source, type) {
|
|
75
|
+
if (type === 'source') {
|
|
76
|
+
return { className, source };
|
|
77
|
+
}
|
|
78
|
+
// Very simple regex-based parsing to extract methods and javadocs
|
|
79
|
+
// This is heuristic and won't be perfect, but it's fast and dependency-free
|
|
80
|
+
const signatures = [];
|
|
81
|
+
let doc = "";
|
|
82
|
+
const allDocs = [];
|
|
83
|
+
const lines = source.split('\n');
|
|
84
|
+
let currentDoc = [];
|
|
85
|
+
let inDoc = false;
|
|
86
|
+
// Regex to match method signatures (public/protected, return type, name, args)
|
|
87
|
+
// ignoring annotations for simplicity
|
|
88
|
+
const methodRegex = /^\s*(public|protected)\s+(?:[\w<>?\[\]]+\s+)+(\w+)\s*\([^)]*\)\s*(?:throws\s+[\w,.\s]+)?\s*\{?/;
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
const trimmed = line.trim();
|
|
91
|
+
// Javadoc extraction
|
|
92
|
+
if (trimmed.startsWith('/**')) {
|
|
93
|
+
inDoc = true;
|
|
94
|
+
currentDoc = [];
|
|
95
|
+
}
|
|
96
|
+
if (inDoc) {
|
|
97
|
+
currentDoc.push(trimmed.replace(/^\/\*\*|\*\/|^\*\s?/g, '').trim());
|
|
98
|
+
}
|
|
99
|
+
if (trimmed.endsWith('*/')) {
|
|
100
|
+
inDoc = false;
|
|
101
|
+
if (currentDoc.length > 0) {
|
|
102
|
+
const docBlock = currentDoc.filter(s => s.length > 0).join('\n');
|
|
103
|
+
allDocs.push(docBlock);
|
|
104
|
+
// If we found a class doc (usually before class definition), keep it as primary doc
|
|
105
|
+
if (doc === "") {
|
|
106
|
+
doc = docBlock;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Method extraction
|
|
111
|
+
const match = line.match(methodRegex);
|
|
112
|
+
if (match) {
|
|
113
|
+
// match[0] is the whole line up to {
|
|
114
|
+
// Clean it up
|
|
115
|
+
let sig = match[0].trim();
|
|
116
|
+
if (sig.endsWith('{'))
|
|
117
|
+
sig = sig.slice(0, -1).trim();
|
|
118
|
+
signatures.push(sig);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
className,
|
|
123
|
+
signatures,
|
|
124
|
+
doc: type === 'docs' ? allDocs.join('\n\n') : undefined
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "maven-indexer-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for indexing local Maven repository",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"maven-indexer-mcp": "build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"directories": {
|
|
16
|
+
"doc": "doc"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"watch": "tsc --watch",
|
|
21
|
+
"start": "node build/index.js",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/tangcent/maven-indexer-mcp.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"maven",
|
|
32
|
+
"index"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/tangcent/maven-indexer-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/tangcent/maven-indexer-mcp#readme",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
42
|
+
"@types/node": "^25.0.2",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.0.15"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
48
|
+
"@types/xml2js": "^0.4.14",
|
|
49
|
+
"@types/yauzl": "^2.10.3",
|
|
50
|
+
"better-sqlite3": "^12.5.0",
|
|
51
|
+
"chokidar": "^5.0.0",
|
|
52
|
+
"xml2js": "^0.6.2",
|
|
53
|
+
"yauzl": "^3.2.0",
|
|
54
|
+
"zod": "^4.1.13"
|
|
55
|
+
}
|
|
56
|
+
}
|