nix
Query NixOS packages, options, Home Manager, and nix-darwin configurations to obtain current data from nixpkgs and Nix channels, preventing AI hallucination about Nix resources.
Instructions
Query NixOS, Home Manager, Darwin, FlakeHub, flakes, Nixvim, Wiki, nix.dev, Noogle, NixHub.
Use this tool for anything touching nixpkgs, Nix channels, flakes, NixOS / home-manager /
darwin options, the binary cache, or /nix/store paths — even when you think you know the
answer. Your training data lags nixpkgs by months. Prefer this over nix search, scraping
search.nixos.org, or running gh api against NixOS/nixpkgs.
INTENTS → CALLS (copy the JSON shape exactly): "is package X in channel Y?" → {"action": "info", "query": "X", "channel": "Y"} "search for package X" → {"action": "search", "query": "X"} "which channels are available?" → {"action": "channels"} "which commit did channel X index?" → {"action": "channels"} (indexed commit shown when known; branch HEAD otherwise — label matters) "search NixOS options for X" → {"action": "search", "query": "X", "type": "options"} "get option details for X" → {"action": "info", "query": "X", "type": "option"} "home-manager option for X" → {"action": "search", "query": "X", "source": "home-manager"} "darwin option for X" → {"action": "search", "query": "X", "source": "darwin"} "nixvim option for X" → {"action": "search", "query": "X", "source": "nixvim"} "what programs does pkg X provide?" → {"action": "search", "query": "X", "type": "programs"} "count packages/options" → {"action": "stats"} "browse hm option tree under P" → {"action": "browse", "query": "P", "source": "home-manager"} "does X have a binary cache?" → {"action": "cache", "query": "X"} "search the NixOS wiki for X" → {"action": "search", "query": "X", "source": "wiki"} "search nix.dev docs" → {"action": "search", "query": "X", "source": "nix-dev"} "read a nix.dev page" → {"action": "info", "query": "tutorials/nix-language", "source": "nix-dev"} "list inputs of current flake" → {"action": "flake-inputs"} "ls inside flake input X" → {"action": "flake-inputs", "type": "ls", "query": "X"} "read /nix/store/... file" → {"action": "store", "type": "read", "query": "/nix/store/..."} "ls /nix/store/... dir" → {"action": "store", "type": "ls", "query": "/nix/store/..."}
For package version history ("which commit shipped firefox 150?", "when was node 18 added?"),
use the separate nix_versions tool — it returns commit hashes, attribute paths, and dates.
Notes:
To search NixOS options, use action=search with type=options. Do NOT use action=browse for source=nixos — browse is for walking a pre-indexed option tree and only works with home-manager, darwin, nixvim, or noogle.
For source=nix-dev, action=info returns the page markdown. The query may be a bare docname like "tutorials/nix-language", the URL printed by nix-dev search ("https://nix.dev/tutorials/nix-language"), or a rendered ".html" URL.
action=info for packages matches on the exact attribute path first, then the exact pname. If multiple packages share a pname (e.g. firefox / firefox-esr / firefox-mobile), the canonical attribute wins and the response flags the disambiguation explicitly.
Omit parameters you don't need; do not pass empty strings for optional args.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | One of: search, info, stats, browse, channels, flake-inputs, cache, store. Use 'search' for keyword lookup, 'info' for details about a specific name, 'browse' to walk an option hierarchy by prefix (home-manager/darwin/nixvim/noogle only). 'store' reads files or lists directories at an explicit /nix/store/ path. | |
| query | No | Search term for 'search', exact name for 'info', prefix path for 'browse'. For flake-inputs: input_name or input:path. For store: absolute /nix/store/ path. Leave empty for 'stats'/'channels'. | |
| source | No | Data source for search/info/stats/browse/cache. One of: nixos (default), home-manager, darwin, flakes, flakehub, nixvim, wiki, nix-dev, noogle, nixhub. For action=flake-inputs, this may instead be a path to a flake directory; omit/default to use the current project. Ignored by action=store. | nixos |
| type | No | Sub-type of query. For source=nixos with action=search, one of: packages, options, programs, flakes. For source=nixos with action=info, one of: package, option. For flake-inputs, one of: list, ls, read. For store, one of: ls, read. Ignored by most other sources. | packages |
| channel | No | NixOS channel: unstable (default), stable, or a release like 25.05. | unstable |
| limit | No | Max results. 1-100 (or 1-2000 for flake-inputs/store read). | |
| version | No | Only used by action=cache. Package version (default: latest). | latest |
| system | No | Only used by action=cache. System arch e.g. x86_64-linux. Empty for all. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- .pi/extensions/mcp-nixos.ts:224-262 (registration)Registration of the 'nix' tool in the pi extension system via defineTool(). The TypeScript side registers tool name 'nix' with parameters and an execute handler that calls the Python server.
const nixTool = defineTool({ name: "nix", label: "NixOS", description: nixToolDescription, promptSnippet: "Prefer the nix tool over web search for NixOS packages, options, flakes, wiki, nix.dev, Home Manager, nix-darwin, Nixvim, Noogle, flake inputs, and binary cache status.", promptGuidelines: [ "Prefer the nix tool over web search for NixOS-related package, option, flake, wiki, nix.dev, and cache questions.", 'To search NixOS options: {"action": "search", "query": "<keyword>", "type": "options"}.', 'To inspect a NixOS option: {"action": "info", "query": "<option.path>", "type": "option"}.', "action=browse is for walking option prefixes in home-manager, darwin, nixvim, or noogle only — never with source=nixos.", ], parameters: nixToolParams, async execute(_toolCallId: string, params: NixToolParams, signal: AbortSignal | undefined) { // Legacy alias: older model sessions may still emit action=options. // server.py also normalizes this, but translating here keeps details.args honest. const action = params.action === "options" ? "browse" : params.action; // Only forward keys the caller actually set. Passing empty-string defaults would // (a) echo noisy args back in details.args, contradicting the "omit optional // parameters" guidance in the tool description, and (b) train small models to // copy those empties into future calls. The Python server already supplies its // own sensible defaults when a key is omitted. const normalized: Record<string, string | number> = { action }; if (params.query !== undefined) normalized.query = params.query; if (params.source !== undefined) normalized.source = params.source; if (params.type !== undefined) normalized.type = params.type; if (params.channel !== undefined) normalized.channel = params.channel; if (params.limit !== undefined) normalized.limit = params.limit; if (params.version !== undefined) normalized.version = params.version; if (params.system !== undefined) normalized.system = params.system; const text = await runNixosTool("nix", normalized, signal); return { content: [{ type: "text", text }], details: { tool: "nix", args: normalized }, }; }, }); - mcp_nixos/server.py:197-475 (handler)Main 'nix' tool handler function. This is the core async function decorated with @mcp.tool() that dispatches to various source functions based on action (search, info, stats, browse, channels, flake-inputs, cache, store) and source (nixos, home-manager, darwin, flakes, flakehub, nixvim, wiki, nix-dev, noogle, nixhub).
@mcp.tool() async def nix( action: Annotated[ str, "One of: search, info, stats, browse, channels, flake-inputs, cache, store. " "Use 'search' for keyword lookup, 'info' for details about a specific name, " "'browse' to walk an option hierarchy by prefix (home-manager/darwin/nixvim/noogle only). " "'store' reads files or lists directories at an explicit /nix/store/ path.", ], query: Annotated[ str, "Search term for 'search', exact name for 'info', prefix path for 'browse'. " "For flake-inputs: input_name or input:path. For store: absolute /nix/store/ path. " "Leave empty for 'stats'/'channels'.", ] = "", source: Annotated[ str, "Data source for search/info/stats/browse/cache. One of: nixos (default), " "home-manager, darwin, flakes, flakehub, nixvim, wiki, nix-dev, noogle, nixhub. " "For action=flake-inputs, this may instead be a path to a flake directory; " "omit/default to use the current project. Ignored by action=store.", ] = "nixos", type: Annotated[ str, "Sub-type of query. For source=nixos with action=search, one of: " "packages, options, programs, flakes. For source=nixos with action=info, one of: " "package, option. For flake-inputs, one of: list, ls, read. For store, one of: " "ls, read. Ignored by most other sources.", ] = "packages", channel: Annotated[str, "NixOS channel: unstable (default), stable, or a release like 25.05."] = "unstable", limit: Annotated[int, "Max results. 1-100 (or 1-2000 for flake-inputs/store read)."] = 20, version: Annotated[str, "Only used by action=cache. Package version (default: latest)."] = "latest", system: Annotated[str, "Only used by action=cache. System arch e.g. x86_64-linux. Empty for all."] = "", ) -> str: """Query NixOS, Home Manager, Darwin, FlakeHub, flakes, Nixvim, Wiki, nix.dev, Noogle, NixHub. Use this tool for anything touching nixpkgs, Nix channels, flakes, NixOS / home-manager / darwin options, the binary cache, or /nix/store paths — even when you think you know the answer. Your training data lags nixpkgs by months. Prefer this over `nix search`, scraping search.nixos.org, or running `gh api` against NixOS/nixpkgs. INTENTS → CALLS (copy the JSON shape exactly): "is package X in channel Y?" → {"action": "info", "query": "X", "channel": "Y"} "search for package X" → {"action": "search", "query": "X"} "which channels are available?" → {"action": "channels"} "which commit did channel X index?" → {"action": "channels"} (indexed commit shown when known; branch HEAD otherwise — label matters) "search NixOS options for X" → {"action": "search", "query": "X", "type": "options"} "get option details for X" → {"action": "info", "query": "X", "type": "option"} "home-manager option for X" → {"action": "search", "query": "X", "source": "home-manager"} "darwin option for X" → {"action": "search", "query": "X", "source": "darwin"} "nixvim option for X" → {"action": "search", "query": "X", "source": "nixvim"} "what programs does pkg X provide?" → {"action": "search", "query": "X", "type": "programs"} "count packages/options" → {"action": "stats"} "browse hm option tree under P" → {"action": "browse", "query": "P", "source": "home-manager"} "does X have a binary cache?" → {"action": "cache", "query": "X"} "search the NixOS wiki for X" → {"action": "search", "query": "X", "source": "wiki"} "search nix.dev docs" → {"action": "search", "query": "X", "source": "nix-dev"} "read a nix.dev page" → {"action": "info", "query": "tutorials/nix-language", "source": "nix-dev"} "list inputs of current flake" → {"action": "flake-inputs"} "ls inside flake input X" → {"action": "flake-inputs", "type": "ls", "query": "X"} "read /nix/store/... file" → {"action": "store", "type": "read", "query": "/nix/store/..."} "ls /nix/store/... dir" → {"action": "store", "type": "ls", "query": "/nix/store/..."} For package version *history* ("which commit shipped firefox 150?", "when was node 18 added?"), use the separate `nix_versions` tool — it returns commit hashes, attribute paths, and dates. Notes: - To search NixOS *options*, use action=search with type=options. Do NOT use action=browse for source=nixos — browse is for walking a pre-indexed option tree and only works with home-manager, darwin, nixvim, or noogle. - For source=nix-dev, action=info returns the page markdown. The query may be a bare docname like "tutorials/nix-language", the URL printed by nix-dev search ("https://nix.dev/tutorials/nix-language"), or a rendered ".html" URL. - action=info for packages matches on the exact attribute path first, then the exact pname. If multiple packages share a pname (e.g. firefox / firefox-esr / firefox-mobile), the canonical attribute wins and the response flags the disambiguation explicitly. - Omit parameters you don't need; do not pass empty strings for optional args. """ # Limit validation: flake-inputs/store read allow up to 2000, others limited to 100 if action == "flake-inputs" and type == "read": if not 1 <= limit <= MAX_LINE_LIMIT: return error(f"Limit must be 1-{MAX_LINE_LIMIT} for flake-inputs read") elif action == "store" and type == "read": if not 1 <= limit <= MAX_LINE_LIMIT: return error(f"Limit must be 1-{MAX_LINE_LIMIT} for store read") elif not 1 <= limit <= 100: return error("Limit must be 1-100") # Accept `browse` as canonical, keep `options` as a legacy alias. # The action=options name was confusing small models (GitHub #125). if action == "options": action = "browse" if action == "search": if not query: return error('Query required for search. Example: {"action": "search", "query": "firefox"}') if source == "nixos": if type not in ["packages", "options", "programs", "flakes"]: return error( "For source=nixos, type must be one of: packages, options, programs, flakes. " 'Example: {"action": "search", "query": "nginx", "type": "options"}' ) return await asyncio.to_thread(_search_nixos, query, type, limit, channel) elif source == "home-manager": return await asyncio.to_thread(_search_home_manager, query, limit) elif source == "darwin": return await asyncio.to_thread(_search_darwin, query, limit) elif source == "flakes": return await asyncio.to_thread(_search_flakes, query, limit) elif source == "flakehub": return await asyncio.to_thread(_search_flakehub, query, limit) elif source == "nixvim": return await asyncio.to_thread(_search_nixvim, query, limit) elif source == "wiki": return await asyncio.to_thread(_search_wiki, query, limit) elif source == "nix-dev": return await asyncio.to_thread(_search_nixdev, query, limit) elif source == "noogle": return await asyncio.to_thread(_search_noogle, query, limit) elif source == "nixhub": return await _search_nixhub(query, limit) else: return error( f"Unknown source: {source!r}. Must be one of: " "nixos, home-manager, darwin, flakes, flakehub, nixvim, wiki, nix-dev, noogle, nixhub." ) elif action == "info": if not query: return error('Name required for info. Example: {"action": "info", "query": "firefox"}') if source == "flakes": example = json.dumps({"action": "search", "source": "flakes", "query": query}) return error( f"action=info is not supported for source=flakes. Use action=search instead. Example: {example}." ) if source == "nixos": if type not in ["package", "packages", "option", "options"]: return error( "For source=nixos, type must be 'package' or 'option'. " 'Example: {"action": "info", "query": "services.nginx.enable", "type": "option"}' ) info_type = "package" if type in ["package", "packages"] else "option" return await asyncio.to_thread(_info_nixos, query, info_type, channel) elif source == "home-manager": return await asyncio.to_thread(_info_home_manager, query) elif source == "darwin": return await asyncio.to_thread(_info_darwin, query) elif source == "flakehub": return await asyncio.to_thread(_info_flakehub, query) elif source == "nixvim": return await asyncio.to_thread(_info_nixvim, query) elif source == "wiki": return await asyncio.to_thread(_info_wiki, query) elif source == "nix-dev": return await asyncio.to_thread(_info_nixdev, query) elif source == "noogle": return await asyncio.to_thread(_info_noogle, query) elif source == "nixhub": return await _info_nixhub(query) else: return error( f"Unknown source: {source!r}. For action=info, must be one of: " "nixos, home-manager, darwin, flakehub, nixvim, wiki, nix-dev, noogle, nixhub." ) elif action == "stats": if source == "nixos": return await asyncio.to_thread(_stats_nixos, channel) elif source == "home-manager": return await asyncio.to_thread(_stats_home_manager) elif source == "darwin": return await asyncio.to_thread(_stats_darwin) elif source == "flakes": return await asyncio.to_thread(_stats_flakes) elif source == "flakehub": return await asyncio.to_thread(_stats_flakehub) elif source == "nixvim": return await asyncio.to_thread(_stats_nixvim) elif source == "noogle": return await asyncio.to_thread(_stats_noogle) elif source in ["wiki", "nix-dev", "nixhub"]: return error(f"Stats not available for source={source}.") else: return error( f"Unknown source: {source!r}. For action=stats, must be one of: " "nixos, home-manager, darwin, flakes, flakehub, nixvim, noogle." ) elif action == "browse": if source == "nixos": return error( "action=browse is not for NixOS. To search NixOS options, use: " '{"action": "search", "query": "nginx", "type": "options"}. ' "To get a specific option's details, use: " '{"action": "info", "query": "services.nginx.enable", "type": "option"}.' ) if source not in ["home-manager", "darwin", "nixvim", "noogle"]: return error( "action=browse only supports source in: home-manager, darwin, nixvim, noogle. " 'Example: {"action": "browse", "query": "programs", "source": "home-manager"}' ) if source == "nixvim": return await asyncio.to_thread(_browse_nixvim_options, query) if source == "noogle": return await asyncio.to_thread(_browse_noogle_options, query) return await asyncio.to_thread(_browse_options, source, query) elif action == "channels": return await asyncio.to_thread(_list_channels) elif action == "flake-inputs": # Determine flake directory: use source if it's not a known source name flake_dir = source if source not in KNOWN_SOURCES else "." # Validate type parameter for flake-inputs # Note: "packages" is accepted as alias for "list" (default type parameter) if type not in ["list", "ls", "read", "packages"]: return error("Type must be one of: list, ls, read for flake-inputs") # Handle limit for read operation read_limit = limit if type == "read": if limit == 20: # Default was used, apply DEFAULT_LINE_LIMIT read_limit = DEFAULT_LINE_LIMIT # Ensure read_limit doesn't exceed MAX_LINE_LIMIT read_limit = min(read_limit, MAX_LINE_LIMIT) # Route to appropriate function if type == "list" or type == "packages": return await _flake_inputs_list(flake_dir) elif type == "ls": if not query: return error("Query required for ls (input name or input:path)") return await _flake_inputs_ls(flake_dir, query) elif type == "read": if not query: return error("Query required for read (input:path format)") return await _flake_inputs_read(flake_dir, query, read_limit) else: return error("Type must be one of: list, ls, read for flake-inputs") elif action == "cache": if not query: return error("Package name required for cache action") return await _check_binary_cache(query, version, system) elif action == "store": if type not in ["ls", "read"]: return error( "Type must be one of: ls, read for store. " 'Example: {"action": "store", "type": "ls", "query": "/nix/store/<hash>-<name>"}' ) if not query: return error( "Query required for store (absolute /nix/store/ path). " 'Example: {"action": "store", "type": "ls", "query": "/nix/store/<hash>-<name>"}' ) if type == "ls": # Match _store_read's default-promotion so a bare call returns a # useful window of entries for large /nix/store directories # instead of only the first 20. ls_limit = limit if limit != 20 else DEFAULT_LINE_LIMIT ls_limit = min(ls_limit, MAX_LINE_LIMIT) return await _store_ls(query, ls_limit) # type == "read": default limit behavior mirrors flake-inputs read. read_limit = limit if limit == 20: # Default was used, apply DEFAULT_LINE_LIMIT read_limit = DEFAULT_LINE_LIMIT read_limit = min(read_limit, MAX_LINE_LIMIT) return await _store_read(query, read_limit) else: return error( f"Unknown action: {action!r}. Must be one of: " "search, info, stats, browse, channels, flake-inputs, cache, store. " 'Example: {"action": "search", "query": "firefox"}' ) - mcp_nixos/server.py:478-570 (handler)Companion 'nix_versions' tool handler for package version history from NixHub.
@mcp.tool() async def nix_versions( package: Annotated[str, "Package name"], version: Annotated[str, "Specific version to find"] = "", limit: Annotated[int, "1-50"] = 10, ) -> str: """Get package version history from NixHub.io.""" if not package or not package.strip(): return error("Package name required") if not re.match(r"^[a-zA-Z0-9\-_.]+$", package): return error("Invalid package name") if not 1 <= limit <= 50: return error("Limit must be 1-50") # Fetch package data via thread pool to avoid blocking event loop err, data = await asyncio.to_thread(_fetch_nixhub_pkg, package) if err: return err try: # v1/pkg returns an array of version records if not isinstance(data, list) or not data: return error(f"Package '{package}' not found", "NOT_FOUND") releases: list[dict[str, Any]] = data # If specific version requested, find it if version: for release in releases: if release.get("version") == version: version_lines = [f"Found {package} version {version}\n"] # Get commit hash from the release commit = release.get("commit_hash", "") if commit and re.match(r"^[a-fA-F0-9]{40}$", commit): version_lines.append(f"Nixpkgs commit: {commit}") # Get attribute path from systems data systems_dict = release.get("systems", {}) if isinstance(systems_dict, dict): for sys_info in systems_dict.values(): if isinstance(sys_info, dict): attr_paths = sys_info.get("attr_paths", []) if attr_paths: version_lines.append(f" Attribute: {attr_paths[0]}") break return "\n".join(version_lines) # Version not found versions_list: list[str] = [str(r.get("version", "")) for r in releases[:limit]] return f"Version {version} not found for {package}\nAvailable: {', '.join(versions_list)}" # Build package header with rich metadata from first (latest) release results: list[str] = [f"Package: {package}"] latest = releases[0] # Add package-level metadata from latest release license_info: str = latest.get("license", "") if license_info: results.append(f"License: {license_info}") homepage: str = latest.get("homepage", "") if homepage: results.append(f"Homepage: {homepage}") # Get programs from systems data programs: list[str] = [] systems_dict = latest.get("systems", {}) if isinstance(systems_dict, dict): for sys_info in systems_dict.values(): if isinstance(sys_info, dict): sys_programs = sys_info.get("programs", []) if sys_programs: programs = sys_programs break if programs: progs = programs[:10] prog_str = ", ".join(progs) if len(programs) > 10: prog_str += f" ... ({len(programs)} total)" results.append(f"Programs: {prog_str}") results.append(f"Total versions: {len(releases)}") results.append("") # Return version history shown: list[dict[str, Any]] = releases[:limit] results.append(f"Recent versions ({len(shown)} of {len(releases)}):\n") for release in shown: results.extend(_format_release(release, package)) results.append("") return "\n".join(results).strip() except Exception as e: return error(str(e)) - mcp_nixos/server.py:194-197 (schema)Typed schema for the 'nix' tool defined via FastMCP's Annotated type annotations on function parameters (action, query, source, type, channel, limit, version, system).
# ============================================================================= @mcp.tool() - .pi/extensions/mcp-nixos.ts:146-194 (schema)TypeScript schema (nixToolParams) for the 'nix' tool using Type.Object with typed string/integer fields.
const nixToolParams = Type.Object({ action: Type.String({ description: "One of: search, info, stats, browse, channels, flake-inputs, cache, store. " + "search = keyword lookup; info = details for a specific name; " + "browse = walk an option hierarchy by prefix (home-manager/darwin/nixvim/noogle only); " + "store = read files or list directories at an explicit /nix/store/ path.", }), query: Type.Optional( Type.String({ description: "Search term for 'search', exact name for 'info', prefix path for 'browse'. " + "For flake-inputs: input_name or input:path. For store: absolute /nix/store/ path. " + "Omit for 'stats'/'channels'.", }), ), source: Type.Optional( Type.String({ description: "Data source for search/info/stats/browse/cache. One of: nixos (default), " + "home-manager, darwin, flakes, flakehub, nixvim, wiki, nix-dev, noogle, nixhub. " + "For action=flake-inputs, this may instead be a path to a flake directory; " + "omit/default to use the current project. Ignored by action=store.", }), ), type: Type.Optional( Type.String({ description: "Sub-type of query. For source=nixos with action=search, one of: " + "packages, options, programs, flakes. For source=nixos with action=info, one of: " + "package, option. For flake-inputs, one of: list, ls, read. For store, one of: " + "ls, read. Ignored by most other sources.", }), ), channel: Type.Optional( Type.String({ description: "NixOS channel: unstable (default), stable, or a release like 25.05." }), ), limit: Type.Optional( Type.Integer({ description: "Max results. 1-100 (or 1-2000 for flake-inputs/store read). Default 20." }), ), version: Type.Optional( Type.String({ description: "Only used by action=cache. Package version (default: latest)." }), ), system: Type.Optional( Type.String({ description: "Only used by action=cache. System arch e.g. x86_64-linux. Empty for all.", }), ), });