From bdd10b391879db9aa4c90723cc2f5082fde45811 Mon Sep 17 00:00:00 2001 From: Farouk Brown Date: Sun, 5 Oct 2025 20:09:50 +0100 Subject: [PATCH] assistant/mcphub-nvim: init --- configuration.nix | 1 + flake/pkgs/by-name/mcphub-nvim/bin.nix | 25 ++ flake/pkgs/by-name/mcphub-nvim/package.nix | 26 ++ modules/plugins/assistant/default.nix | 1 + modules/plugins/assistant/mcphub/config.nix | 104 +++++++ modules/plugins/assistant/mcphub/default.nix | 6 + .../plugins/assistant/mcphub/mcphub-nvim.nix | 291 ++++++++++++++++++ modules/wrapper/build/config.nix | 2 +- npins/sources.json | 29 ++ 9 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 flake/pkgs/by-name/mcphub-nvim/bin.nix create mode 100644 flake/pkgs/by-name/mcphub-nvim/package.nix create mode 100644 modules/plugins/assistant/mcphub/config.nix create mode 100644 modules/plugins/assistant/mcphub/default.nix create mode 100644 modules/plugins/assistant/mcphub/mcphub-nvim.nix diff --git a/configuration.nix b/configuration.nix index 68776638..80993150 100644 --- a/configuration.nix +++ b/configuration.nix @@ -246,6 +246,7 @@ isMaximal: { }; assistant = { + mcphub-nvim.enable = isMaximal; chatgpt.enable = false; copilot = { enable = false; diff --git a/flake/pkgs/by-name/mcphub-nvim/bin.nix b/flake/pkgs/by-name/mcphub-nvim/bin.nix new file mode 100644 index 00000000..a4c65d2b --- /dev/null +++ b/flake/pkgs/by-name/mcphub-nvim/bin.nix @@ -0,0 +1,25 @@ +{ + pins, + pkgs, + ... +}: let + pin = pins.mcp-hub; + src = pkgs.fetchFromGitHub { + inherit (pin.repository) owner repo; + tag = pin.version; + sha256 = pin.hash; + }; + + inherit (pkgs) nodejs; +in + pkgs.buildNpmPackage { + pname = "mcp-hub"; + inherit (pin) version; + inherit src nodejs; + + nativeBuildInputs = [nodejs]; + npmDeps = pkgs.importNpmLock { + npmRoot = src; + }; + inherit (pkgs.importNpmLock) npmConfigHook; + } diff --git a/flake/pkgs/by-name/mcphub-nvim/package.nix b/flake/pkgs/by-name/mcphub-nvim/package.nix new file mode 100644 index 00000000..ce505a16 --- /dev/null +++ b/flake/pkgs/by-name/mcphub-nvim/package.nix @@ -0,0 +1,26 @@ +{ + pins, + vimUtils, + pkgs, + ... +}: let + mcp-hub = import ./bin.nix {inherit pins pkgs;}; + pin = pins.mcphub-nvim; + version = pin.branch; + src = pkgs.fetchFromGitHub { + inherit (pin.repository) owner repo; + rev = pin.revision; + sha256 = pin.hash; + }; +in + vimUtils.buildVimPlugin { + pname = "mcphub-nvim"; + inherit src version; + + doCheck = false; + + postInstall = '' + mkdir -p $out/bundled/mcp-hub/node_modules/.bin + ln -s ${mcp-hub}/bin/mcp-hub $out/bundled/mcp-hub/node_modules/.bin/mcp-hub + ''; + } diff --git a/modules/plugins/assistant/default.nix b/modules/plugins/assistant/default.nix index a4da583a..6e133856 100644 --- a/modules/plugins/assistant/default.nix +++ b/modules/plugins/assistant/default.nix @@ -5,5 +5,6 @@ ./codecompanion ./supermaven-nvim ./avante + ./mcphub ]; } diff --git a/modules/plugins/assistant/mcphub/config.nix b/modules/plugins/assistant/mcphub/config.nix new file mode 100644 index 00000000..7ce04e1d --- /dev/null +++ b/modules/plugins/assistant/mcphub/config.nix @@ -0,0 +1,104 @@ +{ + config, + lib, + ... +}: let + inherit (lib.modules) mkIf; + + cfg = config.vim.assistant.mcphub-nvim; +in { + config = mkIf cfg.enable { + vim = { + startPlugins = [ + "plenary-nvim" + ]; + + lazy.plugins = { + mcphub-nvim = { + package = "mcphub-nvim"; + setupModule = "mcphub"; + inherit (cfg) setupOpts; + event = ["DeferredUIEnter"]; + }; + }; + + # avante-nvim + assistant.avante-nvim.setupOpts = { + system_prompt = lib.generators.mkLuaInline '' + function() + local hub = require("mcphub").get_hub_instance() + return hub and hub:get_active_servers_prompt() or "" + end + ''; + custom_tools = lib.generators.mkLuaInline '' + function() + return { + require("mcphub.extensions.avante").mcp_tool(), + } + end + ''; + }; + + # codecompanion-nvim + assistant.codecompanion-nvim.setupOpts.extensions.mcphub = { + callback = "mcphub.extensions.codecompanion"; + opts = { + ## MCP Tools + make_tools = true; + show_server_tools_in_chat = true; + add_mcp_prefix_to_tool_names = false; + show_result_in_chat = true; + format_tool = null; + ## MCP Resources + make_vars = true; + ## MCP Prompts + make_slash_commands = true; + }; + }; + + # lualine + statusline.lualine.setupOpts.sections.lualine_x = lib.generators.mkLuaInline '' + {{ + function() + -- Check if MCPHub is loaded + if not vim.g.loaded_mcphub then + return "󰐻 -" + end + + local count = vim.g.mcphub_servers_count or 0 + local status = vim.g.mcphub_status or "stopped" + local executing = vim.g.mcphub_executing + + -- Show "-" when stopped + if status == "stopped" then + return "󰐻 -" + end + + -- Show spinner when executing, starting, or restarting + if executing or status == "starting" or status == "restarting" then + local frames = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } + local frame = math.floor(vim.loop.now() / 100) % #frames + 1 + return "󰐻 " .. frames[frame] + end + + return "󰐻 " .. count + end, + color = function() + if not vim.g.loaded_mcphub then + return { fg = "#6c7086" } -- Gray for not loaded + end + + local status = vim.g.mcphub_status or "stopped" + if status == "ready" or status == "restarted" then + return { fg = "#50fa7b" } -- Green for connected + elseif status == "starting" or status == "restarting" then + return { fg = "#ffb86c" } -- Orange for connecting + else + return { fg = "#ff5555" } -- Red for error/stopped + end + end, + },} + ''; + }; + }; +} diff --git a/modules/plugins/assistant/mcphub/default.nix b/modules/plugins/assistant/mcphub/default.nix new file mode 100644 index 00000000..8b2f33a0 --- /dev/null +++ b/modules/plugins/assistant/mcphub/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./config.nix + ./mcphub-nvim.nix + ]; +} diff --git a/modules/plugins/assistant/mcphub/mcphub-nvim.nix b/modules/plugins/assistant/mcphub/mcphub-nvim.nix new file mode 100644 index 00000000..483e6424 --- /dev/null +++ b/modules/plugins/assistant/mcphub/mcphub-nvim.nix @@ -0,0 +1,291 @@ +{lib, ...}: let + inherit (lib.options) mkOption mkEnableOption literalMD; + inherit (lib.types) int bool enum str nullOr attrs listOf either attrsOf anything; + inherit (lib.strings) toUpper; + inherit (lib.nvim.types) mkPluginSetupOption luaInline; + inherit (lib.generators) mkLuaInline; +in { + options.vim.assistant = { + mcphub-nvim = { + enable = mkEnableOption "MCPHub"; + + setupOpts = mkPluginSetupOption "mcphub-nvim" { + use_bundled_binary = + mkEnableOption "Use local `mcp-hub` binary." + // { + default = true; + }; + port = mkOption { + type = int; + default = 37373; + description = "The port for the mcp-hub Express server."; + }; + + server_url = mkOption { + type = nullOr str; + default = null; + description = "The URL for the mcp-hub server in cases where it is hosted somewhere else."; + example = "http://mydomain.com:customport"; + }; + + config_path = mkOption { + type = str; + default = "~/.config/mcphub/servers.json"; + description = "The absolute path to your mcpservers.json configuration file. Defaults to ~/.config/mcphub/servers.json in Lua."; + example = "~/.config/nvim/mcpservers.json"; + }; + + shutdown_delay = mkOption { + type = int; + default = 300000; # 5 minutes in milliseconds + description = "The delay in milliseconds before the server shuts down when the last client disconnects."; + }; + + request_timeout = mkOption { + type = int; + default = 60000; + description = "Timeout for MCP requests in milliseconds."; + }; + + auto_approve = mkOption { + type = either luaInline bool; + default = false; + description = '' + How to approve MCP calls. + + The system checks auto-approval in this order: + 1. Function: Custom auto_approve function (if provided) + 1. Server-specific: autoApprove field in server config + 1. Default: Show confirmation dialog + ''; + }; + + auto_toggle_servers = + mkEnableOption "Let LLMs start and stop MCP servers automatically." + // { + default = true; + }; + + cmd = mkOption { + type = nullOr str; + default = null; + description = "Custom command to start the mcp-hub binary."; + }; + + cmd_args = mkOption { + type = nullOr (listOf str); + default = null; + description = "Custom arguments for the mcp-hub command."; + }; + + global_env = mkOption { + type = nullOr (either luaInline (attrsOf anything)); + default = null; + description = '' + Global environment variables available to all MCP servers. + You can use either a table or a function that returns a table. + ''; + }; + + log = { + level = mkOption { + description = "Logging level, e.g., 'vim.log.levels.WARN'."; + type = enum ["debug" "info" "warn" "error" "trace"]; + default = "info"; + apply = filter: mkLuaInline "vim.log.levels.${toUpper filter}"; + }; + to_file = mkEnableOption "log to a file."; + file_path = mkOption { + type = nullOr str; + default = null; + description = "Path to the log file."; + }; + prefix = mkOption { + type = str; + default = "MCPHub"; + description = "The prefix for log messages."; + }; + }; + + ui = { + window = mkOption { + type = attrs; + default = { + width = 0.8; + height = 0.8; + align = "center"; + relative = "editor"; + zindex = 50; + border = "rounded"; + }; + description = "Options for the UI window."; + }; + wo = mkOption { + type = attrs; + default = { + winhl = "Normal:MCPHubNormal,FloatBorder:MCPHubBorder"; + }; + description = "Window options."; + }; + }; + + extensions = { + avante = { + enabled = + mkEnableOption "the Avante extension." + // { + default = true; + }; + make_slash_commands = + mkEnableOption "create slash commands for Avante." + // { + default = true; + }; + }; + copilotchat = { + enabled = + mkEnableOption "the CopilotChat extension." + // { + default = true; + }; + convert_tools_to_functions = + mkEnableOption "convert tools to functions." + // { + default = true; + }; + convert_resources_to_functions = + mkEnableOption "convert resources to functions." + // { + default = true; + }; + add_mcp_prefix = mkEnableOption "add an mcp prefix."; + }; + }; + + builtin_tools = { + edit_file = { + parser = { + track_issues = + mkEnableOption "track issues during parsing." + // { + default = true; + }; + extract_inline_content = + mkEnableOption "extract inline content." + // { + default = true; + }; + }; + locator = { + fuzzy_threshold = mkOption { + type = nullOr int; + default = null; + description = "Fuzzy matching threshold."; + }; + enable_fuzzy_matching = + mkEnableOption "fuzzy matching." + // { + default = true; + }; + }; + ui = { + go_to_origin_on_complete = + mkEnableOption "go to the origin on complete." + // { + default = true; + }; + keybindings = mkOption { + type = attrs; + default = { + accept = "."; + reject = ","; + next = "n"; + prev = "p"; + accept_all = "ga"; + reject_all = "gr"; + }; + description = "Keybindings for the edit file UI."; + }; + }; + }; + }; + + workspace = { + enabled = + mkEnableOption "workspace-specific hubs." + // { + default = true; + }; + look_for = mkOption { + type = listOf str; + default = [".mcphub/servers.json" ".vscode/mcp.json" ".cursor/mcp.json"]; + description = "Files to search for in order."; + }; + reload_on_dir_changed = + mkEnableOption "listen to DirChanged events to reload workspace config." + // { + default = true; + }; + port_range = mkOption { + type = attrs; + default = { + min = 40000; + max = 41000; + }; + description = "Port range for workspace hubs, with `min` and `max` attributes."; + }; + get_port = mkOption { + type = nullOr luaInline; + default = null; + description = "Function that returns the port."; + }; + }; + + on_ready = mkOption { + type = luaInline; + default = mkLuaInline '' + function() end + ''; + description = '' + A Lua function to be executed once the mcp-hub server is ready. + It receives the hub object as an argument. + ''; + example = literalMD '' + ```lua + function(hub) + vim.notify('MCPHub is ready', vim.log.levels.INFO) + end + ``` + ''; + }; + + on_error = mkOption { + type = luaInline; + default = mkLuaInline '' + function(msg) end + ''; + description = '' + A Lua function to be executed when an error occurs. + It receives the error message as an argument. + ''; + example = literalMD '' + ```lua + function(msg) + vim.notify('An error occurred in MCPHub: ' .. msg, vim.log.levels.ERROR) + end + ``` + ''; + }; + json_decode = mkOption { + type = nullOr luaInline; + default = null; + description = '' + Custom JSON parser function for configuration files. + + This is particularly useful for supporting JSON5 syntax (comments and trailing commas). + ''; + }; + }; + }; + }; +} diff --git a/modules/wrapper/build/config.nix b/modules/wrapper/build/config.nix index a1807388..c5053ecb 100644 --- a/modules/wrapper/build/config.nix +++ b/modules/wrapper/build/config.nix @@ -48,7 +48,7 @@ doCheck = false; }; - inherit (inputs.self.packages.${pkgs.stdenv.system}) blink-cmp avante-nvim; + inherit (inputs.self.packages.${pkgs.stdenv.system}) blink-cmp avante-nvim mcphub-nvim; }; buildConfigPlugins = plugins: diff --git a/npins/sources.json b/npins/sources.json index e97ce170..0aa58762 100644 --- a/npins/sources.json +++ b/npins/sources.json @@ -961,6 +961,35 @@ "url": "https://github.com/OXY2DEV/markview.nvim/archive/de79a7626d54d7785436105ef72f37ee8fe8fa16.tar.gz", "hash": "032i6m9pld1zyhd7lq49dg4qh98w6vmmzqp2f46drhq0ds26hs4h" }, + "mcp-hub": { + "type": "GitRelease", + "repository": { + "type": "GitHub", + "owner": "ravitemer", + "repo": "mcp-hub" + }, + "pre_releases": false, + "version_upper_bound": null, + "release_prefix": null, + "submodules": false, + "version": "v4.2.1", + "revision": "aac67ba3e163712d07674c154532a71965015e57", + "url": "https://api.github.com/repos/ravitemer/mcp-hub/tarball/v4.2.1", + "hash": "127d91xnfqaxqvk76682n8cghjhw1b02xzi4rxm3ggpljxfjza99" + }, + "mcphub-nvim": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "ravitemer", + "repo": "mcphub.nvim" + }, + "branch": "main", + "submodules": false, + "revision": "8ff40b5edc649959bb7e89d25ae18e055554859a", + "url": "https://github.com/ravitemer/mcphub.nvim/archive/8ff40b5edc649959bb7e89d25ae18e055554859a.tar.gz", + "hash": "1saw3xfrbnwpjklcffp144q2y100kd51yrhvmxnhgc7niy0ip893" + }, "mind-nvim": { "type": "Git", "repository": {