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:
| Hook | Arguments | Return | When |
|---|---|---|---|
on_search(query, category, params) | query string, category string, search params table | modified query string (or nil to keep original) | Before search is dispatched to engines |
on_result(result, engine_id) | result table, engine ID string | modified result table, or nil to filter it out | After each engine returns results, before merging |
on_render(html, query, category) | HTML string, query, category | modified HTML string | After 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 processinghttp_get(url)for fetching URLslog(message)for logging to the server logconfig_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
- Configure MCP servers in
ai-research.kdl(or theai-researchsection of your unified KDL config) - At startup, 4got connects to each MCP server and discovers available tools
- These tools are added to the AI research agent’s tool list alongside the built-in tools
- 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
- All
.luafiles inplugins/are loaded alphabetically - MCP servers are connected in the order they appear in config
- Plugins are loaded after all engines are registered but before the HTTP server starts
- Plugin hooks run in load order; the first
on_searchreturn 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