maven-central-mcp 0.2.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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/README.md +59 -0
  3. package/dist/discovery/discover.d.ts +2 -0
  4. package/dist/discovery/discover.js +56 -0
  5. package/dist/discovery/gradle-parser.d.ts +2 -0
  6. package/dist/discovery/gradle-parser.js +38 -0
  7. package/dist/discovery/maven-parser.d.ts +2 -0
  8. package/dist/discovery/maven-parser.js +14 -0
  9. package/dist/discovery/types.d.ts +9 -0
  10. package/dist/discovery/types.js +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +84 -0
  13. package/dist/maven/client.d.ts +8 -0
  14. package/dist/maven/client.js +40 -0
  15. package/dist/maven/repository.d.ts +18 -0
  16. package/dist/maven/repository.js +43 -0
  17. package/dist/maven/resolver.d.ts +8 -0
  18. package/dist/maven/resolver.js +61 -0
  19. package/dist/maven/types.d.ts +23 -0
  20. package/dist/maven/types.js +1 -0
  21. package/dist/project/find-project-root.d.ts +2 -0
  22. package/dist/project/find-project-root.js +24 -0
  23. package/dist/tools/check-multiple-dependencies.d.ts +20 -0
  24. package/dist/tools/check-multiple-dependencies.js +26 -0
  25. package/dist/tools/check-version-exists.d.ts +15 -0
  26. package/dist/tools/check-version-exists.js +27 -0
  27. package/dist/tools/compare-dependency-versions.d.ts +31 -0
  28. package/dist/tools/compare-dependency-versions.js +41 -0
  29. package/dist/tools/get-latest-version.d.ts +15 -0
  30. package/dist/tools/get-latest-version.js +17 -0
  31. package/dist/version/classify.d.ts +3 -0
  32. package/dist/version/classify.js +24 -0
  33. package/dist/version/compare.d.ts +2 -0
  34. package/dist/version/compare.js +22 -0
  35. package/dist/version/types.d.ts +7 -0
  36. package/dist/version/types.js +1 -0
  37. package/package.json +49 -0
  38. package/plugin/.claude-plugin/plugin.json +12 -0
  39. package/plugin/hooks/hooks.json +15 -0
  40. package/plugin/hooks/post-edit-deps.sh +28 -0
  41. package/plugin/skills/check-deps/SKILL.md +36 -0
  42. package/plugin/skills/latest-version/SKILL.md +34 -0
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "maven-mcp",
4
+ "description": "Maven dependency intelligence for Claude Code",
5
+ "owner": {
6
+ "name": "kirich1409"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "maven-mcp",
11
+ "description": "Maven dependency intelligence — auto-registers MCP server, provides /check-deps and /latest-version skills",
12
+ "author": {
13
+ "name": "kirich1409"
14
+ },
15
+ "version": "0.2.0",
16
+ "category": "development",
17
+ "homepage": "https://github.com/kirich1409/maven-central-mcp",
18
+ "source": "./plugin"
19
+ }
20
+ ]
21
+ }
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # maven-central-mcp
2
+
3
+ MCP server for Maven Central dependency intelligence. Provides AI assistants with structured, live dependency data from Maven Central.
4
+
5
+ Works with any JVM build tool that uses Maven Central coordinates (Maven, Gradle, SBT, etc).
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx maven-central-mcp
11
+ ```
12
+
13
+ ### Claude Desktop
14
+
15
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "maven-central": {
21
+ "command": "npx",
22
+ "args": ["maven-central-mcp"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ### VS Code
29
+
30
+ Create `.vscode/mcp.json` in your workspace:
31
+
32
+ ```json
33
+ {
34
+ "servers": {
35
+ "maven-central": {
36
+ "type": "stdio",
37
+ "command": "npx",
38
+ "args": ["maven-central-mcp"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Tools
45
+
46
+ | Tool | Description |
47
+ |------|-------------|
48
+ | `get_latest_version` | Find the latest version with stability-aware selection (STABLE_ONLY / PREFER_STABLE / ALL) |
49
+ | `check_version_exists` | Verify a specific version exists and classify its stability |
50
+ | `check_multiple_dependencies` | Bulk lookup of latest versions for a list of dependencies |
51
+ | `compare_dependency_versions` | Compare current versions against latest, with upgrade type (major/minor/patch) |
52
+
53
+ ## Version Stability
54
+
55
+ Versions are classified as: `stable`, `rc`, `beta`, `alpha`, `milestone`, or `snapshot`.
56
+
57
+ ## License
58
+
59
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { DiscoveryResult } from "./types.js";
2
+ export declare function discoverRepositories(projectRoot: string): DiscoveryResult;
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseGradleRepositories } from "./gradle-parser.js";
4
+ import { parseMavenRepositories } from "./maven-parser.js";
5
+ const GRADLE_FILES = [
6
+ "settings.gradle.kts",
7
+ "settings.gradle",
8
+ "build.gradle.kts",
9
+ "build.gradle",
10
+ ];
11
+ export function discoverRepositories(projectRoot) {
12
+ const allRepos = [];
13
+ const seen = new Set();
14
+ let buildSystem = "unknown";
15
+ function addRepos(repos) {
16
+ for (const repo of repos) {
17
+ if (!seen.has(repo.url)) {
18
+ seen.add(repo.url);
19
+ allRepos.push(repo);
20
+ }
21
+ }
22
+ }
23
+ // Try Gradle files
24
+ for (const file of GRADLE_FILES) {
25
+ const path = join(projectRoot, file);
26
+ if (existsSync(path)) {
27
+ buildSystem = "gradle";
28
+ try {
29
+ const content = readFileSync(path, "utf-8");
30
+ addRepos(parseGradleRepositories(content));
31
+ }
32
+ catch {
33
+ console.error(`Failed to parse ${path}`);
34
+ }
35
+ }
36
+ }
37
+ // Try Maven pom.xml (only if no Gradle files found)
38
+ if (buildSystem === "unknown") {
39
+ const pomPath = join(projectRoot, "pom.xml");
40
+ if (existsSync(pomPath)) {
41
+ buildSystem = "maven";
42
+ try {
43
+ const content = readFileSync(pomPath, "utf-8");
44
+ addRepos(parseMavenRepositories(content));
45
+ }
46
+ catch {
47
+ console.error(`Failed to parse ${pomPath}`);
48
+ }
49
+ }
50
+ }
51
+ return {
52
+ repositories: allRepos,
53
+ buildSystem,
54
+ projectRoot,
55
+ };
56
+ }
@@ -0,0 +1,2 @@
1
+ import type { RepositoryConfig } from "./types.js";
2
+ export declare function parseGradleRepositories(content: string): RepositoryConfig[];
@@ -0,0 +1,38 @@
1
+ import { MAVEN_CENTRAL, GOOGLE_MAVEN, GRADLE_PLUGIN_PORTAL } from "../maven/repository.js";
2
+ const WELL_KNOWN_REPOS = {
3
+ mavenCentral: { name: MAVEN_CENTRAL.name, url: MAVEN_CENTRAL.url },
4
+ google: { name: GOOGLE_MAVEN.name, url: GOOGLE_MAVEN.url },
5
+ gradlePluginPortal: { name: GRADLE_PLUGIN_PORTAL.name, url: GRADLE_PLUGIN_PORTAL.url },
6
+ };
7
+ export function parseGradleRepositories(content) {
8
+ const repos = [];
9
+ // Well-known: mavenCentral(), google(), gradlePluginPortal()
10
+ for (const [funcName, config] of Object.entries(WELL_KNOWN_REPOS)) {
11
+ const pattern = new RegExp(`\\b${funcName}\\s*\\(\\s*\\)`, "g");
12
+ if (pattern.test(content)) {
13
+ repos.push(config);
14
+ }
15
+ }
16
+ // maven("url") or maven('url')
17
+ const mavenDirectRegex = /\bmaven\s*\(\s*["']([^"']+)["']\s*\)/g;
18
+ let match;
19
+ while ((match = mavenDirectRegex.exec(content)) !== null) {
20
+ repos.push({ name: match[1], url: match[1] });
21
+ }
22
+ // maven(url = "url") or maven(url = 'url')
23
+ const mavenUrlParamRegex = /\bmaven\s*\(\s*url\s*=\s*["']([^"']+)["']\s*\)/g;
24
+ while ((match = mavenUrlParamRegex.exec(content)) !== null) {
25
+ repos.push({ name: match[1], url: match[1] });
26
+ }
27
+ // maven { url = uri("url") } or maven { url = uri('url') }
28
+ const mavenBlockUriRegex = /\bmaven\s*\{[^}]*url\s*=\s*uri\s*\(\s*["']([^"']+)["']\s*\)/g;
29
+ while ((match = mavenBlockUriRegex.exec(content)) !== null) {
30
+ repos.push({ name: match[1], url: match[1] });
31
+ }
32
+ // Groovy: maven { url 'url' } or maven { url "url" }
33
+ const mavenBlockGroovyRegex = /\bmaven\s*\{[^}]*url\s+["']([^"']+)["']/g;
34
+ while ((match = mavenBlockGroovyRegex.exec(content)) !== null) {
35
+ repos.push({ name: match[1], url: match[1] });
36
+ }
37
+ return repos;
38
+ }
@@ -0,0 +1,2 @@
1
+ import type { RepositoryConfig } from "./types.js";
2
+ export declare function parseMavenRepositories(content: string): RepositoryConfig[];
@@ -0,0 +1,14 @@
1
+ export function parseMavenRepositories(content) {
2
+ const repos = [];
3
+ const repoBlockRegex = /<repository>([\s\S]*?)<\/repository>/g;
4
+ let match;
5
+ while ((match = repoBlockRegex.exec(content)) !== null) {
6
+ const block = match[1];
7
+ const url = block.match(/<url>([^<]+)<\/url>/)?.[1]?.trim();
8
+ if (!url)
9
+ continue;
10
+ const id = block.match(/<id>([^<]+)<\/id>/)?.[1]?.trim();
11
+ repos.push({ name: id ?? url, url });
12
+ }
13
+ return repos;
14
+ }
@@ -0,0 +1,9 @@
1
+ export interface RepositoryConfig {
2
+ name: string;
3
+ url: string;
4
+ }
5
+ export interface DiscoveryResult {
6
+ repositories: RepositoryConfig[];
7
+ buildSystem: "gradle" | "maven" | "unknown";
8
+ projectRoot: string;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { HttpMavenRepository, MAVEN_CENTRAL, GOOGLE_MAVEN, GRADLE_PLUGIN_PORTAL } from "./maven/repository.js";
6
+ import { findProjectRoot } from "./project/find-project-root.js";
7
+ import { discoverRepositories } from "./discovery/discover.js";
8
+ import { getLatestVersionHandler } from "./tools/get-latest-version.js";
9
+ import { checkVersionExistsHandler } from "./tools/check-version-exists.js";
10
+ import { checkMultipleDependenciesHandler } from "./tools/check-multiple-dependencies.js";
11
+ import { compareDependencyVersionsHandler } from "./tools/compare-dependency-versions.js";
12
+ const server = new McpServer({
13
+ name: "maven-central-mcp",
14
+ version: "0.2.0",
15
+ });
16
+ let cachedRepos = null;
17
+ function getRepositories() {
18
+ if (cachedRepos)
19
+ return cachedRepos;
20
+ const repos = [];
21
+ const projectRoot = findProjectRoot(process.cwd());
22
+ if (projectRoot) {
23
+ const discovery = discoverRepositories(projectRoot);
24
+ console.error(`Discovered ${discovery.repositories.length} repositories from ${discovery.buildSystem} project at ${projectRoot}`);
25
+ for (const config of discovery.repositories) {
26
+ repos.push(new HttpMavenRepository(config.name, config.url));
27
+ }
28
+ }
29
+ // Add well-known repos as fallback (skip if already discovered)
30
+ for (const fallback of [GOOGLE_MAVEN, GRADLE_PLUGIN_PORTAL, MAVEN_CENTRAL]) {
31
+ if (!repos.some((r) => r.url === fallback.url)) {
32
+ repos.push(fallback);
33
+ }
34
+ }
35
+ cachedRepos = repos;
36
+ return repos;
37
+ }
38
+ server.tool("get_latest_version", "Find the latest version of a Maven artifact with stability-aware selection", {
39
+ groupId: z.string().describe("Maven group ID (e.g. io.ktor)"),
40
+ artifactId: z.string().describe("Maven artifact ID (e.g. ktor-server-core)"),
41
+ stabilityFilter: z
42
+ .enum(["STABLE_ONLY", "PREFER_STABLE", "ALL"])
43
+ .optional()
44
+ .describe("Version stability filter (default: PREFER_STABLE)"),
45
+ }, async (params) => {
46
+ const result = await getLatestVersionHandler(getRepositories(), params);
47
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
48
+ });
49
+ server.tool("check_version_exists", "Verify a specific version exists and classify its stability", {
50
+ groupId: z.string().describe("Maven group ID"),
51
+ artifactId: z.string().describe("Maven artifact ID"),
52
+ version: z.string().describe("Version to check"),
53
+ }, async (params) => {
54
+ const result = await checkVersionExistsHandler(getRepositories(), params);
55
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
56
+ });
57
+ server.tool("check_multiple_dependencies", "Bulk lookup of latest versions for a list of Maven dependencies", {
58
+ dependencies: z.array(z.object({
59
+ groupId: z.string().describe("Maven group ID"),
60
+ artifactId: z.string().describe("Maven artifact ID"),
61
+ })).describe("List of dependencies to check"),
62
+ }, async (params) => {
63
+ const result = await checkMultipleDependenciesHandler(getRepositories(), params);
64
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
65
+ });
66
+ server.tool("compare_dependency_versions", "Compare current dependency versions against latest available, showing upgrade type (major/minor/patch)", {
67
+ dependencies: z.array(z.object({
68
+ groupId: z.string().describe("Maven group ID"),
69
+ artifactId: z.string().describe("Maven artifact ID"),
70
+ currentVersion: z.string().describe("Currently used version"),
71
+ })).describe("Dependencies with current versions to compare"),
72
+ }, async (params) => {
73
+ const result = await compareDependencyVersionsHandler(getRepositories(), params);
74
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
75
+ });
76
+ async function main() {
77
+ const transport = new StdioServerTransport();
78
+ await server.connect(transport);
79
+ console.error("maven-central-mcp running on stdio");
80
+ }
81
+ main().catch((error) => {
82
+ console.error("Fatal error:", error);
83
+ process.exit(1);
84
+ });
@@ -0,0 +1,8 @@
1
+ import type { MavenMetadata, MavenSearchResponse } from "./types.js";
2
+ export declare class MavenCentralClient {
3
+ buildSearchUrl(groupId: string, artifactId: string, rows: number): string;
4
+ buildMetadataUrl(groupId: string, artifactId: string): string;
5
+ searchArtifact(groupId: string, artifactId: string): Promise<MavenSearchResponse>;
6
+ fetchMetadata(groupId: string, artifactId: string): Promise<MavenMetadata>;
7
+ parseMetadataXml(xml: string, groupId: string, artifactId: string): MavenMetadata;
8
+ }
@@ -0,0 +1,40 @@
1
+ const SEARCH_BASE = "https://search.maven.org/solrsearch/select";
2
+ const REPO_BASE = "https://repo1.maven.org/maven2";
3
+ export class MavenCentralClient {
4
+ buildSearchUrl(groupId, artifactId, rows) {
5
+ return `${SEARCH_BASE}?q=g:${groupId}+AND+a:${artifactId}&rows=${rows}&wt=json`;
6
+ }
7
+ buildMetadataUrl(groupId, artifactId) {
8
+ const groupPath = groupId.replace(/\./g, "/");
9
+ return `${REPO_BASE}/${groupPath}/${artifactId}/maven-metadata.xml`;
10
+ }
11
+ async searchArtifact(groupId, artifactId) {
12
+ const url = this.buildSearchUrl(groupId, artifactId, 1);
13
+ const response = await fetch(url);
14
+ if (!response.ok) {
15
+ throw new Error(`Maven Central search failed: ${response.status} ${response.statusText}`);
16
+ }
17
+ return response.json();
18
+ }
19
+ async fetchMetadata(groupId, artifactId) {
20
+ const url = this.buildMetadataUrl(groupId, artifactId);
21
+ const response = await fetch(url);
22
+ if (!response.ok) {
23
+ throw new Error(`Maven Central metadata fetch failed: ${response.status} ${response.statusText}`);
24
+ }
25
+ const xml = await response.text();
26
+ return this.parseMetadataXml(xml, groupId, artifactId);
27
+ }
28
+ parseMetadataXml(xml, groupId, artifactId) {
29
+ const versions = [];
30
+ const versionRegex = /<version>([^<]+)<\/version>/g;
31
+ let match;
32
+ while ((match = versionRegex.exec(xml)) !== null) {
33
+ versions.push(match[1]);
34
+ }
35
+ const latest = xml.match(/<latest>([^<]+)<\/latest>/)?.[1];
36
+ const release = xml.match(/<release>([^<]+)<\/release>/)?.[1];
37
+ const lastUpdated = xml.match(/<lastUpdated>([^<]+)<\/lastUpdated>/)?.[1];
38
+ return { groupId, artifactId, versions, latest, release, lastUpdated };
39
+ }
40
+ }
@@ -0,0 +1,18 @@
1
+ import type { MavenMetadata } from "./types.js";
2
+ export interface MavenRepository {
3
+ readonly name: string;
4
+ readonly url: string;
5
+ fetchMetadata(groupId: string, artifactId: string): Promise<MavenMetadata>;
6
+ }
7
+ export declare class HttpMavenRepository implements MavenRepository {
8
+ readonly name: string;
9
+ readonly url: string;
10
+ constructor(name: string, url: string);
11
+ buildMetadataUrl(groupId: string, artifactId: string): string;
12
+ fetchMetadata(groupId: string, artifactId: string): Promise<MavenMetadata>;
13
+ parseMetadataXml(xml: string, groupId: string, artifactId: string): MavenMetadata;
14
+ }
15
+ export declare const MAVEN_CENTRAL: HttpMavenRepository;
16
+ export declare const GOOGLE_MAVEN: HttpMavenRepository;
17
+ export declare const GRADLE_PLUGIN_PORTAL: HttpMavenRepository;
18
+ export declare const PROXY_TARGET_URLS: Set<string>;
@@ -0,0 +1,43 @@
1
+ export class HttpMavenRepository {
2
+ name;
3
+ url;
4
+ constructor(name, url) {
5
+ this.name = name;
6
+ this.url = url.replace(/\/+$/, "");
7
+ }
8
+ buildMetadataUrl(groupId, artifactId) {
9
+ const groupPath = groupId.replace(/\./g, "/");
10
+ return `${this.url}/${groupPath}/${artifactId}/maven-metadata.xml`;
11
+ }
12
+ async fetchMetadata(groupId, artifactId) {
13
+ const url = this.buildMetadataUrl(groupId, artifactId);
14
+ const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
15
+ if (!response.ok) {
16
+ throw new Error(`Metadata fetch failed from ${this.name}: ${response.status} ${response.statusText}`);
17
+ }
18
+ const xml = await response.text();
19
+ return this.parseMetadataXml(xml, groupId, artifactId);
20
+ }
21
+ parseMetadataXml(xml, groupId, artifactId) {
22
+ const versions = [];
23
+ const versionRegex = /<version>([^<]+)<\/version>/g;
24
+ let match;
25
+ while ((match = versionRegex.exec(xml)) !== null) {
26
+ versions.push(match[1]);
27
+ }
28
+ const latest = xml.match(/<latest>([^<]+)<\/latest>/)?.[1];
29
+ const release = xml.match(/<release>([^<]+)<\/release>/)?.[1];
30
+ const lastUpdated = xml.match(/<lastUpdated>([^<]+)<\/lastUpdated>/)?.[1];
31
+ return { groupId, artifactId, versions, latest, release, lastUpdated };
32
+ }
33
+ }
34
+ export const MAVEN_CENTRAL = new HttpMavenRepository("Maven Central", "https://repo1.maven.org/maven2");
35
+ export const GOOGLE_MAVEN = new HttpMavenRepository("Google", "https://maven.google.com");
36
+ export const GRADLE_PLUGIN_PORTAL = new HttpMavenRepository("Gradle Plugin Portal", "https://plugins.gradle.org/m2");
37
+ // URLs commonly proxied by corporate repos (Nexus, Artifactory).
38
+ // When a custom repo returns results, these are deprioritized to avoid
39
+ // duplicate/stale metadata. Google Maven and Gradle Plugin Portal are NOT
40
+ // included — they host unique artifacts not typically proxied.
41
+ export const PROXY_TARGET_URLS = new Set([
42
+ MAVEN_CENTRAL.url,
43
+ ]);
@@ -0,0 +1,8 @@
1
+ import type { MavenRepository } from "./repository.js";
2
+ import type { MavenMetadata } from "./types.js";
3
+ export interface ResolveFirstResult {
4
+ metadata: MavenMetadata;
5
+ repository: MavenRepository;
6
+ }
7
+ export declare function resolveFirst(repos: MavenRepository[], groupId: string, artifactId: string): Promise<ResolveFirstResult | null>;
8
+ export declare function resolveAll(repos: MavenRepository[], groupId: string, artifactId: string): Promise<MavenMetadata>;
@@ -0,0 +1,61 @@
1
+ import { PROXY_TARGET_URLS } from "./repository.js";
2
+ export async function resolveFirst(repos, groupId, artifactId) {
3
+ for (const repo of repos) {
4
+ try {
5
+ const metadata = await repo.fetchMetadata(groupId, artifactId);
6
+ return { metadata, repository: repo };
7
+ }
8
+ catch {
9
+ continue;
10
+ }
11
+ }
12
+ return null;
13
+ }
14
+ export async function resolveAll(repos, groupId, artifactId) {
15
+ if (repos.length === 0) {
16
+ throw new Error(`No repositories configured to search for ${groupId}:${artifactId}`);
17
+ }
18
+ const results = await Promise.all(repos.map(async (repo) => {
19
+ try {
20
+ return await repo.fetchMetadata(groupId, artifactId);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }));
26
+ // Custom repos (Nexus, Artifactory) typically proxy Maven Central.
27
+ // When a custom repo returns results for an artifact, skip Maven Central
28
+ // results to avoid duplicates and stale metadata from proxy caching.
29
+ // Google Maven and Gradle Plugin Portal are NOT proxy targets —
30
+ // they host unique artifacts and always contribute results.
31
+ const all = results.map((r, i) => ({ result: r, repo: repos[i] }));
32
+ const hasCustomResult = all.some(({ result, repo }) => result !== null && !PROXY_TARGET_URLS.has(repo.url));
33
+ const successful = all
34
+ .filter(({ result, repo }) => result !== null && !(hasCustomResult && PROXY_TARGET_URLS.has(repo.url)))
35
+ .map(({ result }) => result);
36
+ if (successful.length === 0) {
37
+ throw new Error(`Artifact ${groupId}:${artifactId} not found in any repository`);
38
+ }
39
+ const orderedVersions = [];
40
+ const seen = new Set();
41
+ for (const meta of successful) {
42
+ for (const v of meta.versions) {
43
+ if (!seen.has(v)) {
44
+ seen.add(v);
45
+ orderedVersions.push(v);
46
+ }
47
+ }
48
+ }
49
+ // Pick the most recent latest/release across all repos
50
+ const allLatest = successful.map((m) => m.latest).filter(Boolean);
51
+ const allRelease = successful.map((m) => m.release).filter(Boolean);
52
+ const lastVersion = orderedVersions[orderedVersions.length - 1];
53
+ return {
54
+ groupId,
55
+ artifactId,
56
+ versions: orderedVersions,
57
+ latest: allLatest.includes(lastVersion) ? lastVersion : allLatest[allLatest.length - 1] ?? lastVersion,
58
+ release: allRelease.includes(lastVersion) ? lastVersion : allRelease[allRelease.length - 1] ?? lastVersion,
59
+ lastUpdated: successful[0].lastUpdated,
60
+ };
61
+ }
@@ -0,0 +1,23 @@
1
+ export interface MavenSearchResponse {
2
+ response: {
3
+ numFound: number;
4
+ docs: MavenArtifact[];
5
+ };
6
+ }
7
+ export interface MavenArtifact {
8
+ id: string;
9
+ g: string;
10
+ a: string;
11
+ v: string;
12
+ latestVersion: string;
13
+ timestamp: number;
14
+ versionCount: number;
15
+ }
16
+ export interface MavenMetadata {
17
+ groupId: string;
18
+ artifactId: string;
19
+ versions: string[];
20
+ latest?: string;
21
+ release?: string;
22
+ lastUpdated?: string;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const BUILD_FILE_MARKERS: readonly ["settings.gradle.kts", "settings.gradle", "build.gradle.kts", "build.gradle", "pom.xml"];
2
+ export declare function findProjectRoot(startDir: string): string | null;
@@ -0,0 +1,24 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ export const BUILD_FILE_MARKERS = [
4
+ "settings.gradle.kts",
5
+ "settings.gradle",
6
+ "build.gradle.kts",
7
+ "build.gradle",
8
+ "pom.xml",
9
+ ];
10
+ export function findProjectRoot(startDir) {
11
+ let current = resolve(startDir);
12
+ while (true) {
13
+ for (const marker of BUILD_FILE_MARKERS) {
14
+ if (existsSync(join(current, marker))) {
15
+ return current;
16
+ }
17
+ }
18
+ const parent = dirname(current);
19
+ if (parent === current) {
20
+ return null;
21
+ }
22
+ current = parent;
23
+ }
24
+ }
@@ -0,0 +1,20 @@
1
+ import type { MavenRepository } from "../maven/repository.js";
2
+ interface Dependency {
3
+ groupId: string;
4
+ artifactId: string;
5
+ }
6
+ export interface CheckMultipleDependenciesInput {
7
+ dependencies: Dependency[];
8
+ }
9
+ export interface DependencyResult {
10
+ groupId: string;
11
+ artifactId: string;
12
+ latestVersion: string;
13
+ stability: string;
14
+ error?: string;
15
+ }
16
+ export interface CheckMultipleDependenciesResult {
17
+ results: DependencyResult[];
18
+ }
19
+ export declare function checkMultipleDependenciesHandler(repos: MavenRepository[], input: CheckMultipleDependenciesInput): Promise<CheckMultipleDependenciesResult>;
20
+ export {};
@@ -0,0 +1,26 @@
1
+ import { classifyVersion, findLatestVersion } from "../version/classify.js";
2
+ import { resolveAll } from "../maven/resolver.js";
3
+ export async function checkMultipleDependenciesHandler(repos, input) {
4
+ const results = await Promise.all(input.dependencies.map(async (dep) => {
5
+ try {
6
+ const metadata = await resolveAll(repos, dep.groupId, dep.artifactId);
7
+ const latest = findLatestVersion(metadata.versions);
8
+ return {
9
+ groupId: dep.groupId,
10
+ artifactId: dep.artifactId,
11
+ latestVersion: latest,
12
+ stability: classifyVersion(latest),
13
+ };
14
+ }
15
+ catch (e) {
16
+ return {
17
+ groupId: dep.groupId,
18
+ artifactId: dep.artifactId,
19
+ latestVersion: "",
20
+ stability: "",
21
+ error: e instanceof Error ? e.message : String(e),
22
+ };
23
+ }
24
+ }));
25
+ return { results };
26
+ }
@@ -0,0 +1,15 @@
1
+ import type { MavenRepository } from "../maven/repository.js";
2
+ export interface CheckVersionExistsInput {
3
+ groupId: string;
4
+ artifactId: string;
5
+ version: string;
6
+ }
7
+ export interface CheckVersionExistsResult {
8
+ groupId: string;
9
+ artifactId: string;
10
+ version: string;
11
+ exists: boolean;
12
+ stability?: string;
13
+ repository?: string;
14
+ }
15
+ export declare function checkVersionExistsHandler(repos: MavenRepository[], input: CheckVersionExistsInput): Promise<CheckVersionExistsResult>;
@@ -0,0 +1,27 @@
1
+ import { classifyVersion } from "../version/classify.js";
2
+ export async function checkVersionExistsHandler(repos, input) {
3
+ for (const repo of repos) {
4
+ try {
5
+ const metadata = await repo.fetchMetadata(input.groupId, input.artifactId);
6
+ if (metadata.versions.includes(input.version)) {
7
+ return {
8
+ groupId: input.groupId,
9
+ artifactId: input.artifactId,
10
+ version: input.version,
11
+ exists: true,
12
+ stability: classifyVersion(input.version),
13
+ repository: repo.name,
14
+ };
15
+ }
16
+ }
17
+ catch {
18
+ continue;
19
+ }
20
+ }
21
+ return {
22
+ groupId: input.groupId,
23
+ artifactId: input.artifactId,
24
+ version: input.version,
25
+ exists: false,
26
+ };
27
+ }
@@ -0,0 +1,31 @@
1
+ import type { MavenRepository } from "../maven/repository.js";
2
+ interface DependencyWithVersion {
3
+ groupId: string;
4
+ artifactId: string;
5
+ currentVersion: string;
6
+ }
7
+ export interface CompareDependencyVersionsInput {
8
+ dependencies: DependencyWithVersion[];
9
+ }
10
+ export interface CompareResult {
11
+ groupId: string;
12
+ artifactId: string;
13
+ currentVersion: string;
14
+ latestVersion: string;
15
+ latestStability: string;
16
+ upgradeType: string;
17
+ upgradeAvailable: boolean;
18
+ error?: string;
19
+ }
20
+ export interface CompareDependencyVersionsResult {
21
+ results: CompareResult[];
22
+ summary: {
23
+ total: number;
24
+ upgradeable: number;
25
+ major: number;
26
+ minor: number;
27
+ patch: number;
28
+ };
29
+ }
30
+ export declare function compareDependencyVersionsHandler(repos: MavenRepository[], input: CompareDependencyVersionsInput): Promise<CompareDependencyVersionsResult>;
31
+ export {};
@@ -0,0 +1,41 @@
1
+ import { classifyVersion, findLatestVersion } from "../version/classify.js";
2
+ import { getUpgradeType } from "../version/compare.js";
3
+ import { resolveAll } from "../maven/resolver.js";
4
+ export async function compareDependencyVersionsHandler(repos, input) {
5
+ const results = await Promise.all(input.dependencies.map(async (dep) => {
6
+ try {
7
+ const metadata = await resolveAll(repos, dep.groupId, dep.artifactId);
8
+ const latest = findLatestVersion(metadata.versions);
9
+ const upgradeType = getUpgradeType(dep.currentVersion, latest);
10
+ return {
11
+ groupId: dep.groupId,
12
+ artifactId: dep.artifactId,
13
+ currentVersion: dep.currentVersion,
14
+ latestVersion: latest,
15
+ latestStability: classifyVersion(latest),
16
+ upgradeType,
17
+ upgradeAvailable: upgradeType !== "none",
18
+ };
19
+ }
20
+ catch (e) {
21
+ return {
22
+ groupId: dep.groupId,
23
+ artifactId: dep.artifactId,
24
+ currentVersion: dep.currentVersion,
25
+ latestVersion: "",
26
+ latestStability: "",
27
+ upgradeType: "none",
28
+ upgradeAvailable: false,
29
+ error: e instanceof Error ? e.message : String(e),
30
+ };
31
+ }
32
+ }));
33
+ const summary = {
34
+ total: results.length,
35
+ upgradeable: results.filter((r) => r.upgradeAvailable).length,
36
+ major: results.filter((r) => r.upgradeType === "major").length,
37
+ minor: results.filter((r) => r.upgradeType === "minor").length,
38
+ patch: results.filter((r) => r.upgradeType === "patch").length,
39
+ };
40
+ return { results, summary };
41
+ }
@@ -0,0 +1,15 @@
1
+ import type { StabilityFilter } from "../version/types.js";
2
+ import type { MavenRepository } from "../maven/repository.js";
3
+ export interface GetLatestVersionInput {
4
+ groupId: string;
5
+ artifactId: string;
6
+ stabilityFilter?: StabilityFilter;
7
+ }
8
+ export interface GetLatestVersionResult {
9
+ groupId: string;
10
+ artifactId: string;
11
+ latestVersion: string;
12
+ stability: string;
13
+ allVersionsCount: number;
14
+ }
15
+ export declare function getLatestVersionHandler(repos: MavenRepository[], input: GetLatestVersionInput): Promise<GetLatestVersionResult>;
@@ -0,0 +1,17 @@
1
+ import { classifyVersion, findLatestVersion } from "../version/classify.js";
2
+ import { resolveAll } from "../maven/resolver.js";
3
+ export async function getLatestVersionHandler(repos, input) {
4
+ const metadata = await resolveAll(repos, input.groupId, input.artifactId);
5
+ const filter = input.stabilityFilter ?? "PREFER_STABLE";
6
+ const selected = findLatestVersion(metadata.versions, filter);
7
+ if (!selected) {
8
+ throw new Error(`No stable version found for ${input.groupId}:${input.artifactId}`);
9
+ }
10
+ return {
11
+ groupId: input.groupId,
12
+ artifactId: input.artifactId,
13
+ latestVersion: selected,
14
+ stability: classifyVersion(selected),
15
+ allVersionsCount: metadata.versions.length,
16
+ };
17
+ }
@@ -0,0 +1,3 @@
1
+ import type { StabilityFilter, StabilityType } from "./types.js";
2
+ export declare function classifyVersion(version: string): StabilityType;
3
+ export declare function findLatestVersion(versions: string[], filter?: StabilityFilter): string | undefined;
@@ -0,0 +1,24 @@
1
+ const STABILITY_PATTERNS = [
2
+ [/[-.]?SNAPSHOT$/i, "snapshot"],
3
+ [/[-.](?:alpha|a(?=\d|[-.]|$))[-.]?\d*/i, "alpha"],
4
+ [/[-.](?:beta|b(?=\d|[-.]|$))[-.]?\d*/i, "beta"],
5
+ [/[-.](?:M|milestone)[-.]?\d*/i, "milestone"],
6
+ [/[-.](?:RC|CR)[-.]?\d*/i, "rc"],
7
+ ];
8
+ export function classifyVersion(version) {
9
+ for (const [pattern, stability] of STABILITY_PATTERNS) {
10
+ if (pattern.test(version)) {
11
+ return stability;
12
+ }
13
+ }
14
+ return "stable";
15
+ }
16
+ export function findLatestVersion(versions, filter = "PREFER_STABLE") {
17
+ const reversed = [...versions].reverse();
18
+ if (filter === "ALL")
19
+ return reversed[0];
20
+ const stable = reversed.find((v) => classifyVersion(v) === "stable");
21
+ if (filter === "STABLE_ONLY")
22
+ return stable;
23
+ return stable ?? reversed[0];
24
+ }
@@ -0,0 +1,2 @@
1
+ import type { UpgradeType } from "./types.js";
2
+ export declare function getUpgradeType(current: string, latest: string): UpgradeType;
@@ -0,0 +1,22 @@
1
+ function parseSegments(version) {
2
+ return version
3
+ .replace(/[-+].*$/, "")
4
+ .split(".")
5
+ .map((s) => parseInt(s, 10) || 0);
6
+ }
7
+ export function getUpgradeType(current, latest) {
8
+ const cur = parseSegments(current);
9
+ const lat = parseSegments(latest);
10
+ const maxLen = Math.max(cur.length, lat.length);
11
+ while (cur.length < maxLen)
12
+ cur.push(0);
13
+ while (lat.length < maxLen)
14
+ lat.push(0);
15
+ if (lat[0] !== cur[0])
16
+ return lat[0] > cur[0] ? "major" : "none";
17
+ if (lat[1] !== cur[1])
18
+ return lat[1] > cur[1] ? "minor" : "none";
19
+ if (lat[2] !== cur[2])
20
+ return lat[2] > cur[2] ? "patch" : "none";
21
+ return "none";
22
+ }
@@ -0,0 +1,7 @@
1
+ export type StabilityType = "stable" | "rc" | "beta" | "alpha" | "milestone" | "snapshot";
2
+ export type StabilityFilter = "STABLE_ONLY" | "PREFER_STABLE" | "ALL";
3
+ export type UpgradeType = "major" | "minor" | "patch" | "none";
4
+ export interface VersionInfo {
5
+ version: string;
6
+ stability: StabilityType;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "maven-central-mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for Maven Central dependency intelligence",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "maven-central-mcp": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "lint": "eslint src/",
13
+ "test": "vitest run",
14
+ "dev": "tsc --watch"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "maven",
19
+ "maven-central",
20
+ "dependencies",
21
+ "jvm"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/kirich1409/maven-central-mcp"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ ".claude-plugin",
32
+ "plugin"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.27.1",
39
+ "zod": "^4.3.6"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^10.0.1",
43
+ "@types/node": "^25.3.5",
44
+ "eslint": "^10.0.3",
45
+ "typescript": "^5.9.3",
46
+ "typescript-eslint": "^8.56.1",
47
+ "vitest": "^4.0.18"
48
+ }
49
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "maven-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Maven dependency intelligence — auto-registers MCP server, provides /check-deps and /latest-version skills",
5
+ "mcpServers": {
6
+ "maven-mcp": {
7
+ "command": "npx",
8
+ "args": ["-y", "maven-central-mcp"]
9
+ }
10
+ },
11
+ "skills": "./skills"
12
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Edit|Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-deps.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+
3
+ # Read hook input from stdin
4
+ HOOK_INPUT=$(cat)
5
+
6
+ # Extract tool name and file path
7
+ TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // empty')
8
+ FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty')
9
+
10
+ # Only care about Edit and Write tools
11
+ if [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then
12
+ exit 0
13
+ fi
14
+
15
+ # Check if the file is a build dependency file
16
+ BASENAME=$(basename "$FILE_PATH")
17
+ case "$BASENAME" in
18
+ build.gradle|build.gradle.kts|settings.gradle|settings.gradle.kts|pom.xml|libs.versions.toml)
19
+ ;;
20
+ *)
21
+ exit 0
22
+ ;;
23
+ esac
24
+
25
+ # Output reminder as JSON systemMessage
26
+ cat <<'EOF'
27
+ {"systemMessage":"Build dependency file was modified. Consider running /check-deps to verify dependency versions are up to date."}
28
+ EOF
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: check-deps
3
+ description: Scan current project build files for Maven/Gradle dependencies and check for available updates. Use when user says "check deps", "check dependencies", "outdated dependencies", "update dependencies", or "/check-deps".
4
+ ---
5
+
6
+ # Check Dependencies
7
+
8
+ Scan the current project for Maven/Gradle dependencies and report available updates.
9
+
10
+ ## Steps
11
+
12
+ 1. Find build dependency files in the project root:
13
+ - `gradle/libs.versions.toml` (Gradle version catalog)
14
+ - `build.gradle.kts` or `build.gradle`
15
+ - `pom.xml`
16
+
17
+ 2. Read the found files and extract all dependencies with their current versions.
18
+ - For `libs.versions.toml`: parse the `[versions]` and `[libraries]` sections
19
+ - For Gradle files: find `implementation`, `api`, `compileOnly`, `testImplementation` etc. with group:artifact:version
20
+ - For `pom.xml`: find `<dependency>` blocks with `<groupId>`, `<artifactId>`, `<version>`
21
+
22
+ 3. Call the `check_multiple_dependencies` MCP tool (from maven-mcp server) with all extracted dependencies.
23
+
24
+ 4. Present results as a markdown table:
25
+
26
+ | Artifact | Current | Latest | Upgrade |
27
+ |----------|---------|--------|---------|
28
+ | io.ktor:ktor-server-core | 2.3.5 | 2.3.8 | PATCH |
29
+
30
+ 5. If all dependencies are up to date, say so.
31
+
32
+ ## Important
33
+
34
+ - Always check `libs.versions.toml` first — it's the modern Gradle standard for version management.
35
+ - Dependencies in `libs.versions.toml` may use version references in build files — resolve them.
36
+ - Skip dependencies without explicit versions (e.g., BOM-managed or platform dependencies).
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: latest-version
3
+ description: Find the latest version of a Maven artifact. Use when user says "latest version", "find version", "what version", or "/latest-version groupId:artifactId".
4
+ ---
5
+
6
+ # Latest Version
7
+
8
+ Find the latest version of a specific Maven artifact.
9
+
10
+ ## Arguments
11
+
12
+ The user provides `groupId:artifactId`, for example:
13
+ - `io.ktor:ktor-server-core`
14
+ - `org.jetbrains.kotlin:kotlin-stdlib`
15
+ - `com.google.dagger:hilt-android`
16
+
17
+ ## Steps
18
+
19
+ 1. Parse the user's input to extract `groupId` and `artifactId` (split by `:`).
20
+
21
+ 2. Call the `get_latest_version` MCP tool (from maven-mcp server) with:
22
+ - `groupId`: extracted group ID
23
+ - `artifactId`: extracted artifact ID
24
+ - `stabilityFilter`: `PREFER_STABLE` (default)
25
+
26
+ 3. Display the result:
27
+ - Latest stable version
28
+ - Latest version (if different from stable)
29
+ - All available versions (abbreviated if more than 10)
30
+
31
+ ## Error Handling
32
+
33
+ - If the artifact is not found, suggest checking the groupId and artifactId spelling.
34
+ - If the format is wrong (no `:`), ask the user to provide `groupId:artifactId`.