# `HexpmMcp`
[🔗](https://github.com/joshrotenberg/hexpm-mcp/blob/main/lib/hexpm_mcp.ex#L1)

Public API for querying hex.pm and hexdocs.pm.

All functions return `{:ok, result}` or `{:error, reason}` and are designed
to be used from iex, other Elixir code, or as the backend for MCP tools.

## Quick Start

    iex> HexpmMcp.search("json")
    {:ok, [%{name: "jason", version: "1.4.4", downloads_all: 197_000_000, ...}, ...]}

    iex> HexpmMcp.get_info("plug")
    {:ok, %{name: "plug", description: "Composable modules for web applications",
            downloads: %{all: 156_000_000, recent: 3_100_000, week: 250_000, day: 35_000},
            licenses: ["Apache-2.0"], latest_stable_version: "1.19.1", ...}}

    iex> HexpmMcp.health_check("req")
    {:ok, %{name: "req",
            maintenance: %{status: "Active", age: "4 years ago", days_since_release: 57, total_versions: 52},
            popularity: %{all: 11_500_000, recent: 1_600_000, week: 141_000},
            quality: %{has_docs: true, licenses: ["Apache-2.0"], required_deps: 3, optional_deps: 4},
            risk: %{owner_count: 1, retired_count: 0},
            links: %{...}}}

## Function Groups

### Simple lookups
`search/2`, `get_info/1`, `get_downloads/1`, `get_owners/1`, `get_versions/1`

### Version-resolving lookups
`get_release/2`, `get_dependencies/2`, `get_features/2` -- pass `nil` for version
to automatically resolve to the latest stable version.

### Composite analysis
`compare_packages/1`, `health_check/1`, `audit_dependencies/2`,
`find_alternatives/1`, `dependency_tree/3` -- these make multiple API calls
in parallel and return aggregated results.

### HexDocs browsing
`get_readme/2`, `get_docs/2`, `get_doc_item/3`, `search_docs/3`

## Error Values

All functions return `{:error, reason}` on failure. Common reasons:

- `:not_found` -- package or version does not exist on hex.pm
- `:rate_limited` -- hex.pm API rate limit exceeded
- `:too_few_packages` / `:too_many_packages` -- invalid input to `compare_packages/1`
- `{:api_error, status, body}` -- unexpected HTTP status from hex.pm
- `{:request_failed, reason}` -- network error

# `error`

```elixir
@type error() ::
  :not_found
  | :rate_limited
  | {:api_error, integer(), any()}
  | {:request_failed, any()}
```

Common error reasons from hex.pm API calls.

# `audit_dependencies`

```elixir
@spec audit_dependencies(String.t(), String.t() | nil) ::
  {:ok, map()} | {:error, error()}
```

Audit a package's dependencies for risks.

Checks each dependency in parallel for:
- Retired versions
- Stale packages (no release in 2+ years)
- Single-maintainer packages (bus factor risk)
- Known vulnerabilities via OSV.dev

## Examples

    iex> HexpmMcp.audit_dependencies("phoenix")
    {:ok, %{
      name: "phoenix", version: "1.8.5",
      total_checked: 10, total_warnings: 8, deps_with_warnings: 7,
      results: [
        %{name: "jason", issues: ["single maintainer"]},
        %{name: "plug", issues: ["2 retired version(s)", "3 known vulnerability(ies)"]},
        %{name: "telemetry", issues: []},
        ...
      ]
    }}

    iex> HexpmMcp.audit_dependencies("phoenix", "1.7.0")
    {:ok, %{name: "phoenix", version: "1.7.0", ...}}

# `audit_mix_deps`

```elixir
@spec audit_mix_deps(String.t()) :: {:ok, map()} | {:error, :no_deps_found}
```

Audit a list of mix.exs dependencies for risks.

Accepts a deps string (as it appears in mix.exs) and runs a comprehensive
audit on each package: staleness, retirement, bus factor, and CVEs via OSV.dev.

## Examples

    iex> deps = """
    ...> {:phoenix, "~> 1.7"},
    ...> {:ecto, "~> 3.10"},
    ...> {:jason, "~> 1.0"}
    ...> """
    iex> HexpmMcp.audit_mix_deps(deps)
    {:ok, %{
      total_checked: 3,
      total_warnings: 2,
      deps_with_warnings: 2,
      results: [
        %{name: "ecto", pinned_version: "~> 3.10", issues: []},
        %{name: "jason", pinned_version: "~> 1.0", issues: ["single maintainer"]},
        %{name: "phoenix", pinned_version: "~> 1.7", issues: ["3 retired version(s)"]},
        ...
      ]
    }}

# `compare_packages`

```elixir
@spec compare_packages([String.t()]) ::
  {:ok, [map()]} | {:error, :too_few_packages | :too_many_packages}
```

Compare 2-5 hex.pm packages side by side.

Fetches package info and dependency counts in parallel for each package.

## Examples

    iex> HexpmMcp.compare_packages(["plug", "bandit", "cowboy"])
    {:ok, [
      %{name: "plug", downloads_all: 156_000_000, downloads_recent: 3_100_000,
        latest_version: "1.19.1", updated_at: "2025-12-09T...",
        licenses: "Apache-2.0", dep_count: 3},
      %{name: "bandit", downloads_all: 9_900_000, ...},
      %{name: "cowboy", downloads_all: 78_000_000, ...}
    ]}

    iex> HexpmMcp.compare_packages(["only_one"])
    {:error, :too_few_packages}

# `dependency_tree`

```elixir
@spec dependency_tree(String.t(), String.t() | nil, keyword()) ::
  {:ok, map()} | {:error, error()}
```

Get the full transitive dependency tree for a package (BFS, max depth 5).

Traverses dependencies breadth-first, resolving each to its latest version.
Deduplicates by package name (each package appears once in the tree).

## Options

- `:max_depth` -- maximum depth to traverse (default 5, capped at 5)

## Examples

    iex> HexpmMcp.dependency_tree("req", nil, max_depth: 2)
    {:ok, %{
      name: "req", version: "0.5.17", total_unique_deps: 8,
      tree: [
        %{name: "req", version: "0.5.17", depth: 0, deps: [
          %{name: "finch", requirement: "~> 0.17", optional: false, depth: 1},
          %{name: "mime", requirement: "~> 2.0 or ~> 1.0", optional: false, depth: 1},
          ...
        ]},
        %{name: "finch", version: "0.21.0", depth: 1, deps: [...]},
        ...
      ]
    }}

# `find_alternatives`

```elixir
@spec find_alternatives(String.t()) :: {:ok, map()} | {:error, error()}
```

Find and compare alternative packages for a given hex.pm package.

Extracts keywords from the package description, searches for similar packages,
deduplicates, and returns the top 10 sorted by recent downloads.

## Examples

    iex> HexpmMcp.find_alternatives("httpoison")
    {:ok, %{
      package: %{name: "httpoison", description: "HTTP client for Elixir",
                 downloads_all: 42_000_000, downloads_recent: 500_000},
      alternatives: [
        %{name: "req", version: "0.5.17", downloads_all: 11_500_000,
          downloads_recent: 1_600_000, updated_at: "2026-02-07T...",
          status: "Active", description: "Req is a batteries-included HTTP client...",
          licenses: "Apache-2.0"},
        %{name: "finch", ...},
        ...
      ]
    }}

# `get_dependencies`

```elixir
@spec get_dependencies(String.t(), String.t() | nil) ::
  {:ok, map()} | {:error, error()}
```

Get the dependencies of a package version.

If `version` is nil, resolves to the latest stable version.

## Examples

    iex> HexpmMcp.get_dependencies("req")
    {:ok, %{name: "req", version: "0.5.17", dependencies: [
      %{name: "finch", requirement: "~> 0.17", optional: false},
      %{name: "jason", requirement: "~> 1.0", optional: true},
      %{name: "mime", requirement: "~> 2.0 or ~> 1.0", optional: false},
      ...
    ]}}

    iex> HexpmMcp.get_dependencies("plug", "1.19.1")
    {:ok, %{name: "plug", version: "1.19.1", dependencies: [...]}}

# `get_doc_item`

```elixir
@spec get_doc_item(String.t(), String.t(), String.t() | nil) ::
  {:ok, String.t()} | {:error, error()}
```

Get full documentation for a specific module or function.

Fetches the module's hexdocs.pm page and converts it to markdown.

## Examples

    iex> HexpmMcp.get_doc_item("plug", "Plug.Conn")
    {:ok, "# Plug.Conn\n\nThe Plug connection.\n\n..."}

    iex> HexpmMcp.get_doc_item("plug", "Plug.Conn", "1.19.1")
    {:ok, "# Plug.Conn\n\n..."}

# `get_docs`

```elixir
@spec get_docs(String.t(), String.t() | nil) :: {:ok, [map()]} | {:error, error()}
```

Get the module listing for a package's documentation.

Parses the hexdocs.pm sidebar data to extract all modules, behaviours,
and protocols with their function/type counts.

## Examples

    iex> HexpmMcp.get_docs("plug")
    {:ok, [
      %{name: "Plug", type: "module", doc: "Types: 1, Callbacks: 2, Functions: 2"},
      %{name: "Plug.Conn", type: "module", doc: "Types: 22, Functions: 52"},
      %{name: "Plug.Router", type: "module", doc: "Functions: 12"},
      ...
    ]}

# `get_downloads`

```elixir
@spec get_downloads(String.t()) :: {:ok, map()} | {:error, error()}
```

Get download statistics for a hex.pm package.

## Examples

    iex> HexpmMcp.get_downloads("phoenix")
    {:ok, %{name: "phoenix", all: 148_000_000, recent: 2_600_000, week: 223_000, day: 13_000}}

# `get_features`

```elixir
@spec get_features(String.t(), String.t() | nil) :: {:ok, map()} | {:error, error()}
```

Get optional features/extras for a package release.

If `version` is nil, resolves to the latest stable version.

## Examples

    iex> HexpmMcp.get_features("req")
    {:ok, %{
      name: "req", version: "0.5.17",
      optional_deps: [
        %{name: "brotli", requirement: "~> 0.3.1"},
        %{name: "jason", requirement: "~> 1.0"},
        ...
      ],
      extra_metadata: %{}
    }}

# `get_info`

```elixir
@spec get_info(String.t()) :: {:ok, map()} | {:error, error()}
```

Get detailed information about a hex.pm package.

Returns metadata, download stats, licenses, links, and version info.

## Examples

    iex> HexpmMcp.get_info("plug")
    {:ok, %{
      name: "plug",
      description: "Composable modules for web applications",
      latest_stable_version: "1.19.1",
      latest_version: "1.19.1",
      downloads: %{all: 156_000_000, recent: 3_100_000, week: 250_000, day: 35_000},
      licenses: ["Apache-2.0"],
      build_tools: ["mix"],
      elixir_requirement: "~> 1.14",
      inserted_at: "2013-12-31T...",
      updated_at: "2025-12-09T...",
      docs_url: "https://hexdocs.pm/plug/",
      hex_url: "https://hex.pm/packages/plug",
      links: %{"GitHub" => "https://github.com/elixir-plug/plug"}
    }}

    iex> HexpmMcp.get_info("nonexistent")
    {:error, :not_found}

# `get_owners`

```elixir
@spec get_owners(String.t()) :: {:ok, [map()]} | {:error, error()}
```

Get the owners/maintainers of a hex.pm package.

## Examples

    iex> HexpmMcp.get_owners("phoenix")
    {:ok, [%{username: "josevalim", email: "jose@example.com"}, ...]}

# `get_readme`

```elixir
@spec get_readme(String.t(), String.t() | nil) ::
  {:ok, String.t()} | {:error, error()}
```

Get the README content for a package as markdown.

Fetches the README from hexdocs.pm and converts HTML to markdown.

## Examples

    iex> HexpmMcp.get_readme("req")
    {:ok, "# Req\n\nReq is a batteries-included HTTP client for Elixir.\n\n..."}

    iex> HexpmMcp.get_readme("req", "0.5.0")
    {:ok, "# Req\n\n..."}

# `get_release`

```elixir
@spec get_release(String.t(), String.t() | nil) :: {:ok, map()} | {:error, error()}
```

Get detailed information about a specific release.

If `version` is nil, resolves to the latest stable version.

## Examples

    iex> HexpmMcp.get_release("plug", "1.19.1")
    {:ok, %{
      name: "plug", version: "1.19.1", publisher: "josevalim",
      inserted_at: "2025-12-09T...", updated_at: "2025-12-09T...",
      downloads: 500_000, has_docs: true,
      build_tools: ["mix"], elixir_requirement: "~> 1.14",
      dependencies: [
        %{name: "mime", requirement: "~> 2.0", optional: false},
        %{name: "plug_crypto", requirement: "~> 2.1", optional: false}
      ],
      retired: nil
    }}

    # Resolves to latest stable when version is nil
    iex> HexpmMcp.get_release("plug")
    {:ok, %{name: "plug", version: "1.19.1", ...}}

# `get_versions`

```elixir
@spec get_versions(String.t()) :: {:ok, map()} | {:error, error()}
```

List all versions of a hex.pm package with retirement status.

## Examples

    iex> HexpmMcp.get_versions("plug")
    {:ok, %{name: "plug", versions: [
      %{version: "1.19.1", inserted_at: "2025-12-09T...", has_docs: true, retired: nil},
      %{version: "1.14.0", inserted_at: "2023-03-15T...", has_docs: true,
        retired: %{reason: "security", message: "CVE-2024-..."}},
      ...
    ]}}

# `health_check`

```elixir
@spec health_check(String.t()) :: {:ok, map()} | {:error, error()}
```

Comprehensive health check for a hex.pm package.

Fetches package info, owners, and latest release in parallel, then computes
maintenance status, popularity metrics, quality indicators, and risk factors.

## Examples

    iex> HexpmMcp.health_check("req")
    {:ok, %{
      name: "req",
      maintenance: %{
        age: "4 years ago",
        total_versions: 52,
        status: "Active",        # Active (<90d), Recent (<1y), Aging (<2y), Stale (2y+)
        days_since_release: 57
      },
      popularity: %{all: 11_500_000, recent: 1_600_000, week: 141_000},
      quality: %{
        has_docs: true,
        licenses: ["Apache-2.0"],
        build_tools: ["mix"],
        elixir_requirement: "~> 1.14",
        required_deps: 3,
        optional_deps: 4
      },
      risk: %{
        owner_count: 1,          # 1 = "single maintainer" warning
        retired_count: 0
      },
      links: %{
        hex_url: "https://hex.pm/packages/req",
        docs_url: "https://hexdocs.pm/req/",
        "GitHub" => "https://github.com/wojtekmach/req"
      }
    }}

# `search`

```elixir
@spec search(
  String.t(),
  keyword()
) :: {:ok, [map()]} | {:error, error()}
```

Search for packages on hex.pm by name/keywords.

## Options

- `:sort` -- sort order: `"name"`, `"recent_downloads"`, `"total_downloads"`,
  `"inserted_at"`, `"updated_at"`
- `:page` -- page number (1-indexed, 100 results per page)

## Examples

    iex> HexpmMcp.search("json")
    {:ok, [%{name: "jason", version: "1.4.4", description: "A blazing fast JSON parser...",
             downloads_all: 197_000_000, downloads_recent: 4_000_000,
             url: "https://hex.pm/packages/jason"}, ...]}

    iex> HexpmMcp.search("http client", sort: "recent_downloads", page: 1)
    {:ok, [%{name: "req", ...}, %{name: "finch", ...}, ...]}

# `search_docs`

```elixir
@spec search_docs(String.t(), String.t(), String.t() | nil) ::
  {:ok, [map()]} | {:error, error()}
```

Search within a package's documentation by name.

Searches module names and doc snippets in the hexdocs.pm sidebar data.
Returns up to 20 matching items.

## Examples

    iex> HexpmMcp.search_docs("phoenix", "Router")
    {:ok, [
      %{"title" => "Phoenix.Router", "type" => "module", "doc" => "Reflection: 3, Functions: 21"},
      %{"title" => "Phoenix.Router.NoRouteError", "type" => "module", "doc" => ""},
      ...
    ]}

# `toolbox_category`

List curated projects in an Elixir Toolbox category.

# `toolbox_group`

Get a single Elixir Toolbox group by slug.

# `toolbox_groups`

List all Elixir Toolbox groups with their categories.

# `toolbox_search`

Search packages via Elixir Toolbox.

# `toolbox_trending`

List trending Elixir Toolbox projects.

# `upgrade_check`

```elixir
@spec upgrade_check(String.t()) :: {:ok, map()} | {:error, :no_deps_found}
```

Check which mix.exs dependencies have newer versions available.

Accepts a deps string and checks each against the latest version on hex.pm.
Flags major version bumps as potentially breaking.

## Examples

    iex> deps = """
    ...> {:phoenix, "~> 1.7"},
    ...> {:jason, "~> 1.0"}
    ...> """
    iex> HexpmMcp.upgrade_check(deps)
    {:ok, %{
      total_checked: 2,
      upgrades_available: 1,
      results: [
        %{name: "jason", pinned_version: "~> 1.0", latest_version: "1.4.4",
          status: :up_to_date},
        %{name: "phoenix", pinned_version: "~> 1.7", latest_version: "1.8.5",
          status: :minor_upgrade, retired: false}
      ]
    }}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
