Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plugin System

The plugin system supports Lua plugins and MCP tool integration.

Lua plugins

Lua plugins are the primary extension mechanism. Drop a .lua file in the plugins/ directory (relative to the data directory) and it will be loaded at startup.

Hooks

Plugins can register handlers for these hooks:

HookArgumentsReturnWhen
on_search(query, category, params)query string, category string, search params tablemodified query string (or nil to keep original)Before search is dispatched to engines
on_result(result, engine_id)result table, engine ID stringmodified result table, or nil to filter it outAfter each engine returns results, before merging
on_render(html, query, category)HTML string, query, categorymodified HTML stringAfter the results page HTML is generated, before sending to client

Plugin structure

-- plugins/example.lua
-- Plugin metadata
plugin = {
    name = "Example Plugin",
    version = "1.0",
    author = "your name",
    description = "An example plugin"
}

-- Called before search dispatch
function on_search(query, category, params)
    -- Modify query, return nil to keep original
    return nil
end

-- Called for each result from each engine
function on_result(result, engine_id)
    -- Return the result to keep it, nil to filter it out
    return result
end

-- Called after HTML is generated
function on_render(html, query, category)
    -- Inject custom HTML, modify existing HTML, etc.
    return html
end

Available Lua APIs

Plugins have access to the same sandbox as the AI research agent:

  • string, table, math standard libraries
  • json_decode(s) / json_encode(t) for JSON processing
  • http_get(url) for fetching URLs
  • log(message) for logging to the server log
  • config_get(key) for reading plugin-specific config values

Example: Ad domain blocker

-- plugins/block_ads.lua
plugin = {
    name = "Ad Blocker",
    version = "1.0",
    description = "Filters results from known advertising domains"
}

local ad_domains = {
    ["doubleclick.net"] = true,
    ["googleadservices.com"] = true,
    ["facebook.com/ads"] = true,
    ["amazon.com/gp/slredirect"] = true,
    ["taboola.com"] = true,
    ["outbrain.com"] = true,
    ["criteo.com"] = true,
}

-- Extract domain from URL
local function get_domain(url)
    local domain = url:match("https?://([^/]+)")
    if domain then
        domain = domain:gsub("^www%.", "")
        return domain
    end
    return ""
end

function on_result(result, engine_id)
    local domain = get_domain(result.url or "")
    if ad_domains[domain] then
        log("Blocked ad domain: " .. domain)
        return nil  -- filter out
    end
    return result
end

Example: Result enrichment

-- plugins/enrich_github.lua
plugin = {
    name = "GitHub Enricher",
    version = "1.0",
    description = "Adds star count and language to GitHub repo results"
}

function on_result(result, engine_id)
    local url = result.url or ""
    local owner, repo = url:match("github%.com/([^/]+)/([^/]+)")
    if owner and repo then
        -- Fetch repo info from GitHub API
        local api_url = "https://api.github.com/repos/" .. owner .. "/" .. repo
        local data = http_get(api_url)
        if data then
            local info = json_decode(data)
            if info and info.stargazers_count then
                result.snippet = result.snippet ..
                    " [★" .. tostring(info.stargazers_count) ..
                    " | " .. (info.language or "unknown") .. "]"
            end
        end
    end
    return result
end

MCP for AI tools

The AI research agent can be extended with MCP (Model Context Protocol) servers to give it access to additional tools beyond the built-in set.

How MCP integration works

  1. Configure MCP servers in ai-research.kdl (or the ai-research section of your unified KDL config)
  2. At startup, 4got connects to each MCP server and discovers available tools
  3. These tools are added to the AI research agent’s tool list alongside the built-in tools
  4. When the agent calls an MCP tool, 4got proxies the call to the appropriate MCP server

Configuration

// In ai-research.kdl
mcp-server "local-kb" {
    command "python3" "-m" "knowledge_base_mcp"
    env "KB_PATH=/path/to/knowledge-base"
}

mcp-server "calculator" {
    url "http://localhost:3001/mcp"
}

MCP servers can be launched as subprocesses (command) or connected to over HTTP (url).

Example: Local knowledge base MCP server

Create an MCP server that gives the AI agent access to a local knowledge base:

# knowledge_base_mcp.py
import json
import sys
import os
from pathlib import Path

KB_PATH = Path(os.environ.get("KB_PATH", "./kb"))

def search_kb(query):
    """Search local markdown files for relevant content."""
    results = []
    for md_file in KB_PATH.glob("**/*.md"):
        content = md_file.read_text()
        if query.lower() in content.lower():
            # Extract surrounding context
            idx = content.lower().index(query.lower())
            start = max(0, idx - 200)
            end = min(len(content), idx + 200)
            results.append({
                "file": str(md_file.relative_to(KB_PATH)),
                "excerpt": content[start:end]
            })
    return results[:5]

# MCP protocol handler
def handle_request(request):
    if request["method"] == "tools/list":
        return {
            "tools": [{
                "name": "search_knowledge_base",
                "description": "Search the local knowledge base for information",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "Search query"}
                    },
                    "required": ["query"]
                }
            }]
        }
    elif request["method"] == "tools/call":
        tool_name = request["params"]["name"]
        if tool_name == "search_knowledge_base":
            query = request["params"]["arguments"]["query"]
            results = search_kb(query)
            return {"content": [{"type": "text", "text": json.dumps(results, indent=2)}]}
    return {"error": "unknown method"}

# stdio transport
for line in sys.stdin:
    request = json.loads(line)
    response = handle_request(request)
    response["id"] = request.get("id")
    print(json.dumps(response), flush=True)

Example: Giving the AI agent calculator access

If the built-in calculator oracle isn’t sufficient, you could connect a Wolfram Language MCP server:

mcp-server "wolfram-lang" {
    command "wolframscript" "-code" "MCPServer[]"
}

This would let the agent evaluate arbitrary Wolfram Language expressions during research.

Plugin loading order

  1. All .lua files in plugins/ are loaded alphabetically
  2. MCP servers are connected in the order they appear in config
  3. Plugins are loaded after all engines are registered but before the HTTP server starts
  4. Plugin hooks run in load order; the first on_search return value that is non-nil wins

Plugin safety

  • Lua plugins run in the same sandbox as the AI research agent (no file/OS access)
  • http_get() goes through Go’s HTTP client with SSRF protection
  • Plugins that throw errors are logged and disabled, they don’t crash the server
  • MCP servers run as separate processes with their own permissions
  • Plugin execution has a per-call timeout (configurable, default 5 seconds)

Planned but not yet implemented

  • Plugin hot-reloading (SIGHUP or admin endpoint to reload without restart)
  • Per-plugin configuration in KDL
  • Plugin marketplace/registry
  • TypeScript/WASM plugin support
  • Plugin-specific rate limiting
  • Plugin metrics in the admin dashboard