maven-indexer-mcp 1.0.0 → 1.0.2

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
@@ -33,6 +33,7 @@ If the auto-detection fails, or if you want to filter which packages are indexed
33
33
 
34
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
35
  * **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
36
+ * **`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
37
 
37
38
  Example with optional configuration:
38
39
 
@@ -44,7 +45,8 @@ Example with optional configuration:
44
45
  "args": ["-y", "maven-indexer-mcp@latest"],
45
46
  "env": {
46
47
  "MAVEN_REPO": "/Users/yourname/.m2/repository",
47
- "INCLUDED_PACKAGES": "com.mycompany.*"
48
+ "INCLUDED_PACKAGES": "com.mycompany.*",
49
+ "MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
48
50
  }
49
51
  }
50
52
  }
package/build/config.js CHANGED
@@ -7,6 +7,7 @@ export class Config {
7
7
  localRepository = "";
8
8
  javaBinary = "java";
9
9
  includedPackages = ["*"];
10
+ cfrPath = null;
10
11
  constructor() { }
11
12
  static async getInstance() {
12
13
  if (!Config.instance) {
@@ -62,10 +63,24 @@ export class Config {
62
63
  .map(p => p.trim())
63
64
  .filter(p => p.length > 0);
64
65
  }
66
+ // Load CFR Path
67
+ if (process.env.MAVEN_INDEXER_CFR_PATH) {
68
+ this.cfrPath = process.env.MAVEN_INDEXER_CFR_PATH;
69
+ }
70
+ else {
71
+ // Fallback to default bundled lib
72
+ this.cfrPath = path.resolve(process.cwd(), 'lib/cfr-0.152.jar');
73
+ }
65
74
  // Log to stderr so it doesn't interfere with MCP protocol on stdout
66
75
  console.error(`Using local repository: ${this.localRepository}`);
67
76
  console.error(`Using Java binary: ${this.javaBinary}`);
68
77
  console.error(`Included packages: ${JSON.stringify(this.includedPackages)}`);
78
+ if (this.cfrPath) {
79
+ console.error(`Using CFR jar: ${this.cfrPath}`);
80
+ }
81
+ }
82
+ getCfrJarPath() {
83
+ return this.cfrPath;
69
84
  }
70
85
  getJavapPath() {
71
86
  if (this.javaBinary === 'java')
package/build/db/index.js CHANGED
@@ -25,6 +25,7 @@ export class DB {
25
25
  version TEXT NOT NULL,
26
26
  abspath TEXT NOT NULL,
27
27
  has_source INTEGER DEFAULT 0,
28
+ is_indexed INTEGER DEFAULT 0,
28
29
  UNIQUE(group_id, artifact_id, version)
29
30
  );
30
31
 
@@ -35,11 +36,15 @@ export class DB {
35
36
  tokenize="trigram"
36
37
  );
37
38
 
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
- );
39
+ -- Cleanup old table if exists
40
+ DROP TABLE IF EXISTS indexed_artifacts;
42
41
  `);
42
+ try {
43
+ this.db.exec('ALTER TABLE artifacts ADD COLUMN is_indexed INTEGER DEFAULT 0');
44
+ }
45
+ catch (e) {
46
+ // Column likely already exists
47
+ }
43
48
  }
44
49
  getDb() {
45
50
  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,124 @@ 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 artifacts in the local Maven repository",
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
- };
44
+ server.registerTool("search_classes", {
45
+ 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).",
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("get_class_details", {
64
+ description: "Get details about a specific class from an artifact, including method signatures and javadocs (if source is available).",
65
+ inputSchema: z.object({
66
+ className: z.string().describe("Fully qualified class name"),
67
+ artifactId: z.number().describe("The internal ID of the artifact (returned by search_classes)"),
68
+ type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
69
+ }),
70
+ }, async ({ className, artifactId, type }) => {
71
+ const artifact = indexer.getArtifactById(artifactId);
72
+ if (!artifact) {
73
+ return { content: [{ type: "text", text: "Artifact not found." }] };
105
74
  }
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
- };
75
+ let detail = null;
76
+ let usedDecompilation = false;
77
+ // 1. If requesting source/docs, try Source JAR first
78
+ if (type === 'source' || type === 'docs') {
79
+ if (artifact.hasSource) {
80
+ const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
81
+ try {
82
+ detail = await SourceParser.getClassDetail(sourceJarPath, className, type);
83
+ }
84
+ catch (e) {
85
+ // Ignore error and fallthrough to main jar (decompilation)
86
+ }
87
+ }
88
+ // If not found in source jar (or no source jar), try main jar (decompilation)
89
+ if (!detail) {
90
+ const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
91
+ try {
92
+ // SourceParser will try to decompile if source file not found in jar
93
+ detail = await SourceParser.getClassDetail(mainJarPath, className, type);
94
+ if (detail && detail.source) {
95
+ usedDecompilation = true;
96
+ }
97
+ }
98
+ catch (e) {
99
+ // Ignore
100
+ }
101
+ }
120
102
  }
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." }] };
103
+ else {
104
+ // Signatures -> Use Main JAR
105
+ const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
106
+ detail = await SourceParser.getClassDetail(mainJarPath, className, type);
107
+ }
108
+ try {
109
+ if (!detail) {
110
+ return { content: [{ type: "text", text: `Class ${className} not found in artifact ${artifact.artifactId}.` }] };
128
111
  }
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`);
112
+ let resultText = `Class: ${detail.className}\n\n`;
113
+ if (usedDecompilation) {
114
+ resultText += "*Source code decompiled from binary class file.*\n\n";
133
115
  }
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`);
116
+ if (type === 'source') {
117
+ resultText += "```java\n" + detail.source + "\n```";
140
118
  }
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}.` }] };
119
+ else {
120
+ if (detail.doc) {
121
+ resultText += "Documentation:\n" + detail.doc + "\n\n";
145
122
  }
146
- let resultText = `Class: ${detail.className}\n\n`;
147
- if (type === 'source') {
148
- resultText += "```java\n" + detail.source + "\n```";
123
+ if (detail.signatures) {
124
+ resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
149
125
  }
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
126
  }
127
+ return { content: [{ type: "text", text: resultText }] };
163
128
  }
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
- };
129
+ catch (e) {
130
+ return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
170
131
  }
171
- throw new Error("Tool not found");
132
+ });
133
+ server.registerTool("refresh_index", {
134
+ description: "Trigger a re-scan of the Maven repository",
135
+ }, async () => {
136
+ // Re-run index
137
+ indexer.index().catch(console.error);
138
+ return {
139
+ content: [{ type: "text", text: "Index refresh started." }]
140
+ };
172
141
  });
173
142
  const transport = new StdioServerTransport();
174
143
  await server.connect(transport);
package/build/indexer.js CHANGED
@@ -5,6 +5,10 @@ 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
+ /**
9
+ * Singleton class responsible for indexing Maven artifacts.
10
+ * It scans the local repository, watches for changes, and indexes Java classes.
11
+ */
8
12
  export class Indexer {
9
13
  static instance;
10
14
  isIndexing = false;
@@ -17,6 +21,10 @@ export class Indexer {
17
21
  }
18
22
  return Indexer.instance;
19
23
  }
24
+ /**
25
+ * Starts watching the local repository for changes.
26
+ * Debounces changes to trigger re-indexing.
27
+ */
20
28
  async startWatch() {
21
29
  const config = await Config.getInstance();
22
30
  const repoPath = config.localRepository;
@@ -52,6 +60,12 @@ export class Indexer {
52
60
  onChange();
53
61
  });
54
62
  }
63
+ /**
64
+ * Main indexing process.
65
+ * 1. Scans the file system for Maven artifacts.
66
+ * 2. Synchronizes the database with found artifacts.
67
+ * 3. Indexes classes for artifacts that haven't been indexed yet.
68
+ */
55
69
  async index() {
56
70
  if (this.isIndexing)
57
71
  return;
@@ -71,40 +85,28 @@ export class Indexer {
71
85
  const artifacts = await this.scanRepository(repoPath);
72
86
  console.error(`Found ${artifacts.length} artifacts on disk.`);
73
87
  // 2. Persist artifacts and determine what needs indexing
74
- const artifactsToIndex = [];
88
+ // We use is_indexed = 0 for new artifacts.
89
+ const insertArtifact = db.prepare(`
90
+ INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
91
+ VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
92
+ `);
93
+ // Use a transaction only for the batch insert of artifacts
75
94
  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
95
  for (const art of artifacts) {
88
96
  insertArtifact.run({
89
97
  ...art,
90
98
  hasSource: art.hasSource ? 1 : 0
91
99
  });
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
100
  }
105
101
  });
102
+ // 3. Find artifacts that need indexing (is_indexed = 0)
103
+ const artifactsToIndex = db.prepare(`
104
+ SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
105
+ FROM artifacts
106
+ WHERE is_indexed = 0
107
+ `).all();
106
108
  console.error(`${artifactsToIndex.length} artifacts need indexing.`);
107
- // 3. Scan JARs for classes and update DB
109
+ // 4. Scan JARs for classes and update DB
108
110
  const CHUNK_SIZE = 50;
109
111
  let processedCount = 0;
110
112
  for (let i = 0; i < artifactsToIndex.length; i += CHUNK_SIZE) {
@@ -124,6 +126,9 @@ export class Indexer {
124
126
  this.isIndexing = false;
125
127
  }
126
128
  }
129
+ /**
130
+ * Recursively scans a directory for Maven artifacts (POM files).
131
+ */
127
132
  async scanRepository(rootDir) {
128
133
  const results = [];
129
134
  const scanDir = async (dir) => {
@@ -168,6 +173,10 @@ export class Indexer {
168
173
  await scanDir(rootDir);
169
174
  return results;
170
175
  }
176
+ /**
177
+ * Extracts classes from the artifact's JAR and indexes them.
178
+ * Updates the 'is_indexed' flag upon completion.
179
+ */
171
180
  async indexArtifactClasses(artifact) {
172
181
  const jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
173
182
  const db = DB.getInstance();
@@ -178,12 +187,13 @@ export class Indexer {
178
187
  catch {
179
188
  // If jar missing, mark as indexed so we don't retry endlessly?
180
189
  // Or maybe it's a pom-only artifact.
181
- db.prepare('INSERT OR IGNORE INTO indexed_artifacts (artifact_id) VALUES (?)').run(artifact.id);
190
+ db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
182
191
  return;
183
192
  }
184
193
  return new Promise((resolve) => {
185
194
  yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
186
195
  if (err || !zipfile) {
196
+ // If we can't open it, maybe it's corrupt. Skip for now.
187
197
  resolve();
188
198
  return;
189
199
  }
@@ -215,7 +225,8 @@ export class Indexer {
215
225
  const simpleName = cls.split('.').pop() || cls;
216
226
  insertClass.run(artifact.id, cls, simpleName);
217
227
  }
218
- db.prepare('INSERT OR IGNORE INTO indexed_artifacts (artifact_id) VALUES (?)').run(artifact.id);
228
+ // Mark as indexed
229
+ db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
219
230
  });
220
231
  }
221
232
  catch (e) {
@@ -229,6 +240,9 @@ export class Indexer {
229
240
  });
230
241
  });
231
242
  }
243
+ /**
244
+ * Checks if a class package is included in the configuration patterns.
245
+ */
232
246
  isPackageIncluded(className, patterns) {
233
247
  if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*'))
234
248
  return true;
@@ -249,6 +263,9 @@ export class Indexer {
249
263
  }
250
264
  return false;
251
265
  }
266
+ /**
267
+ * Searches for artifacts by group ID or artifact ID.
268
+ */
252
269
  search(query) {
253
270
  // Artifact coordinates search
254
271
  const db = DB.getInstance();
@@ -260,12 +277,20 @@ export class Indexer {
260
277
  `).all(`%${query}%`, `%${query}%`);
261
278
  return rows;
262
279
  }
280
+ /**
281
+ * Searches for classes matching the pattern.
282
+ * Uses Full-Text Search (FTS) for efficient matching.
283
+ */
263
284
  searchClass(classNamePattern) {
264
285
  const db = DB.getInstance();
265
286
  // Use FTS for smart matching
266
287
  // If pattern has no spaces, we assume it's a prefix or exact match query
267
288
  // "String" -> match "String" in simple_name or class_name
268
- const query = `"${classNamePattern}"* OR ${classNamePattern}`;
289
+ const escapedPattern = classNamePattern.replace(/"/g, '""');
290
+ const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
291
+ const query = safeQuery.length > 0
292
+ ? `"${escapedPattern}"* OR ${safeQuery}`
293
+ : `"${escapedPattern}"*`;
269
294
  try {
270
295
  const rows = db.prepare(`
271
296
  SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
@@ -301,6 +326,9 @@ export class Indexer {
301
326
  return [];
302
327
  }
303
328
  }
329
+ /**
330
+ * Retrieves an artifact by its database ID.
331
+ */
304
332
  getArtifactById(id) {
305
333
  const db = DB.getInstance();
306
334
  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,38 @@ 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);
63
+ }
64
+ return null;
65
+ }
66
+ static async decompileClass(jarPath, className) {
67
+ try {
68
+ const config = await Config.getInstance();
69
+ const cfrPath = config.getCfrJarPath();
70
+ if (!cfrPath || !fs.existsSync(cfrPath)) {
71
+ // If we can't find CFR, we can't decompile.
72
+ return null;
73
+ }
74
+ // Use java -cp cfr.jar:target.jar org.benf.cfr.reader.Main className
75
+ const classpath = `${cfrPath}${path.delimiter}${jarPath}`;
76
+ const { stdout } = await execAsync(`java -cp "${classpath}" org.benf.cfr.reader.Main "${className}"`);
77
+ return {
78
+ className,
79
+ source: stdout // Return as source
80
+ };
81
+ }
82
+ catch (e) {
83
+ // console.error(`CFR failed for ${className} in ${jarPath}:`, e);
84
+ return null;
85
+ }
48
86
  }
49
87
  static async getSignaturesWithJavap(jarPath, className) {
50
88
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maven-indexer-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP server for indexing local Maven repository",
5
5
  "main": "build/index.js",
6
6
  "type": "module",