{ config, lib, pkgs, ... }: let cfg = config.services.git-explorer; mkHttpFlags = httpCfg: lib.concatStringsSep " " ([ "--transport http" "--http-address '${httpCfg.address}'" "--http-endpoint '${httpCfg.endpoint}'" "--session-ttl '${httpCfg.sessionTTL}'" ] ++ lib.optionals (httpCfg.allowedOrigins != []) ( map (origin: "--allowed-origins '${origin}'") httpCfg.allowedOrigins ) ++ lib.optionals httpCfg.tls.enable [ "--tls-cert '${httpCfg.tls.certFile}'" "--tls-key '${httpCfg.tls.keyFile}'" ]); in { options.services.git-explorer = { enable = lib.mkEnableOption "Git Explorer MCP server"; package = lib.mkPackageOption pkgs "git-explorer" { }; repoPath = lib.mkOption { type = lib.types.str; description = "Path to the git repository to serve."; }; defaultRemote = lib.mkOption { type = lib.types.str; default = "origin"; description = "Default remote name for ref resolution."; }; http = { address = lib.mkOption { type = lib.types.str; default = "127.0.0.1:8085"; description = "HTTP listen address for the MCP server."; }; endpoint = lib.mkOption { type = lib.types.str; default = "/mcp"; description = "HTTP endpoint path for MCP requests."; }; allowedOrigins = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Allowed Origin headers for CORS."; }; sessionTTL = lib.mkOption { type = lib.types.str; default = "30m"; description = "Session TTL for HTTP transport."; }; tls = { enable = lib.mkEnableOption "TLS for HTTP transport"; certFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Path to TLS certificate file."; }; keyFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Path to TLS private key file."; }; }; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to open the firewall for the MCP HTTP server."; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = !cfg.http.tls.enable || (cfg.http.tls.certFile != null && cfg.http.tls.keyFile != null); message = "services.git-explorer.http.tls: both certFile and keyFile must be set when TLS is enabled"; } ]; systemd.services.git-explorer = { description = "Git Explorer MCP Server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment = { GIT_REPO_PATH = cfg.repoPath; GIT_DEFAULT_REMOTE = cfg.defaultRemote; }; script = let httpFlags = mkHttpFlags cfg.http; in '' exec ${cfg.package}/bin/git-explorer serve ${httpFlags} ''; serviceConfig = { Type = "simple"; DynamicUser = true; Restart = "on-failure"; RestartSec = "5s"; # Hardening NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = "read-only"; PrivateTmp = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = true; LockPersonality = true; # Read-only access to repo path ReadOnlyPaths = [ cfg.repoPath ]; }; }; networking.firewall = lib.mkIf cfg.openFirewall (let addressParts = lib.splitString ":" cfg.http.address; port = lib.toInt (lib.last addressParts); in { allowedTCPPorts = [ port ]; }); }; }