HexpmMcp (hexpm_mcp v0.3.2)

Copy Markdown View Source

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

Summary

Types

Common error reasons from hex.pm API calls.

Functions

Audit a package's dependencies for risks.

Audit a list of mix.exs dependencies for risks.

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

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

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

Get the dependencies of a package version.

Get full documentation for a specific module or function.

Get the module listing for a package's documentation.

Get download statistics for a hex.pm package.

Get optional features/extras for a package release.

Get detailed information about a hex.pm package.

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

Get the README content for a package as markdown.

Get detailed information about a specific release.

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

Comprehensive health check for a hex.pm package.

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

Search within a package's documentation by name.

Check which mix.exs dependencies have newer versions available.

Types

error()

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

Common error reasons from hex.pm API calls.

Functions

audit_dependencies(name, version \\ nil)

@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(deps_string)

@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(names)

@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(name, version \\ nil, opts \\ [])

@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(name)

@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(name, version \\ nil)

@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(name, module, version \\ nil)

@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(name, version \\ nil)

@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(name)

@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(name, version \\ nil)

@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(name)

@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(name)

@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(name, version \\ nil)

@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(name, version \\ nil)

@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(name)

@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(name)

@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(query, opts \\ [])

@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(name, query, version \\ nil)

@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" => ""},
  ...
]}

upgrade_check(deps_string)

@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}
  ]
}}