feat: add git-explorer MCP server for read-only repository access
Implements a new MCP server that provides read-only access to git repositories using go-git. Designed for deployment verification by comparing deployed flake revisions against source repositories. 9 tools: resolve_ref, get_log, get_commit_info, get_diff_files, get_file_at_commit, is_ancestor, commits_between, list_branches, search_commits. Includes CLI commands, NixOS module, and comprehensive tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
141
nix/git-explorer-module.nix
Normal file
141
nix/git-explorer-module.nix
Normal file
@@ -0,0 +1,141 @@
|
||||
{ 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 ];
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@ buildGoModule {
|
||||
inherit pname src;
|
||||
version = "0.3.0";
|
||||
|
||||
vendorHash = "sha256-D0KIxQC9ctIAaHBFTvkhBE06uOZwDUcIw8471Ug2doY=";
|
||||
vendorHash = "sha256-XrTtiaQT5br+0ZXz8//rc04GZn/HlQk7l8Nx/+Uil/I=";
|
||||
|
||||
subPackages = [ subPackage ];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user