This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

mcp-nixos / flake.nix
15 kB 408 lines
1{ 2 description = "MCP-NixOS - Model Context Protocol server for NixOS, Home Manager, and nix-darwin"; 3 4 inputs = { 5 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 flake-parts.url = "github:hercules-ci/flake-parts"; 7 devshell = { 8 url = "github:numtide/devshell"; 9 inputs.nixpkgs.follows = "nixpkgs"; 10 }; 11 }; 12 13 outputs = 14 inputs@{ 15 self, 16 nixpkgs, 17 flake-parts, 18 ... 19 }: 20 let 21 mkMcpNixos = 22 { 23 pkgs, 24 python3Packages ? pkgs.python3Packages, 25 }: 26 let 27 pyproject = pkgs.lib.importTOML ./pyproject.toml; 28 in 29 python3Packages.buildPythonApplication { 30 pname = pyproject.project.name; 31 inherit (pyproject.project) version; 32 pyproject = true; 33 src = pkgs.lib.fileset.toSource { 34 root = ./.; 35 fileset = pkgs.lib.fileset.unions [ 36 ./pyproject.toml 37 ./README.md 38 ./LICENSE 39 ./RELEASE_NOTES.md 40 ./mcp_nixos 41 ./tests 42 ]; 43 }; 44 45 build-system = [ python3Packages.hatchling ]; 46 dependencies = with python3Packages; [ 47 fastmcp 48 requests 49 beautifulsoup4 50 ]; 51 52 pythonRelaxDeps = true; 53 doCheck = true; 54 nativeCheckInputs = with python3Packages; [ 55 pytest 56 pytest-asyncio 57 pytest-cov 58 pytest-rerunfailures 59 ]; 60 checkPhase = '' 61 pytest tests/ -m unit 62 ''; 63 dontCheckRuntimeDeps = true; 64 pythonImportsCheck = [ "mcp_nixos" ]; 65 66 meta = { 67 inherit (pyproject.project) description; 68 homepage = "https://github.com/utensils/mcp-nixos"; 69 license = pkgs.lib.licenses.mit; 70 mainProgram = "mcp-nixos"; 71 }; 72 }; 73 in 74 flake-parts.lib.mkFlake { inherit inputs; } { 75 imports = [ 76 inputs.devshell.flakeModule 77 ]; 78 79 systems = [ 80 "x86_64-linux" 81 "aarch64-linux" 82 "x86_64-darwin" 83 "aarch64-darwin" 84 ]; 85 86 flake = { 87 # Upgrade fastmcp to 3.2.4 ahead of nixpkgs. 88 # Mirrors nixpkgs PR #510339 (PrefectHQ/fastmcp v3.2.4). Can be removed 89 # once that PR merges and our flake input moves past it. 90 overlays.fastmcp3 = final: prev: { 91 pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [ 92 (pyFinal: pyPrev: { 93 fastmcp = pyPrev.fastmcp.overridePythonAttrs (old: rec { 94 version = "3.2.4"; 95 src = prev.fetchFromGitHub { 96 owner = "PrefectHQ"; 97 repo = "fastmcp"; 98 tag = "v${version}"; 99 hash = "sha256-rJpxPvqAaa6/vXhG1+R9dI32cY/54e6I+F/zyBVoqBM="; 100 }; 101 # Drop pydocket (moved to optional-dependencies.tasks upstream in 102 # nixpkgs PR #510339) and add fastmcp 3's new transitive deps. 103 # pydocket's build pulls in lupa → luajit, which fails to link on 104 # aarch64-linux (bundled libluajit.a is in the wrong format), and 105 # we don't use fastmcp task features. 106 # 107 # griffelib and uncalled-for are recent additions to nixos-unstable 108 # (March 2026) and absent from stable channels. Use upstream if the 109 # consumer's nixpkgs has them; otherwise fall back to our inline 110 # definitions so `inputs.nixpkgs.follows = "nixpkgs"` works against 111 # older pins. See issue #135. 112 dependencies = builtins.filter (d: (d.pname or "") != "pydocket") (old.dependencies or [ ]) ++ [ 113 (pyFinal.griffelib or (pyFinal.callPackage ./nix/griffelib.nix { })) 114 pyFinal.opentelemetry-api 115 (pyFinal.uncalled-for or (pyFinal.callPackage ./nix/uncalled-for.nix { })) 116 pyFinal.watchfiles 117 pyFinal.pyyaml 118 ]; 119 dontCheckRuntimeDeps = true; 120 doCheck = false; 121 }); 122 }) 123 ]; 124 }; 125 126 # Downstream consumers who apply `mcp-nixos.overlays.default` get both 127 # mcp-nixos itself and the fastmcp 3 upgrade needed to satisfy our 128 # fastmcp>=3.2.0 dependency against nixpkgs that still ships 2.x. 129 overlays.default = nixpkgs.lib.composeExtensions self.overlays.fastmcp3 ( 130 final: _: { 131 mcp-nixos = mkMcpNixos { pkgs = final; }; 132 } 133 ); 134 135 lib.mkMcpNixos = mkMcpNixos; 136 }; 137 138 perSystem = 139 { system, ... }: 140 let 141 pkgs = import nixpkgs { 142 inherit system; 143 overlays = [ self.overlays.fastmcp3 ]; 144 }; 145 146 # One unified Python environment with app runtime deps, dev tools, 147 # and type stubs all sharing a single site-packages. Without this, 148 # each `python3Packages.*` is its own isolated env so `mypy` can't 149 # see `types-requests` and `python -m build` fails with "No module 150 # named build" — which is how the old mkShell + inputsFrom setup 151 # silently passed: the propagated env leaked in. numtide/devshell 152 # is stricter, so we build the env explicitly. 153 pythonEnv = pkgs.python3.withPackages ( 154 ps: with ps; [ 155 # app runtime 156 fastmcp 157 requests 158 beautifulsoup4 159 # build 160 hatchling 161 build 162 twine 163 # lint / type-check 164 ruff 165 mypy 166 types-requests 167 types-beautifulsoup4 168 # test 169 pytest 170 pytest-asyncio 171 pytest-cov 172 pytest-rerunfailures 173 pytest-xdist 174 ] 175 ); 176 177 # Shared docs/website commands — available in both the default and 178 # `web` devshells so you can pick the right weight class (full Python 179 # + docs vs docs-only). 180 docsCommands = [ 181 { 182 category = "docs"; 183 name = "docs-install"; 184 help = "install VitePress + theme deps (first-time setup)"; 185 command = "cd \"$PRJ_ROOT/website\" && npm install \"$@\""; 186 } 187 { 188 category = "docs"; 189 name = "docs-dev"; 190 help = "VitePress dev server with hot reload (auto-increments port if 5173 is taken)"; 191 command = '' 192 cd "$PRJ_ROOT/website" 193 [ -d node_modules ] || npm install 194 npm run dev -- "$@" 195 ''; 196 } 197 { 198 category = "docs"; 199 name = "docs-build"; 200 help = "build the documentation site into website/out/"; 201 command = '' 202 cd "$PRJ_ROOT/website" 203 [ -d node_modules ] || npm install 204 npm run build 205 ''; 206 } 207 { 208 category = "docs"; 209 name = "docs-preview"; 210 help = "serve the built docs site (auto-increments port if 4173 is taken)"; 211 command = "cd \"$PRJ_ROOT/website\" && npm run preview -- \"$@\""; 212 } 213 { 214 category = "docs"; 215 name = "docs-check"; 216 help = "type-check Vue components with vue-tsc"; 217 command = "cd \"$PRJ_ROOT/website\" && npm run check -- \"$@\""; 218 } 219 { 220 category = "docs"; 221 name = "docs-clean"; 222 help = "remove VitePress build + cache artifacts"; 223 command = "rm -rf \"$PRJ_ROOT/website/.vitepress/cache\" \"$PRJ_ROOT/website/.vitepress/dist\" \"$PRJ_ROOT/website/out\""; 224 } 225 ]; 226 in 227 { 228 packages = rec { 229 mcp-nixos = mkMcpNixos { inherit pkgs; }; 230 default = mcp-nixos; 231 232 docker = pkgs.dockerTools.buildLayeredImage { 233 name = "ghcr.io/utensils/mcp-nixos"; 234 tag = mcp-nixos.version; 235 # Format: YYYYMMDDHHMMSS -> YYYY-MM-DDTHH:MM:SSZ 236 created = 237 let 238 d = self.lastModifiedDate; 239 in 240 "${builtins.substring 0 4 d}-${builtins.substring 4 2 d}-${builtins.substring 6 2 d}T${builtins.substring 8 2 d}:${builtins.substring 10 2 d}:${builtins.substring 12 2 d}Z"; 241 contents = [ 242 mcp-nixos 243 pkgs.cacert 244 ]; 245 config = { 246 Entrypoint = [ (pkgs.lib.getExe mcp-nixos) ]; 247 Env = [ 248 "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" 249 ]; 250 }; 251 }; 252 }; 253 254 apps = rec { 255 mcp-nixos = { 256 type = "app"; 257 program = pkgs.lib.getExe self.packages.${system}.mcp-nixos; 258 meta.description = "MCP server for NixOS, Home Manager, and nix-darwin"; 259 }; 260 default = mcp-nixos; 261 }; 262 263 formatter = pkgs.nixfmt-rfc-style; 264 265 # Default dev shell — Python backend + docs tooling in one place. 266 # Enter with: `nix develop` 267 devshells.default = { 268 name = "mcp-nixos"; 269 270 motd = '' 271 {202}mcp-nixos{reset} Model Context Protocol server for NixOS ({bold}${system}{reset}) 272 $(type menu &>/dev/null && menu) 273 ''; 274 275 packages = [ 276 pythonEnv 277 pkgs.nodejs_20 278 pkgs.git 279 pkgs.gh 280 pkgs.jq 281 pkgs.nixfmt-rfc-style 282 ]; 283 284 commands = [ 285 # ── server / run ─────────────────────────────────────────────── 286 { 287 category = "run"; 288 name = "run"; 289 help = "start the MCP server over STDIO"; 290 command = "mcp-nixos \"$@\""; 291 } 292 { 293 category = "run"; 294 name = "run-http"; 295 help = "start the MCP server over HTTP (http://127.0.0.1:8000/mcp)"; 296 command = '' 297 MCP_NIXOS_TRANSPORT=http \ 298 MCP_NIXOS_HOST="''${MCP_NIXOS_HOST:-127.0.0.1}" \ 299 MCP_NIXOS_PORT="''${MCP_NIXOS_PORT:-8000}" \ 300 mcp-nixos "$@" 301 ''; 302 } 303 304 # ── checks / tests ───────────────────────────────────────────── 305 { 306 category = "check"; 307 name = "run-tests"; 308 help = "pytest tests/ -n auto (matches CI)"; 309 command = "cd \"$PRJ_ROOT\" && pytest tests/ -v -n auto --cov=mcp_nixos \"$@\""; 310 } 311 { 312 category = "check"; 313 name = "test-unit"; 314 help = "run unit tests only (fast, offline)"; 315 command = "cd \"$PRJ_ROOT\" && pytest tests/ -m unit \"$@\""; 316 } 317 { 318 category = "check"; 319 name = "test-integration"; 320 help = "run integration tests (hits real APIs)"; 321 command = "cd \"$PRJ_ROOT\" && pytest tests/ -m integration \"$@\""; 322 } 323 { 324 category = "check"; 325 name = "lint"; 326 help = "ruff check + format check (matches CI)"; 327 command = '' 328 cd "$PRJ_ROOT" 329 ruff check mcp_nixos/ tests/ 330 ruff format --check mcp_nixos/ tests/ 331 ''; 332 } 333 { 334 category = "check"; 335 name = "format"; 336 help = "ruff format mcp_nixos/ tests/"; 337 command = "cd \"$PRJ_ROOT\" && ruff format mcp_nixos/ tests/"; 338 } 339 { 340 category = "check"; 341 name = "typecheck"; 342 help = "mypy mcp_nixos/"; 343 command = "cd \"$PRJ_ROOT\" && mypy mcp_nixos/"; 344 } 345 { 346 category = "check"; 347 name = "ci-local"; 348 help = "run the same sequence CI runs: lint, typecheck, tests"; 349 command = '' 350 set -euo pipefail 351 cd "$PRJ_ROOT" 352 ruff check mcp_nixos/ tests/ 353 ruff format --check mcp_nixos/ tests/ 354 mypy mcp_nixos/ 355 pytest tests/ -v -n auto --cov=mcp_nixos 356 ''; 357 } 358 359 # ── build / release ──────────────────────────────────────────── 360 { 361 category = "build"; 362 name = "build"; 363 help = "build the Python wheel + sdist into dist/"; 364 command = "cd \"$PRJ_ROOT\" && python -m build \"$@\""; 365 } 366 { 367 category = "build"; 368 name = "build-check"; 369 help = "twine check validate package metadata"; 370 command = "cd \"$PRJ_ROOT\" && twine check dist/*"; 371 } 372 { 373 category = "build"; 374 name = "build-nix"; 375 help = "nix build full flake build (matches CI)"; 376 command = "cd \"$PRJ_ROOT\" && nix build \"$@\""; 377 } 378 { 379 category = "build"; 380 name = "build-docker"; 381 help = "nix build .#docker build the multi-arch Docker image"; 382 command = "cd \"$PRJ_ROOT\" && nix build .#docker \"$@\""; 383 } 384 ] 385 ++ docsCommands; 386 }; 387 388 # Lightweight docs-only dev shell — just Node + VitePress helpers. 389 # Enter with: `nix develop .#web` 390 devshells.web = { 391 name = "mcp-nixos-website"; 392 393 motd = '' 394 {202}mcp-nixos-website{reset} VitePress docs ({bold}${system}{reset}) 395 $(type menu &>/dev/null && menu) 396 ''; 397 398 packages = with pkgs; [ 399 nodejs_20 400 git 401 jq 402 ]; 403 404 commands = docsCommands; 405 }; 406 }; 407 }; 408}