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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ require('opencode').setup({
model_picker = {
toggle_favorite = { '<C-f>', mode = { 'i', 'n' } },
},
mcp_picker = {
toggle_connection = { '<C-t>', mode = { 'i', 'n' } }, -- Toggle MCP server connection in the MCP picker
},
},
ui = {
position = 'right', -- 'right' (default), 'left' or 'current'. Position of the UI split. 'current' uses the current window for the output.
Expand Down
40 changes: 2 additions & 38 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -518,44 +518,8 @@ function M.help()
end

M.mcp = Promise.async(function()
local mcp = config_file.get_mcp_servers():await()
if not mcp then
vim.notify('No MCP configuration found. Please check your opencode config file.', vim.log.levels.WARN)
return
end

state.display_route = '/mcp'
M.open_input()

local msg = M.with_header({
'### Available MCP servers',
'',
'| Name | Type | cmd/url |',
'|--------|------|---------|',
})

for name, def in pairs(mcp) do
local cmd_or_url
if def.type == 'local' then
cmd_or_url = def.command and table.concat(def.command, ' ')
elseif def.type == 'remote' then
cmd_or_url = def.url
end

table.insert(
msg,
string.format(
'| %s %-10s | %s | %s |',
(def.enabled and icons.get('status_on') or icons.get('status_off')),
name,
def.type,
cmd_or_url
)
)
end

table.insert(msg, '')
ui.render_lines(msg)
local mcp_picker = require('opencode.ui.mcp_picker')
mcp_picker.pick()
end)

M.commands_list = Promise.async(function()
Expand Down
31 changes: 31 additions & 0 deletions lua/opencode/api_client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,37 @@ function OpencodeApiClient:list_tools(provider, model, directory)
})
end

-- MCP endpoints

--- List all MCP servers
--- @param directory string|nil Directory path
--- @return Promise<table<string, table>>
function OpencodeApiClient:list_mcp_servers(directory)
return self:_call('/mcp', 'GET', nil, { directory = directory })
end

--- Connect an MCP server
--- @param name string MCP server name (required)
--- @param directory string|nil Directory path
--- @return Promise<boolean>
function OpencodeApiClient:connect_mcp(name, directory)
if not name or name == '' then
return require('opencode.promise').new():reject('MCP server name is required')
end
return self:_call('/mcp/' .. name .. '/connect', 'POST', nil, { directory = directory })
end

--- Disconnect an MCP server
--- @param name string MCP server name (required)
--- @param directory string|nil Directory path
--- @return Promise<boolean>
function OpencodeApiClient:disconnect_mcp(name, directory)
if not name or name == '' then
return require('opencode.promise').new():reject('MCP server name is required')
end
return self:_call('/mcp/' .. name .. '/disconnect', 'POST', nil, { directory = directory })
end

--- Create a factory function for the module
--- @param base_url? string The base URL of the opencode server
--- @return OpencodeApiClient
Expand Down
3 changes: 3 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ M.defaults = {
model_picker = {
toggle_favorite = { '<C-f>', mode = { 'i', 'n' } },
},
mcp_picker = {
toggle_connection = { '<C-t>', mode = { 'i', 'n' } },
},
quick_chat = {
cancel = { '<C-c>', mode = { 'i', 'n' } },
},
Expand Down
96 changes: 72 additions & 24 deletions lua/opencode/ui/base_picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ local Promise = require('opencode.promise')
---@class MiniPickSelected
---@field current MiniPickItem?

---@class PickerItemPart
---@field text string The text content
---@field highlight? string Optional highlight group

---@class PickerItem
---@field content string Main content text
---@field time_text? string Optional time text
---@field debug_text? string Optional debug text
---@field parts PickerItemPart[] Array of text parts with optional highlights
---@field to_string fun(self: PickerItem): string
---@field to_formatted_text fun(self: PickerItem): table

Expand Down Expand Up @@ -80,24 +82,35 @@ local function telescope_ui(opts)
local action_state = require('telescope.actions.state')
local action_utils = require('telescope.actions.utils')
local entry_display = require('telescope.pickers.entry_display')
local displayer = entry_display.create({
separator = ' ',
items = { {}, {}, config.debug.show_ids and {} or nil },
})

-- Create displayer dynamically based on number of parts
local function create_displayer(picker_item)
local items = {}
for _ in ipairs(picker_item.parts) do
table.insert(items, {})
end
return entry_display.create({
separator = ' ',
items = items,
})
end

local current_picker

---Creates entry maker function for telescope
---@param item any
---@return TelescopeEntry
local function make_entry(item)
local picker_item = opts.format_fn(item)
local displayer = create_displayer(picker_item)

local entry = {
value = item,
display = function(entry)
local formatted = opts.format_fn(entry.value):to_formatted_text()
return displayer(formatted)
end,
ordinal = opts.format_fn(item):to_string(),
ordinal = picker_item:to_string(),
}

if type(item) == 'table' then
Expand Down Expand Up @@ -549,35 +562,70 @@ function M.align(text, width, opts)
end

---Creates a generic picker item that can format itself for different pickers
---@param text string Array of text parts to join
---@param time? number Optional time text to highlight
---@param parts PickerItemPart[] Array of text parts with optional highlights
---@return PickerItem
function M.create_picker_item(parts)
local item = {
parts = parts,
}

function item:to_string()
local texts = {}
for _, part in ipairs(self.parts) do
table.insert(texts, part.text)
end
return table.concat(texts, ' ')
end

function item:to_formatted_text()
local formatted = {}
for _, part in ipairs(self.parts) do
if part.highlight then
table.insert(formatted, { ' ' .. part.text, part.highlight })
else
table.insert(formatted, { part.text })
end
end
return formatted
end

return item
end

---Helper function to create a simple picker item with content, time, and debug text
---This is a convenience wrapper around create_picker_item for common use cases
---@param text string Main content text
---@param time? number Optional time to format
---@param debug_text? string Optional debug text to append
---@param width? number Optional width override
---@return PickerItem
function M.create_picker_item(text, time, debug_text, width)
function M.create_time_picker_item(text, time, debug_text, width)
local time_width = time and #util.format_time(time) + 1 or 0
local debug_width = config.debug.show_ids and debug_text and #debug_text + 1 or 0
local item_width = width or vim.api.nvim_win_get_width(0)
local text_width = item_width - (debug_width + time_width)
local item = {
content = M.align(text, text_width --[[@as integer]], { truncate = true }),
time_text = time and M.align(util.format_time(time), time_width, { align = 'right' }),
debug_text = config.debug.show_ids and debug_text or nil,

local parts = {
{
text = M.align(text, text_width --[[@as integer]], { truncate = true }),
},
}

function item:to_string()
return table.concat({ self.content, self.time_text or '', self.debug_text or '' }, ' ')
if time then
table.insert(parts, {
text = M.align(util.format_time(time), time_width, { align = 'right' }),
highlight = 'OpencodePickerTime',
})
end

function item:to_formatted_text()
return {
{ self.content },
self.time_text and { ' ' .. self.time_text, 'OpencodePickerTime' } or { '' },
self.debug_text and { ' ' .. self.debug_text, 'OpencodeDebugText' } or { '' },
}
if config.debug.show_ids and debug_text then
table.insert(parts, {
text = debug_text,
highlight = 'OpencodeDebugText',
})
end

return item
return M.create_picker_item(parts)
end

---Generic picker that abstracts common logic for different picker UIs
Expand Down
2 changes: 1 addition & 1 deletion lua/opencode/ui/history_picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ local history = require('opencode.history')
local function format_history_item(item, width)
local entry = item.content or item.text or ''

return base_picker.create_picker_item(entry:gsub('\n', '↵'), nil, 'ID: ' .. item.id, width)
return base_picker.create_time_picker_item(entry:gsub('\n', '↵'), nil, 'ID: ' .. item.id, width)
end

function M.pick(callback)
Expand Down
Loading