This repository has no description
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}