From d8dd7f43f642e22742ad4642552bcc9ac9e0649f Mon Sep 17 00:00:00 2001 From: Erik Bogado Date: Fri, 2 Oct 2026 08:29:15 -0300 Subject: [PATCH] fix(mcp): avoid runtime flake resolution Realize the pinned mcp-nixos closure with Home Manager so network cache retries cannot consume the MCP startup budget. Keep the shared V1-compatible configuration that V2 normalizes. --- .gitignore | 1 + .graphifyignore | 1 + checks.nix | 17 ++++++++ docs/nix-mcp-startup.md | 57 ++++++++++++++++++++++++ flake.lock | 94 +++++++++++++++++++++++++++++++++++++++- flake.nix | 4 +- modules/home-manager.nix | 8 ++-- 7 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 .graphifyignore create mode 100644 docs/nix-mcp-startup.md diff --git a/.gitignore b/.gitignore index ce7b4b1..78ad051 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ result result-* .direnv/ +/worktrees/ diff --git a/.graphifyignore b/.graphifyignore new file mode 100644 index 0000000..902a47f --- /dev/null +++ b/.graphifyignore @@ -0,0 +1 @@ +/worktrees/ diff --git a/checks.nix b/checks.nix index fa5a04c..ece2291 100644 --- a/checks.nix +++ b/checks.nix @@ -33,6 +33,10 @@ ]; }; + withoutMcp = defaults.extendModules { + modules = [{programs.opencode-profile.mcpNixos.enable = false;}]; + }; + enabledFiles = enabled.config.xdg.configFile; defaultsFiles = defaults.config.xdg.configFile; @@ -102,6 +106,8 @@ in { jq -e '.permission.edit."flake.lock" == "ask"' ${settingsJson} jq -e '.permission.bash."rm -rf /*" == "deny"' ${settingsJson} jq -e '.mcp.nix.type == "local"' ${settingsJson} + jq -e '.mcp.nix.enabled == true and (.mcp.nix.command | length) == 1' ${settingsJson} + test -x "$(jq -r '.mcp.nix.command[0]' ${settingsJson})" jq -e '.theme == "tokyonight"' ${tuiJson} grep -q "profile-check-marker" ${agentsMd} grep -q "caveman" ${agentsMd} @@ -121,6 +127,17 @@ in { assert lib.assertMsg (lib.count (p: lib.hasPrefix "opencode" (lib.getName p)) defaults.config.home.packages == 1) "expected exactly one opencode package in home.packages"; + assert lib.assertMsg + (let + command = defaults.config.programs.opencode.settings.mcp.nix.command; + in + builtins.length command + == 1 + && lib.hasPrefix "${builtins.storeDir}/" (builtins.head command) + && lib.hasSuffix "/bin/mcp-nixos" (builtins.head command)) + "nix MCP must launch a built executable without runtime flake resolution"; + assert lib.assertMsg (!((withoutMcp.config.programs.opencode.settings.mcp or {}) ? nix)) + "nix MCP registered despite mcpNixos.enable = false"; pkgs.runCommand "opencode-defaults-posture" {} "touch $out"; updater-mutation-only = pkgs.runCommand "opencode-updater-mutation-only" {} '' diff --git a/docs/nix-mcp-startup.md b/docs/nix-mcp-startup.md new file mode 100644 index 0000000..d680401 --- /dev/null +++ b/docs/nix-mcp-startup.md @@ -0,0 +1,57 @@ +# Nix MCP startup fix + +## Human seed and scope + +> cant we keep both v1 and v2 working on durable option? +> +> do fix, but just merge to main now. No need to deploy yes + +The verified source fix preserves the shared V1-compatible configuration +that V2 normalizes. mcp-nixos 2.4.3 is now a locked flake input, and the +Home Manager module launches its built executable directly. Deployment +realizes the closure; server startup no longer resolves a flake with `nix run`. +Local merge only; no push, consumer update, or deployment. + +## Evidence + +The original command initialized in 65.601 seconds during an isolated probe +with Nix download/cache retries. OpenCode V2's default startup budget is +30 seconds. The subsequent tool catalog loaded in 0.005 seconds. + +Baseline `nix flake check --offline --no-build --no-write-lock-file` passed. +RED: `just check` failed with the new regression assertion: +`nix MCP must launch a built executable without runtime flake resolution`. +Direct execution of the already-built 2.4.3 package initialized in 1.914 +seconds and listed tools in 0.005 seconds without executing any tool. + +GREEN: `just check-full` passed all checks, including rendered V1-schema +validation, executable existence, single store-path command, and disabled +MCP registration. Existing direct dependency pins are unchanged; Nix renamed +the root nixpkgs lock node to `nixpkgs_2` when adding upstream dependencies. + +Isolated runtime checks connected V1 1.18.30 in 7.876 seconds and V2 2.0.21 +in 4.676 seconds using the same legacy MCP entry. Only initialization and +catalog discovery ran; no MCP tools executed. Temporary processes were stopped. + +The initial full-check attempt exceeded its 120-second budget while fetching +dependencies through an unreachable cache. A 600-second rerun passed. V2 probe +repairs addressed diagnostic setup, not source: a one-shot list saw pending +initialization; direct HTTP lacked authentication. The successful probe used +an isolated registered service and OpenCode's authenticated API client. + +## Review, limits, and recovery + +Independent review found no code blockers; its stale-progress-note finding +is corrected here. No extra timeout, wrapper, package implementation, or +configuration schema was introduced. + +Deployment remains intentionally deferred. The active V2 configuration is an +independent copied snapshot and is unchanged; a future separately authorized +refresh must adopt the generated command. No live service was restarted. +Recovery is to revert the fix commit and redeploy through the owning consumer +only when separately authorized. + +Feedback: "this machine does have git credentials". Confirmed: V2's isolated +`XDG_CONFIG_HOME` hid the host Git configuration, including its author identity. +Git delivery commands use the host XDG config path temporarily; no identity, +credential, or global Git configuration changes are needed. diff --git a/flake.lock b/flake.lock index 23573eb..f3568f4 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "devshell": { + "inputs": { + "nixpkgs": [ + "mcp-nixos", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -20,6 +41,24 @@ "type": "github" } }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1767609335, + "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "250481aafeb741edfe23d29195671c19b36b6dca", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -40,7 +79,59 @@ "type": "github" } }, + "mcp-nixos": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1777087374, + "narHash": "sha256-mWq9nnL4IGhUFkXJr8+t6BresOTDFS1caG8NuFqjrJg=", + "owner": "utensils", + "repo": "mcp-nixos", + "rev": "fd51bb236e75cfec2fed16353a143faa1398bf2a", + "type": "github" + }, + "original": { + "owner": "utensils", + "ref": "v2.4.3", + "repo": "mcp-nixos", + "type": "github" + } + }, "nixpkgs": { + "locked": { + "lastModified": 1776548001, + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1788316716, "narHash": "sha256-bc7rSpXIdn9QWGNqfWcPZWOhEVF8NoeAZkWq0XWnf/k=", @@ -60,7 +151,8 @@ "inputs": { "flake-parts": "flake-parts", "home-manager": "home-manager", - "nixpkgs": "nixpkgs" + "mcp-nixos": "mcp-nixos", + "nixpkgs": "nixpkgs_2" } } }, diff --git a/flake.nix b/flake.nix index 8c44c42..ac9f8ec 100644 --- a/flake.nix +++ b/flake.nix @@ -13,6 +13,8 @@ url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; + + mcp-nixos.url = "github:utensils/mcp-nixos/v2.4.3"; }; outputs = inputs @ { @@ -85,7 +87,7 @@ flake = { homeManagerModules = { - default = import ./modules/home-manager.nix; + default = import ./modules/home-manager.nix {mcpNixos = inputs.mcp-nixos;}; withPackage = {pkgs, ...}: { imports = [self.homeManagerModules.default]; programs.opencode.package = self.packages.${pkgs.stdenv.hostPlatform.system}.opencode; diff --git a/modules/home-manager.nix b/modules/home-manager.nix index f5f6230..699c91e 100644 --- a/modules/home-manager.nix +++ b/modules/home-manager.nix @@ -1,6 +1,7 @@ -{ +{mcpNixos}: { config, lib, + pkgs, ... }: let cfg = config.programs.opencode-profile; @@ -205,11 +206,10 @@ in { permission = permissionDefaults; }) // (lib.optionalAttrs cfg.mcpNixos.enable { - # Pinned tag, not a floating branch: `nix run` re-resolves at MCP - # server start, so an unpinned ref would drift per session. + # Realize the pinned package at deployment, not during MCP startup. mcp.nix = { type = "local"; - command = ["nix" "run" "github:utensils/mcp-nixos/v2.4.3"]; + command = [(lib.getExe mcpNixos.packages.${pkgs.stdenv.hostPlatform.system}.mcp-nixos)]; enabled = true; }; });