home_manager_list_options
Enumerate Home Manager configuration option categories with their counts to help users discover available settings for customizing their NixOS environment.
Instructions
List all Home Manager option categories.
Enumerates all top-level categories with their option counts.
Returns: Plain text list of categories sorted alphabetically with option counts
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- mcp_nixos/server.py:791-865 (handler)The core handler function for the 'home_manager_list_options' MCP tool. Registered via @mcp.tool() decorator. Parses Home Manager documentation HTML to extract and count top-level option categories, returning a sorted plain-text list.@mcp.tool() async def home_manager_list_options() -> str: """List all Home Manager option categories. Enumerates all top-level categories with their option counts. Returns: Plain text list of categories sorted alphabetically with option counts """ try: # Get more options to see all categories (default 100 is too few) options = parse_html_options(HOME_MANAGER_URL, limit=5000) categories: dict[str, int] = {} for opt in options: name = opt["name"] # Process option names if name and not name.startswith("."): if "." in name: cat = name.split(".")[0] else: cat = name # Option without dot is its own category # Valid categories should: # - Be more than 1 character # - Be a valid identifier (allows underscores) # - Not be common value words # - Match typical nix option category patterns if ( len(cat) > 1 and cat.isidentifier() and (cat.islower() or cat.startswith("_")) ): # This ensures valid identifier # Additional filtering for known valid categories valid_categories = { "accounts", "dconf", "editorconfig", "fonts", "gtk", "home", "i18n", "launchd", "lib", "manual", "news", "nix", "nixgl", "nixpkgs", "pam", "programs", "qt", "services", "specialisation", "systemd", "targets", "wayland", "xdg", "xresources", "xsession", } # Only include if it's in the known valid list or looks like a typical category if cat in valid_categories or (len(cat) >= 3 and not any(char.isdigit() for char in cat)): categories[cat] = categories.get(cat, 0) + 1 results = [] results.append(f"Home Manager option categories ({len(categories)} total):\n") # Sort by count descending, then alphabetically sorted_cats = sorted(categories.items(), key=lambda x: (-x[1], x[0])) for cat, count in sorted_cats: results.append(f"• {cat} ({count} options)") return "\n".join(results) except Exception as e: return error(str(e))
- mcp_nixos/server.py:247-336 (helper)Key helper function used by home_manager_list_options (and other *_manager tools) to parse HTML documentation pages from Home Manager/nix-darwin sites, extracting option names, descriptions, and types.def parse_html_options(url: str, query: str = "", prefix: str = "", limit: int = 100) -> list[dict[str, str]]: """Parse options from HTML documentation.""" try: resp = requests.get(url, timeout=30) # Increase timeout for large docs resp.raise_for_status() # Use resp.content to let BeautifulSoup handle encoding detection # This prevents encoding errors like "unknown encoding: windows-1252" soup = BeautifulSoup(resp.content, "html.parser") options = [] # Get all dt elements dts = soup.find_all("dt") for dt in dts: # Get option name name = "" if "home-manager" in url: # Home Manager uses anchor IDs like "opt-programs.git.enable" anchor = dt.find("a", id=True) if anchor: anchor_id = anchor.get("id", "") # Remove "opt-" prefix and convert underscores if anchor_id.startswith("opt-"): name = anchor_id[4:] # Remove "opt-" prefix # Convert _name_ placeholders back to <name> name = name.replace("_name_", "<name>") else: # Fallback to text content name_elem = dt.find(string=True, recursive=False) if name_elem: name = name_elem.strip() else: name = dt.get_text(strip=True) else: # Darwin and fallback - use text content name = dt.get_text(strip=True) # Skip if it doesn't look like an option (must contain a dot) # But allow single-word options in some cases if "." not in name and len(name.split()) > 1: continue # Filter by query or prefix if query and query.lower() not in name.lower(): continue if prefix and not (name.startswith(prefix + ".") or name == prefix): continue # Find the corresponding dd element dd = dt.find_next_sibling("dd") if dd: # Extract description (first p tag or direct text) desc_elem = dd.find("p") if desc_elem: description = desc_elem.get_text(strip=True) else: # Get first text node, handle None case text = dd.get_text(strip=True) description = text.split("\n")[0] if text else "" # Extract type info - look for various patterns type_info = "" # Pattern 1: <span class="term">Type: ...</span> type_elem = dd.find("span", class_="term") if type_elem and "Type:" in type_elem.get_text(): type_info = type_elem.get_text(strip=True).replace("Type:", "").strip() # Pattern 2: Look for "Type:" in text elif "Type:" in dd.get_text(): text = dd.get_text() type_start = text.find("Type:") + 5 type_end = text.find("\n", type_start) if type_end == -1: type_end = len(text) type_info = text[type_start:type_end].strip() options.append( { "name": name, "description": description[:200] if len(description) > 200 else description, "type": type_info, } ) if len(options) >= limit: break return options except Exception as exc: raise DocumentParseError(f"Failed to fetch docs: {str(exc)}") from exc