Skip to content

Adding Tools ​

Tools are functions that LLMs can call with specific parameters. This guide covers how to implement tools with proper typing and best practices.

Tool Definition ​

lua
---@class MCPTool
---@field name string Required: Tool identifier
---@field description string|fun():string Optional: Tool description
---@field inputSchema? table|fun():table Optional: JSON Schema for validation
---@field handler fun(req: ToolRequest, res: ToolResponse): nil|table Required: Implementation

Request Context ​

Tool handlers receive a request object with:

lua
---@class ToolRequest
---@field params table Tool arguments (validated against inputSchema)
---@field tool MCPTool Complete tool definition
---@field server NativeServer Server instance
---@field caller table Additional context from caller
---@field editor_info EditorInfo Current editor state

Response Builder ​

Tool handlers use a chainable response builder:

lua
---@class ToolResponse
---@field text fun(text: string): ToolResponse Add text content
---@field image fun(data: string, mime: string): ToolResponse Add image
---@field audio fun(data: string, mime: string): ToolResponse Add audio
---@field resource fun(resource: MCPResourceContent): ToolResponse Add resource
---@field error fun(message: string, details?: table): table Send error
---@field send fun(result?: table): table Send final response

Examples ​

Basic Example ​

Here's a simple greeting tool:

lua
mcphub.add_tool("example", {
    name = "greet",
    description = "Greet a user",
    inputSchema = {
        type = "object",
        properties = {
            name = {
                type = "string",
                description = "Name to greet"
            }
        },
        required = { "name" }
    },
    handler = function(req, res)
        return res:text("Hello " .. req.params.name):send()
    end
})

Real Example: File Reading ​

Here's how the built-in Neovim server implements file reading:

lua
mcphub.add_tool("neovim", {
    name = "read_file",
    description = "Read contents of a file",
    inputSchema = {
        type = "object",
        properties = {
            path = {
                type = "string",
                description = "Path to the file to read",
            },
            start_line = {
                type = "number",
                description = "Start reading from this line (1-based)",
                default = 1
            },
            end_line = {
                type = "number",
                description = "Read until this line (inclusive)",
                default = -1
            }
        },
        required = { "path" }
    },
    handler = function(req, res)
        local params = req.params
        local p = Path:new(params.path)
        
        -- Validate file exists
        if not p:exists() then
            return res:error("File not found: " .. params.path)
        end

        -- Handle line range reading
        if params.start_line and params.end_line then
            local extracted = {}
            local current_line = 0
            
            for line in p:iter() do
                current_line = current_line + 1
                if current_line >= params.start_line 
                and (params.end_line == -1 or current_line <= params.end_line) then
                    table.insert(extracted, 
                        string.format("%4d │ %s", current_line, line))
                end
                if params.end_line ~= -1 and current_line > params.end_line then
                    break
                end
            end
            return res:text(table.concat(extracted, "\n")):send()
        end

        -- Read entire file
        return res:text(p:read()):send()
    end
})

Advanced Features ​

Dynamic Descriptions ​

Descriptions can be functions for dynamic content:

lua
mcphub.add_tool("files", {
    name = "search",
    description = function()
        local count = #vim.api.nvim_list_bufs()
        return string.format("Search %d open buffers", count)
    end,
    handler = function(req, res)
        -- Implementation
    end
})

Dynamic Schemas ​

Input schemas can also be dynamic:

lua
mcphub.add_tool("buffer", {
    name = "edit",
    inputSchema = function()
        -- Get open buffers
        local bufs = vim.api.nvim_list_bufs()
        local options = {}
        for _, bufnr in ipairs(bufs) do
            if vim.api.nvim_buf_is_loaded(bufnr) then
                table.insert(options, tostring(bufnr))
            end
        end
        
        return {
            type = "object",
            properties = {
                buffer = {
                    type = "string",
                    enum = options,
                    description = "Buffer to edit"
                }
            }
        }
    end,
    handler = function(req, res)
        -- Implementation
    end
})

Rich Responses ​

Tools can return multiple content types:

lua
mcphub.add_tool("diagram", {
    name = "generate",
    handler = function(req, res)
        -- Generate diagram image
        local image_data = generate_diagram(req.params)
        
        -- Return both text and image
        return res
            :text("Generated diagram:")
            :image(image_data, "image/png")
            :text("Diagram shows relationship between A and B")
            :send()
    end
})

Error Handling ​

Use proper error handling with details:

lua
mcphub.add_tool("git", {
    name = "commit",
    handler = function(req, res)
        -- Check git repository
        if not is_git_repo() then
            return res:error("Not a git repository", {
                cwd = vim.fn.getcwd(),
                suggestion = "Initialize git with 'git init'"
            })
        end
        
        -- Check for changes
        if git_is_clean() then
            return res:error("No changes to commit", {
                status = vim.fn.system("git status"),
                suggestion = "Make changes before committing"
            })
        end
        
        -- Implementation
    end
})

Using Editor Context ​

Access current editor state:

lua
mcphub.add_tool("buffer", {
    name = "analyze",
    handler = function(req, res)
        -- Get active buffer info
        local buf = req.editor_info.last_active
        if not buf then
            return res:error("No active buffer")
        end
        
        -- Get buffer details
        local details = {
            name = buf.filename,
            type = buf.filetype,
            lines = buf.line_count,
            modified = buf.is_modified
        }
        
        -- Format response
        return res:text(vim.inspect(details)):send()
    end
})

Caller-Aware Tools ​

Adapt behavior based on caller:

lua
mcphub.add_tool("context", {
    name = "get_code",
    handler = function(req, res)
        if req.caller.type == "codecompanion" then
            -- Get context from CodeCompanion chat
            local chat = req.caller.codecompanion.chat
            return handle_codecompanion(chat, res)
        
        elseif req.caller.type == "avante" then
            -- Get context from Avante
            local code = req.caller.avante.code
            return handle_avante(code, res)
            
        else
            -- Default behavior
            return res:text("No special context"):send()
        end
    end
})

Next, learn about Adding Resources to provide data through URIs.

Released under the MIT License.