--- name: maven-central-scraper description: Search the Maven Central index for JVM artifacts and return structured rows via the Apify Actor arman-bd/maven-central-scraper. Each row carries the full coordinate split into groupId, artifactId and version, plus packaging, the published classifier list, version count, publish timestamp and links to the artifact page, the exact version page and the repository directory holding the files. Use when a task needs dependency and SBOM audits, outdated-dependency reports across whole groups, release-history reconstruction or release-cadence tracking for JVM libraries. Not for downloading JARs, resolving transitive dependency trees, reading POM contents or any non-JVM registry. --- # Maven Central Scraper Apify Actor `arman-bd/maven-central-scraper`. Give it queries, get one row per artifact or one row per published version, depending on a single switch. It runs without credentials. ## When to use it - Resolving every coordinate in a lockfile to its true latest version and publish date. - Outdated-dependency reports across a whole group, not just the artifacts you pinned. - Reconstructing a library's full release history with dates, one row per version. - Supply-chain checks: artifacts publishing no sources jar, unexpected packaging, a suspiciously recent first publish. - Release cadence as a liveness proxy for a JVM project. ## When not to use it - Downloading artifacts. You get `repositoryUrl`, the directory holding the files, and fetch what you need yourself. - Dependency resolution. POM contents, transitive trees and conflict resolution are not here. - Download or usage statistics. Maven Central publishes none through this index. - Snapshot repositories, private Nexus or Artifactory instances, or any non-JVM package registry. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/maven-central-scraper').call({ queries: ['com.google.guava:guava', 'g:org.apache.commons'], latestOnly: true, maxResultsPerQuery: 100, packagingFilter: [], }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~maven-central-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"queries":["com.google.guava:guava","g:org.apache.commons"],"latestOnly":true,"maxResultsPerQuery":50}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/maven-central-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `queries` | string[] | yes | | One search per entry. Field syntax (`g:com.google.guava`, `a:spring-core`, `v:5.3.0`), a Gradle coordinate (`com.google.guava:guava` or `com.google.guava:guava:33.4.8-jre`) which is rewritten into field clauses for you, or free text. Anything already containing `g:`, `a:`, `v:`, `AND` or `OR` passes through untouched. Duplicates are removed after rewriting. | | `latestOnly` | boolean | no | `true` | On: one row per artifact, its newest version. Off: one row per published version, which is how you get a release history. The two modes read different indexes and return different fields, see below. | | `maxResultsPerQuery` | integer | no | `100` | Rows **saved** per query, counted after `packagingFilter`. `0` means everything the query matches. Results are paged 200 at a time. | | `packagingFilter` | string[] | no | `[]` | Keep only these packaging types (`jar`, `pom`, `bundle`, `aar`, `war`). Case-insensitive. Applied after fetching, so it shrinks the dataset, not the request count. Empty keeps all. | **`latestOnly` is not a filter, it selects which index answers the query, and that decides both the row count and the field set.** On, one artifact is one row carrying `versionCount` and `repositoryId` but no `keywords`. Off, one artifact becomes one row per version (`org.springframework:spring-core` alone is over 300), carrying `keywords` but neither `versionCount` nor `repositoryId`. The per-version index also has no free-text field: a keyword query with `latestOnly: false` is rejected before it is ever sent, with a message in `RUN_SUMMARY.failures` telling you to switch the flag on or name a group. Both modes emit the same key set, with `null` where the index has nothing, so records never gain or lose fields. ## Output One record per row saved. | Field | Type | Notes | |---|---|---| | `query` | string | The normalised query the row came from, after coordinate rewriting. Join back to your input on this. | | `coordinate` | string \| null | `group:artifact` in latest mode, `group:artifact:version` in per-version mode. The shape changes with the mode. | | `groupId` | string \| null | Group. | | `artifactId` | string \| null | Artifact name. | | `version` | string \| null | The newest version in latest mode, that row's version in per-version mode. | | `isLatestVersion` | boolean | Mirrors the `latestOnly` input for every row in the run. Not a per-row assertion, see below. | | `versionCount` | number \| null | Total published versions. Latest mode only, `null` otherwise. | | `packaging` | string \| null | `jar`, `pom`, `bundle`, `aar`, `war` and friends. | | `classifiers` | string[] | Every extension and classifier published for the version: `[".jar", ".pom", "-sources.jar", "-javadoc.jar"]`. Empty array when the index lists none. | | `keywords` | string[] | Description tags. Per-version mode only, empty array in latest mode. | | `repositoryId` | string \| null | Source repository, in practice `central`. Latest mode only. | | `publishedAt` | string \| null | Publish time, ISO 8601. When the file landed in the repository. | | `publishedAtMs` | number \| null | The same instant as raw epoch milliseconds. | | `artifactUrl` | string \| null | Artifact page. | | `versionUrl` | string \| null | Page for that exact version. | | `repositoryUrl` | string \| null | Directory holding the actual files for that version. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record from `com.google.guava:guava` in latest mode: ```json { "query": "g:com.google.guava AND a:guava", "coordinate": "com.google.guava:guava", "groupId": "com.google.guava", "artifactId": "guava", "version": "33.4.8-jre", "isLatestVersion": true, "versionCount": 150, "packaging": "bundle", "classifiers": [".module", "-sources.jar", ".pom", "-javadoc.jar", ".jar"], "keywords": [], "repositoryId": "central", "publishedAt": "2025-04-14T17:25:22.422Z", "publishedAtMs": 1744651522422, "artifactUrl": "https://central.sonatype.com/artifact/com.google.guava/guava", "versionUrl": "https://central.sonatype.com/artifact/com.google.guava/guava/33.4.8-jre", "repositoryUrl": "https://repo1.maven.org/maven2/com/google/guava/guava/33.4.8-jre/", "scrapedAt": "2026-08-06T11:45:18.892Z" } ``` The same query with `latestOnly: false` returns 150 rows instead of one, each with its own `version`, `publishedAt`, `packaging` and `classifiers`, `keywords` populated, and `versionCount` and `repositoryId` both `null`. ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** A query that was rejected before it was sent shows up here and nowhere else. ```json { "queriesRequested": ["g:com.google.guava AND a:guava", "g:org.apache.commons"], "queriesFailed": 0, "failures": [], "artifactsSaved": 148, "filters": { "latestOnly": true, "maxResultsPerQuery": 100, "packagingFilter": [] }, "finishedAt": "2026-08-06T11:45:22.104Z" } ``` `queriesRequested` is the **list of normalised queries**, not a count, so it is also the lookup table from your input coordinates to the `query` value on each row. `failures` carries one `{ query, error }` per failed search; an error mentioning free text means the query needed `latestOnly: true`, and the other queries in the run were unaffected. `artifactsSaved` sitting exactly on `filters.maxResultsPerQuery` times the query count means the cap bit, not that the results ran out. ## Behaviour to plan around - **`isLatestVersion` is the mode, not a check.** It equals `latestOnly` on every row of the run. In per-version mode it is `false` even on the row holding the newest version. Sort by `publishedAt` or compare `version` strings yourself. - **`coordinate` changes shape between modes.** Two colons in per-version mode, one in latest mode. Split on `groupId`, `artifactId` and `version` rather than parsing it. - **`versionCount`, `repositoryId` and `keywords` are mode-dependent nulls.** A `null` `versionCount` means you were in per-version mode, not that the artifact has no versions. - **There is no pre-release flag.** Maven has none, so the version string is the only signal: `-M6`, `-RC1`, `-alpha`, `-SNAPSHOT`. Filter on `version` yourself if you want stable releases only. - **Filters are applied after fetching.** `packagingFilter` does not reduce requests, and `maxResultsPerQuery` counts rows saved, so a strict filter pages further to fill the quota. A narrow filter over a large group is a slow run. - **Free text only works with `latestOnly: true`.** The per-version index takes field queries only, and the Actor rejects the combination locally rather than sending it. - **The index is intermittently slow.** Requests that normally take under a second occasionally stall. Each gets 30 seconds and up to three attempts with linear backoff, and a stalled request usually succeeds on the retry. Give broad runs a generous Actor timeout. - **`publishedAt` is when the file landed in the repository**, not when the project announced the release. For a staged release the two are usually minutes apart. - **One failing query never aborts the run.** The Actor throws only when every query failed and nothing was saved. ## Recipes **Resolve a lockfile to current versions.** One row per artifact, newest version, version count included. ```json { "queries": [ "com.google.guava:guava", "org.springframework:spring-core", "com.fasterxml.jackson.core:jackson-databind" ], "latestOnly": true, "maxResultsPerQuery": 1 } ``` Compare `version` against your pinned version and use `publishedAt` to see how stale the pin is. **Sweep a whole group for outdated dependencies.** Jars and bundles only. ```json { "queries": ["g:com.fasterxml.jackson.core", "g:org.apache.commons"], "latestOnly": true, "maxResultsPerQuery": 200, "packagingFilter": ["jar", "bundle"] } ``` Group the rows by `groupId` and diff `artifactId` against what your build actually declares to find artifacts you never adopted. **Full release history for one library.** Per-version mode, cap raised. ```json { "queries": ["org.springframework:spring-core"], "latestOnly": false, "maxResultsPerQuery": 500 } ``` Bucket `publishedAt` by quarter for cadence, and check `classifiers` per version to spot releases that shipped without sources or javadoc.