HexpmMcp (hexpm_mcp v0.3.4)

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.

List curated projects in an Elixir Toolbox category.

Get a single Elixir Toolbox group by slug.

List all Elixir Toolbox groups with their categories.

Search packages via Elixir Toolbox.

List trending Elixir Toolbox projects.

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

toolbox_category(group, category, opts \\ [])

List curated projects in an Elixir Toolbox category.

toolbox_group(slug)

Get a single Elixir Toolbox group by slug.

toolbox_groups()

List all Elixir Toolbox groups with their categories.

toolbox_search(query)

Search packages via Elixir Toolbox.

toolbox_trending(opts \\ [])

List trending Elixir Toolbox projects.

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