From 016f4f7efb872eaab625f232dfcda3abecda1edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Fri, 6 Mar 2026 15:37:37 -0800 Subject: [PATCH] feat: setup mcp-nixos --- hosts/glyph/services/default.nix | 5 ++ modules/home/development.nix | 3 ++ modules/nixos/llm/default.nix | 1 + modules/nixos/llm/mcp-nixos.nix | 81 ++++++++++++++++++++++++++++++++ modules/nixos/llm/mcpjungle.nix | 23 ++++++++- 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 modules/nixos/llm/mcp-nixos.nix diff --git a/hosts/glyph/services/default.nix b/hosts/glyph/services/default.nix index 44ca37b4..5fe409e4 100644 --- a/hosts/glyph/services/default.nix +++ b/hosts/glyph/services/default.nix @@ -61,11 +61,16 @@ }; services.basic-memory.enable = true; + services.mcp-nixos.enable = true; services.mcpjungle = { enable = true; servers.basic-memory = { url = "http://127.0.0.1:8091/mcp"; description = "Knowledge management with markdown files"; }; + servers.mcp-nixos = { + url = "http://127.0.0.1:8092/mcp"; + description = "NixOS options, packages, and Home Manager search"; + }; }; } diff --git a/modules/home/development.nix b/modules/home/development.nix index d5f87ee8..f7c55c80 100644 --- a/modules/home/development.nix +++ b/modules/home/development.nix @@ -58,6 +58,9 @@ in { "Bash(git branch *)" "Bash(gh api:*)" "Bash(mkdir *)" + "Bash(journalctl:*)" + "Bash(systemctl list-jobs:*)" + "Bash(systemctl status:*)" "Bash(* --version)" "Bash(* --help *)" "WebFetch(domain:raw.githubusercontent.com)" diff --git a/modules/nixos/llm/default.nix b/modules/nixos/llm/default.nix index e8b889b3..14b47fe5 100644 --- a/modules/nixos/llm/default.nix +++ b/modules/nixos/llm/default.nix @@ -1,6 +1,7 @@ { imports = [ ./basic-memory.nix + ./mcp-nixos.nix ./mcpjungle.nix ]; } diff --git a/modules/nixos/llm/mcp-nixos.nix b/modules/nixos/llm/mcp-nixos.nix new file mode 100644 index 00000000..79784f33 --- /dev/null +++ b/modules/nixos/llm/mcp-nixos.nix @@ -0,0 +1,81 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.services.mcp-nixos; + + startScript = pkgs.writeShellScript "mcp-nixos-start" '' + exec ${lib.getExe pkgs.uv} run --with mcp-nixos python -c " + from mcp_nixos.server import mcp + mcp.run(transport='streamable-http', port=${toString cfg.port}, host='${cfg.host}') + " + ''; +in { + options.services.mcp-nixos = { + enable = lib.mkEnableOption "MCP NixOS server"; + + port = lib.mkOption { + type = lib.types.port; + default = 8092; + 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."; + }; + + openFirewall = lib.mkEnableOption "opening firewall ports for MCP NixOS"; + }; + + config = lib.mkIf cfg.enable { + users.users.mcp-nixos = { + isSystemUser = true; + group = "mcp-nixos"; + home = "/var/lib/mcp-nixos"; + }; + users.groups.mcp-nixos = {}; + + systemd.services.mcp-nixos = { + description = "MCP NixOS Server"; + after = ["network-online.target"]; + wants = ["network-online.target"]; + wantedBy = ["multi-user.target"]; + + environment = { + HOME = "/var/lib/mcp-nixos"; + UV_CACHE_DIR = "/var/lib/mcp-nixos/.cache/uv"; + }; + + serviceConfig = { + ExecStart = "${startScript}"; + User = "mcp-nixos"; + Group = "mcp-nixos"; + WorkingDirectory = "/var/lib/mcp-nixos"; + StateDirectory = "mcp-nixos"; + Restart = "on-failure"; + RestartSec = 5; + + # Hardening + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectHome = "tmpfs"; + BindPaths = ["/var/lib/mcp-nixos"]; + ProtectSystem = "strict"; + ReadWritePaths = ["/var/lib/mcp-nixos"]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + }; + + programs.nix-ld.enable = 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 0c91297c..e5639416 100644 --- a/modules/nixos/llm/mcpjungle.nix +++ b/modules/nixos/llm/mcpjungle.nix @@ -82,12 +82,33 @@ in { Type = "oneshot"; RemainAfterExit = true; }; + path = [pkgs.curl]; script = let registry = "http://127.0.0.1:${toString cfg.port}"; bin = lib.getExe cfg.package; registrations = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: server: '' - if ! ${bin} list servers --registry ${registry} 2>/dev/null | grep -q '${name}'; then + if ${bin} list servers --registry ${registry} 2>/dev/null | grep -q '${name}'; then + echo "${name} already registered, skipping." + else + + # 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) + if [ "$http_code" != "000" ]; then + ready=true + break + fi + echo "Waiting for ${name} at ${server.url} (attempt $i/30)..." + sleep 2 + done + + if [ "$ready" = true ]; then ${bin} register --name '${name}' --description '${server.description}' --url '${server.url}' --registry ${registry} + else + echo "WARNING: ${name} at ${server.url} not reachable after 60s, skipping registration." + fi + fi '') cfg.servers);