{ config, lib, pkgs, ... }: let cfg = config.services.nixos-options-mcp; databaseUrl = if cfg.database.type == "sqlite" then "sqlite://${cfg.dataDir}/${cfg.database.name}" else cfg.database.connectionString; in { options.services.nixos-options-mcp = { enable = lib.mkEnableOption "NixOS Options MCP server"; package = lib.mkPackageOption pkgs "nixos-options-mcp" { }; user = lib.mkOption { type = lib.types.str; default = "nixos-options-mcp"; description = "User account under which the service runs."; }; group = lib.mkOption { type = lib.types.str; default = "nixos-options-mcp"; description = "Group under which the service runs."; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/nixos-options-mcp"; description = "Directory to store data files."; }; database = { type = lib.mkOption { type = lib.types.enum [ "sqlite" "postgres" ]; default = "sqlite"; description = "Database backend to use."; }; name = lib.mkOption { type = lib.types.str; default = "nixos-options.db"; description = "SQLite database filename (when using sqlite backend)."; }; connectionString = lib.mkOption { type = lib.types.str; default = ""; description = '' PostgreSQL connection string (when using postgres backend). Example: "postgres://user:password@localhost/nixos_options?sslmode=disable" ''; }; }; indexOnStart = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "nixos-unstable" "nixos-24.11" ]; description = '' List of nixpkgs revisions to index on service start. Can be channel names (nixos-unstable) or git hashes. Indexing is skipped if the revision is already indexed. ''; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to open the firewall for the MCP server. Note: MCP typically runs over stdio, so this is usually not needed. ''; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.database.type == "sqlite" || cfg.database.connectionString != ""; message = "services.nixos-options-mcp.database.connectionString must be set when using postgres backend"; } ]; users.users.${cfg.user} = lib.mkIf (cfg.user == "nixos-options-mcp") { isSystemUser = true; group = cfg.group; home = cfg.dataDir; description = "NixOS Options MCP server user"; }; users.groups.${cfg.group} = lib.mkIf (cfg.group == "nixos-options-mcp") { }; systemd.tmpfiles.rules = [ "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" ]; systemd.services.nixos-options-mcp = { description = "NixOS Options MCP Server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ] ++ lib.optional (cfg.database.type == "postgres") "postgresql.service"; environment = { NIXOS_OPTIONS_DATABASE = databaseUrl; }; preStart = lib.mkIf (cfg.indexOnStart != [ ]) '' ${lib.concatMapStringsSep "\n" (rev: '' echo "Indexing revision: ${rev}" ${cfg.package}/bin/nixos-options index "${rev}" || true '') cfg.indexOnStart} ''; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; ExecStart = "${cfg.package}/bin/nixos-options serve"; Restart = "on-failure"; RestartSec = "5s"; # Hardening NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = true; LockPersonality = true; ReadWritePaths = [ cfg.dataDir ]; WorkingDirectory = cfg.dataDir; StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/nixos-options-mcp") "nixos-options-mcp"; }; }; }; }