Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions doc/mcp/servers_json.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ Adding, editing, deleting and securing MCP servers in easy and intuitive with MC

![Image](https://github.com/user-attachments/assets/f5c8adfa-601e-4d03-8745-75180a9d3648)

#### One click AI install with Avante and CodeCompanion
![Image](https://github.com/user-attachments/assets/2d0a0d8b-18ca-4ac8-a207-4758d09d359d)
#### One Click Install/Uninstall

#### Or Simple copy paste `mcpServers` json block in the README
Choose from different install options:

![Image](https://github.com/user-attachments/assets/359bc81e-d6fe-47bb-a25b-572bf280851e)
<!-- ![Image](https://github.com/user-attachments/assets/f58fcba3-8670-4b4e-998b-cd70b9e6c7ec) -->
![Image](https://github.com/user-attachments/assets/560bddda-e48d-488b-a9f8-7b188178914c)


### From Hub View
Expand Down
2 changes: 1 addition & 1 deletion doc/other/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Run `:checkhealth mcphub` in Neovim to check for common issues
{
level = vim.log.levels.DEBUG,
to_file = true,
file_path = vim.fn.expande("~/mcphub.log"),
file_path = vim.fn.expand("~/mcphub.log"),
}
```
- Test tools and resources individually to isolate issues
Expand Down
46 changes: 2 additions & 44 deletions lua/mcphub/hub.lua
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,7 @@ end
---@param server_name string Name of the server to update
---@param updates table|nil Key-value pairs to update in the server config or nil to remove
---@param opts? { callback?: function ,merge?:boolean} Optional callback(success: boolean)
---@return boolean, string|nil Returns success status and error message if any
function MCPHub:update_server_config(server_name, updates, opts)
opts = opts or {}
-- Load and validate current config
Expand Down Expand Up @@ -1343,7 +1344,7 @@ function MCPHub:get_marketplace_catalog(opts)
marketplace_state = {
status = "loaded",
catalog = {
items = response.items or {},
items = response.servers or {},
last_updated = response.timestamp,
},
},
Expand All @@ -1352,47 +1353,4 @@ function MCPHub:get_marketplace_catalog(opts)
})
end

--- Get detailed information about a marketplace server
--- @param mcpId string The server's unique identifier
--- @param opts? { callback?: function, timeout?: number }
--- @return table|nil, string|nil If no callback is provided, returns response and error
function MCPHub:get_marketplace_server_details(mcpId, opts)
opts = opts or {}

-- Check if we have cached details that are still valid
local cached = State.marketplace_state.server_details[mcpId]
if cached then
return cached
end
-- Fetch fresh details
return self:api_request("POST", "marketplace/details", {
timeout = opts.timeout or TOOL_TIMEOUT,
body = { mcpId = mcpId },
callback = function(response, err)
if err then
local market_err = Error(
"MARKETPLACE",
Error.Types.MARKETPLACE.FETCH_ERROR,
"Failed to fetch server details",
{ mcpId = mcpId, error = err }
)
State:add_error(market_err)
-- Keep server details as nil to indicate error state
else
-- Update state with new details
State:update({
marketplace_state = {
server_details = {
[mcpId] = {
data = response.server,
timestamp = vim.loop.now(),
},
},
},
}, "marketplace")
end
end,
})
end

return MCPHub
9 changes: 4 additions & 5 deletions lua/mcphub/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,13 @@ function State:get_errors(type)
end
return vim.deepcopy(self.errors.items)
end

--- Check if a server is installed by comparing mcpId
--- @param mcpId string Server ID to check
--- Check if a server is installed by comparing id
--- @param id string Server ID to check
--- @return boolean true if server is installed
function State:is_server_installed(mcpId)
function State:is_server_installed(id)
local servers = self.server_state.servers or {}
for _, server in ipairs(servers) do
if server.name == mcpId then
if server.name == id then
return true
end
end
Expand Down
35 changes: 24 additions & 11 deletions lua/mcphub/types.lua
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
---@meta

---@class MarketplaceItem
---@field mcpId string
---@field id string
---@field name string
---@field author string
---@field description string
---@field codiconIcon string
---@field logoUrl string
---@field url string
---@field license? string
---@field category string
---@field tags string[]
---@field requiresApiKey boolean
---@field isRecommended boolean
---@field githubStars integer
---@field githubUrl string
---@field downloadCount integer
---@field createdAt string
---@field updatedAt string
---@field installations MarketplaceInstallation[]
---@field featured? boolean
---@field verified? boolean
---@field stars? integer
---@field lastCommit? integer
---@field updatedAt? integer

---@class MarketplaceInstallation
---@field name string
---@field description? string
---@field config string
---@field prerequisites? string[]
---@field parameters? MarketplaceParameter[]
---@field transports? string[]

---@class MarketplaceParameter
---@field name string
---@field key string
---@field description? string
---@field placeholder? string
---@field required? boolean

---@class CustomMCPServerConfig.CustomInstructions
---@field text string
Expand Down
16 changes: 14 additions & 2 deletions lua/mcphub/ui/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local utils = require("mcphub.utils")
---@class MCPHub.UI
---@field window number Window handle
---@field buffer number Buffer handle
---@field current_view string Current view name
---@field current_view MCPHub.UI.ViewName Current view name
---@field views table Table of view instances
---@field is_shown boolean Whether the UI is currently visible
---@field cursor_states table Store cursor positions by view name
Expand All @@ -18,6 +18,15 @@ local utils = require("mcphub.utils")
local UI = {}
UI.__index = UI

---@enum MCPHub.UI.ViewName
local ViewName = {
MAIN = "main",
LOGS = "logs",
HELP = "help",
CONFIG = "config",
MARKETPLACE = "marketplace",
}

-- Default window settings
---@class MCPHub.UIConfig
local defaults = {
Expand Down Expand Up @@ -347,6 +356,9 @@ function UI:restart()
vim.notify("Failed to restart")
end
end)
vim.schedule(function()
self:switch_view("main")
end)
else
vim.notify("No hub instance available")
end
Expand Down Expand Up @@ -398,7 +410,7 @@ function UI:toggle(args)
end

--- Switch to a different view
---@param view_name string Name of view to switch to
---@param view_name MCPHub.UI.ViewName Name of view to switch to
function UI:switch_view(view_name)
-- Leave current view if any
if self.current_view and self.views[self.current_view] and self.is_shown then
Expand Down
9 changes: 2 additions & 7 deletions lua/mcphub/ui/views/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local ns_id = vim.api.nvim_create_namespace("MCPHub")
local renderer = require("mcphub.utils.renderer")

local VIEW_TYPES = {
---@type MCPHub.UI.ViewName[]
SETUP_INDEPENDENT = { "logs", "help", "config" },
}

Expand Down Expand Up @@ -234,13 +235,7 @@ end
--- Whether the view should show setup errors
---@return boolean
function View:should_show_setup_error()
-- Don't show setup errors in certain views
for _, name in ipairs(VIEW_TYPES.SETUP_INDEPENDENT) do
if self.name == name then
return false
end
end
return true
return not vim.tbl_contains(VIEW_TYPES.SETUP_INDEPENDENT, self.name)
end

--- Get window width for centering
Expand Down
84 changes: 17 additions & 67 deletions lua/mcphub/ui/views/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ function MainView:handle_custom_instructions(context)
end

function MainView:add_server()
utils.open_server_editor()
utils.open_server_editor({
title = "Paste Server Config",
start_insert = true,
})
end

function MainView:handle_edit()
Expand All @@ -153,60 +156,19 @@ function MainView:handle_edit()
local text = utils.pretty_json(vim.json.encode({
[server_name] = config,
}) or "")
ui_utils.multiline_input("Edit '" .. server_name .. "' Config", text, function(input)
if text == input then
return
end
local new_name, new_config = next(vim.json.decode(input))
if new_name ~= server_name then
State.hub_instance:remove_server_config(server_name)
vim.notify("Server " .. server_name .. " deleted", vim.log.levels.INFO)
end
---@cast new_name string
---@cast new_config table
State.hub_instance:update_server_config(new_name, new_config, { merge = false })
vim.notify("Server " .. new_name .. " updated", vim.log.levels.INFO)
end, {
filetype = "json",
utils.open_server_editor({
title = "Edit '" .. server_name .. "' Config",
is_native = is_native ~= nil,
old_server_name = server_name,
placeholder = text,
start_insert = false,
show_footer = false,
--instead of closing the input, validate and show errors
validate = function(content)
local success, result = pcall(vim.json.decode, content)
if not success then
vim.notify("Invalid JSON: " .. result, vim.log.levels.ERROR)
return
end
-- Case 3: Single server name:config pair
-- {
-- "server_name": {}
-- }
if vim.tbl_count(result) ~= 1 then
vim.notify("Config should have exactly one key i.e server name", vim.log.levels.ERROR)
return false
end
local new_name, new_config = next(result)
---@cast new_name string
---@cast new_config table

-- For native servers, we only need to validate basic structure
-- since they don't require command/url fields
if is_native then
if type(new_config) ~= "table" then
vim.notify("Config must be a table", vim.log.levels.ERROR)
return false
end
-- Native servers only need basic config validation
return true
else
local valid = validation.validate_server_config(new_name, new_config)
if not valid.ok then
vim.notify(valid.error.message, vim.log.levels.ERROR)
return false
end
end
return true
end,
virtual_lines = {
{
Text.icons.hint .. " ${VARIABLES} will be resolved from environment if not replaced",
"DiagnosticHint",
},
{ Text.icons.hint .. " ${cmd: echo 'secret'} will run command and replace ${}", "DiagnosticHint" },
},
})
elseif (line_type == "customInstructions") and context then
self:handle_custom_instructions(context)
Expand All @@ -228,19 +190,7 @@ function MainView:handle_delete()
if is_native then
return vim.notify("Native servers cannot be deleted, only their configuration can be edited")
end

-- Using vim.ui.select instead of vim.fn.confirm
vim.ui.select({ "Yes", "No" }, {
prompt = "Are you sure you want to delete " .. server_name .. "?",
format_item = function(item)
return item
end,
}, function(choice)
if choice == "Yes" then
State.hub_instance:remove_server_config(server_name)
vim.notify("Server " .. server_name .. " deleted", vim.log.levels.INFO)
end
end)
utils.confirm_and_delete_server(server_name)
end
end

Expand Down
Loading
Loading