maven-indexer-mcp 1.0.1 → 1.0.3

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
@@ -5,6 +5,7 @@ A Model Context Protocol (MCP) server that indexes your local Maven repository (
5
5
  ## Features
6
6
 
7
7
  * **Semantic Class Search**: Search for classes by name (e.g., `StringUtils`) or purpose (e.g., `JsonToXml`).
8
+ * **Inheritance Search**: Find all implementations of an interface or subclasses of a class.
8
9
  * **On-Demand Analysis**: Extracts method signatures (`javap`) and Javadocs directly from JARs without extracting the entire archive.
9
10
  * **Source Code Retrieval**: Provides full source code if `-sources.jar` is available.
10
11
  * **Real-time Monitoring**: Watches the Maven repository for changes (e.g., new `mvn install`) and automatically updates the index.
@@ -33,6 +34,7 @@ If the auto-detection fails, or if you want to filter which packages are indexed
33
34
 
34
35
  * **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if your repository is in a non-standard location.
35
36
  * **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
37
+ * **`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.
36
38
 
37
39
  Example with optional configuration:
38
40
 
@@ -44,7 +46,8 @@ Example with optional configuration:
44
46
  "args": ["-y", "maven-indexer-mcp@latest"],
45
47
  "env": {
46
48
  "MAVEN_REPO": "/Users/yourname/.m2/repository",
47
- "INCLUDED_PACKAGES": "com.mycompany.*"
49
+ "INCLUDED_PACKAGES": "com.mycompany.*",
50
+ "MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
48
51
  }
49
52
  }
50
53
  }
@@ -88,6 +91,10 @@ If you prefer to run from source:
88
91
  * Input: `className`, `artifactId`, `type` ("signatures", "docs", "source")
89
92
  * Output: Method signatures, Javadocs, or full source code.
90
93
  * **`search_artifacts`**: Search for artifacts by coordinate (groupId, artifactId).
94
+ * **`search_implementations`**: Search for classes that implement a specific interface or extend a specific class.
95
+ * Input: `className` (e.g. "java.util.List")
96
+ * Output: List of implementation/subclass names and their artifacts.
97
+ * **`refresh_index`**: Trigger a re-scan of the Maven repository.
91
98
 
92
99
  ## Development
93
100
 
@@ -0,0 +1,120 @@
1
+ export class ClassParser {
2
+ buffer;
3
+ offset = 0;
4
+ constantPool = [];
5
+ constructor(buffer) {
6
+ this.buffer = buffer;
7
+ }
8
+ static parse(buffer) {
9
+ const parser = new ClassParser(buffer);
10
+ return parser.parse();
11
+ }
12
+ parse() {
13
+ if (this.buffer.length < 10) {
14
+ throw new Error("Invalid class file: too short");
15
+ }
16
+ const magic = this.readU4();
17
+ if (magic !== 0xCAFEBABE) {
18
+ throw new Error("Invalid magic number");
19
+ }
20
+ this.readU2(); // minor
21
+ this.readU2(); // major
22
+ const cpCount = this.readU2();
23
+ this.constantPool = new Array(cpCount);
24
+ // Constant Pool is 1-indexed (1 to count-1)
25
+ for (let i = 1; i < cpCount; i++) {
26
+ const tag = this.readU1();
27
+ switch (tag) {
28
+ case 1: // UTF8
29
+ const len = this.readU2();
30
+ const str = this.buffer.toString('utf-8', this.offset, this.offset + len);
31
+ this.offset += len;
32
+ this.constantPool[i] = { tag, value: str };
33
+ break;
34
+ case 3: // Integer
35
+ case 4: // Float
36
+ this.offset += 4;
37
+ break;
38
+ case 5: // Long
39
+ case 6: // Double
40
+ this.offset += 8;
41
+ i++; // Takes two slots
42
+ break;
43
+ case 7: // Class
44
+ const nameIndex = this.readU2();
45
+ this.constantPool[i] = { tag, nameIndex };
46
+ break;
47
+ case 8: // String
48
+ this.offset += 2;
49
+ break;
50
+ case 9: // Fieldref
51
+ case 10: // Methodref
52
+ case 11: // InterfaceMethodref
53
+ this.offset += 4;
54
+ break;
55
+ case 12: // NameAndType
56
+ this.offset += 4;
57
+ break;
58
+ case 15: // MethodHandle
59
+ this.offset += 3;
60
+ break;
61
+ case 16: // MethodType
62
+ this.offset += 2;
63
+ break;
64
+ case 17: // Dynamic
65
+ case 18: // InvokeDynamic
66
+ this.offset += 4;
67
+ break;
68
+ case 19: // Module
69
+ case 20: // Package
70
+ this.offset += 2;
71
+ break;
72
+ default:
73
+ throw new Error(`Unknown constant pool tag: ${tag} at offset ${this.offset - 1}`);
74
+ }
75
+ }
76
+ this.readU2(); // Access flags
77
+ const thisClassIndex = this.readU2();
78
+ const superClassIndex = this.readU2();
79
+ const className = this.resolveClass(thisClassIndex);
80
+ const superClass = superClassIndex === 0 ? undefined : this.resolveClass(superClassIndex);
81
+ const interfacesCount = this.readU2();
82
+ const interfaces = [];
83
+ for (let i = 0; i < interfacesCount; i++) {
84
+ const interfaceIndex = this.readU2();
85
+ interfaces.push(this.resolveClass(interfaceIndex));
86
+ }
87
+ return {
88
+ className: className.replace(/\//g, '.'),
89
+ superClass: superClass ? superClass.replace(/\//g, '.') : undefined,
90
+ interfaces: interfaces.map(i => i.replace(/\//g, '.'))
91
+ };
92
+ }
93
+ resolveClass(index) {
94
+ const entry = this.constantPool[index];
95
+ if (!entry || entry.tag !== 7) {
96
+ // Fallback or error?
97
+ return "Unknown";
98
+ }
99
+ const nameEntry = this.constantPool[entry.nameIndex];
100
+ if (!nameEntry || nameEntry.tag !== 1) {
101
+ return "Unknown";
102
+ }
103
+ return nameEntry.value;
104
+ }
105
+ readU1() {
106
+ const val = this.buffer.readUInt8(this.offset);
107
+ this.offset += 1;
108
+ return val;
109
+ }
110
+ readU2() {
111
+ const val = this.buffer.readUInt16BE(this.offset);
112
+ this.offset += 2;
113
+ return val;
114
+ }
115
+ readU4() {
116
+ const val = this.buffer.readUInt32BE(this.offset);
117
+ this.offset += 4;
118
+ return val;
119
+ }
120
+ }
package/build/config.js CHANGED
@@ -2,11 +2,13 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import xml2js from 'xml2js';
5
+ import { fileURLToPath } from 'url';
5
6
  export class Config {
6
7
  static instance;
7
8
  localRepository = "";
8
9
  javaBinary = "java";
9
10
  includedPackages = ["*"];
11
+ cfrPath = null;
10
12
  constructor() { }
11
13
  static async getInstance() {
12
14
  if (!Config.instance) {
@@ -62,10 +64,26 @@ export class Config {
62
64
  .map(p => p.trim())
63
65
  .filter(p => p.length > 0);
64
66
  }
67
+ // Load CFR Path
68
+ if (process.env.MAVEN_INDEXER_CFR_PATH) {
69
+ this.cfrPath = process.env.MAVEN_INDEXER_CFR_PATH;
70
+ }
71
+ else {
72
+ // Fallback to default bundled lib
73
+ const __filename = fileURLToPath(import.meta.url);
74
+ const __dirname = path.dirname(__filename);
75
+ this.cfrPath = path.resolve(__dirname, '../lib/cfr-0.152.jar');
76
+ }
65
77
  // Log to stderr so it doesn't interfere with MCP protocol on stdout
66
78
  console.error(`Using local repository: ${this.localRepository}`);
67
79
  console.error(`Using Java binary: ${this.javaBinary}`);
68
80
  console.error(`Included packages: ${JSON.stringify(this.includedPackages)}`);
81
+ if (this.cfrPath) {
82
+ console.error(`Using CFR jar: ${this.cfrPath}`);
83
+ }
84
+ }
85
+ getCfrJarPath() {
86
+ return this.cfrPath;
69
87
  }
70
88
  getJavapPath() {
71
89
  if (this.javaBinary === 'java')
package/build/db/index.js CHANGED
@@ -17,6 +17,17 @@ export class DB {
17
17
  return DB.instance;
18
18
  }
19
19
  initSchema() {
20
+ // Register REGEXP function
21
+ this.db.function('regexp', { deterministic: true }, (regex, text) => {
22
+ if (!regex || !text)
23
+ return 0;
24
+ try {
25
+ return new RegExp(regex).test(text) ? 1 : 0;
26
+ }
27
+ catch (e) {
28
+ return 0;
29
+ }
30
+ });
20
31
  this.db.exec(`
21
32
  CREATE TABLE IF NOT EXISTS artifacts (
22
33
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -25,6 +36,7 @@ export class DB {
25
36
  version TEXT NOT NULL,
26
37
  abspath TEXT NOT NULL,
27
38
  has_source INTEGER DEFAULT 0,
39
+ is_indexed INTEGER DEFAULT 0,
28
40
  UNIQUE(group_id, artifact_id, version)
29
41
  );
30
42
 
@@ -34,12 +46,26 @@ export class DB {
34
46
  simple_name, -- Just the class name
35
47
  tokenize="trigram"
36
48
  );
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
49
+
50
+ CREATE TABLE IF NOT EXISTS inheritance (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ artifact_id INTEGER NOT NULL,
53
+ class_name TEXT NOT NULL,
54
+ parent_class_name TEXT NOT NULL,
55
+ type TEXT NOT NULL
41
56
  );
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_inheritance_parent ON inheritance(parent_class_name);
59
+
60
+ -- Cleanup old table if exists
61
+ DROP TABLE IF EXISTS indexed_artifacts;
42
62
  `);
63
+ try {
64
+ this.db.exec('ALTER TABLE artifacts ADD COLUMN is_indexed INTEGER DEFAULT 0');
65
+ }
66
+ catch (e) {
67
+ // Column likely already exists
68
+ }
43
69
  }
44
70
  getDb() {
45
71
  return this.db;
package/build/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
4
  import path from 'path';
5
+ import { z } from "zod";
6
6
  import { Indexer } from "./indexer.js";
7
7
  import { SourceParser } from "./source_parser.js";
8
- const server = new Server({
8
+ const server = new McpServer({
9
9
  name: "maven-indexer",
10
10
  version: "1.0.0",
11
11
  }, {
@@ -20,155 +20,152 @@ indexer.index().then(() => {
20
20
  // Start watching for changes after initial index
21
21
  return indexer.startWatch();
22
22
  }).catch(err => console.error("Initial indexing failed:", err));
23
- server.setRequestHandler(ListToolsRequestSchema, async () => {
23
+ server.registerTool("search_artifacts", {
24
+ description: "Search for Maven artifacts (libraries) in the local repository by coordinate (groupId, artifactId) or keyword. Use this to find available versions of a library.",
25
+ inputSchema: z.object({
26
+ query: z.string().describe("Search query (groupId, artifactId, or keyword)"),
27
+ }),
28
+ }, async ({ query }) => {
29
+ const matches = indexer.search(query);
30
+ // Limit results to avoid overflow
31
+ const limitedMatches = matches.slice(0, 20);
32
+ const text = limitedMatches.length > 0
33
+ ? limitedMatches.map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
34
+ : "No artifacts found matching the query.";
24
35
  return {
25
- tools: [
36
+ content: [
26
37
  {
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
- },
38
+ type: "text",
39
+ text: `Found ${matches.length} matches${matches.length > 20 ? ' (showing first 20)' : ''}:\n${text}`,
39
40
  },
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
41
  ],
86
42
  };
87
43
  });
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
- };
44
+ server.registerTool("search_classes", {
45
+ description: "Search for Java classes in the local Maven repository. Use this to find classes from external dependencies/libraries when their source code is not available in the current workspace. You can search by class name (e.g. 'StringUtils') or keywords (e.g. 'Json').",
46
+ inputSchema: z.object({
47
+ className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
48
+ }),
49
+ }, async ({ className }) => {
50
+ const matches = indexer.searchClass(className);
51
+ const text = matches.length > 0
52
+ ? matches.map(m => {
53
+ // Group by artifact ID to allow easy selection
54
+ const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
55
+ const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
56
+ return `Class: ${m.className}\n ${artifacts}${more}`;
57
+ }).join("\n\n")
58
+ : "No classes found matching the query. Try different keywords.";
59
+ return {
60
+ content: [{ type: "text", text }]
61
+ };
62
+ });
63
+ server.registerTool("search_implementations", {
64
+ description: "Search for classes that implement a specific interface or extend a specific class. This is useful for finding implementations of SPIs or base classes.",
65
+ inputSchema: z.object({
66
+ className: z.string().describe("Fully qualified class name of the interface or base class (e.g. 'java.util.List')"),
67
+ }),
68
+ }, async ({ className }) => {
69
+ const matches = indexer.searchImplementations(className);
70
+ const text = matches.length > 0
71
+ ? matches.map(m => {
72
+ const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}`).join("\n ");
73
+ const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
74
+ return `Implementation: ${m.className}\n ${artifacts}${more}`;
75
+ }).join("\n\n")
76
+ : `No implementations found for ${className}. Ensure the index is up to date and the class name is correct.`;
77
+ return {
78
+ content: [{ type: "text", text }]
79
+ };
80
+ });
81
+ server.registerTool("get_class_details", {
82
+ description: "Get details about a specific class from a Maven artifact. Use this to inspect external classes where source code is missing. It can provide method signatures (using javap), Javadocs, or even decompiled source code if the original source jar is unavailable.",
83
+ inputSchema: z.object({
84
+ className: z.string().describe("Fully qualified class name"),
85
+ artifactId: z.number().describe("The internal ID of the artifact (returned by search_classes)"),
86
+ type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
87
+ }),
88
+ }, async ({ className, artifactId, type }) => {
89
+ const artifact = indexer.getArtifactById(artifactId);
90
+ if (!artifact) {
91
+ return { content: [{ type: "text", text: "Artifact not found." }] };
120
92
  }
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}.` }] };
93
+ let detail = null;
94
+ let usedDecompilation = false;
95
+ let lastError = "";
96
+ // 1. If requesting source/docs, try Source JAR first
97
+ if (type === 'source' || type === 'docs') {
98
+ if (artifact.hasSource) {
99
+ const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
100
+ try {
101
+ detail = await SourceParser.getClassDetail(sourceJarPath, className, type);
145
102
  }
146
- let resultText = `Class: ${detail.className}\n\n`;
147
- if (type === 'source') {
148
- resultText += "```java\n" + detail.source + "\n```";
103
+ catch (e) {
104
+ // Ignore error and fallthrough to main jar (decompilation)
105
+ lastError = e.message;
149
106
  }
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";
107
+ }
108
+ // If not found in source jar (or no source jar), try main jar (decompilation)
109
+ if (!detail) {
110
+ const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
111
+ try {
112
+ // SourceParser will try to decompile if source file not found in jar
113
+ detail = await SourceParser.getClassDetail(mainJarPath, className, type);
114
+ if (detail && detail.source) {
115
+ usedDecompilation = true;
156
116
  }
157
117
  }
158
- return { content: [{ type: "text", text: resultText }] };
118
+ catch (e) {
119
+ console.error(`Decompilation/MainJar access failed: ${e.message}`);
120
+ lastError = e.message;
121
+ }
122
+ }
123
+ }
124
+ else {
125
+ // Signatures -> Use Main JAR
126
+ const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
127
+ try {
128
+ detail = await SourceParser.getClassDetail(mainJarPath, className, type);
159
129
  }
160
130
  catch (e) {
161
- return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
131
+ lastError = e.message;
132
+ }
133
+ }
134
+ try {
135
+ if (!detail) {
136
+ const debugInfo = `Artifact path: ${artifact.abspath}, hasSource: ${artifact.hasSource}`;
137
+ const errorMsg = lastError ? `\nLast error: ${lastError}` : "";
138
+ return { content: [{ type: "text", text: `Class ${className} not found in artifact ${artifact.artifactId}. \nDebug info: ${debugInfo}${errorMsg}` }] };
162
139
  }
140
+ let resultText = `Class: ${detail.className}\n\n`;
141
+ if (usedDecompilation) {
142
+ resultText += "*Source code decompiled from binary class file.*\n\n";
143
+ }
144
+ if (type === 'source') {
145
+ resultText += "```java\n" + detail.source + "\n```";
146
+ }
147
+ else {
148
+ if (detail.doc) {
149
+ resultText += "Documentation:\n" + detail.doc + "\n\n";
150
+ }
151
+ if (detail.signatures) {
152
+ resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
153
+ }
154
+ }
155
+ return { content: [{ type: "text", text: resultText }] };
163
156
  }
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
- };
157
+ catch (e) {
158
+ return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
170
159
  }
171
- throw new Error("Tool not found");
160
+ });
161
+ server.registerTool("refresh_index", {
162
+ description: "Trigger a re-scan of the Maven repository. This will re-index all artifacts.",
163
+ }, async () => {
164
+ // Re-run index
165
+ indexer.refresh().catch(console.error);
166
+ return {
167
+ content: [{ type: "text", text: "Index refresh started. All artifacts will be re-indexed." }]
168
+ };
172
169
  });
173
170
  const transport = new StdioServerTransport();
174
171
  await server.connect(transport);
package/build/indexer.js CHANGED
@@ -5,6 +5,11 @@ import yauzl from 'yauzl';
5
5
  import chokidar from 'chokidar';
6
6
  import { Config } from './config.js';
7
7
  import { DB } from './db/index.js';
8
+ import { ClassParser } from './class_parser.js';
9
+ /**
10
+ * Singleton class responsible for indexing Maven artifacts.
11
+ * It scans the local repository, watches for changes, and indexes Java classes.
12
+ */
8
13
  export class Indexer {
9
14
  static instance;
10
15
  isIndexing = false;
@@ -17,6 +22,10 @@ export class Indexer {
17
22
  }
18
23
  return Indexer.instance;
19
24
  }
25
+ /**
26
+ * Starts watching the local repository for changes.
27
+ * Debounces changes to trigger re-indexing.
28
+ */
20
29
  async startWatch() {
21
30
  const config = await Config.getInstance();
22
31
  const repoPath = config.localRepository;
@@ -52,59 +61,76 @@ export class Indexer {
52
61
  onChange();
53
62
  });
54
63
  }
64
+ /**
65
+ * Forces a full re-index of the repository.
66
+ */
67
+ async refresh() {
68
+ const db = DB.getInstance();
69
+ console.error("Refreshing index...");
70
+ db.transaction(() => {
71
+ db.prepare('UPDATE artifacts SET is_indexed = 0').run();
72
+ db.prepare('DELETE FROM classes_fts').run();
73
+ db.prepare('DELETE FROM inheritance').run();
74
+ });
75
+ return this.index();
76
+ }
77
+ /**
78
+ * Main indexing process.
79
+ * 1. Scans the file system for Maven artifacts.
80
+ * 2. Synchronizes the database with found artifacts.
81
+ * 3. Indexes classes for artifacts that haven't been indexed yet.
82
+ */
55
83
  async index() {
56
84
  if (this.isIndexing)
57
85
  return;
58
86
  this.isIndexing = true;
59
87
  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
88
  try {
89
+ const config = await Config.getInstance();
90
+ const repoPath = config.localRepository;
91
+ const db = DB.getInstance();
92
+ if (!repoPath) {
93
+ console.error("No local repository path found.");
94
+ return;
95
+ }
69
96
  // 1. Scan for artifacts
70
97
  console.error("Scanning repository structure...");
71
98
  const artifacts = await this.scanRepository(repoPath);
72
99
  console.error(`Found ${artifacts.length} artifacts on disk.`);
73
100
  // 2. Persist artifacts and determine what needs indexing
74
- const artifactsToIndex = [];
101
+ // We use is_indexed = 0 for new artifacts.
102
+ const insertArtifact = db.prepare(`
103
+ INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
104
+ VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
105
+ `);
106
+ // Use a transaction only for the batch insert of artifacts
75
107
  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
108
  for (const art of artifacts) {
88
109
  insertArtifact.run({
89
110
  ...art,
90
111
  hasSource: art.hasSource ? 1 : 0
91
112
  });
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
113
  }
105
114
  });
115
+ // Check if we need to backfill inheritance data (migration)
116
+ const inheritanceCount = db.prepare('SELECT COUNT(*) as count FROM inheritance').get();
117
+ const indexedArtifactsCount = db.prepare('SELECT COUNT(*) as count FROM artifacts WHERE is_indexed = 1').get();
118
+ if (inheritanceCount.count === 0 && indexedArtifactsCount.count > 0) {
119
+ console.error("Detected missing inheritance data. Forcing re-index of classes...");
120
+ db.transaction(() => {
121
+ db.prepare('UPDATE artifacts SET is_indexed = 0').run();
122
+ db.prepare('DELETE FROM classes_fts').run();
123
+ // inheritance is already empty
124
+ });
125
+ }
126
+ // 3. Find artifacts that need indexing (is_indexed = 0)
127
+ const artifactsToIndex = db.prepare(`
128
+ SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
129
+ FROM artifacts
130
+ WHERE is_indexed = 0
131
+ `).all();
106
132
  console.error(`${artifactsToIndex.length} artifacts need indexing.`);
107
- // 3. Scan JARs for classes and update DB
133
+ // 4. Scan JARs for classes and update DB
108
134
  const CHUNK_SIZE = 50;
109
135
  let processedCount = 0;
110
136
  for (let i = 0; i < artifactsToIndex.length; i += CHUNK_SIZE) {
@@ -124,6 +150,9 @@ export class Indexer {
124
150
  this.isIndexing = false;
125
151
  }
126
152
  }
153
+ /**
154
+ * Recursively scans a directory for Maven artifacts (POM files).
155
+ */
127
156
  async scanRepository(rootDir) {
128
157
  const results = [];
129
158
  const scanDir = async (dir) => {
@@ -168,6 +197,10 @@ export class Indexer {
168
197
  await scanDir(rootDir);
169
198
  return results;
170
199
  }
200
+ /**
201
+ * Extracts classes from the artifact's JAR and indexes them.
202
+ * Updates the 'is_indexed' flag upon completion.
203
+ */
171
204
  async indexArtifactClasses(artifact) {
172
205
  const jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
173
206
  const db = DB.getInstance();
@@ -178,30 +211,56 @@ export class Indexer {
178
211
  catch {
179
212
  // If jar missing, mark as indexed so we don't retry endlessly?
180
213
  // Or maybe it's a pom-only artifact.
181
- db.prepare('INSERT OR IGNORE INTO indexed_artifacts (artifact_id) VALUES (?)').run(artifact.id);
214
+ db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
182
215
  return;
183
216
  }
184
217
  return new Promise((resolve) => {
185
218
  yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
186
219
  if (err || !zipfile) {
220
+ // If we can't open it, maybe it's corrupt. Skip for now.
187
221
  resolve();
188
222
  return;
189
223
  }
190
224
  const classes = [];
225
+ const inheritance = [];
191
226
  zipfile.readEntry();
192
227
  zipfile.on('entry', (entry) => {
193
228
  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);
229
+ zipfile.openReadStream(entry, (err, readStream) => {
230
+ if (err || !readStream) {
231
+ zipfile.readEntry();
232
+ return;
201
233
  }
202
- }
234
+ const chunks = [];
235
+ readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
236
+ readStream.on('end', () => {
237
+ const buffer = Buffer.concat(chunks);
238
+ try {
239
+ const info = ClassParser.parse(buffer);
240
+ // Simple check to avoid module-info or invalid names
241
+ if (!info.className.includes('$') && info.className.length > 0) {
242
+ // Filter by includedPackages
243
+ if (this.isPackageIncluded(info.className, config.includedPackages)) {
244
+ classes.push(info.className);
245
+ if (info.superClass && info.superClass !== 'java.lang.Object') {
246
+ inheritance.push({ className: info.className, parent: info.superClass, type: 'extends' });
247
+ }
248
+ for (const iface of info.interfaces) {
249
+ inheritance.push({ className: info.className, parent: iface, type: 'implements' });
250
+ }
251
+ }
252
+ }
253
+ }
254
+ catch (e) {
255
+ // console.error(`Failed to parse ${entry.fileName}`, e);
256
+ }
257
+ zipfile.readEntry();
258
+ });
259
+ });
260
+ }
261
+ else {
262
+ zipfile.readEntry();
203
263
  }
204
- zipfile.readEntry();
205
264
  });
206
265
  zipfile.on('end', () => {
207
266
  // Batch insert classes
@@ -210,12 +269,20 @@ export class Indexer {
210
269
  const insertClass = db.prepare(`
211
270
  INSERT INTO classes_fts (artifact_id, class_name, simple_name)
212
271
  VALUES (?, ?, ?)
272
+ `);
273
+ const insertInheritance = db.prepare(`
274
+ INSERT INTO inheritance (artifact_id, class_name, parent_class_name, type)
275
+ VALUES (?, ?, ?, ?)
213
276
  `);
214
277
  for (const cls of classes) {
215
278
  const simpleName = cls.split('.').pop() || cls;
216
279
  insertClass.run(artifact.id, cls, simpleName);
217
280
  }
218
- db.prepare('INSERT OR IGNORE INTO indexed_artifacts (artifact_id) VALUES (?)').run(artifact.id);
281
+ for (const item of inheritance) {
282
+ insertInheritance.run(artifact.id, item.className, item.parent, item.type);
283
+ }
284
+ // Mark as indexed
285
+ db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
219
286
  });
220
287
  }
221
288
  catch (e) {
@@ -229,6 +296,9 @@ export class Indexer {
229
296
  });
230
297
  });
231
298
  }
299
+ /**
300
+ * Checks if a class package is included in the configuration patterns.
301
+ */
232
302
  isPackageIncluded(className, patterns) {
233
303
  if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*'))
234
304
  return true;
@@ -249,6 +319,9 @@ export class Indexer {
249
319
  }
250
320
  return false;
251
321
  }
322
+ /**
323
+ * Searches for artifacts by group ID or artifact ID.
324
+ */
252
325
  search(query) {
253
326
  // Artifact coordinates search
254
327
  const db = DB.getInstance();
@@ -260,25 +333,56 @@ export class Indexer {
260
333
  `).all(`%${query}%`, `%${query}%`);
261
334
  return rows;
262
335
  }
336
+ /**
337
+ * Searches for classes matching the pattern.
338
+ * Uses Full-Text Search (FTS) for efficient matching.
339
+ */
263
340
  searchClass(classNamePattern) {
264
341
  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 escapedPattern = classNamePattern.replace(/"/g, '""');
269
- const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
270
- const query = safeQuery.length > 0
271
- ? `"${escapedPattern}"* OR ${safeQuery}`
272
- : `"${escapedPattern}"*`;
273
342
  try {
274
- const rows = db.prepare(`
275
- SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
276
- FROM classes_fts c
277
- JOIN artifacts a ON c.artifact_id = a.id
278
- WHERE c.classes_fts MATCH ?
279
- ORDER BY rank
280
- LIMIT 100
281
- `).all(query);
343
+ let rows = [];
344
+ if (classNamePattern.startsWith('regex:')) {
345
+ // Regex search
346
+ const regex = classNamePattern.substring(6);
347
+ rows = db.prepare(`
348
+ SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
349
+ FROM classes_fts c
350
+ JOIN artifacts a ON c.artifact_id = a.id
351
+ WHERE c.class_name REGEXP ? OR c.simple_name REGEXP ?
352
+ LIMIT 100
353
+ `).all(regex, regex);
354
+ }
355
+ else if (classNamePattern.includes('*') || classNamePattern.includes('?')) {
356
+ // Glob-style search (using LIKE for standard wildcards)
357
+ // Convert glob wildcards to SQL wildcards if needed, or just rely on user knowing %/_
358
+ // But standard glob is * and ?
359
+ const likePattern = classNamePattern.replace(/\*/g, '%').replace(/\?/g, '_');
360
+ rows = db.prepare(`
361
+ SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
362
+ FROM classes_fts c
363
+ JOIN artifacts a ON c.artifact_id = a.id
364
+ WHERE c.class_name LIKE ? OR c.simple_name LIKE ?
365
+ LIMIT 100
366
+ `).all(likePattern, likePattern);
367
+ }
368
+ else {
369
+ // Use FTS for smart matching
370
+ // If pattern has no spaces, we assume it's a prefix or exact match query
371
+ // "String" -> match "String" in simple_name or class_name
372
+ const escapedPattern = classNamePattern.replace(/"/g, '""');
373
+ const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
374
+ const query = safeQuery.length > 0
375
+ ? `"${escapedPattern}"* OR ${safeQuery}`
376
+ : `"${escapedPattern}"*`;
377
+ rows = db.prepare(`
378
+ SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
379
+ FROM classes_fts c
380
+ JOIN artifacts a ON c.artifact_id = a.id
381
+ WHERE c.classes_fts MATCH ?
382
+ ORDER BY rank
383
+ LIMIT 100
384
+ `).all(query);
385
+ }
282
386
  // Group by class name
283
387
  const resultMap = new Map();
284
388
  for (const row of rows) {
@@ -305,6 +409,64 @@ export class Indexer {
305
409
  return [];
306
410
  }
307
411
  }
412
+ /**
413
+ * Searches for implementations/subclasses of a specific class/interface.
414
+ */
415
+ searchImplementations(className) {
416
+ const db = DB.getInstance();
417
+ try {
418
+ console.error(`Searching implementations for ${className}...`);
419
+ // Debug: Check if we have any inheritance data at all
420
+ const count = db.prepare("SELECT count(*) as c FROM inheritance").get();
421
+ if (count.c === 0) {
422
+ console.error("WARNING: Inheritance table is empty!");
423
+ }
424
+ // Recursive search for all implementations/subclasses
425
+ const rows = db.prepare(`
426
+ WITH RECURSIVE hierarchy(class_name, artifact_id) AS (
427
+ SELECT class_name, artifact_id FROM inheritance WHERE parent_class_name = ?
428
+ UNION
429
+ SELECT i.class_name, i.artifact_id FROM inheritance i JOIN hierarchy h ON i.parent_class_name = h.class_name
430
+ )
431
+ SELECT DISTINCT h.class_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
432
+ FROM hierarchy h
433
+ JOIN artifacts a ON h.artifact_id = a.id
434
+ LIMIT 100
435
+ `).all(className);
436
+ console.error(`Searching implementations for ${className}: found ${rows.length} rows.`);
437
+ if (rows.length === 0) {
438
+ // Fallback: Try searching without recursion to see if direct children exist
439
+ const direct = db.prepare('SELECT count(*) as c FROM inheritance WHERE parent_class_name = ?').get(className);
440
+ console.error(`Direct implementations check for ${className}: ${direct.c}`);
441
+ }
442
+ const resultMap = new Map();
443
+ for (const row of rows) {
444
+ const art = {
445
+ id: row.id,
446
+ groupId: row.group_id,
447
+ artifactId: row.artifact_id,
448
+ version: row.version,
449
+ abspath: row.abspath,
450
+ hasSource: Boolean(row.has_source)
451
+ };
452
+ if (!resultMap.has(row.class_name)) {
453
+ resultMap.set(row.class_name, []);
454
+ }
455
+ resultMap.get(row.class_name).push(art);
456
+ }
457
+ return Array.from(resultMap.entries()).map(([className, artifacts]) => ({
458
+ className,
459
+ artifacts
460
+ }));
461
+ }
462
+ catch (e) {
463
+ console.error("Search implementations failed", e);
464
+ return [];
465
+ }
466
+ }
467
+ /**
468
+ * Retrieves an artifact by its database ID.
469
+ */
308
470
  getArtifactById(id) {
309
471
  const db = DB.getInstance();
310
472
  const row = db.prepare(`
@@ -1,3 +1,5 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import yauzl from 'yauzl';
2
4
  import { exec } from 'child_process';
3
5
  import { promisify } from 'util';
@@ -10,8 +12,12 @@ export class SourceParser {
10
12
  }
11
13
  // className: com.example.MyClass
12
14
  // internalPath: com/example/MyClass.java
13
- const internalPath = className.replace(/\./g, '/') + '.java';
14
- return new Promise((resolve, reject) => {
15
+ const basePath = className.replace(/\./g, '/');
16
+ const candidates = [
17
+ basePath + '.java',
18
+ basePath + '.kt'
19
+ ];
20
+ const result = await new Promise((resolve, reject) => {
15
21
  yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
16
22
  if (err || !zipfile) {
17
23
  resolve(null);
@@ -20,7 +26,7 @@ export class SourceParser {
20
26
  let found = false;
21
27
  zipfile.readEntry();
22
28
  zipfile.on('entry', (entry) => {
23
- if (entry.fileName === internalPath) {
29
+ if (candidates.includes(entry.fileName)) {
24
30
  found = true;
25
31
  zipfile.openReadStream(entry, (err, readStream) => {
26
32
  if (err || !readStream) {
@@ -45,6 +51,47 @@ export class SourceParser {
45
51
  });
46
52
  });
47
53
  });
54
+ if (result) {
55
+ return result;
56
+ }
57
+ // Fallback to decompilation if source not found in this JAR
58
+ // Note: This works best if the provided jarPath is the MAIN jar.
59
+ // If it is the sources jar, decompilation will fail (as it doesn't contain .class files),
60
+ // returning null.
61
+ if (type === 'source' || type === 'docs') {
62
+ return this.decompileClass(jarPath, className, type);
63
+ }
64
+ return null;
65
+ }
66
+ static async decompileClass(jarPath, className, type) {
67
+ const config = await Config.getInstance();
68
+ const cfrPath = config.getCfrJarPath();
69
+ if (!cfrPath || !fs.existsSync(cfrPath)) {
70
+ throw new Error(`CFR jar not found at ${cfrPath}`);
71
+ }
72
+ // Use java -cp cfr.jar:target.jar org.benf.cfr.reader.Main className
73
+ const classpath = `${cfrPath}${path.delimiter}${jarPath}`;
74
+ const cmd = `java -cp "${classpath}" org.benf.cfr.reader.Main "${className}"`;
75
+ // console.error("Running decompile command:", cmd);
76
+ try {
77
+ const { stdout, stderr } = await execAsync(cmd);
78
+ if (!stdout && stderr) {
79
+ console.error(`CFR stderr for ${className}:`, stderr);
80
+ // If stderr has content but stdout is empty, it might be an error
81
+ throw new Error(`CFR stderr: ${stderr}`);
82
+ }
83
+ if (stdout) {
84
+ return this.parse(className, stdout, type);
85
+ }
86
+ return {
87
+ className,
88
+ source: stdout // Return as source
89
+ };
90
+ }
91
+ catch (e) {
92
+ console.error(`CFR failed for ${className} in ${jarPath}:`, e.message);
93
+ throw e; // Rethrow to let caller handle
94
+ }
48
95
  }
49
96
  static async getSignaturesWithJavap(jarPath, className) {
50
97
  try {
@@ -85,7 +132,8 @@ export class SourceParser {
85
132
  let inDoc = false;
86
133
  // Regex to match method signatures (public/protected, return type, name, args)
87
134
  // ignoring annotations for simplicity
88
- const methodRegex = /^\s*(public|protected)\s+(?:[\w<>?\[\]]+\s+)+(\w+)\s*\([^)]*\)\s*(?:throws\s+[\w,.\s]+)?\s*\{?/;
135
+ // Expanded to match decompiled code better (e.g., might not have throws, might be abstract)
136
+ const methodRegex = /^\s*(public|protected)\s+(?:[\w<>?\[\]]+\s+)+(\w+)\s*\([^)]*\)/;
89
137
  for (const line of lines) {
90
138
  const trimmed = line.trim();
91
139
  // Javadoc extraction
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-indexer-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "MCP server for indexing local Maven repository",
5
5
  "main": "build/index.js",
6
6
  "type": "module",