From 9163d2bf054621473591e4233f580c975172a0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Fri, 20 Mar 2026 12:17:59 -0700 Subject: [PATCH 1/2] feat(glyph): add Grafana MCP server Adds mcp-grafana as an MCP service bridged to HTTP via mcp-proxy, giving agents access to Grafana dashboards, Loki logs, and Prometheus metrics. Registered in mcpjungle on port 8095. Requires creating the secret before deploying: agenix -e hosts/glyph/secrets/grafana-mcp-token.age The file should contain: GRAFANA_SERVICE_ACCOUNT_TOKEN= Co-Authored-By: Claude Opus 4.6 --- hosts/glyph/services/default.nix | 16 ++++++ lib/secrets/glyph.nix | 1 + modules/nixos/llm/default.nix | 1 + modules/nixos/llm/grafana-mcp.nix | 91 +++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 modules/nixos/llm/grafana-mcp.nix diff --git a/hosts/glyph/services/default.nix b/hosts/glyph/services/default.nix index 30148950..5bcb18f5 100644 --- a/hosts/glyph/services/default.nix +++ b/hosts/glyph/services/default.nix @@ -78,6 +78,13 @@ mode = "440"; }; + age.secrets.grafana-mcp-token = { + file = ./../secrets/grafana-mcp-token.age; + mode = "440"; + owner = "grafana-mcp"; + group = "grafana-mcp"; + }; + age.secrets.graphite-auth-token = { file = ./../secrets/graphite-auth-token.age; mode = "440"; @@ -100,6 +107,11 @@ enable = true; environmentFile = config.age.secrets.kagi-api-key.path; }; + services.grafana-mcp = { + enable = true; + grafanaUrl = "https://grafana.zx.dev"; + tokenFile = config.age.secrets.grafana-mcp-token.path; + }; services.graphite-mcp = { enable = true; authTokenFile = config.age.secrets.graphite-auth-token.path; @@ -118,6 +130,10 @@ url = "http://127.0.0.1:8093/mcp"; description = "Kagi web search and page summarization"; }; + servers.grafana = { + url = "http://127.0.0.1:8095/mcp"; + description = "Grafana dashboards, Loki logs, and Prometheus metrics"; + }; servers.graphite = { url = "http://127.0.0.1:8094/mcp"; description = "Graphite CLI for stacked PRs and code review"; diff --git a/lib/secrets/glyph.nix b/lib/secrets/glyph.nix index 20f33a62..a178af8c 100644 --- a/lib/secrets/glyph.nix +++ b/lib/secrets/glyph.nix @@ -9,6 +9,7 @@ in { "hosts/glyph/secrets/open-terminal-env.age".publicKeys = keys; "hosts/glyph/secrets/open-webui-api-key.age".publicKeys = keys; "hosts/glyph/secrets/open-webui-env.age".publicKeys = keys; + "hosts/glyph/secrets/grafana-mcp-token.age".publicKeys = keys; "hosts/glyph/secrets/graphite-auth-token.age".publicKeys = keys; "hosts/glyph/secrets/attic-credentials.age".publicKeys = keys; } diff --git a/modules/nixos/llm/default.nix b/modules/nixos/llm/default.nix index 25583d33..38428cec 100644 --- a/modules/nixos/llm/default.nix +++ b/modules/nixos/llm/default.nix @@ -1,6 +1,7 @@ { imports = [ ./basic-memory.nix + ./grafana-mcp.nix ./graphite-mcp.nix ./kagi.nix ./mcp-nixos.nix diff --git a/modules/nixos/llm/grafana-mcp.nix b/modules/nixos/llm/grafana-mcp.nix new file mode 100644 index 00000000..a7b4a805 --- /dev/null +++ b/modules/nixos/llm/grafana-mcp.nix @@ -0,0 +1,91 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.services.grafana-mcp; + + startScript = pkgs.writeShellScript "grafana-mcp-start" '' + exec ${lib.getExe pkgs.mcp-proxy} \ + --host ${cfg.host} \ + --port ${toString cfg.port} \ + --transport streamablehttp \ + -- ${lib.getExe pkgs.mcp-grafana} + ''; +in { + options.services.grafana-mcp = { + enable = lib.mkEnableOption "Grafana MCP server (stdio→HTTP bridge)"; + + port = lib.mkOption { + type = lib.types.port; + default = 8095; + 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."; + }; + + grafanaUrl = lib.mkOption { + type = lib.types.str; + description = "URL of the Grafana instance to connect to."; + }; + + tokenFile = lib.mkOption { + type = lib.types.path; + description = "Path to file containing the Grafana service account token."; + }; + + openFirewall = lib.mkEnableOption "opening firewall ports for Grafana MCP"; + }; + + config = lib.mkIf cfg.enable { + users.users.grafana-mcp = { + isSystemUser = true; + group = "grafana-mcp"; + home = "/var/lib/grafana-mcp"; + }; + users.groups.grafana-mcp = {}; + + systemd.services.grafana-mcp = { + description = "Grafana MCP Server"; + after = ["network-online.target"]; + wants = ["network-online.target"]; + wantedBy = ["multi-user.target"]; + + environment = { + HOME = "/var/lib/grafana-mcp"; + GRAFANA_URL = cfg.grafanaUrl; + }; + + serviceConfig = { + ExecStart = "${startScript}"; + User = "grafana-mcp"; + Group = "grafana-mcp"; + WorkingDirectory = "/var/lib/grafana-mcp"; + StateDirectory = "grafana-mcp"; + EnvironmentFile = cfg.tokenFile; + Restart = "on-failure"; + RestartSec = 5; + + # Hardening + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectHome = "tmpfs"; + BindPaths = ["/var/lib/grafana-mcp"]; + ProtectSystem = "strict"; + ReadWritePaths = ["/var/lib/grafana-mcp"]; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [cfg.port]; + }; +} From bea8c36f69d7c732bff1cdaae10c6001a340873f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Fri, 20 Mar 2026 12:22:05 -0700 Subject: [PATCH 2/2] chore(glyph): add encrypted Grafana service account token Co-Authored-By: Claude Opus 4.6 --- hosts/glyph/secrets/grafana-mcp-token.age | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 hosts/glyph/secrets/grafana-mcp-token.age diff --git a/hosts/glyph/secrets/grafana-mcp-token.age b/hosts/glyph/secrets/grafana-mcp-token.age new file mode 100644 index 00000000..aece6ef2 --- /dev/null +++ b/hosts/glyph/secrets/grafana-mcp-token.age @@ -0,0 +1,7 @@ +age-encryption.org/v1 +-> ssh-ed25519 rSr+rA 2Srq6rfOwBrjWpUlzypP2OQZXTXLBgQ3UY/zLPnfLlA +kw3ptb5pp41bbtAgOQj2tlZTgGO4DsXrqovZRz/elac +-> ssh-ed25519 3EWhnQ +4wBGx4XFGGvVUVc7ymy/3Tm7cGDFJqdoNCVJ7swYVY +6iXVQvvkSNMpzJDhPJgyHmH6XBVuiYqQkP4rSrfIo1s +--- UtH+GQsYUU8aLukR55hOUHfFUahVmUp6l6XZoCk17rk +ZnR:8=>Tg vž/3 {ds >"H"HPBڹ8~ʠ)Jr(#n