From de573124cef800af2d08593cf264fc08a8c443ae Mon Sep 17 00:00:00 2001 From: sid Date: Tue, 19 May 2026 12:56:26 +0200 Subject: [PATCH] add mcp log server via journald --- hosts/rx4/services/default.nix | 1 + hosts/sid/services/default.nix | 1 + modules/nixos/common/default.nix | 1 - modules/nixos/default.nix | 2 + modules/nixos/journald-remote/default.nix | 57 ++++++++++++++++++ modules/nixos/journald-remote/pyproject.toml | 9 +++ modules/nixos/journald-remote/server.py | 60 +++++++++++++++++++ .../default.nix} | 2 +- 8 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 modules/nixos/journald-remote/default.nix create mode 100644 modules/nixos/journald-remote/pyproject.toml create mode 100644 modules/nixos/journald-remote/server.py rename modules/nixos/{common/journald.nix => journald-upload/default.nix} (52%) diff --git a/hosts/rx4/services/default.nix b/hosts/rx4/services/default.nix index 8db7081..3814e36 100644 --- a/hosts/rx4/services/default.nix +++ b/hosts/rx4/services/default.nix @@ -10,6 +10,7 @@ inputs.clients.nixosModules.syncthing outputs.nixosModules.tailscale + outputs.nixosModules.journald-upload ./forgejo.nix ./jirafeau.nix diff --git a/hosts/sid/services/default.nix b/hosts/sid/services/default.nix index 7ca9678..521af0b 100644 --- a/hosts/sid/services/default.nix +++ b/hosts/sid/services/default.nix @@ -9,6 +9,7 @@ inputs.synix.nixosModules.openssh outputs.nixosModules.tailscale + outputs.nixosModules.journald-remote ./headscale.nix ./mailserver.nix diff --git a/modules/nixos/common/default.nix b/modules/nixos/common/default.nix index 075c2dd..eba84dd 100644 --- a/modules/nixos/common/default.nix +++ b/modules/nixos/common/default.nix @@ -2,7 +2,6 @@ { imports = [ - # ./journald.nix # FIXME: start systemd-journal-upload.service after tailscaled ./nix.nix ./overlays.nix diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index f831ea8..2e35096 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -5,6 +5,8 @@ forgejo = import ./forgejo; forgejo-runner = import ./forgejo-runner; gnome = import ./gnome; + journald-remote = import ./journald-remote; + journald-upload = import ./journald-upload; monero = import ./monero; rsshub-oci = import ./rsshub-oci; tailscale = import ./tailscale; diff --git a/modules/nixos/journald-remote/default.nix b/modules/nixos/journald-remote/default.nix new file mode 100644 index 0000000..17012a6 --- /dev/null +++ b/modules/nixos/journald-remote/default.nix @@ -0,0 +1,57 @@ +{ + pkgs, + lib, + ... +}: + +let + python = pkgs.python3Packages; + + mcp-log-server = python.buildPythonApplication { + pname = "mcp-log-server"; + version = "1.0.0"; + + src = ./.; + + pyproject = true; + build-system = [ python.setuptools ]; + + propagatedBuildInputs = with python; [ + fastmcp + ]; + + meta.mainProgram = "mcp-log-server"; + }; +in +{ + services.journald.remote = { + enable = true; + listen = "http"; + port = 19532; + settings.Remote.SplitMode = "host"; + }; + + users.users.sid.extraGroups = [ + "systemd-journal" + "systemd-journal-remote" + ]; + + systemd.services.mcp-log-server = { + description = "AI Log Access MCP Server"; + after = [ + "network.target" + "multi-user.target" + "systemd-journald.service" + ]; + wantedBy = [ "multi-user.target" ]; + + script = lib.getExe mcp-log-server; + + serviceConfig = { + User = "root"; + Group = "root"; + Environment = "PYTHONUNBUFFERED=1"; + Restart = "on-failure"; + }; + }; +} diff --git a/modules/nixos/journald-remote/pyproject.toml b/modules/nixos/journald-remote/pyproject.toml new file mode 100644 index 0000000..809656b --- /dev/null +++ b/modules/nixos/journald-remote/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools"] + +[project] +name = "mcp-log-server" +version = "1.0.0" + +[project.scripts] +mcp-log-server = "server:main" diff --git a/modules/nixos/journald-remote/server.py b/modules/nixos/journald-remote/server.py new file mode 100644 index 0000000..39c8f28 --- /dev/null +++ b/modules/nixos/journald-remote/server.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import json +import subprocess +from fastmcp import FastMCP + +mcp = FastMCP('ClusterLogQuery') + +@mcp.tool() +async def search_cluster_logs(keyword: str, unit: str = '*', limit: int = 500): + """ + Search centralized systemd logs collected on this server. + + Args: + keyword: The string to search for (e.g. 'oom-kill', 'failed'). + unit: The systemd unit to filter (e.g. 'sshd'). If '*', matches all. + limit: The number of log lines to return (Max 1000). + """ + + limit = min(max(limit, 10), 1000) + + cmd = [ + 'journalctl', + '-n', str(limit), + '-o', 'json', + '--no-pager', + '--directory', '/var/log/journal/remote' + ] + + if unit != '*': + cmd.append(f'UNIT={unit}') + if keyword: + cmd.append(keyword) + + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, _ = proc.communicate() + + logs = [] + for line in stdout.decode('utf-8').strip().split('\n'): + if not line: continue + try: + log = json.loads(line) + logs.append({ + 'timestamp': log.get('TIME_ISO8601'), + 'hostname': log.get('_HOSTNAME'), + 'unit': log.get('_COMM'), + 'message': log.get('MESSAGE') + }) + except: pass + + return json.dumps(logs) + + except Exception as e: + return f'Error querying logs: {str(e)}' + +def main(): + mcp.run(transport='sse', host='0.0.0.0', port=9500) + +if __name__ == '__main__': + main() diff --git a/modules/nixos/common/journald.nix b/modules/nixos/journald-upload/default.nix similarity index 52% rename from modules/nixos/common/journald.nix rename to modules/nixos/journald-upload/default.nix index d31ada4..9d8c0c5 100644 --- a/modules/nixos/common/journald.nix +++ b/modules/nixos/journald-upload/default.nix @@ -1,6 +1,6 @@ { services.journald.upload = { enable = true; - settings.Upload.URL = "http://100.64.0.5:19532"; + settings.Upload.URL = "http://100.64.0.6:19532"; }; } -- 2.51.2