diff --git a/hosts/glyph/secrets/graphite-auth-token.age b/hosts/glyph/secrets/graphite-auth-token.age new file mode 100644 index 0000000..4548c7c Binary files /dev/null and b/hosts/glyph/secrets/graphite-auth-token.age differ diff --git a/hosts/glyph/services/default.nix b/hosts/glyph/services/default.nix index 2eeca60..669e446 100644 --- a/hosts/glyph/services/default.nix +++ b/hosts/glyph/services/default.nix @@ -73,6 +73,13 @@ mode = "440"; }; + age.secrets.graphite-auth-token = { + file = ./../secrets/graphite-auth-token.age; + mode = "440"; + owner = "graphite-mcp"; + group = "graphite-mcp"; + }; + services.basic-memory.enable = true; rc.backup = { enable = true; @@ -87,6 +94,10 @@ enable = true; environmentFile = config.age.secrets.kagi-api-key.path; }; + services.graphite-mcp = { + enable = true; + authTokenFile = config.age.secrets.graphite-auth-token.path; + }; services.mcpjungle = { enable = true; servers.basic-memory = { @@ -101,6 +112,10 @@ url = "http://127.0.0.1:8093/mcp"; description = "Kagi web search and page summarization"; }; + servers.graphite = { + url = "http://127.0.0.1:8094/mcp"; + description = "Graphite CLI for stacked PRs and code review"; + }; servers.context7 = { url = "https://mcp.context7.com/mcp"; description = "Up-to-date library documentation and code examples"; diff --git a/lib/secrets/glyph.nix b/lib/secrets/glyph.nix index 89741c5..c9b2dc6 100644 --- a/lib/secrets/glyph.nix +++ b/lib/secrets/glyph.nix @@ -7,4 +7,5 @@ in { "hosts/glyph/secrets/kagi-api-key.age".publicKeys = keys; "hosts/glyph/secrets/context7-api-key.age".publicKeys = keys; "hosts/glyph/secrets/open-webui-env.age".publicKeys = keys; + "hosts/glyph/secrets/graphite-auth-token.age".publicKeys = keys; } diff --git a/modules/home/development.nix b/modules/home/development.nix index 086f013..9211129 100644 --- a/modules/home/development.nix +++ b/modules/home/development.nix @@ -154,6 +154,7 @@ in { "mcp__glyph__basic-memory__view_note" "mcp__glyph__context7__resolve-library-id" "mcp__glyph__context7__get-library-docs" + "mcp__glyph__graphite__learn_gt" ]; deny = []; }; diff --git a/modules/nixos/llm/default.nix b/modules/nixos/llm/default.nix index db9b8ed..25583d3 100644 --- a/modules/nixos/llm/default.nix +++ b/modules/nixos/llm/default.nix @@ -1,6 +1,7 @@ { imports = [ ./basic-memory.nix + ./graphite-mcp.nix ./kagi.nix ./mcp-nixos.nix ./mcpjungle.nix diff --git a/modules/nixos/llm/graphite-mcp.nix b/modules/nixos/llm/graphite-mcp.nix new file mode 100644 index 0000000..e75edd8 --- /dev/null +++ b/modules/nixos/llm/graphite-mcp.nix @@ -0,0 +1,94 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.services.graphite-mcp; + + preStartScript = pkgs.writeShellScript "graphite-mcp-prestart" '' + mkdir -p "$HOME/.config/graphite" + token=$(cat "${cfg.authTokenFile}") + printf '{"authToken":"%s"}' "$token" > "$HOME/.config/graphite/user_config" + chmod 600 "$HOME/.config/graphite/user_config" + ''; + + startScript = pkgs.writeShellScript "graphite-mcp-start" '' + exec ${lib.getExe pkgs.mcp-proxy} \ + --host ${cfg.host} \ + --port ${toString cfg.port} \ + --transport streamablehttp \ + -- ${lib.getExe pkgs.graphite-cli} mcp + ''; +in { + options.services.graphite-mcp = { + enable = lib.mkEnableOption "Graphite MCP server (stdio→HTTP bridge)"; + + port = lib.mkOption { + type = lib.types.port; + default = 8094; + description = "Port for the streamable HTTP transport."; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Address to bind the HTTP server to."; + }; + + authTokenFile = lib.mkOption { + type = lib.types.path; + description = "Path to file containing the Graphite auth token."; + }; + + openFirewall = lib.mkEnableOption "opening firewall ports for Graphite MCP"; + }; + + config = lib.mkIf cfg.enable { + users.users.graphite-mcp = { + isSystemUser = true; + group = "graphite-mcp"; + home = "/var/lib/graphite-mcp"; + }; + users.groups.graphite-mcp = {}; + + systemd.services.graphite-mcp = { + description = "Graphite MCP Server"; + after = ["network-online.target"]; + wants = ["network-online.target"]; + wantedBy = ["multi-user.target"]; + + path = [pkgs.git]; + + environment = { + HOME = "/var/lib/graphite-mcp"; + }; + + serviceConfig = { + ExecStartPre = "${preStartScript}"; + ExecStart = "${startScript}"; + User = "graphite-mcp"; + Group = "graphite-mcp"; + WorkingDirectory = "/var/lib/graphite-mcp"; + StateDirectory = "graphite-mcp"; + Restart = "on-failure"; + RestartSec = 5; + + # Hardening + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectHome = "tmpfs"; + BindPaths = ["/var/lib/graphite-mcp"]; + ProtectSystem = "strict"; + ReadWritePaths = ["/var/lib/graphite-mcp"]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [cfg.port]; + }; +} diff --git a/modules/nixos/llm/mcpjungle.nix b/modules/nixos/llm/mcpjungle.nix index f1c855e..a14815a 100644 --- a/modules/nixos/llm/mcpjungle.nix +++ b/modules/nixos/llm/mcpjungle.nix @@ -132,7 +132,7 @@ in { # Wait for server to be reachable before registering ready=false for i in $(seq 1 30); do - http_code=$(curl -s -o /dev/null -w '%{http_code}' "${server.url}" 2>/dev/null) + http_code=$(curl -s -o /dev/null -w '%{http_code}' "${server.url}" 2>/dev/null || true) if [ "$http_code" != "000" ]; then ready=true break